← Back to Writeups
CUSTOMMediumWeb

Level 23 - Voting Service

XESXOR9/2/20263 min read
#web#custom#medium#cookie_chaining#sequential_request_automation#signature_analysis

Level 23 - Voting Service

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: cookie_chaining, sequential_request_automation, signature_analysis

Summary

Walkthrough of the CUSTOM Web Level 23 - Voting Service. Task: Voting service requiring 1337 votes, with cryptographically signed cookies (N:HASH format). Solution: Automated sequential voting through cookie chaining since signature cannot be forged without

Description

The task is a voting web service available at http://kslweb1.spb.ctf.su/second/level23/

The server runs on nginx/1.18.0 (Ubuntu), proxying Apache/2.4.7 (Ubuntu) with PHP/5.5.9.

On the first request (without cookie) the page returns:

=== Voting service ===

Currently you have 0 votes.

To vote +1, set this cookie: Set-Cookie: votes=1:7bc858c0d521f3bdf696f7187c2da177

To get flag you need 1337 votes here.

Analysis

Cookie format

The cookie has the format votes=N:HASH, where:

  • N — current vote count
  • HASH — 32-character hex string (length corresponds to MD5) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Signature bypass attempts

When attempting to forge a cookie (e.g., votes=1337:md5("1337")) the server returns:

'votes' cookie has an invalid crypto signature. It's ok, you can't cheat this.

The following hash cracking attempts were made:

  • Simple MD5 of the number: md5("1337") — doesn't work
  • MD5 with common salts (prefix/suffix)
  • Brute-force of salts up to 4 characters
  • Dictionary attacks with popular words
  • HMAC-MD5 variations
  • Double-MD5
  • Truncated SHA1

Result: All attempts failed. The hash is generated server-side using a secret key that cannot be guessed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

How it works

  1. When sending a valid cookie votes=N:HASH the server verifies the signature
  2. If the signature is valid, the server returns the next cookie votes=N+1:NEW_HASH
  3. The only way to get a valid cookie for 1337 votes is to go through all 1336 intermediate steps

Solution

Since the cryptographic signature cannot be forged without knowing the secret key, the sequential voting process must be automated.

Algorithm

  1. Start with cookie votes=1:7bc858c0d521f3bdf696f7187c2da177
  2. Send a GET request with the current cookie
  3. Parse the response and extract the new cookie from the Set-Cookie: votes=... line
  4. Repeat 1336 times
  5. On the 1337th step, get the flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solve script

python
#!/usr/bin/env python3
"""
kslweb1 Level 23 - Voting Service Solver
Automating sequential voting through cookie chaining
"""

import asyncio
import aiohttp
import re

async def main():
    url = 'http://kslweb1.spb.ctf.su/second/level23/'
    cookie_val = '1:7bc858c0d521f3bdf696f7187c2da177'
    
    # Use connection pooling for speedup
    connector = aiohttp.TCPConnector(limit=1, force_close=False)
    
    async with aiohttp.ClientSession(connector=connector) as session:
        for i in range(1, 1337):
            async with session.get(url, cookies={'votes': cookie_val}) as resp:
                body = await resp.text()
            
            # Check if we got the flag
            if 'flag' in body.lower() and 'need' not in body.lower():
                print(f'[+] FLAG FOUND at step {i}!')
                print(body)
                return
            
            # Extract next cookie
            for line in body.split('\n'):
                if 'Set-Cookie:' in line:
                    match = re.search(r'Set-Cookie:\s*votes=(\S+)', line)
                    if match:
                        cookie_val = match.group(1)
                        break
            
            # Progress
            if i % 100 == 0:
                print(f'[*] Step {i}/1336: votes={cookie_val[:20]}...')
        
        # Final request with 1337 votes
        async with session.get(url, cookies={'votes': cookie_val}) as resp:
            body = await resp.text()
            print(f'[+] Final response:\n{body}')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if __name__ == '__main__':
    asyncio.run(main())

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering cookie_chaining, sequential_request_automation, signature_analysis was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-03-08 • CUSTOM • Level 23 - Voting Service • Difficulty: Medium