This article was originally presented as a lightning talk during ElixirConf 2023.

Computer security Capture The Flag contests (CTFs for short) are fundamentally about getting secrets out of computer programs. Sometimes the secrets are locked inside or behind a complex binary requiring a decompiler or disassembler and hours of analysis, and this frustrates players who want “web” challenges. Web challenges are often synonymous with SQL injection challenges, which are a particular flavor of CWE-94 Improper Control of Generation of Code.

Unfortunately, sqlmap is a really useful tool for finding and exploiting SQL injection vulnerabilities in web applications. It’s absurd how trivial it is. You point it at a URL and can get the complete database back. Ridiculous.

However, many technologies in the modern web aren’t compatible with the very ’90s view of the web sqlmap assumes. It doesn’t puppet a browser, just parses HTML and makes normal HTTP requests. This means that we can use a JavaScript-based system speaking WebSockets or some other weirdo HTTP subset to handle the user interaction in a way that sqlmap can’t interact with.

The other problem with SQL (or really, any flavor of persistence) injection challenges is that mischief is incentivized. Even if you don’t solve the challenge, there may be opportunities to make it more difficult for other teams through resource consumption attacks or just straight up vandalism that are hard for game organizers to track down or fix during the game.

Hack-A-Sat, in addition to being the first CTF in space, also had a system of tickets and receipts (yes I did come up with it on a train ride) to provide traceability and unique experiences per team. Tickets contain an RNG seed and a per-team key. This allows different instances of a challenge (with no communication between them, good for operations!) to provide either a new random experience per connection or a consistent but random experience per team. Nautilus Institue also uses tickets and receipts for our quals game.

I came up with the “Raw Water” challenge right at the beginning of 2023:

<vito> with the quals service i’ve been thinking about, “raw water” (sqli using websockets so you can’t just sqlmap it), not having to worry about a shared sql instance would be nice
<vito> oh god i have a bad idea though
<vito> handle the ticket myself in the http server and have a named sqlite per slug on a shared fs

I wanted to minimize the processing done on the client; you can’t trust them normally, even less when only scary hackers will use it. (j/k ilu :3) I started working with raw WebSockets in Phoenix (using Phoenix.Socket.Transport) and was on the verge of starting the client JS for it when I realized I was just reimplementing Phoenix LiveView (which is the well-supported and pretty good system for receiving events from the client, processing them on the server, and sending redirects and HTML changes back to the client).

So, I built “Hellform,” which uses the seed from the ticket to make a consistent form of 100 fields, about half required, with one injectable “party” field and one “landmine” field that rejects any single quotes (i.e. attempts at SQL injection). It’s broken into 10 pages of 10 fields each, because that let me hide the page navigation on the first page (because it was funny, to me.) The in-progress form just lives in the LiveView process, and the actual submission of the form is also done over the LiveView too, which sqlmap can’t interact with.

exqlite, an Elixir library wrapping sqlite3, solved the shared resource problem. Sqlite3 databases have a pretty efficient file representation, files are just an array of bytes, and PostgreSQL has a column type just for those. The “Minibase” part of Raw Water really just implements two things: saving an order and loading an order. Wrangling the Postgres data is done elsewhere, with Ecto.

Saving the order is kinda complicated:

  1. Receive either the whole byte array or a big fat NULL from Postgres.
  2. Open a :memory: datbase with
  3. Deserialize the database with Exqlite.Sqlite3.deserialize/2
  4. Validate the schema
  5. If any of the above failed, reopen :memory and create the flags and orders tables.
  6. Generate and insert a flag into flags.
  7. Run the SQL statement to insert the order into orders.
  8. Delete the flag from flags with the SQL statement DELETE FROM flags;
  9. Serialize the database with Exqlite.Sqlite3.serialize/1

Loading the order is much simpler, deserialize and SELECT and just 404 if it fails.

Minibase (or the Minibase concept) is intended to be reused in future challenges. If you’re interested in it or something like it, have a look at the source code, and hit me up for questions, either via email or on Mastodon.