摘要

反序列化

反序列化是将之前序列化的数据重新还原为其原始数据结构或对象的过程。在PHP中,您可以使用unserialize​函数来执行反序列化操作。

以下是一个基本的PHP反序列化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建一个关联数组
$data = array(
'name' => 'John',
'age' => 30,
);

// 将数组序列化为字符串
$serializedData = serialize($data);

// 反序列化字符串以恢复原始数组
$unserializedData = unserialize($serializedData);

// 打印反序列化后的数据
print_r($unserializedData);

在这个示例中,我们首先创建了一个关联数组$data​,然后使用serialize​函数将其序列化为字符串。接下来,我们使用unserialize​函数将字符串反序列化为原始数组,并最后打印出反序列化后的数据。

需要注意的是,反序列化操作具有潜在的安全风险,特别是当您从不受信任的来源接收序列化数据时。恶意用户可以构造特殊的序列化字符串,以触发不安全的操作,例如执行恶意代码。因此,在处理反序列化数据时,务必采取适当的安全措施,例如只接受信任的数据,或使用unserialize​后进行数据验证和过滤。

魔术方法

在PHP中,当对象被反序列化时,如果对象类中定义了特定的魔术方法,这些魔术方法可以被调用。以下是一些与反序列化相关的魔术方法:

  1. __wakeup(): 当对象被反序列化时,如果该对象类中定义了__wakeup()​方法,该方法将会在反序列化后被自动调用。您可以在__wakeup()​方法中执行任何必要的初始化或修复操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
public $data;

public function __wakeup() {
// 在反序列化后执行的操作
$this->data = "Data has been unserialized.";
}
}

$serializedData = serialize(new MyClass());
$unserializedObject = unserialize($serializedData);

echo $unserializedObject->data; // 输出 "Data has been unserialized."
  1. __construct(): 构造函数(__construct()​)也可以用于在对象被反序列化后进行初始化操作,不过在反序列化时,__construct()​方法通常不会被调用,而是__wakeup()​方法被调用。
  2. __sleep(): 在对象被序列化之前,如果对象类中定义了__sleep()​方法,该方法将会在序列化前被自动调用。您可以在__sleep()​方法中指定要序列化的属性列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public $data1;
public $data2;

public function __sleep() {
// 返回一个属性名数组,只有这些属性会被序列化
return ['data1'];
}
}

$obj = new MyClass();
$obj->data1 = 'Value 1';
$obj->data2 = 'Value 2';

$serializedData = serialize($obj);

// 只有$data1属性被序列化

请注意,__sleep()​和__wakeup()​方法是用于在序列化和反序列化对象时进行自定义操作的重要工具,但也需要小心使用,确保它们不会引入安全漏洞。

字符逃逸

此类题目的本质就是改变序列化字符串的长度,导致反序列化漏洞
这种题目有个共同点:

  1. php序列化后的字符串经过了替换或者修改,导致字符串长度发生变化。
  2. 总是先进行序列化,再进行替换修改操作。

第一种情况:替换修改后导致序列化字符串变长

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
// echo serialize($AA)."\n";$res=filter(serialize($AA));

$c=unserialize($res);
echo $c->pass;
?>

以上面代码为例,如何在不直接修改$pass​值的情况下间接修改$pass​的值。
这段代码的流程是,先序列化代码,然后将里面不希望出现的字符bb替换成自定义的字符串ccc。然后进行反序列化,最后输出pass变量。

要解决上面这个问题,先来看一下php序列化代码的特征。

我们可以看到,反序列化字符串都是以一";}​结束的,所以如果我们把";}​带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。

此时的name所读取的数据为aaaa”而正常的语法是需要用”;去闭合当前的变量,而因为长度错误所以此时php把闭合的双引号当做了字符串,所以下一个字符就成了分号,没能闭合导致抛出了错误。

把精力回到开头所说的代码,根据刚才讲的,如果我们将name变量中添加bb则程序就会报错,因为bb将被filter函数替换成ccc,ccc的长度比bb多1,这样前面的s所代表的长度为2但是内容却变长了,成了ccc。如下:

可见在序列化后的字符串在经过filter函数过滤前,s为6,内容为aaaabb;经过filter过滤后,s仍然为6,但内容变为了aaaaccc,长度变成了7,根据反序列化读取变量的原则来讲,此时的name能读取到的只是aaaacc,末尾处的那个c是读取不到的,这就形成了一个字符串的逃逸。当我们添加多个bb,每添加一个bb我们就能逃逸一个字符,那我们将逃逸的字符串的长度填充成我们要反序列化的代码长度的话那就可以控制反序列化的结果以及类里面的变量值了。

假如我们要在name处改为上一个";s:4:"pass";s:6:"hacker";}​来间接修改pass的值,如果我们只是单纯的把它加进去的话,就像下面这样:

1
2
3
4
class A{
public $name='";s:4:"pass";s:6:"hacker";}';
public $pass='123456';
}

由于$name​被序列化后的长度是固定的,在反序列化后$name​仍然为";s:4:"pass";s:6:"hacker";}​,$pass​仍然为123456​:

这里的关键点在于filter函数,这个函数检测并替换了非法字符串,看似增加了代码的安全系数,实则让整段代码更加危险。filter函数中检测序列化后的字符串,如果检测到了非法字符’bb’,就把它替换为’ccc’。
此时我们发现";s:4:"pass";s:6:"hacker";}的长度为27,如果我们再加上27个bb,那最终的长度将增加27,不就能逃逸后面的";s:4:"pass";s:6:"hacker";}了吗?如下:

可见,成功逃逸,成功修改了pass的值。
具体分析如下:

逃逸或者说被 “顶” 出来的payload就会被当做当前类的属性被继续执行。

第二种情况——替换之后导致序列化字符串变短

实验代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function str_rep($string){
return preg_replace( '/php|test/','', $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>');
?>

输入name和sign,number值是固定的’2020’,经过 序列化-->敏感字替换为空(长度变短)-->反序列化​ 的过程之后再输出结果。

接下来利用漏洞,通过输入name和sign来间接修改number的值:
我们要修改number的值,就要在sign中加入";s:6:"number";s:4:"2020";}​,其长度为27

但是就这样硬生生的加进去是不行的,我们要进一步构造一下。
payload:?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}

这样,就将sign和number的值都修改了。原因分析:
在str_rep函数中如果检测到’php’、’test’关键字就把其替换为空,那么就利用这一点,我们故意输入敏感字符,替换为空之后来实现字符逃逸。我们在name中输入了输入了6个test,替换为空后这样就腾出了24个字符的空间,正好包含进了”;s:4:”sign”;s:54:”hello,由于”;s:4:”sign”;s:54:”hello成了name的内容,所以我们还要在后面加个”;s:4:”sign”;s:4:”eval作为sign序列化的内容。

__toString()方法的调用