Sandworm is a Medium HackTheBox machine.

  • Foothold: SSTI in signature verification form; plaintext credentials on disk
  • Pivot: Injecting reverse shell in Rust code
  • Privesc: Firejail CVE

Port scan

Nmap results:

  • 22: OpenSSH server as always, uninteresting
  • 80: redirects to 443, uninteresting
  • 443: some kind of web service that we should investigate

Web service

Without any dirbusting, we can already find all the pages on the website:

  • /home: the bottom says that the web server is based on Flask/Python, which may be useful later
  • /about: nothing of interest
  • `/contact:
    • Could be vulnerable to client-side attacks
    • also leads to a /guide page
  • /guide: a page that does various PGP things (encrypt/decrypt/verify signature)
  • /pgp: SSA’s public key

The contact us page doesn’t look vulnerable, since it’s not responding to any URLs included in the message. That leaves us with the guide page.

  • The first pair of textboxes lets you decrypt any message encrypted with the SSA’s public key. It’s quite unthinkable that a web server would just let you use the organization private key as a service, but whatever. The decryption functionality looks quite benign, unless we encounter an encrypted message later (spoiler alert: we won’t).
  • The second pair of textboxes ecnrypts a message for a given public key. The message doesn’t change much with the encrypted content (only changes based on the key’s UID field and the datetime), and responds the same way to my own key and to the SSA’s public key.
  • The third pair of textboxes is the most interesting. It accepts a public key and a signed cleartext message, then it prints out the verification result of the gpg command

To be frank I didn’t know where I should begin for getting foothold, mostly because I straight up didn’t interact with the signature verification form at all, only after seeing a mention of SSTI on the HTB forum did I begin to test the third pair of textboxes. The user can control the name, comment, and email portion of the gpg command output, which is set at the creation of the public key. Out of the three, name is the most malleable (no < or >), whereas comment disallows parentheses (which disables function calls) and the email field has to be in a valid email format (has to contain @ to start off).

For the SSTI attack, we’ll have to familiarize ourselves with the gpg command. To create a key with gpg, simply run gpg --full-generate-key and follow the prompts. The only fields that matter are the name (where you will place the payload) and the email (which is a shorthand you can use to refer to the key, I usually use <letter>@<letter> for brevity since I had to try a lot of payloads, though you could also script it). To export the public key in ASCII, run gpg --export --armor <email> (“armor” stands for ASCII armor, which encodes the raw pubkey in base64). To sign a message, use echo hi > msg && gpg -u <email> --clear-sign msg, which produces a msg.asc that contains the original content of msg and appends a signature below.

The webserver runs Flask and uses the Jinja templating engine, for which HackTricks has an excellent cheatsheet. I used {{ config }} to confirm the SSTI vuln and got the atlas user’s MySQL password, though unfortunately it wasn’t reused for SSH. I then dumped a list of classes using {{ dict.__base__.__subclasses__() }} so that I could determine the index to subprocess.Popen(), which turned out to be 439. The most annoying part of gaining foothold is to find a payload that works. It seemed that the remote is running either a restricted bash or chroot jail, since I couldn’t even use wget and curl. Bash reverse shells won’t work directly since redirection symbols aren’t allowed in the name field of a key. Python3 payload somehow wasn’t working at all either. At last I used a base64 bash revshell payload to get around the name field restriction.

Stuff I’ve tried:

What worked at last:

atlas (chroot jail)

Since we’re in a chroot jail, we can’t really use automatic enumeration scripts due to the limited number of tools at our disposal. That’s fine—we can just do it ourselves.

The SSA webapp directory contains information about the database in __init__.py and models.py. I copied relevant code into the Python REPL, modified it to get an app context without being in a request (with app.app_context(): ... IIRC), and requested all the user credentials. There are only two users (Odin and silentobserver, the latter of which is on the machine as well), but both of the hashes are salted PBKDF2-SHA256 hashes with 260000 rounds, which is a clear sign that the box author didn’t want us to brute-force it, so mysql is a dead end.

As I manually hunted for juicy information, the ~/.cache directory jumped out at me. While the firejail directory is understandably off-limits, the httpie directory contains plaintext credentials that allows us to pivot to silentobserver:

silentobserver

In a routine LinPEAS scan I discovered a weird SUID binary tipnet, though the owner is atlas. Apart from that, we also have firejail, though we can’t execute it because we’re not part of the jailer group.

If we could somehow somehow pivot to atlas (unrestricted shell) who is a member of the jailer group, then we could potentially exploit firejail.

Looking at the build config for tipnet, it seems that the binary depends on the logger local crate. Whereas the tipnet source code cannot be modified as silentobserver, logger’s can.

From pspy output, we can see that the tipnet binary gets compiled, set as SUID, and executed by a cronjob; which means that we can just insert a reverse shell into the logger crate to pivot to atlas.

Modified logger/src/lib.rs code (Rust reverse shell GH):

Replace the original lib.rs with the above and save a copy for later use.

atlas (unrestricted)

As atlas, we can execute firejail without root. This firejail version is vulnerable to privesc:

This exact exploit appeared in Cerberus (I wondered why it feels so familiar until I remembered that I saw it in IppSec’s video). A glimpse of the privesc:

Running the exploit gives us a message that we have to run firejail --join=<PID> elsewhere.

Now we can reuse the modified Rust code to get another reverse shell. Root!