演示案例:
➢ WEB攻防-文件上传-安全验证&层面逻辑
➢ WEB攻防-文件上传-执行权限&解码还原
➢ WEB攻防-文件上传-分站存储&OSS存储
一、常规文件上传
1、课前一定要明白:
无文件解析安全问题上,格式解析是一对一的(不能jpg解析php)
换句话来说有解析错误配置或后缀解析漏洞时才能实现格式差异解析
2、文件上传安全指的是攻击者通过利用上传实现后门的写入连接后门进行权限控制的安全问题,对于如何确保这类安全问题,一般会从原生态功能中的文件内容,文件后缀,文件类型等方面判断,但是漏洞可能不仅在本身的代码验证逻辑中出现安全问题,也会在语言版本,语言函数,中间件,引用的第三方编辑器等存在缺陷地方配合利用。另外文件上传也有多个存储逻辑,不同的文件存储方案也会给攻击者带来不一样的挑战!
1、无限制
任意上传,什么文件都能上传
2、有限制:
限制后缀格式(判断后缀名(黑白名单),MIME类型,文件头等)
3、测试环境安装参考:
https://github.com/ffffffff0x/f8x 自动化环境部署工具
https://github.com/fuzzdb-project/fuzzdb渗透测试字典全家桶
https://github.com/sqlsec/upload-labs-docker文件上传靶场
https://github.com/sqlsec/upload-labs-docker/blob/main/WP.mdwp在这里
下载上述资源
docker安装
f8x -d 或 f8x -docker
进入项目文件夹
cd upload-labs-docker
一键部署运行
docker-compose up -d
1、第一关(前端JS)
如何判断是否是前端验证呢?
首先抓包监听,如果上传文件的时候还没有抓取到数据包,但是浏览器就提示文件类型不正确的话,那么这个多半就是前端校验了
前端js验证代码审计:

enctype=”multipart/form-data”(上传文件必写),作用就是告诉浏览器这个表单要传文件
on-xxx都是事件:onsubmit提交事件,onclick点击事件,onchange改变事件,onmouseover鼠标悬浮。事件就是触发开关,使得HTML和js联动。
js代码
function checkfilesuffix()
{
var file=document.getElementsByName('file')[0]['value'];
/*这里是一个dom操作document.getElementsByName('file')
通过name找标签,elements带s,返回数组(多个标签列表),[0]数组从0开始,取第一个上传框,value是上传框自带属性,存你选中的文件路径加文件名*/
if(file==""||file==null)
{
swal("请添加上传文件", "", "error");
//这就是一个弹窗,相当于alert的升级版,更好看
return false;
//直接告诉浏览器:不要提交表单,终止上传
}
else
{
var whitelist=new Array(".jpg",".png",".gif",".jpeg");
var file_suffix=file.substring(file.lastIndexOf("."));、
/*substring(起始下标)从指定下标开始,截取字符串后面所有内容,lastIndexOf(".")找到字符串里最后一个小数点的位置,返回下标*/
if(whitelist.indexOf(file_suffix) == -1)
//indexOf()在一个数组/字符串里,查找某个内容,返回它的位置(下标)
{
swal("只允许上传图片类型的文件!", "", "error");
return false;
}
}
}
function error(){
swal("上传失败", "", "error");
}
绕过方法:
判断是前端验证还是后端验证可以通过抓包,能抓到就是传到后端了,抓不到就是没传到服务器是前端验证。可以先把文件换为合法后缀,通过bp抓包后改后缀之后就把目标文件传到服务器了
2、第二关(.htaccess)
AddType application/x-httpd-php .png
后端代码
header("Content-type: text/html;charset=utf-8");
//设置 HTTP 响应头,告诉浏览器页面是 HTML 格式、使用 UTF-8 编码,防止中文乱码。
error_reporting(0);
//关闭所有 PHP 错误提示(警告、报错、通知都不显示)。
//设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
/dirname(FILE) = 当前 PHP 文件所在的绝对路径
定义常量 UPLOAD_PATH = 当前目录下的 /upload/ 文件夹绝对路径
示例:/var/www/html/upload//
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
//str_replace(要去掉什么,换成什么,原字符串),$_SERVER[‘DOCUMENT_ROOT’]网站根目录
if (!file_exists(UPLOAD_PATH)) {
mkdir(UPLOAD_PATH, 0755);
}
$is_upload = false;
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
//获取文件名,去掉前面的路径
$ext = pathinfo($name)['extension'];
//pathinfo(文件名)解析文件名,取各种信息。['extension']专门拿文件后缀
$blacklist = array("php", "php7", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf");
if (!in_array($ext, $blacklist)) {
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
} else {
echo "<script>black();</script>";
}
}</code></pre><p id="u4aafcd55" class="ne-p"><span class="ne-text" style="font-size: 24px">代码审计发现没过滤.htaccess,并且网站还是Apache+php搭建,所以可利用.htaccess配置文件来绕过</span></p></details>
https://blog.csdn.net/weixin_46684578/article/details/119141109
AddType application/x-httpd-php .png
addtype告诉Apache服务器,新增文件后缀对应的解析类型
application:mime类型大类,表示这是可执行应用程序,不是普通文本或图片
x-httpd-php专门给Apache web服务器用的PHP解析器
.htaccess修改解析配置文件
借助这个配置让.png的命名文件做类型application/x-httpd-php(PHP的类型)当做PHP执行
非靶场步骤:先看是不是用apache搭建的,其次看网站用什么语言开发,之后就可以正常测试
3、第三关(MIME类型)
Content-Type:image/png
后端代码
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
if (!file_exists(UPLOAD_PATH)) {
mkdir(UPLOAD_PATH, 0755);
}
$is_upload = false;
if (!empty($_POST['submit'])) {
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) {
echo "<script>black();</script>";
} else {
$name = basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>alert('上传失败')</script>";
}
}
}
?>
可以看出来这是MIME后端验证的
网站过滤逻辑是通过MIME类型过滤,我们可以先把目标文件改为允许上传的文件后缀,我们上传抓包后再改掉后缀即可过滤,文件上传后执行,看的是后缀加中间件解析规则,不看MIME
4、第四关(文件头判断)
GIF89a
代码审计
// 1. 判断是否通过POST方式提交了名为submit的表单数据,且不为空
if (!empty($_POST['submit'])) {
// 2. 判断上传文件的大小是否为0(空文件)
if (!$_FILES['file']['size']) {
// 3. 前端执行error() JS函数,提示上传错误
echo "<script>error();</script>";
} else {
// 4. 以二进制只读模式打开上传的临时文件
$file = fopen($_FILES['file']['tmp_name'], "rb");
// 5. 读取临时文件的前4个字节(二进制数据)
$bin = fread($file, 4);
// 6. 关闭打开的文件句柄,释放资源
fclose($file);
// 7. 校验文件MIME类型,是否为指定图片类型
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/jpg", "image/png", "image/gif"])) {
// 8. 前端执行black() JS函数,非法文件拦截
echo "<script>black();</script>";
}
// 9. 校验文件头(前4字节转16进制),匹配图片文件头
else if (!in_array(bin2hex($bin), ["89504E47", "FFD8FFE0", "47494638"])) {
// 10. 非法文件头,拦截
echo "<script>black();</script>";
}
// 11. 校验全部通过,处理文件上传
else {
// 12. 获取上传文件的原始文件名(过滤路径,只保留文件名)
$name = basename($_FILES['file']['name']);
// 13. 将临时文件移动到指定上传目录
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
// 14. 上传成功标记
$is_upload = true;
} else {
// 15. 移动文件失败,提示错误
echo "<script>error();</script>";
}
}
}
}
通过代码审计可知,文件验证时MIME类型和读文件开头前四个字节,配合010editor工具。后门文件前面写GIF89a gif文件的前几个字符
网站绕过逻辑是直接读文件开头几个字节,因为前面一串字节是代表着文件后缀,但是改MIME类型就上传成功了,这里有疑问。这是要在目标文件内前面加上jpg或其他的文件头前几个字节头,然后搭配mima类型就能绕过
5、第五关(黑名单-过滤不严)
无递归,pphphp
代码审计
$is_upload = false;
if (!empty($_POST['submit'])) {
$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_ireplace($blacklist, "", $name);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
}</code></pre><p id="ua50bba66" class="ne-p"><span class="ne-text" style="font-size: 24px">通过代码审计可知,代码只执行一次,没有循环遍历,所以可以双写绕过例如.pphphp,str_ireplace()函数不区分大小写</span></p></details>
通过文件后缀判断文件后缀
1、黑名单
检测文件上传后缀是不是在黑名单,在就gg不在就通过
2、白名单
检测上传文件后缀是不是在白名单,在就通过不在就gg
这关和第一关的区别是,这一关是后端严格验证,不能通过抓包绕。
通过源代码可以看出它没有递归,只检测一次我们把后缀写成pphphp,后端会把PHP删掉剩下还是php(双写绕过)
6、第六关(黑名单-过滤不严)
系统大小写敏感属性
代码和上一关基本一样,只不过替换函数str_replace()它区分大小写所以可直接绕过
这一关黑白名单还是一样,这一关就是大小写问题可以绕过,windows服务器不区分大小写,linux会严格区分,linux就不行了
7、第七关(低版本GET-%00截断)
自动解码一次
/var/www/html/upload/x.php%00
这个漏洞是php小于5.3.4以下才有的漏洞(00截断)
一种是改文件后缀:shell.php%00.jpg,黑白名单验证后缀顺利通过,但是保存文件时会受影响然后.jpg失效
一种是改文件保存路径shell.php%00,文件正常为shell.jpg但是在保存的时候还是会变为php
8、第八关(低版本POST-%00截断)
手工解码一次
../upload/x.php%00 二次解码
考点和第七关大部分一样
00截断是%00解码后的结果
url上面%00会自动解码一次
post下面%00手工解码一次
9、第九关(黑名单-过滤不严)
php3
可以通过fuzzdb里面关于文件上传字典来过滤,因为很多写法可以解析成你目标后缀
10、逻辑不严-条件竞争
这个一般是通过代码审计才能发现这个漏洞。
代码逻辑:
1、上传1.php
2、1.php存储到upload
3、发现1.php不是合法的后缀
4、删除1.php
原理:在1.php被删除之前触发代码
‘);?>
上传不断发包
请求不断发包
11、二次渲染
先搞个正常图片,上传导出渲染后的图片
对比保留部分,在保留部分添加后门代码
最后利用提示的文件包含执行图片后门代码
12、函数缺陷
函数缺陷:move_uploaded_file()
必须保存名字我们可控就可利用这个漏洞

二、实战遇到的情况
参考:https://konwait12.github.io/my-kon-blog/posts/049web%E6%94%BB%E9%98%B2/
1、执行权限
文件上传后存储目录不给执行权限
绕过条件:能控制上传文件存储目录
2、解码还原
数据做存储,解析固定(文件后缀名无关)
文件上传后利用编码传输解码还原
绕过:无法直接绕过
3、分站存储
upload.xiaodi8.com 上传
images.xiaodi8.com 存储
绕过:看存储的执行策略
4、OSS对象
Access控制-OSS对象存储-Bucket对象
绕过:无法直接绕过
三、应用场景
1、注册用户上传地方
2、JS或API接口的代码
3、后台或其他管理页面
4、源码泄露或盲测文件
5、三方编辑器上传漏洞
6、特定的源码审计漏洞
演示(笔记中不演示,因为也没什么必要):
JS或API接口代码
可能网站页面中没有上传文件的点,但是js中可能有这个功能代码,我们可以根据功能代码写一个上传表单,来测试这个功能点。
三方编辑器上传漏洞
就是看加载的js文件有没有第三方编辑器,然后看编辑器报出的漏洞,例如ueditor

比如这个例子,在js中看到了ueditor。利用exp直接打,就能打进去。
特定的源码审计漏洞
就是前人栽树后人乘凉,看看这个系统是不是什么有名的系统,曾经报过这个漏洞,直接利用payload即可