目录
客户端效验:
后端效验
文件上传的原理和后端防御
黑名单效验
iis6.0解析漏洞:
CGI解析漏洞:
%00截断 和 \0截断:
Apache解析缺陷:
条件竞争
一般是用js脚本进行客户端效验,检验上传文件的后缀名,有白名单方式也有黑名单方式
前端检验绕过
思路1:
思路2:直接禁止js脚本执行。谷歌浏览器,设置->高级设置->内容设置 ->禁止js
前端效验代码:
upload-labs
上传区
前端检测执行流程:
1.点击submit
2.调用checkFile() 函数,得到返回值,如果返回值为true,则上传文件到服务器,如果返回值为false,则取消上传。
后端效验
一般检测上传文件的如下三个方面:
(1)检测HTTP头部中 Content-Type字段:
HTTP头部完全可以伪造,形同虚设
- 扩展名:gif MIME类型:image/gif
- 扩展名:png MIME类型:image/png
- 扩展名:jpg MIME类型:image/jpeg
- 扩展名:js MIME类型:text/javascript
- 扩展名:htmMIME类型:text/html
- 扩展名:htmlMIME类型:text/html
PHP检测方式:
$_FILES数组的值本质上还是从http请求头中提取
绕过方法:
- 抓包改content-type字段
- 改文件名后上传抓包后再改回文件名
- 上传正常文件改文件内容
- 反正最终目的就是一个content-type是正确的就可以
(2)检测后缀
(3)检测文件头
一般会使用函数:getimageszie(文件路径) 来判断文件的文件头是否是图片
如果不是图片则返回false
绕过方法:
方法一:手动添加允许上传的文件头
方法二:使用图片马
做一句话木马前,先补充下php关于eval函数的知识:
eval():接受一个字符串,将字符串作为php代码执行(前提字符串必须符合php代码的语法),如果被解释为代码的字符串中有return语句,那么eval()的返回值为return返回的值,如果没有,返回值为NULL;
那么一句话木马是怎样的呢?
一句话木马的厉害之处,便是木马页面能够做到任意代码执行。给该页面进行 POST传参,例如在hackbar中输入a=2,那么么,传入的参数就会被放置到超全局变量$_POST数组中,并且以参数名为索引,即$_POST['a'] 就是传参的值,那么~~~如果这个值是一个伪装成字符串样子的代码,经过eval函数,它就会被执行。
同理,我们也可以使用GET传参来做到这一点
图片马制作方法一:
创建一个23.txt 文件,在里面写入 一句话木马。然后找一张大小比较小的图片horse.jpg,放在桌面上,然后打开命令行,先cd到Desktop
然后执行 copy horse.jpg/b + 23.txt h123.jpg 新得到的h123.jpg就是图片马了。将图片后缀修改为.txt 打开就可以看到在图片二进制代码的后面追加了一句话木马。
为什么图片马可以绕过Content-Type检测和文件头检测呢?
因为我们只是将一句话木马追加在了图片的二进制码的后面,图片仍然可以正常解析,所以http头中content-Type为image/jpeg.(这个http 头的值也可以手动修改来伪造)
假如后端只检验Content-Type的话,我们通过图片马就可以绕过Content-Type的检验,然后通过抓包,在中途修改文件后缀为.php,那么这个文件上传到服务器上就是.php格式的文件。
复制图片的地址,在url栏中请求图片马,通过hackbar向该页面传参,便可以实现任意代码执行。
例如:在hackbar中输入:a=echo 'Hello world'; 或者 a=phpinfo(); 这样打开图片地址的网页,就会显示 Hello world;
a=phpinfo();echo "Hello world";
这样两句话都会被执行。
总结一下,一句话木马实现需要三点:
- 木马上传成功,未被杀;
- 知道木马的路径在哪;(即如果图片有回显,通过查看图片的地址可以得到木马在服务器中的位置)
- 上传的木马能正常运行
补充知识:
1.php中单双引号的区别:
双引号串中的内容可以被解释而且替换,而单引号串中的内容总被认为是普通字符。
例如:
$foo
= 2;
echo
"foo is $foo"
;
// 打印结果: foo is 2
echo
'foo is $foo'
;
// 打印结果: foo is $foo
echo
"foo is $foo\n"
;
// 打印结果: foo is 2 (同时换行)
echo
'foo is $foo\n'
;
// 打印结果: foo is $foo\n
$foo
= 2;
echo
"foo is $foo"
;
// 打印结果: foo is 2
echo
'foo is $foo'
;
// 打印结果: foo is $foo
echo
"foo is $foo\n"
;
// 打印结果: foo is 2 (同时换行)
echo
'foo is $foo\n'
;
// 打印结果: foo is $foo\n
在单引号串中甚至反斜杠也失去了他的扩展含义(除了插入反斜杠\\和插入单引号\')。所以,当你想在字串中进行变量代换和包 含\n(换行符)等转义序列时,你应该使用双引号。单引号串可以用在其他任何地方,脚本中使用单引号串处理速度会更快些。
代码:
eval("echo'hello world';");
上边代码等同于下边的代码:
echo"hello world";
在浏览器中都输出:hello world
运用eval()要注意几点:
1.eval函数的参数的字符串末尾一定要有分号,在最后还要另加一个分号(这个分号是php限制)
2.注意单引号,双引号和反斜杠的运用。如果参数中带有变量时,并且变量有赋值操作的话,变量前的$符号钱一定要有\来转义。如果没有赋值操作可以不需要。
代码:
$a=100;
eval("echo$a;");
因为没有赋值操作,所以可以不用\来转义$.等同于以下代码:
$a=100;
eval("echo\$a;")
文件上传的原理和后端防御
如何将文件上传给服务器呢?首先你需要有一个表单页面,用表单来上传文件(注意一定要用post方式上传),不论什么格式的文件在你点击提交后,都会被上传到服务器,不过它们存在一个临时文件夹中。于此同时,文件的相关信息会储存在$_FILES['表单中input file的name值']这个超全局变量中,临时文件夹以及其中的文件 的存活期很短,如果没有在php脚本中主动转移到指定文件夹的话,在脚本执行结束之后,这个临时文件夹以及其中上传的文件就会被删除。
所以 所谓的后端验证,其实就是在php脚本中 核对 存储在$_FILES这个超全局变量中的文件信息,如果满足要求,就把上传的文件从临时文件夹这个即将毁灭的世界 中给拉出来,放在特定的文件夹中。如果不满足要求,那么随着脚本的执行结束,上传到服务器中的文件也会被删除。
前端上传表单代码:
upload a file
Upload a File
下面是后端php验证代码:
upload a file
Upload a File....
0){
echo 'Problem';
switch($_FILES['the_file']['error']){
case 1:
echo '文件超过配置文件中指定的大小';
break;
case 2:
echo '文件超过表单中指定的大小';
break;
case 3:
echo '文件只能被部分上传';
break;
case 4:
echo '没有文件被上传';
break;
case 6:
echo '不能上传文件,没有指定特定的文件夹';
break;
case 7:
echo '上传失败,不能写入硬盘';
break;
case 8:
echo 'php扩展阻碍了文件的上传!';
break;
}
exit;
}
if($_FILES['the_file']['type'] != 'image/PNG' && $_FILES['the_file']['type'] !='image/jpeg')
{
echo ‘请上传图片文件!!’;//MIME
exit;
}
$uploaded_file = $_FILES['the_file']['name'];
//echo '文件临时存储路径为:'.$_FILES['the_file']['tmp_name'];
//函数功能:判断所给出的文件是不是HTTP POST上传的,如果是则返回true,如果不是则返回false
if(is_uploaded_file($_FILES['the_file']['tmp_name'])){
//函数功能:首先检查第一个参数对应的文件是不是HTTP POST上传的,如果不是,则直接返回false,如果是,并且移动文件成功则返回true
if(!move_uploaded_file($_FILES['the_file']['tmp_name'],$uploaded_file)){
echo '移动文件失败!';
exit;
}
}else{
echo "该文件不是HTTP POST上传的文件!
";
exit;
}
echo '文件上传成功!!';
echo '你上传的图片如下:
';
echo "";
?>
看完了我们自己写的后端验证代码,现在让我们来看看,靶场第二关的后端源码吧!是不是感觉到一下就可以看懂原理了呢?
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . $_FILES['upload_file']['name'];
$is_upload = true;
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
} else {
$msg = $UPLOAD_ADDR.'文件夹不存在,请手工创建!';
}
}
最后在补充说明下,MIME类型的信息是怎么来的。
在我们上传文件的过程中,浏览器会根据文件的后缀以及文件的内容 两者来判断文件的MIME类型,并且将得出的结论存储在$_FILES['表单中file的name']['type'] 这个超级全局变量中。
黑名单效验
绕过黑名单效验:寻找未在黑名单中的、中间件可以解析的后缀
那些可以解析呢?得看apache或者nginx中怎么配置了
nginx.config 会将那些后缀转发给php 9000端口,而php会解析那些后缀
nginx的配置文件示例
apache下的php7.0.conf 示例
绕过方法:
- 大小写绕过,例如Php、PhP
- 利用黑名单中没有的,但是又能够被解析的后缀名,例如php、php3、php4、php5、php7、pht、phtml、phps
配合Apache的.htaccess 文件绕过:
/etc/apache/apache2.conf 配置文件是全局的配置文件
.htaccess文件是分布式配置文件,也就说是局部配置文件,可以局部配置其所在的文件夹。并覆盖全局配置
也就说可以通过上传一个.htaccess 文件到站点的公开目录,来修改apache的配置,随性所欲的定义那些文件后缀可以被解析为php
例如将php7.jpg作为php来解析
分布式配置文件覆盖全局配置文件的前提:
apache.conf 下对应目录的AllowOverride 配置为ALL 而不是None。
黑名单效验代码示例:
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$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)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR. '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR .'/'. $_FILES['upload_file']['name'];
$is_upload = true;
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
这个黑名单如何绕过呢?开发人员将后缀名提取出来,去掉了前后的空格,全部转化为小写,防止大小写绕过。其实,它的黑名单并不全面,看似层层防御,固若金汤,但是还是百密一疏。
在php被设计出来的时候,默认.php3 .php4 .php5 .phtml 等后缀名都会被当做.php来解析,既然它的黑名单里面没有这些。那么我们就可以上传以这些为后缀的文件。
例题:后缀名大小写绕过
假如之前的黑名单验证的脚本代码中没有防止大小写,那么我们只要修改原本小写的后缀名为大写,直接上传.php文件就可以了。
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
例题:文件后缀名空绕过
假如之前的黑名单脚本代码中没有trim函数,还去掉后缀名之后的空格的话,我们就可以通过在后缀名之后添加空格,来绕过黑名单。
注意,直接修改文件后缀名,添加空格是行不通的,必须通过抓包来修改后缀名。
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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 = $_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
if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
剖析实现原理:我们通过抓包的修改后的文件名,会自动存储在超全局变量$_FILES中,所以在脚本核对$_FILES中文件的相关信息时,后缀名能够成功绕过黑名单,但是,当文件被上传到服务器中的时候,服务器会先自动取掉后缀中最后的空格,然后将它作为.php文件解析。in a word,$_FILES数组中的信息和上传的服务器中的文件的信息是不一致的。
第七关:文件名后缀点绕过
假如后端黑名单代码中没有使用 deldot()函数来先删除文件名后缀的点再进行比对的话,我们就可以通过在文件名后缀添加点来绕过黑名单,同样,必须通过抓包来修改,在系统中直接修改再上传是行不通的,因为点会被忽略掉。类似的,当文件上传到服务器上时,文件名后缀的点同样先被忽略掉了,然后作为.php文件进行解析。
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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_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)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
第八关:::$DATA windows文件流绕过
在进入主题之前,让我们先来学学,如何将一个文件隐藏到另一个文件中吧
在命令行中输入:echo abcd>>a.txt 表示将abcd写入到a.txt中
echo abcd >>a.txt:b.txt 则表示将abcd写入到b.txt,b.txt隐藏在a.txt中,这时桌面上只会创建一个空的a.txt文件,通过资源管理器查看,它的大小只有0kb,但是实际上,a.txt中隐藏着b.txt, b.txt是实际占据硬盘大小的。
那么如何打开b.txt呢?在命令行输入:notepad a.txt:b.txt即可。
那如果在命令行输入:echo abcd>>a.txt::$DATA 会怎么样呢?
系统会忽略::$DATA 然后创建a.txt 文件,将abcd写入。可以理解为::$DATA表示寄宿文件为空。
那么当我上传文件到服务上的时候,如果通过抓包在文件后缀上添加上::$DATA,那么存储在$_FILES超全局变量中的文件后缀中就带有::$DATA,但是当文件上传到服务器的时候,我们自己添加的后缀会被windows操作系统自动忽略,岂不美哉?
不过记得在访问上传文件时,去掉::$DATA哦
第九关:构造文件后缀绕过
is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
从源码来看,这个黑名单机制似乎已经很完善了,但是如果我们通过抓包将 上传文件的后缀修改为 .php....呢?很悲哀,失败了?
那要是修改为.php. .呢?这个后缀经过一系列处理,$file_ext = .php. 正好可以绕过黑名单,而根据windows的文件特性,.php. .和.php效果是相同的。
第十关:文件后缀双写绕过
先让我们来看下源代码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","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);
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $file_name)) {
$img_path = $UPLOAD_ADDR . '/' .$file_name;
$is_upload = true;
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
从源代码可以看到,凡是黑名单后缀,都会被转换为空。那么我们将后缀修改为.pphp5hp怎么样呢?很悲催,新文件的名称为.p5hp 。那么如果将后缀修改为.phtmlhp, 还是很悲催,新文件的名称为 .php成功,那如果修改后缀为.pphphp呢,新的文件名为.php
由此可以推测 str_ireplace($deny_ext,"", $file_name);这个函数是从前往后遍历数组$deny_ext 和$file_name的。
综上所述,黑名单机制是非常不安全的,因为需要防范的点太多,如果建网站,最好用白名单机制。
iis6.0解析漏洞:
解析漏洞一:
iis 6.0除了将asp 后缀的文件解析为asp,还会将 .asa .cer .cdx 也会作为asp来解析。为什么会这样呢?这是因为在iis 6.0的应用程序扩展中默认设置了 .asa .cer .cdx都会调用 asp.dll 来解析。也就是说,我们可以通过修改iis 6.0的应用程序扩展来让后缀 .sr (嘿嘿) 也解析为asp文件。
例题:
$allowedExts = array("gif", "jpeg", "jpg", "png","asa","cer","cdx");
$temp = explode(".", $_FILES["file"]["name"]);
echo $_FILES["file"]["size"];
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts))
{
if ($_FILES["file"]["error"] > 0)
{
echo "错误:: " . $_FILES["file"]["error"] . "";
}
else
{
echo "上传文件名: " . $_FILES["file"]["name"] . "";
echo "文件类型: " . $_FILES["file"]["type"] . "";
echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB";
if (file_exists("./a/image/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " 文件已经存在。 ";
}
else
{
// 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
$ret = move_uploaded_file($_FILES["file"]["tmp_name"], "image/" . $_FILES["file"]["name"]);
echo "文件存储在: " . "./a/image/" . $_FILES["file"]["name"];
}
}
}
else
{
echo "非法的文件格式";
}
查看源码后,我们知道我们可以上传后缀是.asa .cer .cdx的包含有一句话木马的asp文件,但是源码中有一个MIME类型检测,检测MIME类型是不是图片类型。
那么存在超全局变量$_FILES["file"]["type"]中MIME类型 是从哪里来的呢?PHP圣经里面说是根据后缀和文件内容综合判断的,其实也没有错,但是没有指出关键,$_FILES["file"]["type"]中的值其实直接来源于请求头信息中的 content-type,而content-type中的值是在文件还没有被发送到服务器之前,根据文件内容和后缀判断的。
所以,我们这里有两种绕过思路,第一种,我们做一个图片马,这样content-type中的信息一定是image/jpg ,然后,我们通过抓包将上传文件的后缀修改成.asa。 第二种,我们直接上传.asa 文件,然后通过抓包直接修改content-type的值为image/jpg. 这样直接绕过了后端了MIME类型检测。岂不美哉?
解析漏洞二:
IIS6.0有此解析漏洞,IIS 5.1和IIS7.5均无此漏洞。
IIS6.0在处理含有特殊符号的文件路径时会出现逻辑错误,从而造成文件解析漏洞。这一漏洞有两种完全不同的利用方式:
test.asp;jpg 他将当做asp进行解析
test.asp/123.jpg 他将当做asp进行解析
原理:请求/aaa.asp;xxxx.jpg
首先,iis6.0从头部查找“.”号,获得.asp;xxxx.jpg
然后,查找";"如果有则截断。
然后,查找"/"如果有则截断。
最后,将保留下来的.asp字符串,从META_SERIPT_MAP脚本映射表里与扩展名匹配对比,并反馈给asp.dll处理
注意:第二种test.asp/123.jpg 这里的test.asp是一个文件夹的名字,如果你能创建一个这样的文件夹,那么意味请求文件夹中任意文件,都会被作为asp解析。
例题:靶场21关
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
echo $_FILES["file"]["size"];
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts))
{
if ($_FILES["file"]["error"] > 0)
{
echo "错误:: " . $_FILES["file"]["error"] . "";
}
else
{
echo "上传文件名: " . $_FILES["file"]["name"] . "";
echo "文件类型: " . $_FILES["file"]["type"] . "";
echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB";
if (file_exists("./b/image/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " 文件已经存在。 ";
}
else
{
// 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
$ret = move_uploaded_file($_FILES["file"]["tmp_name"], "image/" . $_FILES["file"]["name"]);
echo "文件存储在: " . "./b/image/" . $_FILES["file"]["name"];
echo "";
}
}
}
else
{
echo "非法的文件格式";
}
要绕过这个白名单,从代码上似乎不可能,但是由于对方是iis6.0,所以存在iis6.0的解析漏洞,因此,我们可以将一个写有asp木马的图片的后缀修改为 .asp;.jpg 就可以轻松getwebshell了。嘻嘻嘻
CGI解析漏洞:
PHP CGI解析漏洞
在某些使用Nginx的网站中,访问http://www.xxser.com/1.jpg/1.php,此时的1.jpg会被当作PHP脚本来解析,此时1.php是不存在的。
也就是说我们只要上传图片马,然后复制图片地址,在URL后面加上“/xxx.php”,我们上传的图片马就会被Nginx作为.php文件解析,进而就可以获得网站的WebShell。
对应靶场:第23关
%00截断 和 \0截断:
有些php函数,认为\0 标志着字符串的结束。因为php是在c语言基础上开发的,c语言就认为
\0是字符串的结束标志符号。
\0 经过 url编码后对应 %00,利用%00也可以让有些php函数误认为字符串的结束符。
这个漏洞便称为 %00 截断漏洞。
也可以直接修改报文的16进制代码,将对应的16进制代码修改为00
例题:
漏洞:move_uploaded_file() 解析参数存在00截断 ,也就是说只要传入的字符串中存在\0, 字符串之后的部分就作废了。
靶场:第11关
$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;
//这里是关键点,$img_path的形式为: xxxxx/xx20190627.jpg
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = '上传失败!';
}
}
else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
查看源码后,我们发现这里是利用漏洞的关键点,因为$img_path的形式为: xxxxx/xx20190627.jpg,所以我们可以尝试,通过抓包来修改get传参的值,例如修改成:1.php%00/xx20190627.jpg的形式。那么move_uploaded_file在读取第二参数的时候,读到%00就认为第二参数结束了。那么临时文件中我们上传的文件就会被重新命名为1.php文件,并放在特定的文件夹下。岂不美哉!
为什么要用一个%00,而不是0x00呢?
因为get传参 的参数 会被 服务器进行URL解码,%00被URL解码之后就是\0
靶场第12关:
$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 = $_POST['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类型文件!";
}
}
我们可以看到源码中与上道题的区别仅仅在于这里使用了post传参。那么我首先想到的思路是:抓包,然后直接修改post传参的值为1.php0x00 , 然额,失败了。最后看视屏学到一种新的思路:先在params中修改post为1.phpa,然后在Hex中查看机器码,a的ascii码为61,结合注释找到相应的a,然后将61修改为00,完美!
第19关:move_uploaded_file()截断
在看源码前,我们先补充一些函数的知识:
pathinfo(path,options) 以数组的形式返回文件路径的信息。在未指定第二个参数或者第二参数指定为all的情况下,数组一般有三个元素dirname目录名 basename文件名 extension后缀名
path
必需。规定要检查的路径。
options
可选。规定要返回的数组元素。默认是 all。
可能的值:
- PATHINFO_DIRNAME - 只返回 dirname
- PATHINFO_BASENAME - 只返回 basename
- PATHINFO_EXTENSION - 只返回 extension
实例:
输出:
Array
(
[dirname] => /testweb
[basename] => test.txt
[extension] => txt
)
输出:
test.txt
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$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'];
$file_ext = pathinfo($file_name,PATHINFO_EXTENSION);
if(!in_array($file_ext,$deny_ext)) {
$img_path = $UPLOAD_ADDR . '/' .$file_name;
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $img_path)) {
$is_upload = true;
}else{
$msg = '上传失败!';
}
}else{
$msg = '禁止保存为该类型文件!';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}
首先,查看源代码,看到是黑名单机制,嘿嘿,而且,没有对空格以及windows文件流进行过滤,所以,我通过抓包将在文件后面添加空格,上传成功!说明成功绕过了黑名单,但是,当我复制文件的路径,请求访问时,却返回了404页面,甚是诡异!明白了,这个源代码判断的是我们输入的文件名后缀是否在黑名单中,更原来的文件名没有任何关系。所以关键不在于抓包,而且也不需要抓包改包,我们只需要 在输入save_name这个post参数的时候,在后缀上添加::$DATA进行文件流绕过 或者 在后面直接添加一个空格就能简单的绕过。
其实这道题,出题人的本意是考察move_uploaded_file()这个函数的00截断漏洞,我们只要像上道题一样,在save_name中输入.phpa,然后通过抓包,在hex中修改a的为00即可利用这个漏洞。
Apache解析缺陷:
Apache的解析漏洞主要特性为Apache是从后面开始检查后缀,按最后一个合法后缀来解析
比如说:.php.jpg 因为.jpg不能解析,apache就会从右边往左看,看到.php可以解析,就会将文件作为.php解析
如何检测图片马?
function getRealFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getRealFileType($temp_file);
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = $UPLOAD_ADDR."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = "上传失败";
}
}
}
条件竞争
大马套小马:
';file_put_contents('1.php',$a)?>
条件竞争通常发生并发访问一个共享资源的时候(前提这个资源没有进行锁操作,即没有被单个线程锁定)。当多个线程同时去抢一个资源,不知道到底哪个线程能抢到,这样便形成了竞争。
因为开发人员在做开发防护的时候,一般只认为代码会以单线程的方式执行,他们忽略了并行服务器会并发执行多个线程,这就给了用条件竞争来绕过防护的机会。
例如,靶场17关这道题:
unlink(filename) 函数删除文件,文件名为filename,可以理解为相对路径或者绝对路径。如果删除成功,则返回true,如果失败,就返回false;
rename(oldname,newname)重命名文件或者目录,如果成功,返回true,如果失败,返回false;
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = $UPLOAD_ADDR . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = $UPLOAD_ADDR . '/'. 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 = '上传失败!';
}
}
这个防护程序的思路是:不论上传文件的后缀怎么样,我先把它从临时文件夹中救出来,然后才判断文件的后缀是否匹配。如果匹配,我就随机命名重命名这个文件,如果不匹配,就调用unlink函数删除它。
如果从单线程的角度去理解,这个防护程序没有任何毛病。但是,假如我能在没有上传文件前,提前猜测出上传文件被存储的位置。那么我就能做到在不断上传文件的同时,不断的请求访问这个文件。我们知道脚本程序执行是需要时间的,那么一定存在一个时间段,脚本还没有执行到unlink函数语句,文件还没有被删除,而我这个时候访问到了文件。
有人会质疑,就算在那一刹那,你请求到了文件,文件的代码被执行了,又能怎么样呢?一刹那之后,这个文件又会被删除。
在那短短的一刹那,假如你上传的文件,是个 大马套小马呢?大马虽死,但同文件夹中,小马犹生。一个网站便在弹指一挥间被拿下了。
那么如何不断上传文件的同时,不断请求访问这个文件呢?可以写脚本也可以使用现成的工具
以靶场17关为例,首先,我们要想办法猜出 假如我上传一个1.php文件,这个文件会被存储在哪里?我们可以先上传一张正规的jpg图片看看,它被存储在了什么地方?get到了!http://117.167.136.245:21781/Pass-17/upload//5420190628091046.jpg
他被存在upload文件夹下:那么如果我们上传一个1.php文件,这个文件就会被转存在:http://117.167.136.245:21781/Pass-17/upload//1.php
这个位置,注意哈,rename函数 仅仅做到了对符合白名单的文件进行随机命名,对不符合白名单的文件,例如我们上传的1.php,它仍然是1.php
现在我们只需要,上传1.php,用burpsuit 拦截数据包,然后将数据包放在burpsuit的爆破模块中。
然后访问,http://117.167.136.245:21781/Pass-17/upload//1.php
同样,将请求拦截,放在burpsuit的爆破模块中。最后,在爆破模块中,让两者同时不断进行,等待那关键的一瞬间就可以了。
具体操作:
1.对两者都进行抓包,右键,send to intruder
2.首先 clear $ ,然后在payload中选择 Nullpayload,然后设置Generate 10000表示没有payload,上传一万次,然后在Option中,设置Number of thread线程数为200