-
-
[原创]php(phar)反序列化漏洞及各种绕过姿势
-
发表于: 2022-9-19 20:20 5573
-
序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。简单来说就是我在一个地方构造了一个类,但我要在另一个地方去使用它,那怎么传过去呢?于是就想到了序列化这种东西,将对象先序列化为一个字符串(数据),后续需要使用的时候再进行反序列化即可得到要使用的对象,十分方便。
来看看官方手册怎么说:
所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
为了能够unserialize()一个对象,这个对象的类必须已经定义过。如果序列化类A的一个对象,将会返回一个跟类A相关,而且包含了对象所有变量值的字符串。 如果要想在另外一个文件中反序列化一个对象,这个对象的类必须在反序列化之前定义,可以通过包含一个定义该类的文件或使用函数spl_autoload_register()来实现。
php 将数据序列化和反序列化会用到两个函数
serialize()
将对象格式化成有序的字符串
unserialize()
将字符串还原成原来的对象
序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。
注意:php中创建一个对象和反序列化得到一个对象是有所不同的,例如创建一个对象一般会优先调用 __construct()
方法 ,而反序列化得到一个对象若 存在 __wakeup()
方法则会优先调用它而不去执行 __construct()
。
基本上每个编程语言都有各自的序列化和反序列化方式,格式也各不相同
像有
输出
输出的这一串序列表示的是什么呢
a:3:{i:0;s:2:"aa";i:1;s:2:"bb";s:2:"cc";s:2:"dd";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标(O代表Object,也是类)
s:代表是字符串,后面的2是因为aa长度为2,是字符串长度值
后面类推
同时要注意序列化后只有成员变量,没有成员函数
注意如果变量前是protected,则会在变量名前加上\x00*\x00
,private则会在变量名前加上\x00类名\x00
,输出时一般需要url编码,如下:
直接输出输出则会导致不可见字符\x00
的丢失
详细用法请参考 官方文档
wakeup()魔术方法在执行unserialize()
时,会优先调用这个函数,而不会执行`construct()` 函数
绕过方法:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
如:
其序列化后为 O:4:"test":1:{s:1:"a";s:3:"abc";}
执行反序列化 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
得到结果为 def
,发现优先执行了 __wakeup()
,并没有执行 __construct()
当我们把对象的属性个数改大时,改成 O:4:"test":2:{s:1:"a";s:3:"abc";}
,由原来的1个属性改为2个,但test
类真实的属性只有一个,这样就能绕过 __wakeup()
按没有这个魔术方法一样去执行其他相应的魔术方法。
执行反序列化 unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
得到结果为 abc
, 发现优先执行了 __construct()
,并没有执行 __wakeup()
__destruct
是PHP对象的一个魔术方法,称为析构函数,顾名思义这是当该对象被销毁的时候自动执行的一个函数。其中以下情况会触发__destruct
除此之外,PHP还拥有垃圾回收Garbage collection即我们常说的GC机制。
PHP中GC使用引用计数和回收周期自动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调用函数的__destruct
。
刚才我们提到了引用计数,其实当一个对象没有任何引用的时候,则会被视为“垃圾”,即
test
对象被 变量 a
引用, 所以该对象不是“垃圾”,而如果是这样
或这样
这样在 test
在没有被引用或在失去引用时便会被当作“垃圾”进行回收
如:
输出
这里是当a
第二次赋值时,test('2')
失去引用,执行__destruct
,然后执行echo
,当程序完了后test('3')
销毁,执行它的__destruct
举个栗子:
这里我们要求输出 success!!
,但执行反序列化后得到的对象有了引用,给了 a
变量,后面程序接着就抛出一个异常,非正常结束,导致未正常完成 GC 机制,即没有执行 __destruct
。
直接构造反序列化 test 类得到
所以我们要反序列化手动去 “销毁” 创造的对象。这里我们可以利用数组来完成。构造
得到
传入,成功得到 success!!
我们序列化一个数组对象,考虑反序列化本字符串,因为反序列化的过程是顺序执行的,所以到第一个属性时,会将Array[0]
设置为对象,同时我们又将Array[0]
设置为null
,这样前面的test
对象便丢失了引用,就会被GC所捕获,就可以执行__destruct
了。
如preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头
绕过方法
如下,要求输出 you success,但构造的序列化字符串中不能由 aaa
可以利用引用进行绕过
构造引用使得 $b 和 $a 地址相同从而绕过检测,达成要求
序列字符串中表示字符类型的s大写时,会被当成16进制解析。
举个栗子:
输出
有关phar的基本介绍及利用方法我以前有过总结 , 参考链接
同时应该重点关注官方文档中有关 phar 的介绍 (一定要关注官方文档,官方文档yyds)
这里重点对我以前的总结进行补充
ps:注意phar中存储的对象反序列化之后会被phar对象的metadata属性引用
过滤了__HALT_COMPILER();
参考 https://guokeya.github.io/post/uxwHLckwx (原理)
姿势1:
linux下使用命令gzip phar.phar
生成
姿势2:
对于某些情况,我们需要修改phar文件中的内容而达到某些需求(比如要绕过__wakeup
要修改属性数量),而修改后的phar文件由于文件发生改变,所以须要修改签名才能正常使用,官方文档中是这么说:
Phars containing a signature always have the signature appended to the end of the Phar archive after the loader, manifest, and file contents. The signature formats supported at this time are MD5, SHA1, SHA256, SHA512, and OPENSSL.
用winhex或010-editor查看phar文件签名类型(以上述代码生成的phar文件为例)
以默认的sha1签名为例:
运行脚本得到的 newPhar.phar
就可以正常使用啦
题目来源:NSSCTF Round#4 Team-1zweb(revenge)
前面任意文件读取拿到源码就不说了,重点看它这个phar反序列化
index.php:
upload.php:
分析源码可知ban掉了 __HALT_COMPILER();
标识,没有这个是不认phar的,这个可以使用gzip压缩进行绕过
__wakeup
修改序列属性个数即可绕过,注意修改完phar文件后还需重新签名,用上文的脚本即可
生成phar:
修改签名并上传文件,访问后触发phar反序列化拿到flag
exp:
运行得到flag
ps:注意如果用burp代理抓包上传gzip压缩后的phar可能会有bug,burp会改变gzip数据,导致无法识别phar文件
<?php
$arr
=
array(
'aa'
,
'bb'
,
'cc'
=
>
'dd'
);
$serarr
=
serialize($arr);
echo $serarr;
var_dump($arr);
<?php
$arr
=
array(
'aa'
,
'bb'
,
'cc'
=
>
'dd'
);
$serarr
=
serialize($arr);
echo $serarr;
var_dump($arr);
a:
3
:{i:
0
;s:
2
:
"aa"
;i:
1
;s:
2
:
"bb"
;s:
2
:
"cc"
;s:
2
:
"dd"
;}
array(
3
) {
[
0
]
=
> string(
2
)
"aa"
[
1
]
=
> string(
2
)
"bb"
[
"cc"
]
=
> string(
2
)
"dd"
}
a:
3
:{i:
0
;s:
2
:
"aa"
;i:
1
;s:
2
:
"bb"
;s:
2
:
"cc"
;s:
2
:
"dd"
;}
array(
3
) {
[
0
]
=
> string(
2
)
"aa"
[
1
]
=
> string(
2
)
"bb"
[
"cc"
]
=
> string(
2
)
"dd"
}
<?php
class
test {
protected $name;
private $
pass
;
function __construct($name, $
pass
) {
$this
-
>name
=
$name;
$this
-
>
pass
=
$
pass
;
}
}
$a
=
new test(
'pankas'
,
'123'
);
$seria
=
serialize($a);
echo $seria.
'<br/>'
;
echo urlencode($seria);
<?php
class
test {
protected $name;
private $
pass
;
function __construct($name, $
pass
) {
$this
-
>name
=
$name;
$this
-
>
pass
=
$
pass
;
}
}
$a
=
new test(
'pankas'
,
'123'
);
$seria
=
serialize($a);
echo $seria.
'<br/>'
;
echo urlencode($seria);
O:
4
:
"test"
:
2
:{s:
7
:
"*name"
;s:
6
:
"pankas"
;s:
10
:
"testpass"
;s:
3
:
"123"
;}
O
%
3A4
%
3A
%
22test
%
22
%
3A2
%
3A
%
7Bs
%
3A7
%
3A
%
22
%
00
%
2A
%
00name
%
22
%
3Bs
%
3A6
%
3A
%
22pankas
%
22
%
3Bs
%
3A10
%
3A
%
22
%
00test
%
00pass
%
22
%
3Bs
%
3A3
%
3A
%
22123
%
22
%
3B
%
7D
O:
4
:
"test"
:
2
:{s:
7
:
"*name"
;s:
6
:
"pankas"
;s:
10
:
"testpass"
;s:
3
:
"123"
;}
O
%
3A4
%
3A
%
22test
%
22
%
3A2
%
3A
%
7Bs
%
3A7
%
3A
%
22
%
00
%
2A
%
00name
%
22
%
3Bs
%
3A6
%
3A
%
22pankas
%
22
%
3Bs
%
3A10
%
3A
%
22
%
00test
%
00pass
%
22
%
3Bs
%
3A3
%
3A
%
22123
%
22
%
3B
%
7D
__construct()
/
/
类的构造函数,创建类对象时调用
__destruct()
/
/
类的析构函数,对象销毁时调用
__call()
/
/
在对象中调用一个不可访问方法时调用
__callStatic()
/
/
用静态方式中调用一个不可访问方法时调用
__get()
/
/
获得一个类的成员变量时调用
__set()
/
/
设置一个类的成员变量时调用
__isset()
/
/
当对不可访问属性调用isset()或empty()时调用
__unset()
/
/
当对不可访问属性调用unset()时被调用。
__sleep()
/
/
执行serialize()时,先会调用这个函数
__wakeup()
/
/
执行unserialize()时,先会调用这个函数,执行后不会执行__construct()函数
__toString()
/
/
类被当成字符串时的回应方法
__invoke()
/
/
调用函数的方式调用一个对象时的回应方法
__set_state()
/
/
调用var_export()导出类时,此静态方法会被调用。
__clone()
/
/
当对象复制完成时调用
__autoload()
/
/
尝试加载未定义的类
__debugInfo()
/
/
打印所需调试信息
__construct()
/
/
类的构造函数,创建类对象时调用
__destruct()
/
/
类的析构函数,对象销毁时调用
__call()
/
/
在对象中调用一个不可访问方法时调用
__callStatic()
/
/
用静态方式中调用一个不可访问方法时调用
__get()
/
/
获得一个类的成员变量时调用
__set()
/
/
设置一个类的成员变量时调用
__isset()
/
/
当对不可访问属性调用isset()或empty()时调用
__unset()
/
/
当对不可访问属性调用unset()时被调用。
__sleep()
/
/
执行serialize()时,先会调用这个函数
__wakeup()
/
/
执行unserialize()时,先会调用这个函数,执行后不会执行__construct()函数
__toString()
/
/
类被当成字符串时的回应方法
__invoke()
/
/
调用函数的方式调用一个对象时的回应方法
__set_state()
/
/
调用var_export()导出类时,此静态方法会被调用。
__clone()
/
/
当对象复制完成时调用
__autoload()
/
/
尝试加载未定义的类
__debugInfo()
/
/
打印所需调试信息
<?php
class
test{
public $a;
public function __construct(){
$this
-
>a
=
'abc'
;
}
public function __wakeup(){
$this
-
>a
=
'def'
;
}
public function __destruct(){
echo $this
-
>a;
}
}
<?php
class
test{
public $a;
public function __construct(){
$this
-
>a
=
'abc'
;
}
public function __wakeup(){
$this
-
>a
=
'def'
;
}
public function __destruct(){
echo $this
-
>a;
}
}
$a
=
new test();
$a
=
new test();
new test();
new test();
$a
=
new test();
$a
=
1
;
$a
=
new test();
$a
=
1
;
<?php
class
test{
function __construct($i) {$this
-
>i
=
$i; }
function __destruct() { echo $this
-
>i.
"Destroy...\n"
; }
}
new test(
'1'
);
$a
=
new test(
'2'
);
$a
=
new test(
'3'
);
echo
"————————————<br/>"
;
<?php
class
test{
function __construct($i) {$this
-
>i
=
$i; }
function __destruct() { echo $this
-
>i.
"Destroy...\n"
; }
}
new test(
'1'
);
$a
=
new test(
'2'
);
$a
=
new test(
'3'
);
echo
"————————————<br/>"
;
1Destroy
...
2Destroy
...
————————————
3Destroy
...
1Destroy
...
2Destroy
...
————————————
3Destroy
...
<?php
class
test {
function __destruct()
{
echo
'success!!'
;
}
}
if
(isset($_REQUEST[
'input'
])) {
$a
=
unserialize($_REQUEST[
'input'
]);
throw new Exception(
'lose'
);
}
<?php
class
test {
function __destruct()
{
echo
'success!!'
;
}
}
if
(isset($_REQUEST[
'input'
])) {
$a
=
unserialize($_REQUEST[
'input'
]);
throw new Exception(
'lose'
);
}
class
test {}
$a
=
serialize(array(new test, null));
echo $a.
'<br/>'
;
$a
=
str_replace(
':1'
,
':0'
, $a);
/
/
将序列化的数组下标为
0
的元素给为null
echo $a;
class
test {}
$a
=
serialize(array(new test, null));
echo $a.
'<br/>'
;
$a
=
str_replace(
':1'
,
':0'
, $a);
/
/
将序列化的数组下标为
0
的元素给为null
echo $a;
a:
2
:{i:
0
;O:
4
:
"test"
:
0
:{}i:
1
;N;}
a:
2
:{i:
0
;O:
4
:
"test"
:
0
:{}i:
0
;N;}
/
/
最终payload
a:
2
:{i:
0
;O:
4
:
"test"
:
0
:{}i:
1
;N;}
a:
2
:{i:
0
;O:
4
:
"test"
:
0
:{}i:
0
;N;}
/
/
最终payload
<?php
class
test{
public $a;
public function __construct(){
$this
-
>a
=
'abc'
;
}
public function __destruct(){
echo $this
-
>a.PHP_EOL;
}
}
function match($data){
if
(preg_match(
'/^O:\d+/'
,$data)){
die(
'nonono!'
);
}
else
{
return
$data;
}
}
$a
=
'O:4:"test":1:{s:1:"a";s:3:"abc";}'
;
/
/
+
号绕过
$b
=
str_replace(
'O:4'
,
'O:+4'
, $a);
unserialize(match($b));
/
/
将对象放入数组绕过 serialize(array($a));
unserialize(
'a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}'
);
<?php
class
test{
public $a;
public function __construct(){
$this
-
>a
=
'abc'
;
}
public function __destruct(){
echo $this
-
>a.PHP_EOL;
}
}
function match($data){
if
(preg_match(
'/^O:\d+/'
,$data)){
die(
'nonono!'
);
}
else
{
return
$data;
}
}
$a
=
'O:4:"test":1:{s:1:"a";s:3:"abc";}'
;
/
/
+
号绕过
$b
=
str_replace(
'O:4'
,
'O:+4'
, $a);
unserialize(match($b));
/
/
将对象放入数组绕过 serialize(array($a));
unserialize(
'a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}'
);
<?php
class
test {
public $a;
public $b;
public function __construct(){
$this
-
>a
=
'aaa'
;
}
public function __destruct(){
if
($this
-
>a
=
=
=
$this
-
>b) {
echo
'you success'
;
}
}
}
if
(isset($_REQUEST[
'input'
])) {
if
(preg_match(
'/aaa/'
, $_REQUEST[
'input'
])) {
die(
'nonono'
);
}
unserialize($_REQUEST[
'input'
]);
}
else
{
highlight_file(__FILE__);
}
<?php
class
test {
public $a;
public $b;
public function __construct(){
$this
-
>a
=
'aaa'
;
}
public function __destruct(){
if
($this
-
>a
=
=
=
$this
-
>b) {
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)