php反序列化与POP链
0x00 前言
php反序列化是很久之前就接触的漏洞,但是一直都没有深入的学习,只是知道一个大概,POP链的构造也不是很熟练,于是今天总结一下。本文将详细介绍php反序列化原理,为什么有的时候序列的payload无效,POP链的构造以及Session的反序列化下一节会具体学习如何利用phar协议扩展php反序列化的攻击面。
0x01 反序列化基本知识
1.1 序列化与反序列化
序列化:将变量(通常是数组和对象)转换为可保存或传输的字符串
反序列化:在适当的时候把这个字符串再转化成原来的变量(通常是数组和对象)使用。
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。反序列化本身不是漏洞,但如果反序列化的内容可控,就容易导致漏洞。
1.2 php魔术方法
PHP提供了许多“魔术”方法,这些方法由两个下划线前缀(__)标识。它们充当拦截器,在满足某些条件时会自动调用它们。 魔术方法提供了一些极其有用的功能。
常见的魔术方法有:
__contruct()
当一个对象创建时被调用__destruct()
当一个对象销毁前被调用__sleep()
在对象被序列化前被调用__wakeup
将在反序列化之后立即被调用__toString
当一个对象被当做字符串使用时被调用__get()
,__set()
当调用或设置一个类及其父类方法中未定义的属性时__invoke()
调用函数的方式调用一个对象时的回应方法__call
和__callStatic
前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。
通过调试下面这个程序,会对魔术方法的调用有更直观的认识,强烈建议单步调试一遍。
|
1.3 序列化后的字符串形式
一个序列化的字符串:
//O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;} |
另外,注意到不同权限的属性,序列化之后的字符串存在区别:
public
|
可以看到,public的属性,序列化后的值就是属性的名称和对应的值
private
换成private权限,属性在序列化后也会出现区别,用010editor容易看出。
|
属性名变成了%00Test%00test
和%00Test%00var
也就是%00类名%00属性名
protected
换成protected, 属性序列化之后又变了,属性名变成了%00*%00test
和%00*%00var
也就是%00*%00属性名
注意到这些对构造序列化的字符串很关键,当我们直接将private protected
的属性进行序列化,得到的序列化字符串的payload将无效,因为0x00
的缘故。但是通过urlencode
就可以避免这种当时可能会看起来莫名其妙的”bug“(个人经验==、)。
0x02 php反序列化漏洞
反序列化本身不是漏洞,但是如果类的某些属性可控,那么在反序列的过程中就会自动的执行魔术方法,从而导致安全问题。
所以,通常反序列化漏洞的成因在于代码中的 __unserialize()
,__wakeup()
等魔术方法接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
下面举一个简单的例子:
|
上面的代码是接收一个参数a
,然后将其反序列化,反序列化后,会调用__wakeup()
方法。如果一切正常的话,这个方法是显示一下demo.php
文件的源代码。但是参数a
是可控的,也就是说对象a
的属性是可控的。于是我们可以伪造一个filename
来构造对象。
EXP
|
可以看到,当我们对象参数可控时,可以伪造对象的一些属性,从而实现任意文件读取等操作。
正如,之前所说, 如果我们没有urlencode
,就会得到一个无效的payload:
O:7:"popdemo":1:{s:17: |
0x03 POP链的构造
2.1 什么是POP链
玩过 pwn 的同学应该对 ROP 并不陌生,ROP 的全称是面向返回编程(Return-Oriented Programing),ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,将这些本来无害的片段拼接起来,形成一个连续的层层递进的调用链,最终达到我们的执行 libc 中函数或者是 systemcall 的目的
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的
说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的
我的理解是:构造一个完整的调用链,该调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。
2.2 用一个实例说明如何构造POP链
|
- 先看读文件的函数在哪:
Read.file_get
里面有一个file_get_contents
Show._show()
中有一个highlight_file
- 我们可控的是
hello
参数,调用unserialize()
函数,即__wakeup()
魔术方法,于是就只有Show类
中存在该方法,但是注意到在Show.__wakeup()
中存在一个正则匹配,这个正则匹配会将$this->source
当成字符串来处理。也就是说会调用Show.__toString()
方法。 - 定位到
Show.__toString()
,可以将source
序列化为Show类的对象,就会调用__toString
方法。__toString
又会取一个str['str']->source
,那么如果这个source
不存在的话,就会执行__get()
方法。 __get()
魔术方法会调用一个$p变量
,这个也是可控的,然后会将p当做函数调用,此时触发了Read.__invoke()
魔术方法__invoke魔术方法
会触发file_get()函数
,进而base64_encode(file_get_contents($value))
最终达到读文件的目的。
这样一条完整的链就分析完了:
hello -> __wakeup -> Show._show -> Show.__toString -> (不存在属性)Test.__get() -> Read.__invoke |
注意对象关系(hello是Show的对象,source属性是Test的对象,p属性是Read的对象),然后写一个POP链的对应EXP,就可以了:
|
0x03 php的Session反序列化问题
3.1 PHP的Session存储机制
php.ini
有一下配置项用于控制Session有关的设置:
session.save_path="D:\xampp\tmp" 表明所有的session文件都是存储在xampp/tmp下 |
PHP中有多种session的序列话引擎,当我设置session为$_SESSION["name"] = "V0W";
时。不同的引擎保存的session文件内容如下:
php: |
切换不同引擎使用的函数:
ini_set('session.serialize_handler', '调用引擎');
|
另外文件名,其实是PHPSESSIONID
的值
3.2 PHP的Session反序列化漏洞原理
如果在PHP在反序列化存储的$_SESSION
数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确地反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞。
用原文的一个例子:
session.php
|
session2.php
|
session.php中的Session是可控的,但是反序列的魔术方法在session2.php中,而session中的参数无法直接可控。
这个时候,就可以利用两个的php的session存储机制的不同实现session的反序列化攻击。
具体说:
将payload用session.php,控制存储在指定文件中。
session.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
此时传入的数据会按照php_serialize来进行序列化,并存储到文件中。
再访问session2.php,页面输出
spoock
,成功执行我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据(因为同域PHPSESSIONID
是一样的,之前存的session也适用),此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。可以单步调试一下,更容易理解这两个过程。
3.3 更进一步的Session反序列化利用
上述的利用达到了攻击目的,但是,局限性比较大,我们看一下条件:
- 两个文件session引擎配置不同
- 其中一个session可控
- 两个文件同域
如何更进一步的利用,或者较少限制的利用Session反序列化呢?
在有趣的php反序列化总结中介绍了另一种Session反序列化漏洞的利用方式。
当PHP中session.upload_progress.enabled
打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION
中。phpbugs详情(还有老外的讨论也可以看一下)
看一下这个漏洞(我为其命名:上传程序Session漏洞)出现的条件:
session.upload_progress.enabled = On
(是否启用上传进度报告)session.upload_progress.cleanup = Off
(是否上传完成之后删除session文件)
符合条件时,上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name
同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS
),PHP检测到这种同名请求会在$_SESSION
中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。
3.4 实例
用jarvisoj上一个题目作为实例,题目链接
|
容易发现,OowoO.__destruct()
存在代码执行,但是没有可控参数进行利用。
然后发现符合上传程序Session漏洞的条件:
接下来就是如何利用的问题了,我们知道这个漏洞出在上传时的Session存储问题上,所以我们可以利用上传来写入。
先自己写一个简单的上传页面upload.html:
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> |
poc.php
|
注意到phpinfo中,禁用了exec,system
等函数,注意用print_r
绕过。
再从phpinfo中的SCRIPT_FILENAME
字段得到根目录地址:/opt/lampp/htdocs/
,构造得到payload:
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";} |
0x04 反序列化的防御
因为反序列化的缺陷可能导致远程代码执行等严重的攻击,所以我们需要对其进行防护:
- 对传入
unserilize()
的参数,进行严格地过滤。 - 在文件系统函数的参数可控时,进行严格地过滤。
- 严格检查上传文件内容,不能只是单纯地检查文件头(phar)
- 条件允许的情况下,禁用可执行系统命令、代码的危险函数。
- 注意不同类中的同名方法的编写,避免被用作反序列化的跳板。
- Session方面,一个是多文件间使用一种序列化引擎;二是尽量不要让session可控;三是保持
session.upload_progress.cleanup = On
(上传完成之后删除session文件)
0xff 参考链接
- 一篇文章带你深入理解漏洞之 PHP 反序列化漏洞 2018,11 k0rn3n
- PHP反序列化进阶学习与总结, Threezh1, 先知社区
- PHP中SESSION反序列化机制, Spoock
- 有趣的php反序列化总结
- php反序列化 2019,04 llfam