Камень, ножницы, бумага
Камень, ножницы, бумага
Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: favorable_state_branching, session_checkpoint_abuse, win_streak_preservation
Summary
Task: a PHP web Rock-Paper-Scissors game required a 100-win streak and exposed progress through session state. Solution: abuse broken session handling where each non-tie creates a new child PHPSESSID, keep only winning branches as checkpoints, and discard losses until the streak reaches 100.
Камень, ножницы, бумага — Hackerlab
Overview
Камень, ножницы, бумага
Target: http://62.173.140.174:10500
This challenge looked like a simple Rock-Paper-Scissors streak game: reach 100 wins in a row and get the flag. The key bug was not in randomness, but in how the PHP application handled session state. Instead of updating one session in place, the server forked new sessions on non-tie outcomes, which allowed us to keep only favorable branches.
Recon
Initial recon showed that the target was a PHP web application, not a raw TCP service.
GET /returned the main Rock-Paper-Scissors page.- Frontend JavaScript sent requests to
/play.phpwithchoice=rock|paper|scissors. POST /play.phpreturned JSON in the format:
json read-only • copy allowed{"result":"You won!","botChoice":"scissors","wins":1}
One important behavioral detail was visible early: ties preserved the win counter, while losses reset it.
During testing, a single request revealed something unusual about session handling:
- Start with a parent
PHPSESSIDatwins=0. - Send
POST /play.phpwithchoice=rock. - Receive a winning response such as:
json read-only • copy allowed{"result":"You won!","botChoice":"scissors","wins":1}
- The response headers also contained:
http read-only • copy allowedSet-Cookie: PHPSESSID=<new_sid>; path=/
- Re-checking
GET /with the original session showed that the parent session still had its old state.
That was the clue that the server was not mutating the current session normally.
Failed Approaches
Before identifying the real issue, several natural web/PHP angles were tested and led nowhere:
- PRNG prediction based on the idea that the bot used predictable PHP randomness such as
mt_rand. - Source disclosure attempts using
.phps, backup files, and path tricks. - Parameter manipulation through cookies, headers, and alternate body formats.
- PHP-specific session tricks such as session injection and related tampering attempts.
These were dead ends because the challenge was not about predicting the bot or disclosing source code. The bug was entirely in the server's state transition logic.
Root Cause
The application implemented session updates incorrectly.
Instead of taking the current PHPSESSID and updating its wins counter in place, the backend behaved like this:
- On a win or loss, it returned a new
PHPSESSIDinSet-Cookie. - The original session remained unchanged.
- On a tie, no new useful branch was needed and the current session stayed valid.
So each non-tie request effectively created an immutable child checkpoint derived from the current session:
- parent session: unchanged
- child session: new state after the round
This produced a branching tree of sessions rather than one evolving session.
That design is catastrophic for a streak game, because it lets the attacker keep a safe checkpoint and only move forward when the branch is favorable.
Checkpoint behavior
Two observations confirmed the bug:
- A child session with
wins=1remained atwins=1even after multiple later requests were sent using that same SID, because those later requests created new child sessions rather than modifying the child itself. - Tie responses returned no beneficial new state and did not damage the current checkpoint, so the old session could simply be reused.
Exploitation
Once the session-fork logic was clear, exploitation became trivial.
Maintain exactly one session ID as the current checkpoint:
- Start with an initial
PHPSESSIDandwins=0. - Repeatedly send
choice=rockusing that checkpoint SID. - If the result is loss or tie, ignore the response and keep using the old checkpoint SID.
- If the result is win, read the new
PHPSESSIDfromSet-Cookieand adopt it as the next checkpoint. - Repeat until the checkpoint reaches
wins=100.
Because losses only poison the new child session, not the parent checkpoint, they can be discarded for free. The same is true for ties: they do not reduce progress, so the current checkpoint remains usable.
In other words, the exploit converts a difficult 100-win streak into repeated attempts to find one winning child from the current checkpoint, then moves forward only along that branch.
At wins=100, loading the main page with the final checkpoint SID revealed the flag.
Solver
The following script automates the checkpoint selection logic:
python read-only • copy allowed#!/usr/bin/env python3 import re import requests URL = "http://62.173.140.174:10500" def extract_sid(set_cookie): if not set_cookie or "PHPSESSID=" not in set_cookie: return None return set_cookie.split("PHPSESSID=", 1)[1].split(";", 1)[0] def get_flag(sid): r = requests.get(URL, cookies={"PHPSESSID": sid}, timeout=10) m = re.search(r'CODEBY\{[^}]+\}', r.text) return m.group(0) if m else None s = requests.Session() s.get(URL, timeout=10) current_sid = s.cookies.get("PHPSESSID") current_wins = 0 while current_wins < 100: r = requests.post( URL + "/play.php", data={"choice": "rock"}, cookies={"PHPSESSID": current_sid}, timeout=10, ) data = r.json() child_sid = extract_sid(r.headers.get("Set-Cookie")) if data["result"] == "You won!": current_sid = child_sid current_wins = data["wins"] print(get_flag(current_sid))