Upload-labs通关笔记

前言

从打完校赛开始到现在,身为萌新,寒假就研究了一下文件上传这部分内容,还是自己动手实践一下比较好,还可以学学代码审计;

正文

客户端校验

任何客户端验证都是不安全的,客户端验证只能用来防止用户输入错误,减少服务器的开销,服务器端验证才可以真正抵御攻击者;

pass-01-js验证

php为后缀的文件上传不了,发现前端的javascript验证;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}

如此一来就很简单了,直接禁用js即可上传可解析的脚本文件;还有很多方法,先将脚本文件改为可以上传的后缀名,如less1.jpg,然后bp截取修改后缀为脚本文件的类型上传即可;

上传成功后如下直接可以访问;

kaLoSs.png

服务端校验

pass-02-MIME验证

黑盒情况下上传gardenia.jpg,bp截取后修改后缀为可执行脚本后缀.php可以直接上传成功,访问成功;

kaO8AS.png

审计一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
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 = '文件类型不正确,请重新上传!';
}
} else {
$msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
}

发现了关键的判断是对type的判断,由此想到是对文件MIME类型进行验证;

上面的黑盒条件下的尝试也是偶然的巧合,因为刚开始的脚本被改成了.jpg为后缀,所以bp截包所看见的MIME检测到的类型就理所当然是image/jpeg,所以在这样的情况下将.jpg修改为.php就可以上传成功;

kavVtU.png

正确的姿势如下:

上传.php文件,截包,发现MIME类型如下:

kavGtO.png

修改Content-Type为代码中pass的类型即可:

kavHCF.png

上传成功:

kaxVbt.png

pass-03-上传特殊可解析后缀

solution1

黑盒情况下上传.php文件得到提示:不允许上传.asp .aspx .php .jsp后缀文件!猜测应该是设置了黑名单;于是尝试上传.php5为后缀的文件,上传成功并且成功回显访问路径;

kaxIsA.png

但奇怪的是php5无法被服务器解析,经Google得知是apache服务器配置的问题,在httpd.conf文件中查看是否有如下语句,如果没有,添加即可;

AddType application/x-httpd-php .php .phtml .php5 .phps .php3 .pht

kaz0Ff.png

审计源码:

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
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');//黑名单
$file_name = trim($_FILES['upload_file']['name']);
$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); //收尾去空

if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

很明显是设置了黑名单的$deny_ext = array('.asp','.aspx','.php','.jsp');而且其中将文件名进行了改写,所以访问文件的时候路径需要注意;

solution2

如果发现上传.php5,.phtml,.pht一类无法解析,就可以尝试使用.htaccess;见pass-04;

pass-04-上传.htaccess

这题在黑盒情况下测试pass-03的多数情况都是不可以的,查看一下源码:

$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");

发现黑名单过滤了几乎所有的我了解到的特殊后缀,唯独没有过滤.htaccess,所以可以通过上传此文件达到重写文件解析,前提是apache的配置文件conf里有这样配置:

1
2
AllowOverride All
LoadModule rewrite_module modules/mod_rewrite.so

于是创建.htaccess文件内容如下,上传,用于解析指定路径里指定文件名的文件为php脚本:

1
2
3
<FilesMatch "gardenia">
SetHandler application/x-httpd-php
</FilesMatch>

此时创建文件名为gardenia的php脚本文件,无需后缀名,上传即可解析为php脚本;

解析成功:

kwhR2V.png

pass-05-大小写绕过

在黑盒情况下,发现无法上传.htaccess文件和其他特殊后缀,查看源码:

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
$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

将pass-05的源码与pass-04的对比:

kw5Jpt.md.png

发现pass-05多过滤了.htaccess文件,但是没有将读出的后缀名进行统一转换为小写,所以可以直接构造.Php后缀的文件进行上传即可;

kw5ypq.md.png

访问成功:

kw5RnU.png

pass-06-空格绕过

查看源码,与pass-05进行对比:

kwTBAe.md.png

对后缀进行了小写转换,但是从头到尾没有添加对文件名的首尾去空处理,所以直接在文件后缀后面加上空格即可绕过;

上传成功:

kwT5NQ.md.png

访问成功:

kwT73n.png

pass-07-点绕过

对比pass-06的源码:

kw7R2R.png

发现这次是缺少了$file_name = deldot($file_name);删除文件名末尾的点这一步的操作;

于是想到在文件后缀最后加上.来绕过判断,成功上传:

kw77Ie.png

成功访问:

kw7qGd.png

pass-08-::$DATA绕过

这次经过对比,源码中缺少的是如下语句:

$file_ext = str_ireplace('::$DATA', '', $file_ext);用来去除字符串::$DATA;所以这次可以直接构造gardenia.php::$DATA来绕过源码中的过滤;上传成功如下:

k0abrV.png

访问成功:

k0dwLV.md.png

pass-09-二次绕过

审计源码:

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
$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$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); //首尾去空

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 . '文件夹不存在,请手工创建!';
}
}

这次源码似乎挺正常,没有前面几处的缺失,一开始也是没有想到可以这样绕过,后来经过思考,发现源码对后缀名只是进行了一个轮次的处理,如果逆向思考,构造一个后缀,经过这一轮处理以后还是可以绕过呢?于是构造gardenia.php. .,经过去点,去空处理后得到gardenia.php.相当于点绕过;

上传成功:

k0BCMn.png

访问成功:

k00r8J.png

pass-10-双写绕过

查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$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 = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$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 = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

其中的$file_name = str_ireplace($deny_ext,"", $file_name);是关键点,将文件名中出现的带有黑名单的部分替换为空;

测试脚本:

1
2
3
4
5
6
7
<?php
$black="php";
$filename="gardenia.pphphp";
$ans=str_ireplace($black,"",$filename);
echo $ans;
?>
运行结果:gardenia.php

如上,直接双写构造gardenia.pphphp即可;

k0s6JO.md.png

访问成功:

k0sRQH.md.png

pass-11-00截断(GET)

审计一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$is_upload = false;
$msg = null;
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'];
$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类型文件!";
}
}

查看关键源码:

1
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

发现是得到合法后缀名后给定一个$img_path的文件存储路径,构成方法如上,需要GET一个save_path,但是不知道在哪GET…,没办法抓个包试试,发现了GET的参数;

k0ID0S.md.png

直接修改GET参数即可,构造payload如下;

k0I6Yj.png

%00为url编码的00,可以使$img_path的值从00结束,所以文件的最终路径为../upload/gardenia.php可以解析为php脚本;运行成功如下:

k0I7tJ.md.png

  • Attention:需要特别注意的是00截断的前提:
  1. php版本小于5.3.4;
  2. php.ini的magic_quotes_gpc为Off状态;

pass-12-00截断(POST)

和pass-11几乎一毛一样,就是从GET变成了POST而已,直接放上payload:

k0oGuV.png

Author: Gard3nia
Link: https://gardenia30.top/2019/02/19/Upload-labs通关笔记/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.