目录
前言
upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共21关,每一关都包含着不同上传方式。
(图来自: 先知社区)
Pass-1
直接上传php会被判断处于黑名单中
观察源码可知, 是前端js判断类型:
方法1
直接改前端代码:
上传成功之后并没有显示文件的路径, 那么好办, 直接dir爆破:
扫到上传目录是uplod, 然后直接链接webshell即可:
http://192.168.10.100:14722/upload/door.php
方法2
抓包修改文件后缀, 先把webshell脚本改为png图片后缀, 然后抓包重新修改为php后缀:
Pass-2
可以看到, 这关虽然和上一关相似, 看似是前端js过滤的类型, 实则过滤代码部分在服务端处理:
方法1
通过修改MIME类型绕过服务端的黑名单过滤:
Pass-3
用上一关MIME类型的方法尝试无果, 初步判断是根据文件后缀进行黑名单过滤了
方法1
可以用截断的方法绕过:
方法2
- phtml & php3
PHTML(有时叫做PHP)网页是一种包含PHP(一种和JavaScript或 Microsoft VBScript类似的语言)脚本的网页和ASP一样,PHP脚本镶嵌在网页的HTML代码之中。在页面被发送给请求的用户之前,网页服务器调用PHP解释程序来解释和执行PHP脚本。含有PHP脚本的网页通常都以“.php”、“.php3”或“.phtml”作为后缀。和ASP一样,PHP可以被认为是一种“动态网页”。
由于服务端采用黑名单的过滤方式, 这里可以使用php3或者phtml的后缀上传webshell:
上传成功!
Pass-4
这关也是采用黑名单过滤, 几乎过滤了所有有问题的后缀:
但是唯独没有发现对.htaccess文件的过滤, 于是编写一个.htaccess:
<FilesMatch "hack">
# 修改文件类型 – 下面可以让任何的文件都成为PHP那么被服务器解释
SetHandler application/x-httpd-php
AddType application/x-httpd-php .jpg
</FilesMatch>
然后将.htaccess上传到服务器, 在上传包含webshell的图片格式脚本:
包含hack字段的文件名即可被当做php解析:
为什么要匹配特定字段的文件名呢?
一部分原因是因为要隐藏这个webshell不让用户发现异常。
Pass-5
禁止了.htaccess文件的上传, 同时也无法用大小写混写绕过
方法1
由于服务器的中间件是apache2, 尝试解析漏洞:
然后访问, 发现解析成功了, apache2解析漏洞 (存在于Apache1.x和Apache2.x)
将从右向左解析后缀, 直到遇到一个apache认识的后缀类型位置:
方法2
查看提示:
于是想到.user.ini
- .user.ini
在php执行的过程中,除了主
php.ini
之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER[‘DOCUMENT_ROOT’] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。.user.ini
中可以定义除了PHP_INI_SYSTEM以外的模式的选项,故可以使用.user.ini
加上非php后缀的文件构造一个shell,比如auto_prepend_file=01.gif
具体请参考: https://segmentfault.com/a/1190000011552335?utm_source=tag-newest
创建一个 .user.ini 文件, 内容如下:
auto_prepend_file=123.gif
将其上传到服务器, 然后将包含webshell的脚本命名为 123.gif, 同样也上传服务器
再访问readme.php的时候, 就会自动包含并解析123.gif文件
Pass-6
源码中没有很好地过滤大小写, 采用大小写混写绕过:
Pass-7&8
禁用了上传ini文件类型, 尝试其他的截断上传, 解析上传也无果
方法1
这里就可以利用系统命名漏洞, 且服务端没有进行相应的过滤
在Windows系统中,上传 index.php. 会重命名为 . ,可以绕过后缀检查。 也可尝试 index.php%20 , index.php:1.jpg index.php::$DATA 等。 在Linux系统中,可以尝试上传名为 index.php/. 或 ./aa/../index.php/. 的文件
方法2
没有对后缀后面的空格做处理, 加一个空格即可绕过:
Pass-9
fuzz了所有可解析的后缀, 都无法上传, 查看源码发现对所有可解析的后缀做了过滤:
但并未对::$DATA做过滤。
利用Windows下NTFS文件系统的一个特性,即NTFS文件系统的存储数据流的一个属性 DATA ,就是请求 a.asp 本身的数据,如果a.asp 还包含了其他的数据流,比如 a.asp:b.asp,请求 a.asp:b.asp::$DATA,则是请求a.asp中的流数据b.asp的流数据的内容。
再访问 http://localhost:14722/upload/hack.php 即可。
Pass-10
查看源码, 观察代码逻辑, 可以看到,
先对后缀的最会一个 .(点)进行了删除,
然后再截取了最后一个 .(点) 到字符串结尾的部分,
最后判断截取部分是否在黑名单内。
但是在拼接文件名的时候, 使用的是删除 .(点)后的内容, 出现处理逻辑错误
构造上传文件名: hack.php. .
上传处理之后: hack.php.
继续用Pass-07的做法, 利用Windows系统命名来绕过检测, 上传 index.php. 会自动重命名为 index.php ,可以绕过后缀检查。
Pass-11
测试上传一个php脚本, 发现php后缀被替换为空了:
用双写绕过即可:
再访问脚本:
Pass-12
上传一个door.asd 来判断是黑名单还是白名单:
可见是白名单过滤
继续审计源码, 可以看到GET存在一个参数, 体现在url中; 然后将该参数的路径值(要上传到制定目录的位置)和上传的文件名后缀做拼接, 得到一个全新的上传文件:
1. 在GET参数save_path中传入真正的webshell名, 然后在后面加一个截断符%00
2. 然后在POST的filename为正常白名单的文件名
那么可以采用截断绕过
- %00截断适用条件
PHP 版本 < 5.3.4
php.ini 中 magic_quotes_gpc=off
通过服务端的白名单过滤和拼接之后, save_path内容后面的值就被截断了, 达到了绕过的效果:
Pass-13
与上一关大同小异, 只不过save_path的参数由GET型改为了POST型:
绕过方法与上一关换汤不换药。
Pass-14
有的站点使用文件头来检测文件类型,这种检查可以在Shell前加入对应的字节以绕过检查。几种常见的文件类型的头字节如下表所示
类型 | 二进制值 |
---|---|
JPG | FF D8 FF E0 00 10 4A 46 49 46 |
GIF | 47 49 46 38 39 61 |
PNG | 89 50 4E 47 |
TIF | 49 49 2A 00 |
BMP | 42 4D |
观察源码, 服务端读取了上传文件的前2个字节进行内容的判断, 然后得以判断文件是否处于白名单的类型:
鉴此, 就可以利用向shell图片文件里面添加jpg格式的类型来达到一个免杀的效果, 再加上文件包含漏洞来执行包含shell的图片, 完成攻击:
在shell里添加jpg的二进制头子节:
修改为jpg头子节的10个16进制:
成功绕过:
当然也可以改其他图片类型的16进制, 可自己尝试。
Pass-15
这关利用了getimagesize() 函数来判断文件的类型:
- getimagesize() 函数
用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息。
getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型及图片高度与宽度。
绕过方法和Pass-14一样
Pass-16
同样, 这关使用 exif_imagetype()函数来判断文件类型 (需要开启php_exif模块),
- exif_imagetype() 函数
判断一个图像的类型
exif_imagetype() 读取一个图像的第一个字节并检查其签名。
绕过方法同上。
Pass-17
审计源码:
部分函数:
- imagecreatefromjpeg()函数
由文件或 URL 创建一个新图象。返回一图像标识符,代表了从给定的文件名取得的图像。
- basename() 函数
读取路径中的文件名部分
- imagejpeg() 函数
使用图片对象生成图片文件
首先检测文件是否是jpg, png, gif格式:
if(($fileext == "jpg") && ($filetype=="image/jpeg"))
使用move_uploaded_file函数来做判断条件,如果成功将文件移动到$target_path, 就会进入二次渲染的代码, 反之上传失败
if(move_uploaded_file($tmpname,$target_path))
在这里有一个问题: 在move_uploaded_file()返回true的时候, 就已经成功将图片马上传到服务器了, 所以下面的二次渲染并不会影响到图片马的上传,:
那么二次渲染的代码就很多余, 心里知道就好, 主要考察二次渲染, 那么我这里把move_uploaded_file() 这个函数去掉, 直接应用二次渲染的代码:
由于环境问题, 一下解析参考于: https://xz.aliyun.com/t/2657#toc-1
然后将<?php phpinfo(); ?>添加到door.gif的尾部, 然后上传之后,
成功上传含有一句话的door.gif,但是这并没有成功.我们将上传的图片下载到本地.
可以看到下载下来的文件名已经变化,所以这是经过二次渲染的图片.我们使用16进制编辑器将其打开.
可以发现,我们在gif末端添加的php代码已经被去除.
关于绕过gif的二次渲染,我们只需要找到渲染前后没有变化的位置,然后将php代码写进去,就可以成功上传带有php代码的图片了.
经过对比,蓝色部分是没有发生变化的:
我们将代码写到该位置.
上传后在下载到本地使用16进制编辑器打开
可以看到php代码没有被去除.成功上传图片马。
Pass-18
- 竞争上传
有的服务器采用了先保存,再删除不合法文件的方式,在这种服务器中,可以反复上传一个会生成Web Shell的文件并尝试访问,多次之后即可获得Shell。
审计源码:
基本确定是白名单过滤,
过程是先将上传的临时文件移动到目标路径(upload下);
然后进行白名单判断, 是则以时间戳重命名目标文件; 否则就断开目标文件的软连接(可以理解为删除)。
这里就不适合用截断上传了, 可用竞争上传。
即进行若干次重复的上传脚本文件, 同时不断访问目标文件, 总有一次会趁系统来不及unlink文件而访问到上传的脚本文件, 称之为竞争上传。
Intruder模块进行无数次上传操作:
同时多个线程进行:
同时不断访问上传之后的脚本: http://192.168.1.10/upload/door.php
总可能访问到系统来不及unlink的文件 (虽然我没有成功 = =)
Pass-19
审计代码:
myupload.php :
//myupload.php
class MyUpload{
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );
......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){
$ret = $this->isUploadedFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// if flag to check if the file exists is set to 1
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, we are ready to move the file to destination
$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// check if we need to rename the file
if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}
......
......
......
};
白名单过滤+if流水式判断过滤,
绕过依旧可以采用竞争上传。
Pass-20
上传之后用户可以自定义文件名, 是一个很大的威胁:
抓包可以看到, 用户自定义的文件名是通过POST方式发送给服务器的:
且最终上传的文件名是该save_name参数的值, 所以绕过它即可
判断得知是黑名单过滤:
方法1
大小写绕过
由于是黑名单过滤, 黑名单往往会不全, 所以绕过也是得心应手的容易:
访问:
方法2
利用系统命名
Windows下命名为 door.php. , 系统会自动命名为 door.php ;
Linux下命名为 door.php/. , 系统会自动命名为 door.php ;
Pass-21
题来自CTF
同样是用户可自定义上传的文件名
审计代码:
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name']; // 上传文件名和POST[save_name] 都检查
if (!is_array($file)) {
$file = explode('.', strtolower($file)); // 将文件名转换小写, 再以'.'分割为数组
}
$ext = end($file); // 取数组的最后一个元素
$allow_suffix = array('jpg','png','gif'); // 白名单
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1]; // 取文件名和后缀进行拼接
$temp_file = $_FILES['upload_file']['tmp_name']; // 获取临时文件
$img_path = UPLOAD_PATH . '/' .$file_name; // 上传路径
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}
先检查了MIME文件类型;
然后将上传的文件名 (或者用户自定义的文件名) 转换为小写, 再以 '.' 分割;
再取后缀进行白名单判断;
通过白名单后, 进行新文件名的拼接 (关键);
最后移动文件完成上传。
关键就处在拼接新文件的地方:
$file_name = reset($file) . '.' . $file[count($file) - 1]; // 取文件名和后缀进行拼接
这段代码出现在白名单通过之后可控, 所以只要构造好 $file 就可以绕过。
如何构造呢?
假如可以在POST中传递我们自定义好的 $save_name 数据, 那不是就完成了构造?
举个例子, 看下图:
$a[1] 没有设置, 所以为空, 这样count($a)的数量就是2 (已设置的数量)
当取 $a[count($a) - 1]; 的时候, 就相当于取到 $a[1], 其值为NULL
联想到以上代码,
若构造 $file数组的值为:
$file[0] = upload-21.php
$file[2] = png
通过白名单后的拼接就是: upload-21.php.
这样POST到服务端后就可以绕过了:
再利用Windows的系统命名来执行上传的脚本。