内容纲要

刚才做了一道 PHP 反序列化的题目 , 题目虽简单但考察的点挺经典的 , 这里写个总结记录一下

题目

题目是这样的

很明显是一道 PHP 反序列化的题目 , 直接来看题目给出的流程

  1. 首先判断当前是否存在 GET 参数 " var " , 若存在则对其进行 Base64 解码后存入 $var 变量 . 若不存在则输出当前页面源码

  2. 对 $var 进行一个正则过滤 , 若通过正则过滤 , 则对其进行反序列化操作 , 否则响应提示信息 .

题目中给出一个 Demo 类 , 需要注意一下其中三个魔术方法

  • __wakeup()

    该方法是PHP反序列化时执行的第一个方法 , unserialize()会先检查是否存在 __wakeup() 方法 , 若存在则会先调用该方法 , 来预先准备对象需要的资源( 比如重新建立数据库连接 , 执行其他初始化操作等等 )

  • __construct()

    与其它 OOP( 面向对象 ) 语言类似 , PHP中也存在构造方法 , 具有构造方法的类会在每次创建新对象前调用此方法 ,该方法常用于完成一些初始化工作 .

  • __destruct()

    析构方法 , 当 某个对象的所有引用都被删除 或者 当对象被显式销毁 时 , 析构函数会被执行 .

有关 PHP 其它魔术方法的内容可以参考 PHP 官方文档

有关 PHP 反序列化漏洞的内容可以参考 PHP 反序列化漏洞


解题思路

回到题目中 , 看一看有哪些注意点

  1. unserialize() 方法的参数来源于 GET 请求

    虽然该请求获取的值经过一系列处理 ,包括一个Base64解码和一个正则过滤 , 但至少能确定该参数值是用户可控的 . 事实上这个正则过滤是可以绕过的 .

  2. 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

  1. 先看 var_unserializer.c 文件 441 行

    序列化字符串的第一位为 " O " , 因此这里跳转到 yy13 .

    注意这里区分大小写 !

  2. yy13

    yy13 会判断下一位的字符是否为 " : " , 若是就跳转到 yy17 , 若不是就跳转到 yy3 , 这里会跳转到 yy17

  3. yy17

    yy17 会判断下一位是否为数字 , 若为数字就跳转到 yy20 , 若为 " + " 号就跳转到 yy19

  4. 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 " 这个特殊字符呢 ? 可以实验一下~

  1. 将序列化后的字符串直接存入文件

  2. 将序列化后的字符串复制粘贴存入文件

  3. vim 查看 a.txt 文件

果然 , 输出到命令行的序列化字符串格式已经被破坏 , 因此必须要在脚本中直接构造出完整的 Exp


总结

本题其实没什么难点 , 主要就是绕过正则过滤以及绕过 __wakeup() 函数

最后修改日期:2020年3月9日

作者

留言

支持,讲的非常详细

讲得非常详细,谢谢

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。