[Hack The Box] Developer — Walkthrough

Gerard De Las Armas
14 min readJan 15, 2022

Summary

Developer is a HARD Linux machine that was released on 18th of August 2021. This machine hosts Developer CTF (created using Python and Django) where users can register and solve CTF challenges. When you successfully completed a challenge, you submit the flag and then you can submit a URL to your walkthrough.

The machine is vulnerable to tabnapping.

A design flaw in browsers allows for external links to open in new tabs or windows when “target=_blank” attributes are specified in the HTML href elements.

Exploiting this vulnerability will give you the admin credentials to Developer CTF. While logged in as admin, you can now view the Django administration page located at /admin. In the Sites page, you will see a new domain name — developer-sentry.developer.htb.

The developer-sentry.developer.htb is a Sentry application. The Sentry installed is vulnerable to an Authenticated Remote Code Execution. Exploiting this RCE will give you a shell as www-data.

As www-data, you can look at configuration files such as Sentry configuration files and you will find PostgreSQL credentials that will reveal karl’s PBKDF2 hash. Crack the hash and you now have karl’s password.

As karl, simply running sudo -l will give you an idea how to get root and it requires reverse engineering an ELF file. Once you know the password, the ELF file will ask for your public SSH key and you can now log in as w00t!

Foothold

Nmap

As usual, I ran nmap to discover available services in the machine.

sudo nmap -sC -sV -vvv -oA nmap/developer-default 10.10.11.103
sudo nmap -p- -vvv -oA nmap/developer-all 10.10.11.103

Only two services are available — 22/tcp/ssh and 80/tcp/http.

developer.htb

When I first try to poke the HTTP service, it redirected me to http://developer.htb so I had to map the IP address of the machine to developer.htb in my /etc/hosts.

127.0.0.1       localhost10.10.11.103    developer.htb

The developer.htb is a web application written in Python Django and hosts a web application called Developer CTF, an exclusively free Capture The Flag Platform.

I fired up gobuster to run in the background while I was browsing through the web pages.

gobuster dir -u http://developer.htb -w /usr/share/wordlists/dirbuster/directory-list-2–3-medium.txt -t 30 -o developer.htb.txt

I got a few results after a little while.

I took note that the /admin (or anything that has admin) redirects to a different login page than the login page of Developer CTF. The /media contains files relating to CTF.

I don’t have any credentials yet at this point but I was able to register an account. There are several challenges available across different categories. The easiest challenge would probably be the Phished List.

Phished List

The challenge let me download a zip file named phished_list.zip. Inside it is an Excel file that supposedly contains passwords and the flag for the challenge. I quickly noticed that Column E on the Excel file is hidden and the sheet is password-protected.

The quickest probable way to solve this challenge is to convert the Excel file to CSV which will reveal the hidden columns but I don’t have LibreOffice installed on my machine. I searched Google for online services to convert the Excel file to CSV and stumbled upon CloudConvert. It worked like a charm and I was able to see the hidden column in CSV.

The flag is a bit further down and I had to search for HTB.

The flag is DHTB{H1dD3N_C0LuMn5_FtW}, including the “D” in the beginning.

After successfully submitting the flag, the “Submit Flag” button changed to “Submit A Walkthrough” button. When clicked, a popup dialog box opened and asked for a URL of my walkthrough.

A URL of my walkthrough… The popup dialog box says “Admins will check the walkthrough as soon as they can…”. This got me thinking of ways on how to attack this. I quickly fired up a Python HTTP server locally, entered a URL that points to my local HTTP server and confirmed that the machine, indeed, connected to my local HTTP server.

This means that there is a scheduled job that simulates an administrator visiting the walkthrough URL. But how do I leverage this knowledge? How can I exploit it? Trick the administrator to login to a cloned login page of developer.htb? I have tried that but it didn’t work. After hours of searching on how to exploit this, I stumbled upon an attack called tabnapping.

Tabnapping

A design flaw in browsers allows for external links to open in new tabs or windows when “target=_blank” attributes are specified in the HTML href elements.

Target:_blank → Tabnapping Attack

Tabnapping occurs when a user clicks on a link that has an attribute target=_blank and the newly opened web page in a new tab contains JavaScript code to redirect the opener (the tab that opened the web page) to a different web page.

The idea is to submit a URL that points to a walkthrough HTML file in my local machine and contains the tabnapping JavaScript code and redirect the opener to a phishing page of the Developer CTF login page.

Tabnapping does not work in major browsers anymore. In Firefox, I had to disable the property dom.targetBlankNoOpener.enabled to show this in action.

The tabnapping web page looks like this:

To clone the Developer CTF’s login page, I used httrack.

httrack http://developer.htb/accounts/login/

The phishing page of the Developer CTF login page looks like this:

Having the tabnapping web page and the Developer CTF login phishing page, all I need to do was to figure how to log the post requests coming from the phishing page of the Developer CTF login. The Python HTTP server module does not log the HTTP body of POST requests by default so I have to copy from server.py and modify it to suit my need. My version of the Python HTTP server looks like this:

After doing all this and a few trial and error, I was successful in getting admin’s credentials!

Django Administration

With the admin credentials, I was able to login to Django Administration (as Jacob Taylor) located at /admin which was previously discovered by gobuster. These administration pages lets the administrator configure a lot of stuff such as users, challenges, and most importantly — Sites — where I found a subdomain of developer.htbdeveloper-sentry.developer.htb.

developer-sentry.developer.htb

I had to map the IP address of the machine to developer-sentry.developer.htb in my /etc/hosts too.

127.0.0.1       localhost10.10.11.103    developer.htb
10.10.11.103 developer-sentry.developer.htb

After that, I was greeted with yet another web application — Sentry.

Sentry is a service that helps you monitor and fix crashes in realtime. The server is in Python, but it contains a full API for sending events from any language, in any application.

So far, I only got one credential and that is the admin of developer.htb. I tried to use that to login to Sentry but to no avail. I registered instead in order to login. After logging in, the version of Sentry is displayed in the lower left corner of the page. After a few Google searches, I discovered that Sentry version 8.0.0 is vulnerable to an authenticated remote command execution. But before that, I found that Jacob Taylor, the admin of developer.htb, is also a user of Sentry with an email address of jacob@developer.htb. I used that email and reused his password to login to Sentry and it worked!

Authenticated Remote Code Execution in Sentry

In a security advisory from Synacktiv released in 2015, they detailed the vulnerability in Sentry.

The Sentry Web interface is based on Django. As such, it includes a Django administration interface reachable by any user
with Superuser privileges at the URL http://sentry_host/admin/. Compared to the Sentry regular management interface, it
provides administrators with a more powerful way to configure the platform.

Synacktiv has identified a vulnerability in the Django administration interface Audit log entry page, allowing an attacker to
execute arbitrary code remotely.

This issue is due to a Python Pickle deserialization in the Audit log entry “data” field. No additional processing nor cleaning is
done on this field, which is interpreted as a gzipped pickled object. This allows the deserialization of an arbitrary Python
object and, thus, remote code execution.

Note that this vulnerability is only exploitable by a user with Superuser privileges.

As a side note, due to the use of Python Pickle in several fields, an attacker able to tamper with the sentry database, would
also be able to achieve remote code execution, but this is a less likely attack vector.

All I have to do is to copy their proof of concept code and modify it to use a Python reverse shell with my IP address.

This PoC produced a pickle (Python object serialization) encoded with base64.

I entered the base64 encoded pickle in the data field when I added an audit log entry. I looked at the existing audit log entries to see what should be the acceptable values in other fields.

When I saved this audit log entry, the target machine instantly decoded the base64 and deserialized the pickle — giving me a shell as www-data.

User

wwwe’re in!

The usual stuff — I checked uname, distribution, architecture, operating system and version, users, etc. I briefly checked /etc/passwd and found two regular users of the machine — karl and mark. Then I moved on checking available files and directories. The user www-data mostly have access to web application files and configuration and after leaving no stone unturned, I found /var/www/developer_ctf/developer_ctf/settings.py. It contains a PostgreSQL credentials and I used it to login to PostgreSQL using the command — psql -h 127.0.0.1 -U ctf_admin -d platform -W.

'NAME': 'platform',
'USER': 'ctf_admin',
'PASSWORD': 'CTFOG2021'

I did not find anything new in platform database but I found that sentry database is in there too.

I logged in again using the same credentials but sentry as database — psql -h 127.0.0.1 -U ctf_admin -d sentry -W. However, my current credentials did not have permission to view the tables in sentry database.

Ok, no biggies! Back to searching files and directories again and then I found /etc/sentry/sentry.conf.py when I tried to find files related to sentryfind / -name “*sentry*” 2>/dev/null. It also contains a PostgreSQL credentials and I used it to login to PostgreSQL using the command — psql -h 127.0.0.1 -U sentry -d sentry -W.

'NAME': 'sentry',
'USER': 'sentry',
'PASSWORD': 'SentryPassword2021'

It worked and I found a PBKDF2 hash of Karl Travis inside auth_user table.

Weak password

I used hashcat to crack karl’s PBKDF2 hash. First off, I determined what hashcat mode to use — hashcat --example-hashes | grep pbkdf2_sha256 -B 11 -A 2 which returned Hash mode #10000.

Then I proceeded to cracking the hash — hashcat -a 0 -m 10000 psql.karl.hash /usr/share/wordlists/rockyou.txt. A few minutes later, hashcat successfully cracked the hash and karl’s sentry password came to be insaneclownposse.

user.txt

With karl’s password, it is simply a matter of switching to karl user — su karl. The user.txt was available in karl’s home directory.

Root

On to w00t!

The road to root is simply (not really) running sudo -l and finding that there is an ELF binary that karl can run with sudo privileges — /root/.auth/authenticator.

The authenticator asks for a password to access the super user.

I copied authenticator to my local machine to have a better look.

Reverse Engineering with Ghidra

First and foremost, I am no expert in reverse engineering nor Ghidra. I used to write/rewrite exploits using C and Python especially during the time I was preparing for Offensive Security Certified Professional (OSCP) and Offensive Security Certified Expert (OSCE) exams. I also used a few debuggers and IDA Pro during that time so I was used to seeing assembly language code. As with Ghidra, I have used it a handful of times before. But all that is quite a long time ago and I felt a little rusty when I was doing the reverse engineering for authenticator.

If you have no experience in Ghidra, I suggest to checkout the basics of how to use it.

With all that out of the way, let’s continue. I started Ghidra and loaded the authenticator binary. I looked at the Imports, Exports and Functions and can quickly tell that this binary is created using Rust due to the fact that a lot of symbols have “rust” in their names. I haven’t used Rust before so I researched a little bit about reverse engineering a Rust binary and here are what I learned.

  • Rust is comparable to C and C++. I often see blog posts comparing Rust with these two programming languages.
  • It also contains a main function. That main function loads a pointer to the other main function into rdi and passes that as an argument to std::rt::lang_start_internal.
  • It uses fastcall calling conventions — The first four arguments are placed onto the registers. That means RCX, RDX, R8, R9 for integer, struct or pointer arguments (in that order), and XMM0, XMM1, XMM2, XMM3 for floating point arguments.
  • Rust binaries may contain symbol names that doesn’t go away when they are compiled. You have to manually run strip to remove the symbol names, just like with C and C++.

With these knowledge, I resumed reverse engineering and went straight to authentication::main, the other main function that was called by std::rt::lang_start_internal. Examining the assembly language code in coordination with the decompiled code and Rust’s symbol names side by side in Ghidra proves to be very convenient. I instantly recognized the printed texts from authentication binary, such as Welcome to TheCyberGeek’s super secure login portal! and Enter your password to access the super user: followed by a call to std::io::stdio::_print.

The binary asked for a password next so I looked at where it’s at in the disassembly. The function called was std::io:stdio::Stdin::read_line. This function accepts a buffer where it will store the user input, in this case, the password. In reverse engineering, one of the most important steps to achieve your goal is to make the disassembly readable as possible. It can be done by simply renaming local variables and labels to more meaningful names. I renamed the input buffer to __user_input to make it easier to understand that this variable is where the user input goes to and Ghidra automatically renamed all instances of the variable.

The next function I looked at is crypto::aes::ctr. I won’t go into detail how AES and CTR works because there is a lot of resources out there that explains them in more detail than I could ever have. For our purposes, AES is a symmetric block cipher that can encrypt/decrypt data and CTR is one of AES’s mode of operation. The crypto::aes::ctr function takes key_size, key and iv and returns a SynchronousStreamCipher. In the decompiler, the call to crypto::aes::ctr looks like this.

msg = (&str)crypto::aes::ctr(0x20, &local_118, 0x10, &local_108, 0x10);

I inferred that

  • 0x20 (or 32 bytes in decimal) is the key_size
  • local_118 is the key and 0x10 (or 16 bytes in decimal) is the size
  • local_108 is the iv and 0x10 (or 16 bytes in decimal) is the size

I renamed these variables to more appropriate names and then I was able to determine the key and iv in decompiler.

The next function I looked at is alloc::raw_vec::RawVec<T, A>::reserve. As the name suggests, it is used to reserve a buffer and ensures that it contains enough space. The buffer in question was the variable local_238. Browsing through the decompiler, I saw local_238 used in crypto::symmetriccipher::SynchronousSymmetricCipher::process and in a condition where it is being compared to __ptr. This led me to the assumption that

  • __ptr is the encrypted password of the authenticator binary
  • the call to crypto::symmetriccipher::SynchronousSymmetricCipher::process encrypts the user input and put the encrypted user input to local_238
  • the condition local_238 == __ptr is a way to check if the encrypted user input is the same as the encrypted password

I renamed these variables appropriately and the decompiled code made more sense.

What happens when the encrypted user input is the same as the encrypted password of the authenticator binary? Well, the execution will go to LAB_00107a37 (I renamed it to correct_password) where it will print that “You have successfully authenticated” and will ask for your public SSH key that will then be inserted to root’s authorized_keys so you can login via SSH as root.

Decryptor

With all these information, I was able to decrypt the password of the authenticator binary. I borrowed the Python source from 24 days of Rust — rust-crypto and modified it as shown below.

And I got the password to the authenticator binary — RustForSecurity@Developer@2021:).

root.txt

It’s finally time to get root.txt! I ran authenticator while logged in as karl and entered the password. I supplied my public SSH keys when asked and logged in via SSH as root.

Conclusion

I must admit that this machine presented me with some challenges especially the parts of tabnapping and reverse engineering. I hit a brick wall when I ran out of ideas before stumbling upon tabnapping and Rust binary reverse engineering forced me to research the topic for quite a while. Overall, I have learned a lot while doing this machine and enjoyed writing this walkthrough. Hope you

Source codes are in my GitHub.

--

--