题目
题目是这样的
很明显是一道 PHP 反序列化的题目 , 直接来看题目给出的流程
-
首先判断当前是否存在 GET 参数 " var " , 若存在则对其进行 Base64 解码后存入
$var
变量 . 若不存在则输出当前页面源码 -
对 $var 进行一个正则过滤 , 若通过正则过滤 , 则对其进行反序列化操作 , 否则响应提示信息 .
题目中给出一个 Demo 类 , 需要注意一下其中三个魔术方法
-
__wakeup()
该方法是PHP反序列化时执行的第一个方法 , unserialize()会先检查是否存在
__wakeup()
方法 , 若存在则会先调用该方法 , 来预先准备对象需要的资源( 比如重新建立数据库连接 , 执行其他初始化操作等等 ) -
__construct()
与其它 OOP( 面向对象 ) 语言类似 , PHP中也存在构造方法 , 具有构造方法的类会在每次创建新对象前调用此方法 ,该方法常用于完成一些初始化工作 .
-
__destruct()
析构方法 , 当 某个对象的所有引用都被删除 或者 当对象被显式销毁 时 , 析构函数会被执行 .
有关 PHP 其它魔术方法的内容可以参考 PHP 官方文档
有关 PHP 反序列化漏洞的内容可以参考 PHP 反序列化漏洞
解题思路
回到题目中 , 看一看有哪些注意点
-
unserialize() 方法的参数来源于 GET 请求
虽然该请求获取的值经过一系列处理 ,包括一个Base64解码和一个正则过滤 , 但至少能确定该参数值是用户可控的 . 事实上这个正则过滤是可以绕过的 .
-
unserialize() 的
__wakeup()
方法在反序列化时 , PHP 会先执行
__wakeup()
函数 . 本题中__wakeup()
函数的作用为 : 将 $file 变量强制赋值为index.php
, 而题目又提示 flag 在fl4g.php
中 , 因此这又牵扯到一个老问题了 : 如何绕过__wakeup()
函数
然后就可以拿到 Flag 了 , 本题其实也就考了两个点 : 如何绕过正则表达式 以及 如何绕过 __wakeup()
方法 .
绕过正则表达式
先来看下序列化后字符串的内容是怎么样的 .
而正则匹配的规则是: 在不区分大小写的情况下 , 若字符串出现 "o:数字" 或者 "c:数字' 这样的格式 , 那么就被过滤 .
很明显 , 因为 serialize() 的参数为 object ,因此参数类型肯定为对象 " O " , 又因为序列化字符串的格式为 参数格式:参数名长度
, 因此 " O:4 " 这样的字符串肯定无法通过正则匹配
那么怎么办呢 ? 你可以参考 php反序列unserialize的一个小特性 , 我自己也下载了一份题目中版本的 PHP 源码来验证
题目中泄漏了 phpinfo 信息 , 可以用 dirsearch 扫到, 这里就交代下题目的环境为 PHP 5.3.10
-
先看
var_unserializer.c
文件 441 行序列化字符串的第一位为 " O " , 因此这里跳转到
yy13
.注意这里区分大小写 !
-
yy13
yy13
会判断下一位的字符是否为 " : " , 若是就跳转到yy17
, 若不是就跳转到yy3
, 这里会跳转到yy17
-
yy17
yy17
会判断下一位是否为数字 , 若为数字就跳转到yy20
, 若为 " + " 号就跳转到yy19
-
yy19
yy19
会判断下一位是否为数字 , 若为数字就跳转到yy20
, 否则跳转到yy18
问题来了! 当反序列化操作读取到 " : " 号时 , 下面不管是 " 数字 " 还是 " +数字 " , 都会跳转到 yy20
, 而正则匹配的规则能过滤 O:4
, 却不会过滤 O:+4
.
因此 , 我们可以利用 O:+4
这样的写法来绕过正则过滤 .
值得一提的是 : 该利用方式仅能在 PHP 5 中复现 , 在 PHP7 中 , yy17 的规则被修改了 , 因此 " + " 号无法再被利用了
php 7.3.9 var_unserializer.c
文件 783 行
yy17
已不再识别 " + " 号~
绕过 __wakeup()
函数
这也是一个老问题了 , 具体可以参考 CVE-2016-7124
来看 var_unserializer.re
文件 371 行
object_common2() 函数使用 call_user_function_ex(CG(function_table), rval, &fname, &retval_ptr, 0, 0, 1, NULL TSRMLS_CC)
来调用 __wakeup()
函数 , 但在执行 call_user_function_ex()
函数前 , 需要通过一个条件判断 .
process_nested_data()
函数用于对象的属性检查 , 那么这个对象是何时创建的呢 ? 来看 var_unserializer.re
文件第 359 行
在 object_common1()
中 , 调用 object_init_ex(*rval, ce)
函数创建并返回了该对象 .
流程也就是这样的 : 创建对象之后 , 对对象的属性检查 , 若属性检查通过 , 就调用 __wakeup()
方法
若对象属性检查不通过 , 则会跳出 object_common2()
函数 , 不再调用 __wakeup()
函数 . 由于对象及其属性在 object_common1()
中已经被创建 , 因此这里对象将会被销毁 , 从而触发析构函数__destruct()
.
因此这里我们仅需要破坏对象属性检查就可以绕过 __wakeup()
函数 , 最简单的方法就是增大对象属性的个数 , 使其饭序列化异常 .
PHP 7 中这部分代码被修改 ,无法再用该方式绕过 __wakeup()
方法
构造 Exp
现在两个考点都已经解决了 , 构造 Exp 变得非常简单
Exp : TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
然后就拿到了 Exp , 将其作为 var 变量的参数提交即可拿到 Flag
一个注意点
这里说一个需要注意的点 :
最开始的我是先把序列化后的字符串输出 , 然后手工添加 " + " 号和破坏对象属性 , 最后再对其 Base64 编码后提交 , 但是始终拿不到 Flag
翻看了一会儿以前的笔记 , 突然发现了这个知识点
不同属性的对象序列化后字符格式是不一样的
Private属性 : 数据类型:属性名长度:"\00类名\00属性名";数据类型:属性值长度:"属性值";
Protected属性 : 数据类型:属性名长度:"\00*\00属性名";数据类型:属性值长度:"属性值";
Public属性 : 数据类型:属性名长度:"属性名";数据类型:属性值长度:"属性值";
本题中就有一个 Private 对象 , 会不会在复制粘贴时破坏了 " \00 " 这个特殊字符呢 ? 可以实验一下~
果然 , 输出到命令行的序列化字符串格式已经被破坏 , 因此必须要在脚本中直接构造出完整的 Exp
转载于https://www.guildhab.top/?p=990