php反序列化使用不当造成的漏洞

前言:

  这几天进入了一个疯狂的划水期,作为一个十成十的菜鸟,跟着学长们见识一下CTF的乐趣所在,还真别说,真香。。。。原本是决定借这一次提高一下自己,结果被虐得体无完肤,特别是WEB方面,反序列化这一块就让我很难受,所以准备写这一篇博客纪念一下自己的菜。。。。。

参考文献:Amber小姐姐的讲解视频

正文:

  献上一波自己对序列化和反序列化的理解:php中,序列化就是将一个类的对象转换成一个有一定序列格式的字符串,反之,反序列化就是将这一串有一定序列格式的字符串转换成一个类的对象的过程。其中,php中序列化对应的原型函数为string serialize ( mixed $value ),反序列化对应的原型函数为string unserialize ( mixed $value )

ps:对于一个类来说,它的对象可以有该类的属性和方法,但序列化的字符串只记录该对象的属性及属性值。可能在这里有点 抽象,不过下面的例子会有更详细的解释。

这里先做一个序列化和反序列化的实例演示(手写的,有点挫劣):p1

代码下面阴影区就是输出的结果,同样,对于反序列化的就是p2

序列化和反序列化的常见操作便像是如此,这里先送上一波源码(其实是我懒,以后代码不小心被删了,就来这里找):

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
#serialize.php
<?php
class MySerialize
{
public $nature1;
public $nature2;
public function __construct($nature1, $nature2)
{
$this->nature1 = $nature1;
$this->nature2 = $nature2;
}
public function method()
{
echo "I am method";
}
}

$target = new MySerialize("value1","value2");
echo serialize($target);
?>


#unserialize.php
<?php
class MySerialize
{
public $nature1;
public $nature2;
public function __construct($nature1, $nature2)
{
$this->nature1 = $nature1;
$this->nature2 = $nature2;
}
public function __wakeup()
{
echo "wakeup<br \>";
}
public function method()
{
echo "I am method";
}
}
$target = 'O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";}';
var_dump(unserialize($target));
?>

序列化:

  接下来对上面的代码做一波解释,这里先介绍一下序列化后带格式的字符串的意思,在上面的serialize.php运行过后,可以看到一串有规律的字符串:O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";},这串字符串就规定了这个对象的属性和属性值。对于一个序列化字符串,其中

1
2
3
4
5
6
7
8
object module:
O-->Object,表示后面的变量的属性类型是Object类型,用法:O:size:"value":num:{propety module};
a-->Array,表示后面的变量的属性类型是Array类型,用法:a:size:{propety module};
propety module:
S-->String,表示后面的变量的属性类型是String类型,用法:s:size:"value";
i-->Integer,表示后面的变量的属性类型是Integer类型,用法:i:value;
b-->Boolean,表示后面的变量的属性类型是Boolean类型,用法:b:value(这里的value只有1和0);
N-->NULL,表示后面的变量值为空,用法:N;

  对比上面的解释,字符串O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";}就可以被解释为类对象MySerialize长度为11,有2个属性名:第一个属性名nature1string类型,长度为7,它的属性值为value1,是string类型,长度为6;第二个属性名nature2string类型,长度为7,它的属性值为value2,是string类型,长度为6。

反序列化:

  谈完序列化,再来说说反序列化,这是一个逆序操作,将一个序列化字符串转换成一个对象或数组的过程。反序列化中最神奇的地方就是反序列化会触发特殊的魔法函数,就好像创建类对象的时候会触发它的构造函数一样

1
2
3
4
5
6
7
8
9
10
_construct():创建一个对象时会被调用。
_destruct():销毁一个对象时会被调用。
_wakeup():触发unserialize()方法时会被调用。
_sleep():触发serialize()方法时会被调用。
_toString():类对象被当作一个字符串使用时会被调用。
_get():调出不可访问(private,protect等修饰)属性时会被调用。
_set():修改或写入不可访问(private,protect等修饰)属性时会被调用。
_isset():对不可访问(private,protect等修饰)属性使用empty()或isset()方法时会被调用。
_unset():对不可访问(private,protect等修饰)属性使用unset()方法时会被调用。
_invoke():将实例化对象当作方法使用时会被调用。
详细说明及举例:
  1. _construct()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #unserialize1.php
    <?php
    class MySerialize
    {
    public $nature1;
    public $nature2;
    public function __construct($nature1, $nature2)
    {
    $this->nature1 = $nature1;
    $this->nature2 = $nature2;
    echo "i am here!";
    }
    public function method()
    {
    echo "I am method!";
    }
    }
    $target = new MySerialize("value1",NULL);
    echo "<br />".serialize($target);
    ?>

    执行结果:p3

      按理说这个函数应该归类在序列化里面,因为它只有在创建对象的时候才会被调用,不过它也是个魔法函数,所以就将它放到这里,从上面的演示中可以看到,_construct()函数中的echo语句执行在serialize方法之前,也就是创建MySerialize对象的时候。

  2. _destruct()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #unserialize2.php
    <?php
    class MySerialize
    {
    public $nature1;
    public $nature2;
    public function __destruct()
    {
    echo "<br />I am here!";
    }
    public function method()
    {
    echo "I am method";
    }
    }
    $target = 'O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";}';
    var_dump(unserialize($target));
    ?>

    执行结果:p4

      _destruct()方法就如同析构方法一样,在一个类执行完之后会自动调用,也就是说,如果在该方法中含有危险操作函数,那就有可能存在该函数被恶意执行的可能,比如,在该函数中写有system($a)方法,并且$a=unserialize(可控值),那么,这里就可能通过可控值来执行操作。

  3. _toString()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #unserialize3.php
    <?php
    class MySerialize
    {
    public $nature1;
    public $nature2;
    public function __toString()
    {
    echo "<br />I am here!";
    }
    public function method()
    {
    echo "I am method";
    }
    }
    $target = 'O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";}';
    echo unserialize($target);
    ?>

    执行结果:p5

      这里在执行完unserialize()方法后用echo进行输出,因为此时unserialize()方法的返回值是一个含该魔法函数的对象,而该对象又被当作字符串输出,所以会触发该魔法函数。同样的道理,如果该函数中含有字符串处理的危险函数,就有可能造成源码源码泄漏。比如该魔法函数中存在file_get_contents($file)方法,而$file变量指向某个原本不允许直接访问的文件,并且魔法函数的触发方法可控,就会造成源码泄漏。

  4. _sleep()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #unserialize3.php
    <?php
    class MySerialize
    {
    public $nature1;
    public $nature2;
    public function __sleep()
    {
    echo "i am here!";
    }
    public function method()
    {
    echo "I am method!";
    }
    }
    $target = new MySerialize("value1",123);
    echo "<br />".serialize($target);
    ?>

    执行结果:p6

      这里在触发serialize()方法时直接触发该魔法函数,执行内部代码。这里的变量被NULL序列化,所以看到的是字符N。同样的道理,如果该魔法函数中存在危险函数,就有可能造成漏洞。比如该魔法函数中存在eval()方法,这样就可能造成命令执行漏洞,eval()中如果写有两个 符号,则里面的内容就会在DOS窗口运行,当然,前提是shell_exec()`方法可用。

  5. _wakeup()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #unserialize5.php
    <?php
    class MySerialize
    {
    public $nature1;
    public $nature2;
    public function __wakeup()
    {
    echo "<br />I am here!";
    }
    public function method()
    {
    echo "I am method";
    }
    }
    $target = 'O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";}';
    echo unserialize($target);
    ?>

    执行结果:p7

      这里可以看到当执行unserialize()方法时就会执行该魔法函数,执行内部代码。因为这里的类对象不能作为一个输出对象,所以payload无效果。同理,如果该魔法函数中存在危险函数,就会造成危险操作。比如,该魔法函数中存在exec()方法,并且参数是可以控制的,那么就有可能造成命令执行漏洞。

  6. _get()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #unserialize6.php
    <?php
    class MySerialize
    {
    private $nature1;
    private $nature2;
    public function __get($nature1)
    {
    echo "I am here!";
    }
    }
    $test = new MySerialize();
    echo $test -> $nature1."<br />";
    $target = 'O:11:"MySerialize":2:{s:7:"nature1";s:6:"value1";s:7:"nature2";s:6:"value2";}';
    var_dump(unserialize($target));
    ?>

    执行结果:p8

      按常理来说,私有变量是不允许在类外使用的,这里的echo是无法使用的,但是类中存在该魔法函数,所以系统会自动调用它。同理,该魔法函数中如果存在危险函数,并且变量可控,就会造成危险操作。例如:该魔法函数中存在popen()方法,并且变量可控,就会造成文件内容泄漏。也可使用passthru()方法执行系统外部命令。

到这里,就是容易造成漏洞的几个函数的触发条件,剩下的几个暂时还没有做深入学习(其实是感觉剩下的几个触发条件有点苛刻,而且不常用,就不做笔记)。

CTF题目实例演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#Session.php
<?php
include 'Application.php';
class Session extends Application {
//key建议为8位字符串
var $eancrykey = '';
var $cookie_expiration = 7200;
var $cookie_name = 'ddctf_id';
var $cookie_path = '';
var $cookie_domain = '';
var $cookie_secure = FALSE;
var $activity = "DiDiCTF";
public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}
}
private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('key.txt');
}
public function session_read() {
if(empty($_COOKIE)) {
return FALSE;
}
$session = $_COOKIE[$this->cookie_name];
if(!isset($session)) {
parent::response("session not found",'error');
return FALSE;
}
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);
if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);
if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
return FALSE;
}
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}
if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
parent::response('the ip addree not match'.'error');
return FALSE;
}
if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
parent::response('the user agent not match','error');
return FALSE;
}
return TRUE;
}
private function session_create() {
$sessionid = '';
while(strlen($sessionid) < 32) {
$sessionid .= mt_rand(0,mt_getrandmax());
}
$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);
$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
$expire = $this->cookie_expiration + time();
setcookie(
$this->cookie_name,
$cookiedata,
$expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure
);
}
}
$ddctf = new Session();
$ddctf->index();
?>

  在这个文件中,可以看到它包含了Application.php文件,且接收进来的数据都通过cookie传送一个序列化字符串,并且通过$session = unserialize($session);将对象反序列化,也就是这个部分是我们可以利用的,再看看Application.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#Application.php
<?php
Class Application {
var $path = '';
public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg,
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json');
echo $ret;
}
public function auth() {
$DIDICTF_ADMIN = 'admin';
if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
$this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
return TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
return TRUE;
}
}
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}
public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
}

  这里有一个魔法函数_destruct(),该函数在销毁对象时触发,也就是说只要反序列化对象是我们这个类Application的对象,该函数就会被执行,现在思路就有了,构造cookie为Application的对象,并设置$path路径为要访问的路径就会触发file_get_contents()方法,从而得到文件的值。

  当然这道题还要绕过其他的一些限制,不过今天的重点不在解这道题,而是要利用反序列化漏洞,这里再献上cookie部分的payload:O:11:"Application":1:{s:4:"path";s:8:"flag.php";}

心得总结:

  关于反序列化部分我学的还是不够精通,有些部分理解的不够透彻,但我觉得只要理解了被反序列化的类对象所能执行的魔法函数在什么条件下触发,就可以入门这个漏洞了。而且这个漏洞利用的前提:

  1. 类的对象有被反序列化的过程;
  2. 该类中含有魔法函数;
  3. 该魔法函数中有危险函数;
  4. 过滤条件不够完整。

  以上内容就是我对反序列化的理解,如果哪里不对,请大佬们多多批评~~~~~谢谢🙏大佬!

QQ