Today I tackled the Expose challenge on TryHackMe. As part of improving my pentesting workflow, I’m trying to write these posts in a more formal, report-style format — blending hands-on testing with structured documentation.

Reconnaissance

I started with a simple nmap scan:

The target runs several interesting services. I decided to explore them one by one.

🔐 FTP (Port 21)

Anonymous login was enabled, but no files were listed. Interestingly, the PUT command was allowed — possibly useful later for file uploads.

📡 MQTT (Port 1883)

This was new territory for me — I’ve got MQTT devices at home, but never tested them offensively. From HackTricks:

Authentication is totally optional and even if authentication is being performed, encryption is not used by default (credentials are sent in clear text). MITM attacks can still be executed to steal passwords.

I used python-mqtt-client-shell to connect:

Shortly after, messages started appearing, including:

Nothing sensitive was leaked, but it confirmed the service was unauthenticated and readable.

Wireshark

🧠 Note to self: Learn more about exploiting MQTT topics for lateral movement or code execution.

🌐 HTTP (Port 1337)

The homepage only displayed the word EXPOSED. The HTML source gave no real hints, so I launched a directory brute-force with dirb:

Interesting paths:

  • /admin → fake login page
  • /javascript → 403 Forbidden
  • /phpmyadmin → accessible but unauthenticated
  • /admin_101 → contains a working login form

SQL Injection on /admin_101

The login form at /admin_101 looked suspicious. The username field was pre-filled — possible hint. I inspected the HTML source and captured the request with Burp Suite.

source-code
Burp request

After saving the POST request, I tested it with sqlmap:

Dumping the database:

🔓 Getting Access

With credentials and two hidden pages from the earlier SQL injection, it’s time to move toward initial access. Here’s what we know:

Credentials found:

Discovered hidden paths:

We also extracted a password hash which couldn’t be cracked using default rockyou.txt in Hashcat. However, CrackStation succeeded, revealing:

Page analysis: /file1010111/index.php

After logging into this page, it returned a blank screen. A quick look at the source code gave a subtle hint that file handling might be going on in the background:

Changing the request method to GET in Burp Suite didn’t yield results. But appending a query parameter manually revealed more:

Testing for Local File Inclusion (LFI) worked. Using path traversal, I managed to access /etc/passwd and filtered out users without shells. That revealed:

We now know the valid user: zeamkish

Page analysis: /upload-cv00101011/index.php

This page prompts for a username. Using zeamkish, we were granted access and presented with a file upload form.

The form only accepts .png files. After uploading a test image, a message instructed me to check this path: /upload-cv00101011/upload_thm_1001/ Visiting http://expose.thm:1337/upload-cv00101011/upload_thm_1001/ showed the uploaded file successfully—indicating we had write access.

Checking the source code of the upload page, I found client-side validation:

A simple rename like shell.php.png was blocked.

In Burp, I intercepted the upload request and modified the filename to include a null byte (%00) before the extension. That bypassed the check:

The server saved it as a .php file and executed it — confirming Remote Code Execution (RCE).

Privilege Escalation

After stabilzing my shell I started looking arround and found:

Using zeamkish:easytohack@123, I opened a full SSH session. In the home folder, the user flag was also waiting in flag.txt.

Linpeas

To speed things up, I always run LinPEAS for local privilege escalation enumeration. Some of the more interesting findings:

  • Ports on localhost:
    • 1111
    • 953
    • 1883
    • 33060
    • 3306
  • Uncommon SUID Binaries
    • rwsr-x— 1 root zeamkish 313K Feb 18 2020 /usr/bin/find
    • rwsr-xr-x 1 root root 313K Apr 10 2020 /usr/bin/nano

Analysis

At first, I thought the nano SUID might be limited in use, but it actually gave me enough permissions to access sensitive files as root.

Screenshot from my notes

but only because nano ran as root, not because I became root. So technically, I wasn’t root yet.

Getting Root

Since nano is SUID root and can edit anything, I realized I could modify /etc/passwd and /etc/shadow. Time to add a real root-level user.

First, I generated a password hash using OpenSSL:

I appended the following line using nano:

And then updated /etc/shadow with:

Yes, nano was able to write both files due to the SUID bit. For some reason, the new user didn’t work. Either login was blocked, or something went wrong with the shadow file. Still using nano as root, I opened /etc/shadow again and replaced the hash for the existing root user with my custom hash. That worked

📘 Lessons Learned

This box had some cool gotchas that made it more interesting than expected. Here’s what I took away:

What Went Well

  • Enumeration discipline paid off: LinPEAS gave me everything I needed. The SUID nano was subtle but powerful.
  • Recognizing false positives: Just because you can read /root/root.txt doesn’t mean you’re root. That moment of “wait… am I really root?” helped me pause and rethink.
  • Plan B thinking: When adding a user failed, falling back to modifying the root hash directly saved the day.
  • Null byte upload bypass

What I’d Do Differently

  • Verify user creation more closely: I should’ve double-checked PAM or SSH login behavior before assuming the new user was usable.
  • Dig into shadow file structure: A deeper understanding of how fields in /etc/shadow work would’ve helped debug the login issue faster.