
DbGate의 loadReader 엔드포인트에서 functionName 주입을 통한 원격 코드 실행 — CVSS 8.8
functionName 주입을 통한 DbGate의 원격 코드 실행심각도: 높음 (CVSS 8.8)
CWE: CWE-94 — 코드 생성의 부적절한 제어('코드 주입')
영향받는 버전: dbgate-api ≤ 7.1.8 (7.1.9에서 패치됨)
보안 권고: GHSA-hv83-ggc4-v385
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-48017
공로: Romain Deperne
POST /runners/load-reader 엔드포인트는 functionName 매개변수를 받아 이를
JavaScript 템플릿 문자열에 직접 보간한 뒤, 포크된 프로세스에서 실행합니다 — 아무런
정제나 검증 없이 말입니다. 인증된 사용자라면 누구나(특별한 권한 불필요) 의도된 호출을
벗어나 임의의 JavaScript를 실행할 수 있으며, 서버에서 원격 코드 실행을 달성할 수 있습니다.
포크된 러너는 샌드박스로 require = null을 설정하지만, process.binding("spawn_sync")는 여전히
접근 가능합니다: process.binding("spawn_sync")는 여전히 도달 가능하며 실제 OS 프로세스를 생성합니다.
저는 보호된 코드 실행 경로와 보호되지 않은 코드 실행 경로 사이의 간극을 찾기 위해 DbGate의 러너 하위 시스템을 감사하고 있었습니다. runners.js:292의 start() 러너는 적절히 보호되어 있습니다 — 이는
testStandardPermission('run-shell-script')를 호출하고 platformInfo.allowShellScripting을 확인합니다.
loadReader()는 개념적으로 유사한 작업(JS 로더 스크립트를 빌드하고 실행)을 수행하지만
그러한 검사가 하나도 없습니다. 저는 데이터 흐름을 추적했습니다:
runners.js:353 loadReader({ functionName, props })
runners.js:366 loaderScriptTemplate(prefix, functionName, ...)
runners.js:64 `... ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});`
packageTools.ts:33 return `dbgateApi.${functionName}` // ← no sanitization
functionName은 공격자가 제어하며 실행되는 템플릿 문자열 내부에 삽입됩니다. 앞에 있는 것은 dbgateApi.
접두사뿐이며, 이는 이스케이프가 가능합니다. toString();으로 표현식을 닫고, 페이로드를 주입한 뒤, 뒤따르는 (${props})를 //로 주석 처리하면 유효한 JS가 생성됩니다.
파일: packages/api/src/controllers/runners.js (loadReader → loaderScriptTemplate)
파일: packages/tools/src/packageTools.ts:33 (compileShellApiFunctionName)
// packageTools.ts:33 — functionName flows in unsanitized
return `dbgateApi.${functionName}`;
// runners.js:64 — interpolated into the executed loader template
`const reader = await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});`
생성된 (주입된) 스크립트:
const reader = await dbgateApi.toString();
process.binding("spawn_sync").spawn({ file:"/bin/sh", args:["/bin/sh","-c","id"], ... });
dbgateApi.toString//({});
compileShellApiFunctionName()는 짧은 이름을 완전히 정규화된 API 경로
(dbgateApi.<name>)로 변환하도록 작성되었으며, functionName이 식별자일 것이라고 암묵적으로 신뢰했습니다. 그러나 이 값은
HTTP 요청 본문에서 직접 가져옵니다. 이 값이 나중에 eval/fork되는 소스에 연결되기 때문에,
이 신뢰 가정은 RCE로 이어집니다. require = null 샌드박스는 도움이 되지 않습니다 — Node의
내부 process.binding("spawn_sync")가 이를 우회합니다.
수정 (7.1.9): 로더 스크립트로 컴파일하기 전에 functionName을 엄격한 식별자 허용 목록에 대해 검증합니다.
poc/rce_loadreader_functionname_injection.py — 실제 엔드포인트를 엔드투엔드로 구동합니다.
# with an existing JWT
python3 poc/rce_loadreader_functionname_injection.py http://localhost:3000 <JWT> 'id > /tmp/pwned'
# or log in first
python3 poc/rce_loadreader_functionname_injection.py http://localhost:3000 --login admin password 'id'
스크립트는 functionName 페이로드를 구성하여 POST /runners/load-reader로 전송하며, 주입된 spawn_sync 호출이 포크된 러너 프로세스에서 명령을 실행합니다.
DbGate API 호스트에서의 인증된 RCE. DbGate는 내부 데이터베이스에 대한 광범위한 네트워크 접근 권한과 함께 배포되는 경우가 많은 데이터베이스 관리 GUI로, 이에 대한 코드 실행은 데이터 계층으로 침투할 수 있는 강력한 발판이 됩니다.
책임 있게 공개되었습니다. PoC는 수정 사항이 배포된 후 방어자와 탐지 엔지니어링을 위해 게시되었습니다.