文件上传漏洞总结

目录

客户端效验:

后端效验

文件上传的原理和后端防御

黑名单效验

iis6.0解析漏洞:

CGI解析漏洞:

%00截断 和 \0截断:

Apache解析缺陷:

条件竞争


客户端效验:

一般是用js脚本进行客户端效验,检验上传文件的后缀名,有白名单方式也有黑名单方式

前端检验绕过

思路1:

  1. 修改文件名为符合要求的文件名,以便通过前端检验
  2. 通过前端检验后,点击上传,发送数据包,抓包工具拦截数据包,修改回原来的文件名,发送数据包到服务器。

思路2:直接禁止js脚本执行。谷歌浏览器,设置->高级设置->内容设置 ->禁止js

前端效验代码:



	
	  
	upload-labs




上传区

请选择要上传的图片:

前端检测执行流程:

1.点击submit

2.调用checkFile() 函数,得到返回值,如果返回值为true,则上传文件到服务器,如果返回值为false,则取消上传。

后端效验

一般检测上传文件的如下三个方面:

(1)检测HTTP头部中 Content-Type字段:

HTTP头部完全可以伪造,形同虚设

  • 扩展名:gif    MIME类型:image/gif
  • 扩展名:png MIME类型:image/png
  • 扩展名:jpg MIME类型:image/jpeg
  • 扩展名:js MIME类型:text/javascript
  • 扩展名:htmMIME类型:text/html
  • 扩展名:htmlMIME类型:text/html

PHP检测方式:

$_FILES数组的值本质上还是从http请求头中提取

文件上传漏洞总结_第1张图片

绕过方法:

  1. 抓包改content-type字段
  2. 改文件名后上传抓包后再改回文件名
  3. 上传正常文件改文件内容
  4. 反正最终目的就是一个content-type是正确的就可以

(2)检测后缀

(3)检测文件头

文件上传漏洞总结_第2张图片

一般会使用函数:getimageszie(文件路径) 来判断文件的文件头是否是图片

如果不是图片则返回false

文件上传漏洞总结_第3张图片

绕过方法:

方法一:手动添加允许上传的文件头

文件上传漏洞总结_第4张图片

方法二:使用图片马

做一句话木马前,先补充下php关于eval函数的知识:

eval():接受一个字符串,将字符串作为php代码执行(前提字符串必须符合php代码的语法),如果被解释为代码的字符串中有return语句,那么eval()的返回值为return返回的值,如果没有,返回值为NULL;

那么一句话木马是怎样的呢?


一句话木马的厉害之处,便是木马页面能够做到任意代码执行。给该页面进行 POST传参,例如在hackbar中输入a=2,那么么,传入的参数就会被放置到超全局变量$_POST数组中,并且以参数名为索引,即$_POST['a'] 就是传参的值,那么~~~如果这个值是一个伪装成字符串样子的代码,经过eval函数,它就会被执行。

同理,我们也可以使用GET传参来做到这一点

图片马制作方法一:

创建一个23.txt 文件,在里面写入 一句话木马。然后找一张大小比较小的图片horse.jpg,放在桌面上,然后打开命令行,先cd到Desktop

然后执行 copy horse.jpg/b + 23.txt  h123.jpg 新得到的h123.jpg就是图片马了。将图片后缀修改为.txt 打开就可以看到在图片二进制代码的后面追加了一句话木马。

为什么图片马可以绕过Content-Type检测和文件头检测呢?

因为我们只是将一句话木马追加在了图片的二进制码的后面,图片仍然可以正常解析,所以http头中content-Type为image/jpeg.(这个http 头的值也可以手动修改来伪造)

假如后端只检验Content-Type的话,我们通过图片马就可以绕过Content-Type的检验,然后通过抓包,在中途修改文件后缀为.php,那么这个文件上传到服务器上就是.php格式的文件。

复制图片的地址,在url栏中请求图片马,通过hackbar向该页面传参,便可以实现任意代码执行。

例如:在hackbar中输入:a=echo 'Hello world';  或者 a=phpinfo(); 这样打开图片地址的网页,就会显示 Hello world;

a=phpinfo();echo "Hello world";

这样两句话都会被执行。

总结一下,一句话木马实现需要三点:

  1. 木马上传成功,未被杀;
  2. 知道木马的路径在哪;(即如果图片有回显,通过查看图片的地址可以得到木马在服务器中的位置)
  3. 上传的木马能正常运行

补充知识:

1.php中单双引号的区别:

双引号串中的内容可以被解释而且替换,而单引号串中的内容总被认为是普通字符。

例如:

$foo = 2; 

echo "foo is $foo"// 打印结果: foo is 2 

echo 'foo is $foo'// 打印结果: foo is $foo 

echo "foo is $foo\n"// 打印结果: foo is 2 (同时换行) 

echo 'foo is $foo\n'// 打印结果: foo is $foo\n 

$foo = 2; 

echo "foo is $foo"// 打印结果: foo is 2 

echo 'foo is $foo'// 打印结果: foo is $foo 

echo "foo is $foo\n"// 打印结果: foo is 2 (同时换行) 

echo 'foo is $foo\n'// 打印结果: foo is $foo\n  

在单引号串中甚至反斜杠也失去了他的扩展含义(除了插入反斜杠\\和插入单引号\')。所以,当你想在字串中进行变量代换和包 含\n(换行符)等转义序列时,你应该使用双引号。单引号串可以用在其他任何地方,脚本中使用单引号串处理速度会更快些。

代码:
  eval("echo'hello world';");
上边代码等同于下边的代码:
  echo"hello world";
在浏览器中都输出:hello world

运用eval()要注意几点:
1.eval函数的参数的字符串末尾一定要有分号,在最后还要另加一个分号(这个分号是php限制)
2.注意单引号,双引号和反斜杠的运用。如果参数中带有变量时,并且变量有赋值操作的话,变量前的$符号钱一定要有\来转义。如果没有赋值操作可以不需要。
代码:
  $a=100;
  eval("echo$a;");
因为没有赋值操作,所以可以不用\来转义$.等同于以下代码:
  $a=100;
  eval("echo\$a;")

文件上传的原理和后端防御

如何将文件上传给服务器呢?首先你需要有一个表单页面,用表单来上传文件(注意一定要用post方式上传),不论什么格式的文件在你点击提交后,都会被上传到服务器,不过它们存在一个临时文件夹中。于此同时,文件的相关信息会储存在$_FILES['表单中input file的name值']这个超全局变量中,临时文件夹以及其中的文件 的存活期很短,如果没有在php脚本中主动转移到指定文件夹的话,在脚本执行结束之后,这个临时文件夹以及其中上传的文件就会被删除。

所以 所谓的后端验证,其实就是在php脚本中 核对 存储在$_FILES这个超全局变量中的文件信息,如果满足要求,就把上传的文件从临时文件夹这个即将毁灭的世界 中给拉出来,放在特定的文件夹中。如果不满足要求,那么随着脚本的执行结束,上传到服务器中的文件也会被删除。

前端上传表单代码:



    
        
        upload a file
        
    
    
       

Upload a File

下面是后端php验证代码:




    
        
        upload a file
        
    
    
       

Upload a File....

0){ echo 'Problem'; switch($_FILES['the_file']['error']){ case 1: echo '文件超过配置文件中指定的大小'; break; case 2: echo '文件超过表单中指定的大小'; break; case 3: echo '文件只能被部分上传'; break; case 4: echo '没有文件被上传'; break; case 6: echo '不能上传文件,没有指定特定的文件夹'; break; case 7: echo '上传失败,不能写入硬盘'; break; case 8: echo 'php扩展阻碍了文件的上传!'; break; } exit; } if($_FILES['the_file']['type'] != 'image/PNG' && $_FILES['the_file']['type'] !='image/jpeg') { echo ‘请上传图片文件!!’;//MIME exit; } $uploaded_file = $_FILES['the_file']['name']; //echo '文件临时存储路径为:'.$_FILES['the_file']['tmp_name']; //函数功能:判断所给出的文件是不是HTTP POST上传的,如果是则返回true,如果不是则返回false if(is_uploaded_file($_FILES['the_file']['tmp_name'])){ //函数功能:首先检查第一个参数对应的文件是不是HTTP POST上传的,如果不是,则直接返回false,如果是,并且移动文件成功则返回true if(!move_uploaded_file($_FILES['the_file']['tmp_name'],$uploaded_file)){ echo '移动文件失败!'; exit; } }else{ echo "该文件不是HTTP POST上传的文件!
"; exit; } echo '文件上传成功!!'; echo '

你上传的图片如下:


'; echo ""; ?>

看完了我们自己写的后端验证代码,现在让我们来看看,靶场第二关的后端源码吧!是不是感觉到一下就可以看懂原理了呢?

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
                $img_path = $UPLOAD_ADDR . $_FILES['upload_file']['name'];
                $is_upload = true;

            }
        } else {
            $msg = '文件类型不正确,请重新上传!';
        }
    } else {
        $msg = $UPLOAD_ADDR.'文件夹不存在,请手工创建!';
    }
}

最后在补充说明下,MIME类型的信息是怎么来的。

在我们上传文件的过程中,浏览器会根据文件的后缀以及文件的内容 两者来判断文件的MIME类型,并且将得出的结论存储在$_FILES['表单中file的name']['type'] 这个超级全局变量中。

黑名单效验

绕过黑名单效验:寻找未在黑名单中的、中间件可以解析的后缀

那些可以解析呢?得看apache或者nginx中怎么配置了

nginx.config 会将那些后缀转发给php 9000端口,而php会解析那些后缀

nginx的配置文件示例

文件上传漏洞总结_第5张图片

apache下的php7.0.conf 示例

绕过方法:

  1. 大小写绕过,例如Php、PhP
  2. 利用黑名单中没有的,但是又能够被解析的后缀名,例如php、php3、php4、php5、php7、pht、phtml、phps

配合Apache的.htaccess 文件绕过:

/etc/apache/apache2.conf 配置文件是全局的配置文件

.htaccess文件是分布式配置文件,也就说是局部配置文件,可以局部配置其所在的文件夹。并覆盖全局配置

也就说可以通过上传一个.htaccess 文件到站点的公开目录,来修改apache的配置,随性所欲的定义那些文件后缀可以被解析为php

例如将php7.jpg作为php来解析

分布式配置文件覆盖全局配置文件的前提:

apache.conf 下对应目录的AllowOverride 配置为ALL 而不是None。

文件上传漏洞总结_第6张图片

黑名单效验代码示例:

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array('.asp','.aspx','.php','.jsp');
        $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)) {
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR. '/' . $_FILES['upload_file']['name'])) {
                 $img_path = $UPLOAD_ADDR .'/'. $_FILES['upload_file']['name'];
                 $is_upload = true;
            }
        } else {
            $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
        }
    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

这个黑名单如何绕过呢?开发人员将后缀名提取出来,去掉了前后的空格,全部转化为小写,防止大小写绕过。其实,它的黑名单并不全面,看似层层防御,固若金汤,但是还是百密一疏。

在php被设计出来的时候,默认.php3 .php4 .php5 .phtml 等后缀名都会被当做.php来解析,既然它的黑名单里面没有这些。那么我们就可以上传以这些为后缀的文件。

 

例题:后缀名大小写绕过

假如之前的黑名单验证的脚本代码中没有防止大小写,那么我们只要修改原本小写的后缀名为大写,直接上传.php文件就可以了。

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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)) {
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
                $img_path = $UPLOAD_ADDR . '/' . $file_name;
                $is_upload = true;
            }
        } else {
            $msg = '此文件不允许上传';
        }
    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

例题:文件后缀名空绕过

假如之前的黑名单脚本代码中没有trim函数,还去掉后缀名之后的空格的话,我们就可以通过在后缀名之后添加空格,来绕过黑名单。

注意,直接修改文件后缀名,添加空格是行不通的,必须通过抓包来修改后缀名。

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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 = $_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
        
        if (!in_array($file_ext, $deny_ext)) {
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
                $img_path = $UPLOAD_ADDR . '/' . $file_name;
                $is_upload = true;
            }
        } else {
            $msg = '此文件不允许上传';
        }
    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

剖析实现原理:我们通过抓包的修改后的文件名,会自动存储在超全局变量$_FILES中,所以在脚本核对$_FILES中文件的相关信息时,后缀名能够成功绕过黑名单,但是,当文件被上传到服务器中的时候,服务器会先自动取掉后缀中最后的空格,然后将它作为.php文件解析。in a word,$_FILES数组中的信息和上传的服务器中的文件的信息是不一致的。

第七关:文件名后缀点绕过

假如后端黑名单代码中没有使用 deldot()函数来先删除文件名后缀的点再进行比对的话,我们就可以通过在文件名后缀添加点来绕过黑名单,同样,必须通过抓包来修改,在系统中直接修改再上传是行不通的,因为点会被忽略掉。类似的,当文件上传到服务器上时,文件名后缀的点同样先被忽略掉了,然后作为.php文件进行解析。

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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_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)) {
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
                $img_path = $UPLOAD_ADDR . '/' . $file_name;
                $is_upload = true;
            }
        } else {
            $msg = '此文件不允许上传';
        }
    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

第八关:::$DATA windows文件流绕过

在进入主题之前,让我们先来学学,如何将一个文件隐藏到另一个文件中吧

在命令行中输入:echo abcd>>a.txt 表示将abcd写入到a.txt中

echo abcd >>a.txt:b.txt 则表示将abcd写入到b.txt,b.txt隐藏在a.txt中,这时桌面上只会创建一个空的a.txt文件,通过资源管理器查看,它的大小只有0kb,但是实际上,a.txt中隐藏着b.txt, b.txt是实际占据硬盘大小的。

那么如何打开b.txt呢?在命令行输入:notepad a.txt:b.txt即可。

那如果在命令行输入:echo abcd>>a.txt::$DATA 会怎么样呢?

系统会忽略::$DATA 然后创建a.txt 文件,将abcd写入。可以理解为::$DATA表示寄宿文件为空。

那么当我上传文件到服务上的时候,如果通过抓包在文件后缀上添加上::$DATA,那么存储在$_FILES超全局变量中的文件后缀中就带有::$DATA,但是当文件上传到服务器的时候,我们自己添加的后缀会被windows操作系统自动忽略,岂不美哉?

不过记得在访问上传文件时,去掉::$DATA哦

第九关:构造文件后缀绕过

is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".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 = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空
        
        if (!in_array($file_ext, $deny_ext)) {
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
                $img_path = $UPLOAD_ADDR . '/' . $file_name;
                $is_upload = true;
            }
        } else {
            $msg = '此文件不允许上传';
        }
    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

从源码来看,这个黑名单机制似乎已经很完善了,但是如果我们通过抓包将 上传文件的后缀修改为 .php....呢?很悲哀,失败了?

那要是修改为.php. .呢?这个后缀经过一系列处理,$file_ext = .php. 正好可以绕过黑名单,而根据windows的文件特性,.php. .和.php效果是相同的。

第十关:文件后缀双写绕过

先让我们来看下源代码

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = str_ireplace($deny_ext,"", $file_name);
        if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $file_name)) {
            $img_path = $UPLOAD_ADDR . '/' .$file_name;
            $is_upload = true;
        }
    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

从源代码可以看到,凡是黑名单后缀,都会被转换为空。那么我们将后缀修改为.pphp5hp怎么样呢?很悲催,新文件的名称为.p5hp 。那么如果将后缀修改为.phtmlhp, 还是很悲催,新文件的名称为 .php成功,那如果修改后缀为.pphphp呢,新的文件名为.php

由此可以推测 str_ireplace($deny_ext,"", $file_name);这个函数是从前往后遍历数组$deny_ext 和$file_name的。

综上所述,黑名单机制是非常不安全的,因为需要防范的点太多,如果建网站,最好用白名单机制。

iis6.0解析漏洞:

解析漏洞一:

    iis 6.0除了将asp 后缀的文件解析为asp,还会将 .asa .cer .cdx 也会作为asp来解析。为什么会这样呢?这是因为在iis 6.0的应用程序扩展中默认设置了 .asa .cer .cdx都会调用 asp.dll 来解析。也就是说,我们可以通过修改iis 6.0的应用程序扩展来让后缀 .sr (嘿嘿) 也解析为asp文件。

例题:

$allowedExts = array("gif", "jpeg", "jpg", "png","asa","cer","cdx");
$temp = explode(".", $_FILES["file"]["name"]);
echo $_FILES["file"]["size"];
$extension = end($temp);     // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800)   // 小于 200 kb
&& in_array($extension, $allowedExts))
{
    if ($_FILES["file"]["error"] > 0)
    {
        echo "错误:: " . $_FILES["file"]["error"] . "";
    }
    else
    {
        echo "上传文件名: " . $_FILES["file"]["name"] . "";
        echo "文件类型: " . $_FILES["file"]["type"] . "";
        echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB";
      
        if (file_exists("./a/image/" . $_FILES["file"]["name"]))
        {
            echo $_FILES["file"]["name"] . " 文件已经存在。 ";
        }
        else
        {
            // 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
            $ret = move_uploaded_file($_FILES["file"]["tmp_name"], "image/" . $_FILES["file"]["name"]);
            echo "文件存储在: " . "./a/image/" . $_FILES["file"]["name"];
        }
    }
}
else
{
    echo "非法的文件格式";
}

查看源码后,我们知道我们可以上传后缀是.asa .cer .cdx的包含有一句话木马的asp文件,但是源码中有一个MIME类型检测,检测MIME类型是不是图片类型。

那么存在超全局变量$_FILES["file"]["type"]中MIME类型 是从哪里来的呢?PHP圣经里面说是根据后缀和文件内容综合判断的,其实也没有错,但是没有指出关键,$_FILES["file"]["type"]中的值其实直接来源于请求头信息中的 content-type,而content-type中的值是在文件还没有被发送到服务器之前,根据文件内容和后缀判断的。

所以,我们这里有两种绕过思路,第一种,我们做一个图片马,这样content-type中的信息一定是image/jpg ,然后,我们通过抓包将上传文件的后缀修改成.asa。 第二种,我们直接上传.asa 文件,然后通过抓包直接修改content-type的值为image/jpg. 这样直接绕过了后端了MIME类型检测。岂不美哉?

解析漏洞二:

IIS6.0有此解析漏洞,IIS 5.1和IIS7.5均无此漏洞。

IIS6.0在处理含有特殊符号的文件路径时会出现逻辑错误,从而造成文件解析漏洞。这一漏洞有两种完全不同的利用方式:

test.asp;jpg 他将当做asp进行解析

test.asp/123.jpg 他将当做asp进行解析

原理:请求/aaa.asp;xxxx.jpg

首先,iis6.0从头部查找“.”号,获得.asp;xxxx.jpg

然后,查找";"如果有则截断。

然后,查找"/"如果有则截断。

最后,将保留下来的.asp字符串,从META_SERIPT_MAP脚本映射表里与扩展名匹配对比,并反馈给asp.dll处理

注意:第二种test.asp/123.jpg 这里的test.asp是一个文件夹的名字,如果你能创建一个这样的文件夹,那么意味请求文件夹中任意文件,都会被作为asp解析。

例题:靶场21关



    $allowedExts = array("gif", "jpeg", "jpg", "png");
    $temp = explode(".", $_FILES["file"]["name"]);
    echo $_FILES["file"]["size"];
    $extension = end($temp);     // 获取文件后缀名
    if ((($_FILES["file"]["type"] == "image/gif")
    || ($_FILES["file"]["type"] == "image/jpeg")
    || ($_FILES["file"]["type"] == "image/jpg")
    || ($_FILES["file"]["type"] == "image/pjpeg")
    || ($_FILES["file"]["type"] == "image/x-png")
    || ($_FILES["file"]["type"] == "image/png"))
    && ($_FILES["file"]["size"] < 204800)   // 小于 200 kb
    && in_array($extension, $allowedExts))
    {
        if ($_FILES["file"]["error"] > 0)
        {
            echo "错误:: " . $_FILES["file"]["error"] . "";
        }
        else
        {
            echo "上传文件名: " . $_FILES["file"]["name"] . "";
            echo "文件类型: " . $_FILES["file"]["type"] . "";
            echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB";
          
            if (file_exists("./b/image/" . $_FILES["file"]["name"]))
            {
                echo $_FILES["file"]["name"] . " 文件已经存在。 ";
            }
            else
            {
                // 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
                $ret = move_uploaded_file($_FILES["file"]["tmp_name"], "image/" . $_FILES["file"]["name"]);
                echo "文件存储在: " . "./b/image/" . $_FILES["file"]["name"];
    	    echo "";
            }
        }
    }
    else
    {
        echo "非法的文件格式";
    }


要绕过这个白名单,从代码上似乎不可能,但是由于对方是iis6.0,所以存在iis6.0的解析漏洞,因此,我们可以将一个写有asp木马的图片的后缀修改为 .asp;.jpg 就可以轻松getwebshell了。嘻嘻嘻

CGI解析漏洞:

PHP CGI解析漏洞

在某些使用Nginx的网站中,访问http://www.xxser.com/1.jpg/1.php,此时的1.jpg会被当作PHP脚本来解析,此时1.php是不存在的。

也就是说我们只要上传图片马,然后复制图片地址,在URL后面加上“/xxx.php”,我们上传的图片马就会被Nginx作为.php文件解析,进而就可以获得网站的WebShell。

对应靶场:第23关

%00截断 和 \0截断:

 有些php函数,认为\0 标志着字符串的结束。因为php是在c语言基础上开发的,c语言就认为

\0是字符串的结束标志符号。

\0 经过 url编码后对应 %00,利用%00也可以让有些php函数误认为字符串的结束符。

这个漏洞便称为 %00 截断漏洞。

也可以直接修改报文的16进制代码,将对应的16进制代码修改为00

例题:

漏洞:move_uploaded_file() 解析参数存在00截断 ,也就是说只要传入的字符串中存在\0, 字符串之后的部分就作废了。

靶场:第11关

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
//这里是关键点,$img_path的形式为: xxxxx/xx20190627.jpg

        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        }
        else{
            $msg = '上传失败!';
        }
    }
    else{
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
}

 $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
查看源码后,我们发现这里是利用漏洞的关键点,因为$img_path的形式为: xxxxx/xx20190627.jpg,所以我们可以尝试,通过抓包来修改get传参的值,例如修改成:1.php%00/xx20190627.jpg的形式。那么move_uploaded_file在读取第二参数的时候,读到%00就认为第二参数结束了。那么临时文件中我们上传的文件就会被重新命名为1.php文件,并放在特定的文件夹下。岂不美哉!

为什么要用一个%00,而不是0x00呢?

因为get传参 的参数 会被 服务器进行URL解码,%00被URL解码之后就是\0

靶场第12关:

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        }
        else{
            $msg = "上传失败";
        }
    }
    else{
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
}

我们可以看到源码中与上道题的区别仅仅在于这里使用了post传参。那么我首先想到的思路是:抓包,然后直接修改post传参的值为1.php0x00 , 然额,失败了。最后看视屏学到一种新的思路:先在params中修改post为1.phpa,然后在Hex中查看机器码,a的ascii码为61,结合注释找到相应的a,然后将61修改为00,完美!

第19关:move_uploaded_file()截断

在看源码前,我们先补充一些函数的知识:

pathinfo(path,options) 以数组的形式返回文件路径的信息。在未指定第二个参数或者第二参数指定为all的情况下,数组一般有三个元素dirname目录名 basename文件名 extension后缀名
path 必需。规定要检查的路径。
options

可选。规定要返回的数组元素。默认是 all。

可能的值:

  • PATHINFO_DIRNAME - 只返回 dirname
  • PATHINFO_BASENAME - 只返回 basename
  • PATHINFO_EXTENSION - 只返回 extension

实例:



输出:

Array
(
[dirname] => /testweb
[basename] => test.txt
[extension] => txt
)


输出:

test.txt

 

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists($UPLOAD_ADDR)) {
        $deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

        $file_name = $_POST['save_name'];
        $file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

        if(!in_array($file_ext,$deny_ext)) {
            $img_path = $UPLOAD_ADDR . '/' .$file_name;
            if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $img_path)) { 
                $is_upload = true;
            }else{
                $msg = '上传失败!';
            }
        }else{
            $msg = '禁止保存为该类型文件!';
        }

    } else {
        $msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
    }
}

首先,查看源代码,看到是黑名单机制,嘿嘿,而且,没有对空格以及windows文件流进行过滤,所以,我通过抓包将在文件后面添加空格,上传成功!说明成功绕过了黑名单,但是,当我复制文件的路径,请求访问时,却返回了404页面,甚是诡异!明白了,这个源代码判断的是我们输入的文件名后缀是否在黑名单中,更原来的文件名没有任何关系。所以关键不在于抓包,而且也不需要抓包改包,我们只需要 在输入save_name这个post参数的时候,在后缀上添加::$DATA进行文件流绕过 或者 在后面直接添加一个空格就能简单的绕过。

其实这道题,出题人的本意是考察move_uploaded_file()这个函数的00截断漏洞,我们只要像上道题一样,在save_name中输入.phpa,然后通过抓包,在hex中修改a的为00即可利用这个漏洞。

Apache解析缺陷:

Apache的解析漏洞主要特性为Apache是从后面开始检查后缀,按最后一个合法后缀来解析

比如说:.php.jpg  因为.jpg不能解析,apache就会从右边往左看,看到.php可以解析,就会将文件作为.php解析

如何检测图片马?

function getRealFileType($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 = getRealFileType($temp_file);

    if($file_type == 'unknown'){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = $UPLOAD_ADDR."/".rand(10, 99).date("YmdHis").".".$file_type;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        }
        else{
            $msg = "上传失败";
        }
    }
}

条件竞争

 大马套小马:

';file_put_contents('1.php',$a)?>

条件竞争通常发生并发访问一个共享资源的时候(前提这个资源没有进行锁操作,即没有被单个线程锁定)。当多个线程同时去抢一个资源,不知道到底哪个线程能抢到,这样便形成了竞争。

因为开发人员在做开发防护的时候,一般只认为代码会以单线程的方式执行,他们忽略了并行服务器会并发执行多个线程,这就给了用条件竞争来绕过防护的机会。

例如,靶场17关这道题:

unlink(filename) 函数删除文件,文件名为filename,可以理解为相对路径或者绝对路径。如果删除成功,则返回true,如果失败,就返回false;

rename(oldname,newname)重命名文件或者目录,如果成功,返回true,如果失败,返回false;

$is_upload = false;
$msg = null;

if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_name = $_FILES['upload_file']['name'];
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_ext = substr($file_name,strrpos($file_name,".")+1);
    $upload_file = $UPLOAD_ADDR . '/' . $file_name;

    if(move_uploaded_file($temp_file, $upload_file)){
        if(in_array($file_ext,$ext_arr)){
             $img_path = $UPLOAD_ADDR . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
             rename($upload_file, $img_path);
             $is_upload = true;
        }else{
            $msg = "只允许上传.jpg|.png|.gif类型文件!";
            unlink($upload_file);
        }
    }else{
        $msg = '上传失败!';
    }
}

这个防护程序的思路是:不论上传文件的后缀怎么样,我先把它从临时文件夹中救出来,然后才判断文件的后缀是否匹配。如果匹配,我就随机命名重命名这个文件,如果不匹配,就调用unlink函数删除它。

如果从单线程的角度去理解,这个防护程序没有任何毛病。但是,假如我能在没有上传文件前,提前猜测出上传文件被存储的位置。那么我就能做到在不断上传文件的同时,不断的请求访问这个文件。我们知道脚本程序执行是需要时间的,那么一定存在一个时间段,脚本还没有执行到unlink函数语句,文件还没有被删除,而我这个时候访问到了文件。

有人会质疑,就算在那一刹那,你请求到了文件,文件的代码被执行了,又能怎么样呢?一刹那之后,这个文件又会被删除。

 在那短短的一刹那,假如你上传的文件,是个 大马套小马呢?大马虽死,但同文件夹中,小马犹生。一个网站便在弹指一挥间被拿下了。

那么如何不断上传文件的同时,不断请求访问这个文件呢?可以写脚本也可以使用现成的工具

以靶场17关为例,首先,我们要想办法猜出 假如我上传一个1.php文件,这个文件会被存储在哪里?我们可以先上传一张正规的jpg图片看看,它被存储在了什么地方?get到了!http://117.167.136.245:21781/Pass-17/upload//5420190628091046.jpg 

他被存在upload文件夹下:那么如果我们上传一个1.php文件,这个文件就会被转存在:http://117.167.136.245:21781/Pass-17/upload//1.php

这个位置,注意哈,rename函数 仅仅做到了对符合白名单的文件进行随机命名,对不符合白名单的文件,例如我们上传的1.php,它仍然是1.php

现在我们只需要,上传1.php,用burpsuit 拦截数据包,然后将数据包放在burpsuit的爆破模块中。

然后访问,http://117.167.136.245:21781/Pass-17/upload//1.php

同样,将请求拦截,放在burpsuit的爆破模块中。最后,在爆破模块中,让两者同时不断进行,等待那关键的一瞬间就可以了。

具体操作:

1.对两者都进行抓包,右键,send to intruder

2.首先 clear $ ,然后在payload中选择 Nullpayload,然后设置Generate 10000表示没有payload,上传一万次,然后在Option中,设置Number of thread线程数为200

你可能感兴趣的:(CTF,Web安全)