TryHackMe | TryHack3M: Subscribe | Write-Up

The TryHack3M: Subscribe room hosted by TryHackMe challenges to help Hack3M reach 3M subscribers. More details can be found here:

I used a Kali Linux VM in VirtualBox and connected to the TryHackMe machine via OpenVPN.


Incident Storyline

We have good news and bad news! The good news is that we are about to hit 3 million users on our platform, and the bad news is;Well, last night, the UnderGround (UG) Hackers attacked our website, hackme.thm, and took complete control. They were able to turn off the signup page, so there won’t be any new registrations. Given this, our user count is stuck at 2.99 Million.

Can you help us restore the registration panel on our site to reach our 3 million user milestone?

Room Objectives

To assist the HackM3.thm company in fixing the application, you need to:

  • Explore the web server and find the attack vectors leveraged by the attacker.
  • Regain access and restore the signup functionality for the new users.
  • Investigate the web application logs and track down the root cause.

Lab Connection

Before moving forward, start the lab by clicking the Start Machine button. It will take 3-5 minutes to load properly. We can’t wait to get our site restored and resume our 3M celebrations!

Good luck!

I have successfully started the machine.

After having started the machine, I just added hackme.thm to my hosts file to make the following hacking a bit more comfortable.

cat /etc/hosts
echo " hackme.thm" >> /etc/hosts
cat /etc/hosts

Answer: none


Sometimes, the attacker leaves footprints that allow you to regain access to the server.  Can you help HackM3 restore server access and get 3M subscribers?

As usual, I started doing an nmap scan using nmap -sV hackme.thm.

Apart from ports 22 and 80, port 8000 and 8089 were open for later tasks.

What is the invite code for the hackme.thm website?

I started by visiting the webservice on port 80.

Clicking “Join NOW”, I was prompted to enter an Invite Code.

Trying random codes didn’t work. Before trying an SQL injection, I checked for some client-side hints and found an interesting file.

function e() {
    var e = window.location.hostname;
    if (e === "capture3millionsubscribers.thm") {
        var o = new XMLHttpRequest;"POST", "inviteCode1337HM.php", true);
        o.onload = function() {
            if (this.status == 200) {
                console.log("Invite Code:", this.responseText)
            } else {
                console.error("Error fetching invite code.")
    } else if (e === "hackme.thm") {
        console.log("This function does not operate on hackme.thm")
    } else {
        console.log("Lol!! Are you smart enought to get the invite code?")

As can be seen on line 14 at the bottom, if the domain of the request is “hackme.thm”, nothing is done, but on line 3 it says something should be done if the domain is capture3millionsubscribers.thm. A POST request to inviteCode1337HM.php is made to receive an invite code. I thought about just doing a POST request myself, but I assumed there would be a server-side check, too, and adding capture3millionsubscribers.thm to my hosts file was easy enough with echo " capture3millionsubscribers.thm" >> /etc/hosts

Then I just had to execute the e() function from http://capture3millionsubscribers.thm.

This revealed the Invite Code.

Answer: VkXgo:Invited30MnUsers

What is the password for the user guest@hackme.thm?

Entering the invite code revealed the credentials including the password:

Credentials: guest@hackme.thm:wedidit1010

Answer: wedidit1010

What is the secure token for accessing the admin panel?

Entering the guest credentials, this was the first impression:

The FREE tier room looked like a typical TryHackMe room.

Clicking on “Complete” did not mark the task as complete, though, so the site was not functional.

To enter the VIP training room, I would need a subscription, which is precisely what is impossible as per the overall task description.

Doing some investigation, I realized there was a cookie for the VIP status.

Simply setting that to “true” in my Firefox browser allowed access to the VIP room.

Interestingly, the “Start Machine” button actually worked in this room, but only for VIP users:

When I wanted to capture the request of this interaction by copying it from the Network tab in my browser, I realized there was another interesting request independent of clicking the button (to /BBF813FA941496FCE961EBA46D754FF3.php), but more on that later. And clicking the button did not send a request, but instead, that check was done on client-side again:

// sc-drFUgV fXEjrf
$(document).ready(function() {

  $('#start_machine').click(function(e) {
    var isVIPE = document.getElementById("isVIP");
    var isVIP = (isVIPE.value.toLowerCase() === 'true');
    if (isVIP) {
      $("#splitScreenRight").attr("class", "sc-drFUgV bROZdw");
      $("#main1").attr("class", "sc-bKNmIE bYiuLB");
      $("#main2").attr("class", "sc-hZDbVM bksodH");
      $("#nav1").attr("class", "sc-krITIZ gMgnKr");
    } else {
      alert("This page is only for VIP users")


$(document).ready(function() {
  $('#exit_split').click(function(e) {
    $("#splitScreenRight").attr("class", "sc-drFUgV fXEjrf");
    $("#main1").attr("class", "sc-bKNmIE ipXaXG");
    $("#main2").attr("class", "sc-hZDbVM hgVIhb");
    $("#nav1").attr("class", "sc-krITIZ hoLoqS");

On line 13, the message for non-VIP users is printed. This is done, when the “Start Machine” button is clicked and when the value of the “isVIP” element is not true on line 5 and 7. Getting the content of this element:

Setting it to “true”:

Then clicking the “Start Machine” button actually worked:

Now about the request made to /BBF813FA941496FCE961EBA46D754FF3.php. That request is made in an iframe to get the terminal.

<iframe id="remote-window" allow="fullscreen; clipboard-read; clipboard-write" title="Training Room 2: Advanced Red Teaming" src="./BBF813FA941496FCE961EBA46D754FF3.php" style="background-color: rgb(255, 255, 255);" width="100%" height="100%"></iframe>

Accessing this PHP file directly works, too, instead of changing the “isVIP” variable. Accessing the terminal directly.

Doing some basic reconnaissance there, I realized I was “www-data”, “pwd” was not allowed, and this was the output for “ls”:

I assumed the token for accessing the admin panel was to be found somewhere here, so I searched for it by starting to look into the config.php with cat config.php.

Not only did this reveal the token, but also a new domain and port.


What is the flag value after enabling the registration feature and getting 3M subscribers on the platform?

First, I tried to access http://admin1337special.hackme.thm:40009 in my browser by adding admin1337special.hackme.thm to my hosts file using echo " admin1337special.hackme.thm" >> /etc/hosts, even though my initial nmap scan did not show port 40009, but to be fair, I think the basic scan doesn’t cover that port.

Upon trying to visit that page, I got an error, but the URL being changed to /public/html showed me that I was on the right track:

Since I had an access token for the website, I just needed to figure out how to use it. Unfortunately, no cookie was set to manipulate this time. So I guessed I had to find a login page first. Before using Gobuster, I manually tried http://admin1337special.hackme.thm:40009/public/html/login.php and was lucky.

Upon entering the token, another login mask was presented:

I tried my guest credentials from earlier, but was not lucky this time.

Also, the client-side login code was not vulnerable this time:

function login() {
    var username = document.getElementById('username').value;
    var password = document.getElementById('password').value;
    fetch('../../api/login.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      body: JSON.stringify({
        username: username,
        password: password,
    .then(response => response.json())
    .then(data => {
      if (data.error) {
      } else {
        // Redirect to the dashboard or the admin page based on the role
        window.location.href = data.role == 'admin' ? 'dashboard.php' : 'dashboard.php';
    .catch((error) => {
      console.error('Error:', error);

I thought about getting some more info in my other terminal via BBF813FA941496FCE961EBA46D754FF3.php, but no other file besides config.php was cat’able.

So, I resorted to trying an SQL injection. Entering ' OR 1==1' as the username did not grant me access, but it also did not show the usual message about an invalid username or password, so I knew that something was possible here. I copied the Request Headers and Post Data on my browser and saved them in req.txt to use in sqlmap.

POST /api/login.php HTTP/1.1
Host: admin1337special.hackme.thm:40009
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://admin1337special.hackme.thm:40009/public/html/login.php
Content-Type: application/json
Content-Length: 45
Origin: http://admin1337special.hackme.thm:40009
Connection: keep-alive
Cookie: PHPSESSID=oosokb8l3hko18cqlr6std67eq


Running sqlmap using sqlmap -r req.txt --dbs --batch:

And indeed, an SQL injection was possible:

Obviously, the database of interest hackme, so I tried to find some credentials in there:

sqlmap -r req.txt -D hackme --tables

Database: hackme
[2 tables]
| config |
| users  |

sqlmap -r req.txt -D hackme -T users --dump

Database: hackme
Table: users
[1 entry]
| id | email            | name       | role   | status   | password     | username |
| 1  | admin@hackme.thm | Admin User | admin  | 1        | adminisadm1n | admin    |

Easy enough, I found the credentials for the admin user of the admin portal. After entering admin and adminisadm1n on http://admin1337special.hackme.thm:40009/public/html/login.php, I got in.

The available actions were “Invite Code” and “Sign up”. I wanted to activate the “Sign up” a. k. a. registration feature. I clicked the “Set Options” button a couple of times because nothing seemed to happen, but going back to http://hackme.thm, I saw fireworks.



Investigating the Attack

Our security department detected an alert about a web attack on the 4th of April, 2024. They have ingested the logs into Splunk, which can be accessed using the following credentials:

TryHackMe credentials.

 Splunk Interface for Login

Your task is to analyse the logs and track the attacker’s footprints.

Good luck.

Now it was time to go to port 8000 and do some investigation.

How many logs are ingested in the Splunk instance?

I wasn’t too firm with Splunk, but when I entered “index” into the search, I got some suggestions already:

So, to get the overall number of logs, I searched for index=*. Unfortunately, the splunk license was expired was was already shown in the login screen.

I went to the TryHackMe discord to find a solution and they confirmed this was an unintended issue.

Since I didn’t want to wait for the issue to be resolved and since the necessary searches were already visible in splunk, I looked up another write-up to get the screenshots and answers.

Answer: 10530

What is the web hacking tool used by the attacker to exploit the vulnerability on the website?

Quite a few events. Since I got into the website using sqlmap, I already assumed the attacked did the same. But looking at the different User Agents that made the requests confirmed that:

Answer: sqlmap

How many total events were observed related to the attack?

In the above screenshot, the event count made by sqlmap is 158, which is the answer.

Answer: 158

What is the observed IP address of the attacker?

To get the IP address of the attacker, I would’ve searched for the IP address used with the sqlmap User Agent like this: index=* user_agent="sqlmap/1.2.4#stable ("

The “source_ip” of the events is the one used by the attacker.


How many events were observed from the attacker’s IP?

Now, the attacker did not only make requests using sqlmap, but also using other User Agents. So to find all events for the attacker, I would’ve searched for all events with that same source IP address like this: index=* source_ip=""

Answer: 184

What is the table used by the attacker to execute the attack?

First, I assumed it would just be the “users” table like I did to get the credentials of the admin user, but that was not what was meant here. The attacker did use sqlmap, but not the same way I did to hack the website back. To see all requests made by sqlmap, I could’ve searched through them manually, but splunk also lists interesting fields including the different URI requested by the attacker’s IP address.

Within on of these requests, the payload used by sqlmap can be seen. That URI shows a request where username and password are selected from “TryHack3M_users” where the role is “admin”.

Answer: TryHack3M_users


Congratulations on making it this far! You have clearly completed the challenge and helped HackM3 reach 3M subscribers. We are thrilled that you’ve had the opportunity to learn some fascinating exploitation techniques while gaining valuable detection insights for this kind of attack. Keep up the great work!

Let us know your thoughts on this room on our Discord channel or X account. See you around.

I have successfully completed the room.

Answer: none

That concludes this CTF. Too bad the splunk part was not functional in the way it was intended to be, but I liked the different methods used to exploit the webservice in the first part, like changing the host name, cookies, understanding basic JavaScripts and all that.