Node2Shell: Google Brotli Decoder Out-of-Bounds Write (CVE-2026-111497)

Also reported as Broccoli2Shell, Brotli2Shell and HTTP2Shell (one CVE) · Node.js, Bun, .NET, Envoy, Python · on CISA KEV 2026-10-27 · no named actor
Threat
Node2Shell
Severity
ON CISA KEV
Type
Library memory-corruption RCE
Access
Unauthenticated, if attacker Brotli reaches the decoder
Version
v0.1 · 2026-10-28
Author
HuntPack
Confidence
High (CVE facts) / Medium to Low (exploit reliability, endpoint coverage)
01

Executive Summary

What the attacker wants: code execution as the service account of an internet-facing Node.js (or Bun, .NET, Envoy, Python) workload, reached without credentials by getting the application to run a crafted Brotli stream through the Google Brotli decoder. CVE-2026-111497 is an out-of-bounds write caused by an unchecked return value (CWE-252 leading to CWE-787): the decoder treated a block switch that the stream's own structure makes invalid as "needs more input" instead of a format error, so it kept appending bytes to an 8-byte internal buffer and wrote past its end into neighbouring decoder state. Node2Shell, Broccoli2Shell, Brotli2Shell and HTTP2Shell are nicknames for this single CVE, not separate threats, and there is no named threat actor in any source.

Status: CISA added the CVE to the Known Exploited Vulnerabilities catalog on 2026-10-27 (federal due date 2026-11-10, ransomware use "Unknown") and gave no attack details. The concrete in-the-wild evidence is from ELLIO: a public exposure checker, node2shell-check, swept its sensors, and on 2026-10-26 a real exploit request arrived from 94.154.43.7. ELLIO tested that request and it did not produce a shell in 50 attempts, so treat it as a failed first-generation attempt by an operator who is now clearly trying, not as proof of working mass exploitation.

Exploitability, stated plainly: a bare Node.js HTTP server does not decode incoming Brotli request bodies on its own, so exposure depends on the application: Express 5 with body-parser 2.x, Koa body parsers and Fastify with @fastify/compress decode request bodies, and built-in fetch() decodes Brotli responses from any server an attacker influences. Imperva showed code execution on the official Node.js 24.22.0 Linux arm64 image, and not on every attempt. ELLIO reproduced a shell on x86-64 against four setups with ASLR left on, but each Node.js build needs its own memory offset (the 24.22.0 value gave nothing in 10,000 attempts against 22.23.3 and 26.12.0), and most of its Express attempts crashed the worker. Other researchers reported crashes only, and Sysdig records that blind remote execution with ASLR on did not work for one team. The honest default is that a crash is the likely outcome everywhere and a shell is a credible risk wherever attacker-influenced Brotli reaches an unpatched decoder.

Highest-value defensive angle: the exploit request is invisible to endpoint sensors, so the pack hunts what the runtime does afterwards: a shell, downloader or interpreter under node, a node PID that is replaced in place by /bin/sh, restart loops, odd outbound connections and reverse-shell command lines. Patch, then remove the decoder path, then constrain the process.

Defender priority: inventory every Node.js, Bun, Python and Envoy workload that decodes Brotli the attacker can influence, and patch internet-facing ones first: Node.js 22.23.4, 24.22.1 or 26.12.1; Brotli 1.0.10, 1.1.1 or 1.2.1; Bun 1.4.4. Node.js 20, 23 and 25 are end of life and will not be fixed, so replace them. Until patched, reject Content-Encoding: br on inbound request bodies where no client needs it.

02

Source Review & Web Hunter Notes

TierSourceKey findingCarry forward
1CISA Known Exploited Vulnerabilities feed (snapshot 01)Catalog 2026.10.27. Entry for CVE-2026-111497: Google Brotli "Unchecked Return Value Vulnerability", added 2026-10-27, due 2026-11-10, ransomware use Unknown, CWE-252 and CWE-787. Notes say the flaw is in an open-source component used by different products.Yes
1Node.js October 2026 security releases (snapshot 11)Vendor statement of the mechanism (invalid block switch reported as a need for more input, 8-byte buffer), affected lines 22.x, 24.x, 26.x, fixed versions.Yes
1google/brotli v1.2.1 release (snapshot 12)Upstream release titled with the CVE and the out-of-bounds write; published 21 Oct 19:27.Partial (page is thin)
1ELLIO, Node2Shell on stock Node.js builds (snapshot 02)Only source for the in-the-wild indicators: node2shell-check sweep, the 2026-10-26 exploit request, its SHA-256 and source IP, the failed-chain finding, x86-64 reproduction, per-build offsets, crash and core-dump behaviour, the in-place execvp ending.Yes (single source for all atomic IOCs)
1Sysdig Threat Research, detecting the Node.js Brotli RCE (snapshot 03)Reachability table by framework, demonstrated versus claimed exploitation, runtime detection idea (crash followed by in-place program execution), mitigation list, Debian system-library caveat.Yes
1Tenable FAQ (snapshot 04)Beyond Node.js: .NET fixed versions (8.0.33, 9.0.22, 10.0.14), PHP for Windows, FFmpeg via libjxl, Apache Arrow, ClickHouse; CVSS 4.0 9.2 from Google and 8.1 CVSS 3.1 from Microsoft for .NET.Yes
2Wiz (05), Cycode (06), Aikido (07)Affected and patched version matrices for Node.js, Bun, Envoy, Istio, Python Brotli and brotlicffi, Go andybalholm/brotli, distro status, framework reachability.Yes (versions)
2Help Net Security (08), The Hacker News (09), SecurityWeek (10)Independent confirmation of ELLIO's source IP, of the failed-exploit finding and of the KEV addition; Previdian logged one attempt on 2026-10-27 from a US address (no address published); Broadcom SDDC Manager and PAN-OS hotfix notices.Yes (corroboration; not independent of ELLIO for the IOC)
-Akamai root-cause analysisThe page returned HTTP 403 to an automated fetch, so no snapshot exists and nothing in this pack is attributed to it.No

Decisions. (1) Every atomic indicator is single-sourced to ELLIO, so both ship as detect with a 3-month expiry and nothing ships as prevent. (2) The SHA-256 is the hash of an HTTP request body, not of a file, so it can only match endpoint telemetry if the body was written to disk. It is kept for completeness and for any proxy or WAF that logs body hashes. (3) ELLIO's truncated hash of the Node.js 24.22.0 x86-64 release binary (67ca5783...) is not usable as an indicator and is not shipped. (4) No coined campaign or actor name is used; the only names are those in the snapshots.

The raw fetched text of every source above is saved in Node2Shell-CVE-2026-111497-Hunt-sources/ next to this file. No fetched page contained instructions aimed at an automated agent, and no source was disqualified.

03

Hunt Brief & Attack Chain

Hunt hypotheses (ordered by fidelity)

#HypothesisMITRETelemetryConfQueries
H1A successful exploit ends in a shell: the runtime process is replaced in place by /bin/sh (stdin wired to the attacker's socket), or spawns a shell or interpreter child.T1190, T1059.004Process creation with command line, image path and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents)MediumQ1, Q2
H2Failed attempts crash the worker and the supervisor restarts it, so a burst of identical starts marks an exploitation run.T1190, T1499.004Process creation (as above)MediumQ3
H3The operator pivots to a reverse shell or a staged download.T1059.004, T1095, T1105Process creation with command line; outbound network connection with owning process (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents)High (command line) / Medium (network)Q4, Q5
H4The operator runs discovery and reads secrets from the process environment, files or the cloud metadata address.T1033, T1082, T1552.001, T1552.005Process creation with command lineMediumQ6
H5Unpatched or end-of-life runtimes and decoder hosts exist and are reachable.T1190Process creation with file hash and path as an inventory source (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents)MediumQ7, Q9
H6The known exploit source address touched the estate.T1071Outbound network connection (as above); web and proxy logs for the inbound sideLowQ8, Section 7 N1

Attack chain

StepBehaviourTelemetryHunt angle
0Reconnaissance: node2shell-check sends a GET then 20 POST bodies with Content-Encoding: br in two bursts to find hosts that decode BrotliWeb server, proxy or WAF logs (outside endpoint telemetry)Section 7 N1. Gap for endpoint sensors.
1Delivery: crafted Brotli stream reaches the decoder via a request body (Express 5, Koa, Fastify), via an attacker-influenced fetch() response, or via direct node:zlib useWeb, proxy or WAF logs; application logs (outside endpoint telemetry)Section 7 N1, N2. Gap for endpoint sensors.
2Memory corruption: out-of-bounds write into decoder state; a miss usually segfaults the workerProcess creation for the restart (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents); host crash logs and core dumpsQ3; Section 7 N3
3Code execution: ROP chain ends in execvp of /bin/sh with stdin on the accepted socketProcess creation with parent and PIDQ1, Q2
4Post-exploitation: discovery, secrets, download, reverse shellProcess creation with command line; outbound connectionsQ4, Q5, Q6
5Impact: crash-driven denial of service; core files holding other users' credentials where core dumps are writtenHost logs; coredump listingSection 7 N3. Gap.
04

Consolidated IOC Table

TypeValueConfidenceActionContextExpiry
SHA-2567fcbdaaecd4419e2930927844f3b772668204737a4e7602a2290c991c93c5c4bMedium (single source)detectHash of the 23,069,983-byte Brotli-coded request body observed by ELLIO at 15:08 UTC on 2026-10-26, built for Node.js v24.22.0 x86-64. Request body hash, not a file hash.2027-01-28
IPv494.154.43.7Medium (single source; ELLIO rates it malicious, high confidence)detectSource of that request. First observed by ELLIO 2025-12-15, active on 47 of the last 90 days, also seen on PHP2Shell, VPN and Kubernetes API scanning. Shared mass-exploitation infrastructure, so treat hits as scanner contact, not proof of compromise.2027-01-28
User-AgentMozilla/5.0 (compatible; node2shell-check/1.0)Mediumhunt (web logs)Public exposure checker. First observed 2026-06-13 from AS44620 (Turkey). Web, proxy and WAF logs only.2027-01-28
ASNAS44620LowenrichOrigin of the checker sweep. Context only: an ASN is not a blocking indicator.n/a
Request shapePOST with Content-Encoding: br; first block 1031 bytes, stage two 256 bytes; body about 23 MBMediumhunt (web logs)From ELLIO's analysis of the exploit request. The checker sent malformed 1032-byte bodies of repeated A characters and valid bodies up to 65,743 bytes.n/a
Log stringERR__ERROR_FORMAT_BLOCK_SWITCH / BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17)Highhunt (app logs)A patched server rejects the trigger immediately with a 400 and this error. Seeing it in application logs means someone sent the trigger to a patched host.n/a

No prevent actions. Both atomic indicators are single-sourced to ELLIO (Help Net Security and The Hacker News repeat the IP, but only by citing ELLIO), so they are detect only. The hash describes a network payload, so a prevent action on it would protect nothing. Nothing in this pack blocks node, node.exe, bun or any other stock product binary.

05

Affected Surface & Telemetry Matrix

SurfaceTelemetryPriorityGap
Linux hosts and containers running Node.js 22, 24, 26 (and EOL 20, 23, 25) behind Express 5 / body-parser 2, Koa, FastifyProcess creation with command line and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents)CriticalExploit request is outside endpoint telemetry. Container hosts without a sensor are blind.
Services that call fetch(), axios, got or SuperAgent on attacker-influenced URLs (webhooks, link previews, crawlers, agent tools)Outbound network connection with owning process (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents)HighThe malicious Brotli arrives in a response body; no endpoint telemetry carries it.
Bun (below 1.4.4), including executables from bun build --compileProcess creation (as above)HighCompiled executables need a rebuild, so a host-level package inventory misses them.
.NET apps using ASP.NET Core request decompression, HttpClient decompression, or BrotliStreamProcess creation (as above); Windows and LinuxMediumDecompression is off by default; presence of the runtime is not exposure.
Envoy and Istio with the Brotli decompressor configured; Python services with the Brotli or brotlicffi packageProcess creation (as above)MediumCode execution not shown for the Python packages; hang and denial-of-service behaviour reported.
Appliances that embed Node.js (Broadcom SDDC Manager in VMware Cloud Foundation, PAN-OS hotfixes rolling out)Vendor advisories; appliance logsMediumUsually no endpoint sensor. Patches pending for the VMware product at the time of the SecurityWeek report.
Linux hosts that link the system libbrotli (distro-built Node.js)Package inventory (outside endpoint telemetry)HighThe runtime version does not reveal the library version; the distro package does.
06

ATT&CK Mapping

TacticTechniqueObserved behaviourQuery or control
ReconnaissanceT1595.002 Vulnerability Scanningnode2shell-check sweeps with Brotli bodiesSection 7 N1
Initial AccessT1190 Exploit Public-Facing ApplicationCrafted Brotli request body or fetched responseSection 7 N1; H1, H5 (M1051, M1048)
ExecutionT1059.004 Unix Shellexecvp of /bin/sh from the runtime; shell childrenQ1, Q2, Q5
DiscoveryT1033, T1082, T1083id, uname, environment and file readsQ6
Credential AccessT1552.001, T1552.005Secrets in environment, files, cloud metadataQ6; H7 (M1026)
Command and ControlT1095, T1571, T1105Reverse shell, staged download, odd portsQ4, Q5
ImpactT1499.004 Application or System ExploitationCrash and hang as denial of serviceQ3; Section 7 N3
07

Native Audit-Log Hunts (non-CQL)

Why this section matters. Steps 0 and 1 of the chain (the sweep and the exploit request) and the crash-and-core-dump consequences leave no trace in endpoint process telemetry. They are in web and proxy logs, application logs and host crash records. Treat these checks as first-class hunt work.

N1 · Web server, proxy and WAF logs

Most default access-log formats do not record request headers. Add $http_content_encoding and $request_length to the nginx log format (or the equivalent in your proxy or WAF) and search from 2026-10-21, or earlier for the checker, which was first observed in June.

# The checker user agent
grep -aEi 'node2shell-check' /var/log/nginx/access.log* /var/log/apache2/access.log*

# The ELLIO source address
grep -aF '94.154.43.7' /var/log/nginx/access.log* /var/log/apache2/access.log*

# Requests carrying a Brotli-encoded request body (needs content_encoding in the log format)
grep -aE 'content_encoding="?br"?' /var/log/nginx/access.log* | grep -aE '"(POST|PUT|PATCH)'

# Very large request bodies on Brotli requests (the exploit body was about 23 MB)
awk '/content_encoding="?br"?/ && $NF+0 > 5000000' /var/log/nginx/access.log*

# A patched server rejecting the trigger answers 400 with this error name in the app log
grep -aE 'ERR__ERROR_FORMAT_BLOCK_SWITCH|ERROR_FORMAT_BLOCK_SWITCH' /var/log/app/*.log

Reading the results. A br request body is unusual: browsers do not send compressed request bodies. Any POST with Content-Encoding: br to a public route is worth a look. The exact awk field depends on your log format, so adjust the field number before trusting a zero.

N2 · Application and fetch paths

  • Search the codebase for Express 5 body parsers (express.json, express.raw, express.text, express.urlencoded), Koa body parsers (@koa/bodyparser, koa-body, co-body), @fastify/compress, direct zlib.brotliDecompress and createBrotliDecompress, and server-side fetch(), axios and got calls on user-supplied URLs.
  • Express 4 and body-parser 1.x do not decode Brotli request bodies (Sysdig, Cycode); they are not exposed on that path, but other middleware may be.
  • List every feature that fetches a URL a user can influence: link previews, webhook delivery, crawlers, image proxies, agent tools. These are the outbound path.

N3 · Crash evidence on the host

# systemd hosts: recent node crashes and the cores they left
coredumpctl list node 2>/dev/null | tail -50
journalctl -k --since "2026-10-21" | grep -aEi 'node\[[0-9]+\].*(segfault|general protection)'

# Containers: restart counts and last exit reason
kubectl get pods -A -o wide | awk '$5+0 > 3'
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -i restarting

# Who can read core files, and how long are they kept
ls -l /var/lib/systemd/coredump/ 2>/dev/null
cat /proc/sys/kernel/core_pattern

ELLIO notes that where core dumps are written, every attacker-forced crash leaves a core holding other visitors' session cookies, bearer tokens and key material, and that core files are root-readable and often kept. If you find cores from node crashes after 2026-10-21, restrict access and rotate what they could contain.

N4 · Version and library inventory

# Runtime version. Do NOT rely on process.versions.brotli: it reads 1.2.0 on patched and vulnerable builds.
node --version        # fixed: 22.23.4, 24.22.1, 26.12.1
bun --version         # fixed: 1.4.4
dotnet --list-runtimes   # fixed: 8.0.33, 9.0.22, 10.0.14

# Python bindings (fixed: brotli 1.2.1, brotlicffi 1.2.1.0)
pip list 2>/dev/null | grep -iE '^(brotli|brotlicffi) '

# System library (distro-built Node.js links it): check the package, not the runtime
dpkg -l | grep -i brotli ; rpm -qa | grep -i brotli ; apk info | grep -i brotli

# Images: does the pinned tag still point at a vulnerable build
docker run --rm --entrypoint node IMAGE:TAG --version

Node.js cherry-picked the fix without bumping the bundled version string, so the release number is the check. A Dockerfile that pins 24.22.0 keeps shipping the vulnerable build until that line changes, and Homebrew still shipped unfixed formulae about ten hours after release (Sysdig). Cover staging, CI, build images, serverless runtimes and developer machines, not just production.

08

Hunt Queries: CrowdStrike CQL, SentinelOne PowerQuery and Defender XDR KQL

Show all queries in
Pick your tenant's cloud first: every "Open in Falcon" button below uses this selection.

Nine queries. Q1, Q2 and Q5 are the primary detections; Q9 is the baseline to run first and Q7 is the inventory. Every card declares its rating and tuning. Lookback is set in the console time picker, not in the query.

Q1 · Node.js, Bun, .NET or Envoy process spawning a shell, downloader or interpreter
CONF MEDFP MEDCOST LOW

Looks for: the most common post-exploitation shape for a runtime-hosted RCE: the application runtime becomes the parent of a shell, a download utility or a scripting interpreter. This is the broad net. Production web tiers should show close to zero of these, so a hit on a web-facing host is a lead. FP: npm run and yarn scripts, build and CI hosts, process managers and health-check wrappers all legitimately launch a shell from node. Run Q9 first to baseline which parent-child pairs are normal for each host group.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A child started by a runtime process the sensor did not observe starting is not captured.
  • PowerQuery assumes: On a Process Creation event src.process.name is the creator and tgt.process.name the created process.
  • PowerQuery assumes: src.process.user is the creator's account; the CQL reports the created process's account.
  • PowerQuery assumes: in:anycase and matches are case-insensitive, which reproduces the CQL /i intent.
  • KQL – Defender XDR: A child started by a runtime process the sensor did not observe starting is not captured.
  • KQL – Defender XDR assumes: FileName and InitiatingProcessFileName hold bare image names on both Linux and Windows sensors.
// HUNT: Application runtime (Node.js, Bun, .NET, Envoy) spawning a shell, downloader or interpreter
// MITRE: T1190, T1059.004, T1059.003, T1105 | CONF: medium  FP: medium  COST: low
// REQUIRES: ProcessRollup2 with parent lineage on Linux and Windows
// FALSE POSITIVES: npm and yarn scripts, build and CI hosts, process managers, health-check wrappers
// TUNING: exclude build and CI hosts by ComputerName and developer workstations by UserName
// TUNING: baseline production web tiers for 7 days with Q9, then allowlist exact CommandLine values
// TUNING: rather than binary names, because the binary names are the signal
// LOOKBACK: 14d (set in the console time picker)
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| ParentBaseFileName=/^(node|nodejs|node\.exe|bun|bun\.exe|dotnet|dotnet\.exe|envoy)$/i
| FileName=/^(sh|bash|dash|ash|zsh|ksh|busybox|curl|wget|nc|ncat|socat|python[0-9.]*|perl|ruby|cmd\.exe|powershell\.exe|pwsh\.exe)$/i
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, aid])
| sort(@timestamp, order=desc, limit=200)
Q2 · Runtime process image replaced in place by a shell (same PID, node then sh)
CONF MEDFP MEDCOST MED

Looks for: the exploit chain ELLIO describes ends by calling the runtime's own execvp on /bin/sh. An exec replaces the process image rather than forking a child, so the shell inherits the node PID and its parent is whatever launched node (a supervisor, tini, a container shim), not node. Q1 keys on a node parent and would never see this shape. This query keys on the OS process ID instead: a PID that ran node or Bun and then, within an hour, ran a shell. The order matters, because a container entrypoint that runs exec node server.js goes shell then node, which this query excludes. FP: PID reuse after a wrap on a busy host, and wrapper scripts that exec a shell after node exits. Validate in your tenant: confirm the sensor emits a new process event for an in-place exec on the same RawProcessId by running a harmless test app that calls exec on a shell; this behaviour is inferred from how exec works and is not stated in any source.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: approximate-analyticProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: The CQL's measured gap below one hour and its runtime-before-shell ordering are not expressed: a pair that straddles a clock-hour boundary is missed and PID reuse inside the hour can pair unrelated processes.
  • PowerQuery assumes: SentinelOne reports an in-place exec as a second Process Creation event carrying the same tgt.process.pid; this is unverified on a tenant.
  • PowerQuery assumes: timebucket('1h') buckets on the event's own time key.
  • PowerQuery assumes: matches is case-insensitive, which reproduces the CQL /i intent.
  • KQL – Defender XDR: The runtime-before-shell ordering is not checked.
  • KQL – Defender XDR assumes: An in-place exec is recorded as a second DeviceProcessEvents row with the same ProcessId; this is unverified on a tenant.
  • KQL – Defender XDR assumes: PID reuse inside the 60 minute span can pair unrelated processes.
// HUNT: Runtime PID re-executed as a shell (in-place exec after Node.js or Bun)
// MITRE: T1190, T1059.004 | CONF: medium  FP: medium  COST: medium
// REQUIRES: ProcessRollup2 on Linux with RawProcessId populated
// FALSE POSITIVES: PID reuse after wrap on busy hosts, wrapper scripts that exec a shell after node exits
// TUNING: shorten the gap threshold from one hour to 5 minutes on hosts with high process churn
// TUNING: exclude ComputerName values of build agents; validate with a benign in-place exec test first
// LOOKBACK: 7d (set in the console time picker)
#event_simpleName=/^ProcessRollup2$/
| FileName=/^(node|nodejs|bun|sh|bash|dash|ash|zsh|busybox)$/i
| runtimeTs := if(FileName=/^(node|nodejs|bun)$/i, then=@timestamp, else=0)
| shellTs := if(FileName=/^(sh|bash|dash|ash|zsh|busybox)$/i, then=@timestamp, else=99999999999999)
| groupBy([aid, ComputerName, RawProcessId], function=[max(runtimeTs, as=lastRuntime), min(shellTs, as=firstShell), collect([UserName, ImageFileName, ParentBaseFileName, CommandLine])], limit=max)
| gap := firstShell - lastRuntime
| gap>0
| gap<3600000
| table([ComputerName, aid, RawProcessId, UserName, ImageFileName, ParentBaseFileName, CommandLine, gap])
| sort(gap, order=asc, limit=200)
Q3 · Node.js or Bun restart loop on one host (crash and respawn proxy)
CONF MEDFP MEDCOST LOW

Looks for: ELLIO reports that most exploitation attempts in its lab crashed the worker instead of returning a shell, and that a supervisor restart hands the attacker a fresh memory layout for the next try. Imperva also needed several attempts. A burst of identical node or Bun starts on one host is therefore a plausible footprint of an exploitation run, whether or not any attempt lands. The sensor does not give you a crash event here, so this counts restarts of the same command line instead. FP: autoscaling, nodemon and watch-mode development servers, cluster managers that start many identical workers at once, and rolling deploys all produce repeated starts.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: The span in minutes is not computed; read it from first_seen and last_seen.
  • PowerQuery assumes: first_seen and last_seen replace the CQL's computed span in minutes; the analyst compares them.
  • PowerQuery assumes: in:anycase is case-insensitive, which reproduces the CQL /i intent.
  • KQL – Defender XDR: A start the sensor did not observe is not counted.
  • KQL – Defender XDR assumes: FolderPath holds the full image path of the created process.
// HUNT: Repeated starts of the same Node.js or Bun command line on one host (crash-restart loop)
// MITRE: T1190, T1499.004 | CONF: medium  FP: medium  COST: low
// REQUIRES: ProcessRollup2 for node and bun on Linux and Windows
// FALSE POSITIVES: watch-mode dev servers, cluster workers started together, rolling deploys, autoscaling
// TUNING: exclude developer hosts by ComputerName; for cluster managers, require the restarts to be
// TUNING: spread over time (spanMin above 2) rather than started within the same few seconds
// LOOKBACK: 1h (set in the console time picker) and re-run on a schedule
#event_simpleName=/^ProcessRollup2$/
| FileName=/^(node|nodejs|node\.exe|bun|bun\.exe)$/i
| groupBy([aid, ComputerName, ImageFileName, CommandLine, ParentBaseFileName], function=[count(as=starts), min(@timestamp, as=firstStart), max(@timestamp, as=lastStart), collect([UserName])], limit=max)
| starts>=5
| spanMin := (lastStart - firstStart) / 60000
| table([ComputerName, aid, ImageFileName, CommandLine, ParentBaseFileName, UserName, starts, spanMin])
| sort(starts, order=desc, limit=100)
Q4 · Node.js or Bun connecting to a public address on a non-web port
CONF MEDFP MEDCOST MED

Looks for: new outbound connections from the runtime to somewhere an application would not normally talk. A reverse shell, a staged download or a callback from an exploited node process usually leaves on an arbitrary port, while the application's own API traffic is on 80 and 443. Private ranges are excluded with cidr(). FP: mail submission on 587 and 465, SaaS databases on custom ports, webhook targets on 8080 and 8443, and package registries on internal mirrors. Reverse shells on 443 are not caught here; Q5 covers the command-line shapes.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A connection by a process whose name the sensor reports differently from the CQL's process record is not matched.
  • PowerQuery assumes: On an IP Connect event the acting process is src.process.*.
  • PowerQuery assumes: dst.port.number is numeric on this tenant.
  • PowerQuery assumes: An IPv6 destination is not excluded by the private-range tests, so a public IPv6 connection also appears; the CQL covers IPv4 only.
  • KQL – Defender XDR: A failed or blocked connection attempt is not captured.
  • KQL – Defender XDR assumes: ConnectionSuccess is the action type recorded for completed outbound connections.
  • KQL – Defender XDR assumes: ipv4_is_in_range returns null for an IPv6 address, so IPv6 destinations are excluded, matching the IPv4-only CQL.
// HUNT: Node.js or Bun outbound to a public address on a non-web port
// MITRE: T1071, T1095, T1571, T1105 | CONF: medium  FP: medium  COST: medium
// REQUIRES: NetworkConnectIP4 and ProcessRollup2 for the same process
// FALSE POSITIVES: SMTP submission, SaaS databases on custom ports, webhook targets, internal registries
// TUNING: allowlist the RemoteAddressIP4 and RemotePort pairs your applications use, and exclude
// TUNING: 587, 465, 8080 and 8443 only on the host groups that legitimately use them
// LOOKBACK: 7d (set in the console time picker)
#event_simpleName=NetworkConnectIP4
| RemotePort!=443
| RemotePort!=80
| RemotePort!=53
| !cidr(RemoteAddressIP4, subnet="10.0.0.0/8")
| !cidr(RemoteAddressIP4, subnet="172.16.0.0/12")
| !cidr(RemoteAddressIP4, subnet="192.168.0.0/16")
| !cidr(RemoteAddressIP4, subnet="127.0.0.0/8")
| !cidr(RemoteAddressIP4, subnet="169.254.0.0/16")
| rename(field=ContextProcessId_decimal, as=TargetProcessId_decimal)
| join(query={#event_simpleName=ProcessRollup2 | FileName=/^(node|nodejs|node\.exe|bun|bun\.exe)$/i}, field=[aid, TargetProcessId_decimal], include=[FileName, CommandLine, ParentBaseFileName, UserName])
| table([@timestamp, ComputerName, UserName, FileName, CommandLine, RemoteAddressIP4, RemotePort, aid])
| sort(@timestamp, order=desc, limit=200)
Q5 · Reverse-shell and shell-over-socket command lines
CONF HIGHFP LOWCOST LOW

Looks for: command-line shapes that connect a shell to a network socket: the bash /dev/tcp redirect, nc with -e, a named-pipe relay, socat with an exec address, and a Python one-liner that imports socket and starts a shell. Not limited to a node parent, because the in-place exec in Q2 removes node from the lineage. The pattern is generic and keeps its value for every other RCE on the same host. FP: security testing and authorised red-team activity.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A reverse shell started from a process the sensor did not report as a creation event is not captured.
  • PowerQuery assumes: The pattern is evaluated by the SentinelOne regex engine, which is assumed to read these constructs as the CQL engine does.
  • KQL – Defender XDR: A reverse shell started by a process the sensor did not report is not captured.
  • KQL – Defender XDR assumes: The pattern is evaluated by the Kusto regex engine, which is assumed to read these constructs as the CQL engine does.
// HUNT: Reverse-shell and shell-over-socket command lines (post-exploitation, any parent)
// MITRE: T1059.004, T1095, T1571 | CONF: high  FP: low  COST: low
// REQUIRES: ProcessRollup2 with CommandLine
// FALSE POSITIVES: authorised red-team and penetration-test activity
// LOOKBACK: 14d (set in the console time picker)
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| CommandLine=/dev.tcp.[0-9]|mkfifo.*(nc|ncat|telnet)|(^|\s)(nc|ncat)\s.*\s-e\s|socat.*exec:|bash\s+-i\s*>&|python[0-9.]*\s.*socket.*(pty|sh|bash)/i
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, aid])
| sort(@timestamp, order=desc, limit=200)
Q6 · Reconnaissance and secret access from a shell on a runtime host
CONF MEDFP MEDCOST MED

Looks for: what an operator types once the shell answers: identity and system discovery, environment dumps, the cloud metadata address, and common credential file paths. ELLIO notes the process heap holds session cookies, bearer tokens and key material, and Sysdig lists theft of secrets and cloud tokens from the container as a typical next step. The query is limited to hosts that ran node or Bun in the lookback and to shells and runtimes as the parent. FP: interactive administrators run id, uname and env constantly on the same hosts.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: approximate-analyticProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: The restriction to hosts where node or bun started is not applied, so hosts without a runtime also appear and the result is noisier than the CQL tab.
  • PowerQuery assumes: On a Process Creation event src.process.name is the creator and tgt.process.name the created process.
  • PowerQuery assumes: matches is case-insensitive, which reproduces the CQL /i intent.
  • KQL – Defender XDR: The restriction to hosts where node or bun started is not applied, so hosts without a runtime also appear and the result is noisier than the CQL tab.
  • KQL – Defender XDR assumes: FileName and InitiatingProcessFileName hold bare image names on both Linux and Windows sensors.
// HUNT: Discovery and secret-access commands on hosts running Node.js or Bun
// MITRE: T1033, T1082, T1083, T1552.001, T1552.005 | CONF: medium  FP: medium  COST: medium
// REQUIRES: ProcessRollup2 with CommandLine and parent
// FALSE POSITIVES: interactive administrators, configuration management, container health checks
// TUNING: exclude admin jump-host accounts by UserName and your monitoring agent by CommandLine
// TUNING: then rank by rarity: a command seen on one host once is worth more than one seen daily
// LOOKBACK: 7d (set in the console time picker)
#event_simpleName=/^ProcessRollup2$/
| ParentBaseFileName=/^(node|nodejs|bun|sh|bash|dash|ash|busybox)$/i
| FileName=/^(id|whoami|uname|hostname|env|printenv|cat|ifconfig|ip|ps|ss|netstat|curl|wget)$/i
| CommandLine=/(^|\s)(id|whoami|uname|hostname|printenv|env|ifconfig)(\s|$)|environ|169\.254\.169\.254|passwd|shadow|\.aws|credentials|\.kube|\.npmrc|\.ssh/i
| join(query={#event_simpleName=ProcessRollup2 | FileName=/^(node|nodejs|bun)$/i | groupBy([aid], function=count(as=runtimeStarts))}, field=aid, include=[runtimeStarts])
| groupBy([aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine], function=[count(as=runs), min(@timestamp, as=firstSeen), max(@timestamp, as=lastSeen)], limit=max)
| sort(runs, order=asc, limit=200)
Q7 · Runtime and decoder host inventory by binary hash
CONF MEDFP LOWCOST MED

Looks for: the vulnerable-version inventory angle, and an honest statement of its limit. Falcon process telemetry carries the binary hash and path but not an ELF file version, and the Node.js project cherry-picked the fix without changing the bundled version string, so process.versions.brotli reports 1.2.0 on both patched and vulnerable builds (ELLIO). This query lists every distinct node, Bun, .NET and Envoy binary and how many hosts run it. Map each hash to a version offline against the official release hashes, or run the version commands in Section 7 N4 on the hosts. Container images appear under their in-container path.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A runtime that did not start inside the window is not listed.
  • PowerQuery assumes: tgt.process.image.sha256 is populated for Process Creation events on this tenant.
  • PowerQuery assumes: estimate_distinct is an approximate count of hosts.
  • PowerQuery assumes: in:anycase is case-insensitive, which reproduces the CQL /i intent.
  • KQL – Defender XDR: A runtime that did not start inside the window is not listed.
  • KQL – Defender XDR assumes: FolderPath holds the full image path of the created process.
  • KQL – Defender XDR assumes: SHA256 may be empty; SHA1 is the safer pivot.
// HUNT: Inventory of Node.js, Bun, .NET and Envoy binaries by hash, path and host count
// MITRE: T1190 | CONF: medium  FP: low  COST: medium
// REQUIRES: ProcessRollup2 with SHA256HashData for runtime binaries that have executed in the window
// FALSE POSITIVES: not a detection; every running runtime is listed by design
// LOOKBACK: 30d (set in the console time picker); only binaries that ran in the window appear
#event_simpleName=/^ProcessRollup2$/
| FileName=/^(node|nodejs|node\.exe|bun|bun\.exe|dotnet|dotnet\.exe|envoy|envoy\.exe)$/i
| groupBy([FileName, SHA256HashData, ImageFileName], function=[count(aid, distinct=true, as=hosts), count(as=starts), min(@timestamp, as=firstSeen), max(@timestamp, as=lastSeen)], limit=max)
| sort(hosts, order=desc, limit=500)
Q8 · Connections to the published exploitation source address
CONF LOWFP LOWCOST LOW

Looks for: a sweep for the single source address ELLIO attributes to the 2026-10-26 exploit attempt. That address connected to ELLIO's sensors, so the useful telemetry on your side is an inbound connection, which this query cannot see because no inbound-accept event is confirmed in the reference set. It will only match if one of your hosts connects out to that address. Treat it as a cheap supplement to the Falcon IOC row and to the web and proxy log hunt in Section 7 N1, which is where the real signal is. Single-sourced and perishable.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: An inbound connection from the address is not distinguished from an outbound one.
  • PowerQuery assumes: dst.ip.address holds the remote address of an outbound IP Connect event.
  • KQL – Defender XDR: Connection direction is not filtered, so an inbound connection from the address also matches.
  • KQL – Defender XDR assumes: RemoteIP holds the remote address for the connection rows.
// HUNT: Outbound connection to the source address in the 2026-10-26 exploit attempt (ELLIO)
// MITRE: T1071 | CONF: low  FP: low  COST: low
// REQUIRES: NetworkConnectIP4
// FALSE POSITIVES: none expected; this is an atomic indicator sweep
// LOOKBACK: 30d (set in the console time picker)
#event_simpleName=NetworkConnectIP4
| RemoteAddressIP4="94.154.43.7"
| table([@timestamp, ComputerName, aid, RemoteAddressIP4, RemotePort, ContextProcessId_decimal])
| sort(@timestamp, order=desc, limit=200)
Q9 · Baseline: which child processes do your runtimes normally launch
CONF MEDFP LOWCOST LOW

Looks for: not a detection. It lists every parent and child pair for node, Bun, .NET and Envoy with the number of hosts that show it, rarest first. Run it for seven to thirty days before enabling Q1, Q2 and Q6 as alerts, and build the exclusion lists from this output instead of guessing. A pair seen on one host is a lead; a pair seen on three hundred is your deploy tooling.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: Children of a runtime process the sensor did not observe starting are not listed.
  • PowerQuery assumes: On a Process Creation event src.process.name is the creator and tgt.process.name the created process.
  • PowerQuery assumes: estimate_distinct is an approximate count of hosts.
  • PowerQuery assumes: in:anycase is case-insensitive, which reproduces the CQL /i intent.
  • KQL – Defender XDR: Children of a runtime process the sensor did not observe starting are not listed.
  • KQL – Defender XDR assumes: FileName and InitiatingProcessFileName hold bare image names on both Linux and Windows sensors.
  • KQL – Defender XDR assumes: dcount is an approximate count of devices.
// HUNT: Baseline of child processes launched by Node.js, Bun, .NET and Envoy (rarest first)
// MITRE: T1059.004 | CONF: medium  FP: low  COST: low
// REQUIRES: ProcessRollup2 with parent lineage
// FALSE POSITIVES: not a detection; benign tooling is listed by design
// LOOKBACK: 30d (set in the console time picker)
#event_simpleName=/^ProcessRollup2$/
| ParentBaseFileName=/^(node|nodejs|node\.exe|bun|bun\.exe|dotnet|dotnet\.exe|envoy)$/i
| groupBy([ParentBaseFileName, FileName], function=[count(aid, distinct=true, as=hosts), count(as=execs)], limit=max)
| sort(hosts, order=asc, limit=300)
09

Detection Rules by Platform

Custom IOA candidates

CrowdStrike: Custom IOA

IOA namePatternExclusionsActionFrom
RUNTIME - Application Runtime Spawns Shell or DownloaderProcess creation where the parent image is node, bun, dotnet or envoy and the child image is a shell, curl, wget, nc, socat or a scripting interpreter. Promote Q1.Build and CI host groups; developer workstations; documented process-manager wrappers (derive from the Q9 baseline)Detect (High) on production web host groups; Monitor for 7 days firstQ1
RUNTIME - Reverse Shell Command LineCommand line matches a /dev/tcp redirect, nc -e, named-pipe relay, socat exec address or Python socket-to-shell one-liner. Promote Q5.Authorised red-team host groupsDetect (Critical); low volume, safe to promote quicklyQ5
RUNTIME - Node Process Re-Executed as ShellA PID that ran node or bun later runs a shell within an hour. Promote Q2 only after the in-place exec test in the query card passes in your tenant, because an IOA pattern cannot express "same PID" and this is a scheduled search.Build agents; PID-churn-heavy hostsScheduled search, Monitor firstQ2

Not IOA candidates: Q3 (restart loops) and Q4 (non-web outbound) are volume hunts that need per-application baselines; Q6 needs admin-account tuning; Q7 and Q9 are inventory and baseline; Q8 is a perishable single-source indicator sweep. No IOA in this pack uses a Prevent action: the behaviours are real but application runtimes spawning children is legitimate in enough places that an automatic kill would break deploys, and no corroborated source describes a fixed prevention rule.

Scheduled Search packaging (for the promoted IOAs)

FieldValue
Scheduled Search nameDETECT - Application Runtime Spawns Shell (Node2Shell / CVE-2026-111497)
DescriptionFires when node, Bun, .NET or Envoy becomes the parent of a shell, downloader or interpreter. This is the expected post-exploitation shape of a successful Brotli decoder exploit and of runtime RCE in general, so it keeps its value after this CVE is patched.
ScheduleEvery 15 minutes, 30-minute lookback with overlap
Alert subject[HIGH] Detection: Node2Shell - application runtime spawned a shell

Alert email body (template)

WHAT FIRED
An application runtime (node, Bun, .NET or Envoy) started a shell, downloader or
scripting interpreter on a host in the runtime server group.

  Host          : {ComputerName}
  Account       : {UserName}
  Parent        : {ParentBaseFileName}
  Child         : {FileName}
  Command line  : {CommandLine}

WHY IT MATTERS
CVE-2026-111497 (Node2Shell) can give an attacker a shell as the service account
through a crafted Brotli stream. This alert is the post-exploitation footprint.
It can also be build tooling, so check the host role first.

IMMEDIATE ACTIONS
  1. Confirm the host role. A build agent or developer machine is likely benign.
  2. Check the runtime version (Section 7 N4). Patched or not?
  3. Pull the process tree and the 10 minutes of network connections around the event.
  4. If it is a production web tier and the command line is not a documented script,
     isolate the host (Section 14) and do not restart the service before capturing state.

ESCALATE TO IR WHEN
  - the shell is interactive or the command line is a download or reverse-shell shape
  - the same host shows a node restart burst (Q3) before the shell
  - the runtime is an unpatched 22.x, 24.x or 26.x build or an end-of-life line

Triage checklist (L1)

  1. Identify the host role and owner. Build, CI and developer hosts are expected sources.
  2. Read the full command line. Is it a documented npm script or deploy hook?
  3. Check the runtime version and whether the application decodes Brotli (Section 7 N2, N4).
  4. Look for restart bursts of the same command line in the previous hour (Q3).
  5. Review outbound connections from the same host (Q4) and reverse-shell shapes (Q5).
  6. Look for discovery or secret-access commands afterwards (Q6).
  7. Check web and proxy logs for a br-encoded request in the preceding minutes (Section 7 N1).
  8. If unexplained, escalate and isolate; record the Falcon aid and the process start time.

Pivot queries

// Host-scoped: everything a runtime did on one host around the alert
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| aid=REPLACE_WITH_AID
| ParentBaseFileName=/^(node|nodejs|bun|sh|bash|dash)$/i
| table([@timestamp, ParentBaseFileName, FileName, CommandLine, UserName])
| sort(@timestamp, order=asc, limit=500)

// Environment-wide: same child command line on other hosts
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| CommandLine="REPLACE_WITH_COMMAND_LINE"
| groupBy([ComputerName, aid], function=count())

SentinelOne: custom detection rules

RuleSource queryRun asNotes
RUNTIME - Application Runtime Spawns Shell or DownloaderQ1, PowerQuery tabScheduled custom detection ruleBind entities so the alert names the host instead of "Unknown Device". This pack's own text mentions a baseline or pilot; follow it as written
RUNTIME - Reverse Shell Command LineQ5, PowerQuery tabScheduled custom detection ruleBind entities so the alert names the host instead of "Unknown Device". This pack's own text mentions a baseline or pilot; follow it as written
RUNTIME - Node Process Re-Executed as ShellQ2, PowerQuery tabHunt only until the gap is closedQ2 is only an approximation here. Hunts the same behaviour (one PID seen as a runtime and then as a shell) by a different analytic: PowerQuery has no arithmetic or conditional timestamp aggregation here, so the pair is matched on the same PID inside one clock hour instead of a measured gap, and the order of the two events is not.... This pack's own text mentions a baseline or pilot; follow it as written

Create the rule with POST /web/api/v2.1/cloud-detection/rules, queryType: "scheduled" and queryLang: "2.0", with the query in data.scheduledParams.query. Use 2.0 operator spellings: a rule written with the 1.0 token ContainsCIS was accepted and never fired on a tenant. The API does not warn, so lint the body before deploying.

Microsoft Defender: custom detection rules

RuleSource queryRun asNotes
RUNTIME - Application Runtime Spawns Shell or DownloaderQ1, KQL – Defender XDR tabCustom detection rule, alert only, every hourThe query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself. This pack's own text mentions a baseline or pilot; follow it as written
RUNTIME - Reverse Shell Command LineQ5, KQL – Defender XDR tabCustom detection rule, alert only, every hourThe query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself. This pack's own text mentions a baseline or pilot; follow it as written
RUNTIME - Node Process Re-Executed as ShellQ2, KQL – Defender XDR tabHunt only until the gap is closedQ2 is only an approximation here. Hunts the same behaviour (one PID seen as a runtime and then as a shell) by a different analytic: rows are grouped by ProcessId per device, both name sets must be present, and the span between first and last row must be under 60 minutes. The order of the two events is not checked.. This pack's own text mentions a baseline or pilot; follow it as written

Q3, Q4, Q6, Q7, Q8 and Q9 stay a hunt in this pack, not a rule. A rule can generate only 150 alerts per run. Continuous (NRT) frequency needs a single table and no comment lines, so delete the generated comment header from the query first.

10

Machine-Readable IOC Appendix

Falcon IOC Management CSVbulk import, detect only
type,value,action,severity,expiration,description,tags
sha256,7fcbdaaecd4419e2930927844f3b772668204737a4e7602a2290c991c93c5c4b,detect,medium,2027-01-28,Node2Shell exploit request body observed by ELLIO 2026-10-26 (single source; network payload hash),campaign:Node2Shell
ipv4,94.154.43.7,detect,medium,2027-01-28,Node2Shell exploit attempt source observed by ELLIO 2026-10-26 (single source; shared mass-exploitation infrastructure),campaign:Node2Shell
# No prevent rows. Both indicators are single-sourced to ELLIO. No domains, URLs or file hashes
# of dropped payloads have been published, so none are listed; do not invent placeholders as detect rows.
Microsoft Defender indicators CSVSettings, Endpoints, Indicators, Import: audit only
indicatorType,indicatorValue,action,title,description,expirationTime,severity,recommendedActions,rbacGroups,category,mitretechniques,GenerateAlert
FileSha256,7fcbdaaecd4419e2930927844f3b772668204737a4e7602a2290c991c93c5c4b,Audit,Node2Shell exploit request body observed by ELLIO 2026-10-26,Node2Shell exploit request body observed by ELLIO 2026-10-26 (single source; network payload hash),2027-01-28T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True
IpAddress,94.154.43.7,Audit,Node2Shell exploit attempt source observed by ELLIO 2026-10-,Node2Shell exploit attempt source observed by ELLIO 2026-10-26 (single source; shared mass-exploitation infrastructure),2027-01-28T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True

Column names and allowed values are from Microsoft's documented parameter table. Check each expirationTime before importing: an indicator whose date has passed is already expired. Download the sample CSV from the Import page to confirm the column order before a bulk import. Each file takes at most 500 indicators. Learn describes the import per entity type, so if a mixed file is rejected, split it by indicatorType. An imported indicator does nothing unless the matching Advanced features are on (for example Custom network indicators and Allow or block file).

SentinelOne IOC importnot included

This pack does not ship a SentinelOne import block: the import format could not be verified against SentinelOne's own documentation. Export a sample from your console and the block can be added with confidence.

Behavioural signaturesdurable: survives indicator rotation
PROCESS LINEAGE (endpoint)
  node | nodejs | node.exe | bun | bun.exe | dotnet | envoy
    -> sh | bash | dash | ash | zsh | busybox | curl | wget | nc | ncat | socat | python | perl | ruby
  same PID: node or bun, then within an hour a shell (in-place exec; shell parent is the supervisor)
  burst of identical node or bun starts on one host (crash and respawn)
  node or bun connecting to a public address on a non-web port
  reverse-shell command lines: /dev/tcp redirect, nc -e, mkfifo relay, socat exec, python socket+pty

WEB / PROXY / WAF (outside endpoint telemetry)
  POST, PUT or PATCH with Content-Encoding: br to a public route
  very large (multi-megabyte) Brotli request bodies; exploit body was about 23 MB
  User-Agent: Mozilla/5.0 (compatible; node2shell-check/1.0)
  checker pattern: GET / then 20 POST bodies with Content-Encoding: br in two bursts about two minutes apart
  source address 94.154.43.7
  patched server answering 400 with ERR__ERROR_FORMAT_BLOCK_SWITCH (decoder error -17)

HOST
  node segfault or general protection fault in kernel log after 2026-10-21
  new core files under /var/lib/systemd/coredump from node
  container restart counts rising on Node.js workloads
Named tooling and attributioncontext
THREAT      Node2Shell (nicknames: Broccoli2Shell, Brotli2Shell, HTTP2Shell). One CVE.
CVE         CVE-2026-111497   Google Brotli decoder out-of-bounds write (CWE-787) from an
            unchecked return value (CWE-252). CVSS 4.0 9.2 (Google). Microsoft rates .NET 8.1 (CVSS 3.1).
ACTOR       none named in any source
KEV         added 2026-10-27, due 2026-11-10, known ransomware use: Unknown
DISCLOSED   2026-10-21 (Node.js fixes). Upstream fix commit 0ed4268a, committed 2026-02-27.
NEW ERROR   BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17)
TOOLING     node2shell-check (public exposure checker, first observed 2026-06-13, AS44620)
CREDITS     reported by OpenAI Codex Security (Khai Tran, Sylvie Mayer); fixed by Matteo Collina
            and Antoine du Hamel; arm64 exploitation by Imperva Red Team; x86-64 by ELLIO

RELIABILITY
  Imperva: official Node.js 24.22.0 Linux arm64 image, several attempts; other architectures,
           22.x and 26.x not tested.
  ELLIO:   x86-64, four setups, ASLR on, shell around attempt 5 with ASLR on; most Express
           attempts crashed; offset is per Node.js build (24.22.0 value gave nothing in
           10,000 attempts on 22.23.3 and 26.12.0).
  Others:  crashes only on standard builds; blind RCE with ASLR on did not work for one team.
  ITW:     the 2026-10-26 exploit request scored 0 of 50 shells in ELLIO's own tests.
Patch and exposure auditfixed versions and checklist
FIXED VERSIONS
  Node.js     22.23.4 | 24.22.1 | 26.12.1       (20.x, 23.x, 25.x: no fix, replace)
  Brotli      1.0.10 | 1.1.1 | 1.2.1
  Bun         1.4.4
  .NET        8.0.33 | 9.0.22 | 10.0.14
  PyPI        brotli 1.2.1 | brotlicffi 1.2.1.0
  Envoy       1.40.0 | 1.39.3 | 1.38.6 | 1.37.8 | 1.36.12
  Istio       1.31.2 | 1.30.6 | 1.29.9

AUDIT CHECKLIST
  [ ] node --version on every host, container and CI image; pinned Dockerfile tags updated
  [ ] process.versions.brotli is NOT a valid check (reads 1.2.0 on patched and vulnerable)
  [ ] bun build --compile executables rebuilt, not just Bun upgraded
  [ ] distro Node.js: check the system brotli package, not the runtime version
  [ ] Express 5 / body-parser 2.x, Koa, Fastify compress: which routes decode br
  [ ] server-side fetch() on user-influenced URLs inventoried
  [ ] Content-Encoding: br rejected at the edge where no client needs it
  [ ] services run unprivileged, secrets out of process environment, core dumps restricted
11

Detection Validation Gates

GateCheckPass condition
1 · Telemetry readyConfirm process creation with command line, image path and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) reaches your platform from every Node.js, Bun, .NET and Envoy host group. Falcon check: #event_simpleName=/^ProcessRollup2$/ | FileName=/^(node|bun)$/i | groupBy([ComputerName], limit=500)Every runtime host you believe you own appears. A missing host, and any container host with no sensor, is a blind spot to record, not a clean result.
2 · Parent and PID populatedConfirm the parent image and the OS process ID are populated on Linux and container hostsQ1, Q2 and Q9 key on the parent; Q2 keys on the PID. If either is blank on containerised hosts, those queries are silently useless there.
3 · In-place exec behaviourOn an isolated non-production host, run a harmless test application that calls exec on a shell, then run Q2Q2 returns the PID. If it does not, record Q2 as unvalidated for that sensor version and rely on Q1, Q5 and Q6.
4 · Benign baselineRun Q9 over 30 days, then Q1 and Q3 over 14 days before alertingYou can name every recurring parent-child pair and every restart pattern. The IOA exclusion lists come from this run.
5 · Web log pathConfirm the proxy or web-server log format records Content-Encoding and request length, and that the logs reach a searchable platformSection 7 N1 returns data. Without it the exploit request and the checker sweep are permanently invisible.
6 · Positive testOn a non-production host, an authorised operator runs a benign shell command through a test node script and confirms Q1 returns itQ1 returns the event within expected latency. This validates the detection path without using any exploit.
7 · PromotionMove Q1 and Q5 to Scheduled Searches in Monitor mode for 7 days, then DetectFewer than roughly 5 unexplained hits per week per 100 runtime hosts. Above that, tune before promoting.
12

Hardening: Tiered

Immediate: this week, low compatibility risk

H1 · Patch to a fixed release. MITRE M1051 Update Software. Node.js 22.23.4, 24.22.1 or 26.12.1; Bun 1.4.4; Brotli 1.0.10, 1.1.1 or 1.2.1; .NET 8.0.33, 9.0.22 or 10.0.14; Python brotli 1.2.1 and brotlicffi 1.2.1.0; Envoy and Istio per Section 10. Rebuild images from a patched base, update pinned Dockerfile tags and lockfiles, restart processes, and rebuild bun build --compile executables. Authority: Node.js October 2026 security releases; CISA KEV BOD 26-04 due date 2026-11-10 for federal agencies. Verify with node --version, never process.versions.brotli.

H2 · Reject Brotli request bodies at the edge. MITRE M1042 Disable or Remove Feature or Program, M1031 Network Intrusion Prevention. Browsers do not send compressed request bodies, so dropping Content-Encoding: br on inbound requests costs most services nothing. Playbook P2. It does not cover Brotli responses your services fetch. Authority: ELLIO and Sysdig mitigation lists; Sysdig attributes the proxy rule to Brandefense as its own suggestion, not vendor guidance, and says to test it before relying on it. best-practice, no formal benchmark

H3 · Retire end-of-life Node.js lines. MITRE M1051. Node.js 20.x, 23.x and 25.x get no fix. Move to 22.x or 24.x. Authority: Node.js release schedule (Sysdig table of end-of-life dates).

Near term: 1 to 4 weeks, pilot on a ring first

H4 · Disable request decompression where unused. MITRE M1042. In Express use express.json({ inflate: false }); it rejects compressed bodies with 415 and also rejects gzip, so test clients first. Remove @fastify/compress request decompression and direct zlib.brotli* calls on untrusted data. Playbook P3. Authority: Sysdig mitigation guidance, Node.js and Express documentation.

H5 · Run Node.js as an unprivileged user in a hardened container. MITRE M1026 Privileged Account Management, M1048 Application Isolation and Sandboxing, M1038 Execution Prevention. Non-root user, read-only root filesystem, no added capabilities, no privilege escalation, default seccomp. Code execution lands with the process permissions, and ELLIO's lab shell ran as root in the official image. Playbook P4. Authority: CIS Docker Benchmark (container user, read-only root filesystem, no-new-privileges), Kubernetes Pod Security Standards "restricted" profile, NIST SP 800-190.

H6 · Restrict core dumps and who reads them. MITRE M1022 Restrict File and Directory Permissions. A crash copies live memory including session cookies, tokens and key material (ELLIO). Playbook P5. Authority: CIS Linux benchmarks (core dump restriction). Trade-off: you lose crash diagnostics on those hosts.

H7 · Keep secrets out of the process environment and heap lifetime. MITRE M1026, M1027 Password Policies. Use a secrets manager with short-lived credentials so a stolen environment expires quickly. Authority: Sysdig contain-the-blast-radius guidance; cloud-provider least-privilege guides. best-practice, no formal benchmark

Strategic: 1 to 3 months

H8 · Software bill of materials for every runtime and bundled native library. MITRE M1051, M1016 Vulnerability Scanning. A Node.js upgrade fixes only the copy Node.js bundles; DuckDB, .NET, PHP extensions, distro libbrotli and compiled Bun executables each carry their own (ELLIO). Without an SBOM you cannot answer "where is Brotli" in an hour. Authority: NIST SP 800-218 SSDF; CISA BOD 26-04 as cited in the KEV entry.

H9 · Egress control for application workloads. MITRE M1030 Network Segmentation, M1037 Filter Network Traffic. Default-deny egress with an allowlist per service removes the reverse-shell and staged-download options after exploitation, and bounds the outbound-fetch path. Playbook P4 includes the Kubernetes policy. Authority: Kubernetes NetworkPolicy, CIS Kubernetes Benchmark network-policy controls.

H10 · Treat decompression as a trust boundary in design review. MITRE M1048. Cap decoded size and input size, and put untrusted decoding in a separate low-privilege process. The next native-library bug will arrive on a similar short clock (Sysdig). best-practice, no formal benchmark

13

Deployable Playbooks

Each step lists its prerequisites, whether it needs a reboot, and how to undo it. Content is defensive configuration only.

P1 · Inventory runtimes and libraries (read-only)

Prerequisites: Shell access with read rights to the filesystem; Docker CLI if you want the image check. Run on a canary host first.
Reboot required: No
Rollback: none required: the script only reads and prints. Delete the output if it contains hashes you do not want retained.

#!/usr/bin/env bash
# READ-ONLY inventory of Node.js and Bun binaries on this host. Nothing is modified and
# no discovered binary is executed; versions are mapped from the hash offline.
set -u
echo "== binaries on PATH =="
for b in $(command -v node nodejs bun 2>/dev/null); do
  echo "$b  $("$b" --version 2>&1 | head -1)  $(sha256sum "$b" | cut -d' ' -f1)"
done
echo "== every node or bun executable on local filesystems =="
find / -xdev -type f \( -name node -o -name nodejs -o -name bun \) -perm -u+x 2>/dev/null |
  while read -r f; do echo "$f  $(sha256sum "$f" | cut -d' ' -f1)"; done
echo "== system brotli packages (distro Node.js links these) =="
dpkg -l 2>/dev/null | grep -i brotli; rpm -qa 2>/dev/null | grep -i brotli; apk info 2>/dev/null | grep -i brotli
echo "== local images that contain node (runs each image with --version; only for images you own) =="
command -v docker >/dev/null && for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  printf '%s  ' "$i"; docker run --rm --entrypoint node "$i" --version 2>/dev/null || echo "no node"
done

P2 · Reject Brotli request bodies at the proxy

Prerequisites: nginx or HAProxy in front of the Node.js tier; confirm no client of yours legitimately sends Content-Encoding: br (grep the new access_enc.log for a week in log-only mode first).
Reboot required: No (reload only)
Rollback: delete the include and the if line, then nginx -t && systemctl reload nginx; for HAProxy remove the http-request deny line and reload.

# nginx: reject Brotli-encoded request bodies (http context, e.g. /etc/nginx/conf.d/reject-br.conf)
map $http_content_encoding $reject_br_body {
    default 0;
    "~*(^|[\s,])br($|[\s,])" 1;
}
log_format enc '$remote_addr [$time_local] "$request" $status $request_length content_encoding="$http_content_encoding" ua="$http_user_agent"';

# in each public server or location block:
#   access_log /var/log/nginx/access_enc.log enc;
#   if ($reject_br_body) { return 415; }

# HAProxy equivalent (frontend section)
#   http-request deny deny_status 415 if { req.hdr(content-encoding) -m reg -i (^|[\s,])br($|[\s,]) }

# Validate and apply
#   nginx -t && systemctl reload nginx

P3 · Disable request decompression in the application

Prerequisites: Application owner sign-off; a staging run with every real client type (mobile apps, partner integrations) because this also rejects gzip bodies.
Reboot required: Application restart only
Rollback: revert the commit or redeploy the previous image; the change is one option per parser.

// Express 5: do not decompress request bodies (also rejects gzip and deflate with 415)
app.use(express.json({ inflate: false }));
app.use(express.urlencoded({ extended: false, inflate: false }));
app.use(express.text({ inflate: false }));
app.use(express.raw({ inflate: false }));

// Fastify: do not register request decompression for public routes
//   await app.register(require('@fastify/compress'), { global: false });   // review each route that opts in
// Direct use: never pass client-supplied bytes to zlib.brotliDecompress / createBrotliDecompress

P4 · Constrain the workload and its egress (Kubernetes)

Prerequisites: Cluster at 1.25 or later for Pod Security; a staging namespace; the real list of egress destinations per service (otherwise the policy breaks the application).
Reboot required: No node reboot; pods roll
Rollback: kubectl rollout undo deployment/REPLACE_WITH_APP and kubectl delete networkpolicy REPLACE_WITH_APP-egress.

# Kubernetes: constrain the Node.js workload so a shell is low-privilege and has nowhere to call
apiVersion: apps/v1
kind: Deployment
metadata: { name: REPLACE_WITH_APP }
spec:
  template:
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        seccompProfile: { type: RuntimeDefault }
      containers:
      - name: app
        image: REPLACE_WITH_PATCHED_IMAGE_DIGEST
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities: { drop: ["ALL"] }
        volumeMounts: [{ name: tmp, mountPath: /tmp }]
      volumes: [{ name: tmp, emptyDir: {} }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: REPLACE_WITH_APP-egress }
spec:
  podSelector: { matchLabels: { app: REPLACE_WITH_APP } }
  policyTypes: [Egress]
  egress:
  - to: [{ namespaceSelector: {}, podSelector: { matchLabels: { k8s-app: kube-dns } } }]
    ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]
  - to: [{ ipBlock: { cidr: REPLACE_WITH_ALLOWED_EGRESS_CIDR } }]
    ports: [{ protocol: TCP, port: 443 }]

P5 · Restrict core dumps on hosts running Node.js

Prerequisites: Root access; agree with the platform team that you can lose crash diagnostics (Option A) or accept root-only 2-day retention (Option B).
Reboot required: No
Rollback: delete the drop-in or the tmpfiles line, then systemctl daemon-reload; previously deleted cores are not recoverable, so copy any cores you need as evidence first.

# /etc/systemd/coredump.conf.d/10-no-cores.conf   (Option A: no core files at all)
[Coredump]
Storage=none
ProcessSizeMax=0

# /etc/tmpfiles.d/coredump-retention.conf         (Option B: keep cores, root only, 2 days)
d /var/lib/systemd/coredump 0700 root root 2d

# Apply
#   systemctl daemon-reload
#   sysctl fs.suid_dumpable          # expect 0
#   ls -ld /var/lib/systemd/coredump

P6 · Audit execve by the service account (hosts without an EDR sensor)

Prerequisites: auditd installed; the service account UID; disk budget for the log (a busy service can generate many events, so scope to the one UID).
Reboot required: No
Rollback: remove the rule file and run augenrules --load.

# /etc/audit/rules.d/60-node-exec.rules : log every execve by the Node.js service account
# Replace 1001 with the service account UID. A node process that execs /bin/sh logs here with its PID.
-a always,exit -F arch=b64 -S execve -F uid=1001 -k node_exec

# Apply and read
#   augenrules --load
#   ausearch -k node_exec -i | grep -E 'exe="/bin/(ba|da)?sh"'
# Hosts with a Falcon sensor already record this as process creation; use the rule where there is none.
14

Containment Runbook

PhaseActionsOwnerEvidence
TriageConfirm the host role and the runtime version (Section 7 N4). Check for a br-encoded request in the preceding minutes (N1), a restart burst (Q3) and a shell, in-place exec or reverse-shell shape (Q1, Q2, Q5). Decide if this is a build host, a failed attempt (crash only) or probable code execution.SOC L1/L2Alert, query output, runtime version, relevant log lines
IsolateNetwork-contain the host in the EDR console. On Kubernetes apply a deny-all NetworkPolicy to the pod label and cordon the node, and remove the instance from the load balancer. Do not restart or delete the process or pod yet: a restart discards the live memory and the crash cores that show what an attacker could have read. Block 94.154.43.7 at the edge as a convenience only; it is shared scanning infrastructure and blocking it does not contain a compromise.IR lead, platformContainment confirmation, timestamps, pod and node names
Isolate: by platformCrowdStrike: network-contain the host. SentinelOne: network quarantine (Disconnect from Network; confirm the label in your console version). Microsoft Defender: Isolate device (full network isolation). Keep the host powered on for memory capture on all of them.IR lead, platformContainment confirmation, timestamps, pod and node names
PreserveCopy any core files and the process environment (as root, read-only), the container filesystem diff, web and proxy logs covering the Brotli requests, and Falcon process trees for the window. Record the Node.js build hash.IR / forensicsHashes of preserved artefacts, chain-of-custody note
EradicateRebuild from a patched base image (Section 12 H1); do not patch the live container in place. Rotate every secret that was in the process environment, files, heap or reachable cloud role: session signing keys, bearer and API tokens, database and cloud credentials, TLS keys. Check for persistence (cron, systemd units, authorized_keys, new users) and for lateral movement from the host.Platform, app owner, IAMImage digest, rotation log, persistence sweep result
RecoverRedeploy the patched image with the Section 13 P2 to P5 controls, restore traffic, and watch Q1, Q2, Q3 and Q5 for 14 days. Confirm the patched host rejects the trigger with the format error (N1).Platform, SOCVersion proof, clean query runs, change record
ReviewAdd the Section 7 N2 reachability findings to the application inventory, set the SBOM task (H8), and record whether the web-log gap in the coverage map was closed.Security engineeringUpdated inventory, closed gaps
15

Detection Coverage Map

TechniqueBehaviourCQLIOACrowdStrike coverageSentinelOneDefender XDR
T1595.002Brotli exposure sweep by node2shell-check--GAP Web, proxy and WAF logs only (Section 7 N1). Outside endpoint telemetry.GAPGAP
T1190Crafted Brotli request body or fetched response reaches the decoder--GAP Outside endpoint telemetry. Web and proxy logs (N1) and application logs only, and only if the log format records the encoding and body size.GAPGAP
T1190 / T1499.004Out-of-bounds write: failed attempts crash the worker and the supervisor restarts itQ3-PARTIAL Restart count is a proxy; no crash event is used. Host crash logs and cores are Section 7 N3.Partial: same limit as CrowdStrikePartial: same limit as CrowdStrike
T1059.004Runtime spawns a shell, downloader or interpreterQ1YesPARTIAL Misses the in-place exec shape, where the shell is not a child of node.Partial: same limit as CrowdStrikePartial: same limit as CrowdStrike
T1059.004Runtime PID replaced in place by /bin/shQ2ScheduledPARTIAL Depends on the sensor emitting a new process event for an in-place exec; validate with Gate 3.Partial: Q2 approximatePartial: Q2 approximate
T1059.004 / T1095Reverse shell or shell-over-socket command lineQ5YesGOOD Parent-independent. Does not see a pure socket shell with no telltale command line.Good, with assumptionsGood, with assumptions
T1071 / T1571Runtime connecting to a public address on a non-web portQ4-PARTIAL Misses callbacks on 80 or 443.Partial: same limit as CrowdStrikePartial: same limit as CrowdStrike
T1033 / T1082 / T1552Discovery and secret access after the shellQ6-PARTIAL Noisy on hosts with interactive administrators; tune by account.Partial: Q6 approximatePartial: Q6 approximate
T1190Vulnerable runtime and library versions presentQ7, Q9-PARTIAL Hash-based only; telemetry has no ELF file version, and distro builds that link the system libbrotli cannot be judged from the runtime hash.Partial: same limit as CrowdStrikePartial: same limit as CrowdStrike
T1071Contact with the ELLIO-reported exploit source addressQ8-GAP The address connected inbound to ELLIO's sensors; no inbound-accept event is confirmed in the reference set, so only outbound contact is checkable. Use Section 7 N1.GAP: same limit as CrowdStrikeGAP: same limit as CrowdStrike
T1005Memory disclosure from a crash or partial exploit--GAP No telemetry shows what memory was read; handled by secret rotation (Section 14).GAPGAP
T1190Appliances and products embedding Node.js or Brotli--GAP Vendor advisories only (Broadcom SDDC Manager, PAN-OS hotfixes).GAPGAP

Honest read on this map. The first two steps of the chain and the memory-disclosure outcome are outside endpoint telemetry, and that is a property of the threat: a decompression bug is triggered inside the application before any process event exists. The pack therefore covers what the runtime does after a successful exploit, and treats web-log recording of Content-Encoding and request size as a prerequisite for the earliest detection. Validation gates 1 to 7 in Section 11 define when each query is trustworthy.

16

Hunt Summary Ticket

TITLE       Node2Shell - Google Brotli decoder out-of-bounds write (CVE-2026-111497)
SEVERITY    High - on CISA KEV 2026-10-27; exploit attempts observed; reliability varies by build

SCOPE       Every Node.js, Bun, .NET, Envoy and Python workload that decodes Brotli the
            attacker can influence (request bodies, fetched responses, direct zlib calls),
            internet-facing first, including containers, CI images and appliances.
            Fixed: Node.js 22.23.4 | 24.22.1 | 26.12.1, Brotli 1.0.10 | 1.1.1 | 1.2.1,
            Bun 1.4.4. Node.js 20, 23, 25 get no fix. Hunt window: 2026-10-21 onward
            (checker traffic from June 2026 in web logs).

HYPOTHESIS  A crafted Brotli stream reached an unpatched decoder. A miss crashes the worker and
            the supervisor restarts it; a hit replaces the runtime with a shell as the service
            account. If so, endpoint telemetry shows a restart burst, a shell or in-place exec
            under the runtime, and then discovery, secret access or a reverse shell.

QUERIES     Q1  Runtime spawns shell or downloader            PRIMARY
            Q2  Runtime PID re-executed as a shell            PRIMARY, VALIDATE FIRST
            Q3  Restart loop of node or bun                   CONDITIONAL
            Q4  Runtime to public address, non-web port
            Q5  Reverse-shell command lines                   PRIMARY
            Q6  Discovery and secret access on runtime hosts
            Q7  Runtime and decoder inventory by hash
            Q8  Connections to the reported exploit source    LOW YIELD BY DESIGN
            Q9  Child-process baseline                        RUN FIRST

DO FIRST    1. Patch internet-facing Node.js, Bun and Python Brotli users; replace EOL lines.
            2. Add Content-Encoding and request length to proxy logs; run Section 7 N1.
            3. Q9 baseline, then Q1, Q5 and Q2 over 14 days.
            4. Edge rule to reject br request bodies (Section 13 P2).

FINDINGS    [ ] Unpatched or EOL runtimes found:          ____
            [ ] Services decoding attacker-influenced br: ____
            [ ] br request bodies in web logs:            ____
            [ ] Q1 / Q2 / Q5 hits requiring triage:       ____
            [ ] Node crash cores found and secured:       ____

GAPS        Exploit request and the checker sweep are outside endpoint telemetry (web, proxy
            and WAF logs only). Memory disclosure is unobservable. Inbound contact from the
            reported source address cannot be checked from endpoint events. File version of
            ELF runtimes is not in process telemetry (hash mapping only).

ACTIONS     Patch; reject br bodies at the edge; run as non-root with restricted egress;
            restrict core dumps; rotate secrets for any host with node crash cores or a hit.

OWNER       HuntPack
VERSION     v0.1 2026-10-28
17

Changelog

v0.12026-10-28Initial pack. Nine CQL queries built from twelve saved source snapshots. The hunt thesis is post-exploitation runtime behaviour, because the exploit request is invisible to endpoint sensors; the request-level, log-level and crash-evidence checks are carried as native hunts in Section 7 and recorded as gaps in Section 15. Two atomic indicators (one request-body SHA-256, one IPv4) shipped as detect only with a 3-month expiry, both single-sourced to ELLIO. No prevent actions. The exploit reliability caveats (arm64 only for Imperva, per-build offsets, the failed in-the-wild chain) are stated in Section 1 rather than smoothed over. The Akamai analysis could not be fetched (HTTP 403) and is not cited as a source of any claim. SentinelOne PowerQuery and Microsoft Defender KQL tabs added to Q1-Q9: SIEM Query Rosetta release 0.1.0 (public-beta; static-reviewed translations).
18

References

TierSourceUsed forSnapshotAccessed
1CISA Known Exploited Vulnerabilities feedKEV entry, dates, CWEs, due date01-cisa-kev.txt2026-10-28
1ELLIO: Node2Shell on stock Node.js buildsAll atomic indicators; checker sweep; x86-64 reproduction; failed chain; reliability findings02-ellio.txt2026-10-28
1Sysdig: Detecting the Node.js Brotli RCEReachability table; demonstrated versus claimed; detection and mitigation03-sysdig.txt2026-10-28
1Tenable: Brotli2Shell FAQ.NET and other affected products; fixed versions; CVSS04-tenable.txt2026-10-28
2Wiz: Broccoli2ShellVersion matrix; cloud exposure05-wiz.txt2026-10-28
2Cycode: Node2ShellExposure conditions; framework reach; Python and proxy notes06-cycode.txt2026-10-28
2Aikido: Python packages vulnerable to Node2ShellPython, distro and library remediation07-aikido.txt2026-10-28
2Help Net SecurityCorroboration of source IP; aliases; KEV update08-helpnetsecurity.txt2026-10-28
2The Hacker NewsImperva reliability; ELLIO attempt failed; version-specific exploit09-thehackernews.txt2026-10-28
2SecurityWeekKEV addition; honeypot attempts; Broadcom and PAN-OS notices10-securityweek.txt2026-10-28
1Node.js October 2026 security releasesVendor description; affected lines; fixed versions11-nodejs-october-2026-security-releases.txt2026-10-28
1google/brotli v1.2.1 releaseUpstream fixed release12-brotli-v1.2.1-release.txt2026-10-28
2Akamai: Inside Node2ShellListed in the request; not used (fetch returned HTTP 403)none2026-10-28
StdMITRE ATT&CK T1190Technique mappingn/a (standard)2026-10-28
StdMITRE ATT&CK M1051 Update SoftwareMitigation mappingn/a (standard)2026-10-28
StdCISA BOD 26-04Federal remediation framing, cited in the KEV entryn/a (cited in snapshot 01)2026-10-28
StdCIS Docker BenchmarkContainer hardening controlsn/a (standard)2026-10-28
StdKubernetes Pod Security StandardsRestricted profilen/a (standard)2026-10-28
StdNIST SP 800-190Container security guiden/a (standard)2026-10-28

The raw fetched text of every snapshotted source is saved in Node2Shell-CVE-2026-111497-Hunt-sources/ next to this file. Standards pages are cited for control names only and were not snapshotted.