描述
在对文件进行操作(读,写,删除)的过程中,未对文件路径进行有效的合法性校验,导致文件被任意下载,上传任意文件导致服务器被种植木马,getshell
场景
- 任意文件下载
- 任意文件上传
任意文件下载
任意文件下载漏洞,一些网站由于业务需求,往往需要提供文件查看或文件下载功能,正常的利用手段是下载服务器文件,但若对用户查看或下载的文件不做限制,则恶意用户就能够查看或下载任意敏感文件
代码分析
@RestController
@RequestMapping("/file")
public class FileController {
/**
* 下载任意文件,固定目录+文件名形式
* @param response
* @param fileName
* @throws IOException
*/
@GetMapping(value = "/download")
public void downloadFile(HttpServletResponse response, @RequestParam("fileName") String fileName) throws IOException {
/**
* 在/Users/oscar/Downloads/test/download/file/ 目录下存在多个 txt 文件
* 正常的业务场景是,前端传入fileName文件名(例如 test.txt),下载/Users/oscar/Downloads/test/download/file/下对应的文件
* 黑客在攻击时会可能会传入一个文件路径, 例如 ../../../../etc/passwd等路径,这样就能轻松下载服务器的 passwd 文件
*/
String filePath = "/Users/oscar/Downloads/test/download/file/" + fileName;
this.handlerDownloadFile(response, filePath);
}
}
正常的请求 url:http://localhost:8080/file/download?fileName=test.txt
黑客的请求 url:http://localhost:8080/file/download?fileName=../../../../../etc/passwd
修复方案建议
从修复安全的实践中,总结了两个简单快速检验方法,代码实现如下
public class FortifyUtils {
private final static String DOT_STR = ".";
/**
* 校验文件名是否正常
* @param fileName 文件名
* @return true 正常 false 异常
*/
public static boolean checkFileName (String fileName){
if (StringUtils.isEmpty(fileName)) {
return false;
}
// 判断文件名中是否包含/ \ .
if (fileName.contains("/") || fileName.contains("\\") || fileName.contains(".")) {
return false;
}
return true;
}
/**
* 校验文件名是否正常
* @param fileName 文件名
* @param fileTypes 文件格式
* @return true 正常 false 异常
*/
public static boolean checkFileName (String fileName, String... fileTypes){
if (StringUtils.isEmpty(fileName)) {
return false;
}
if (null == fileTypes || fileTypes.length == 0) {
checkFileName(fileName);
}
boolean flag = false;
int fileEnd = 0;
for (String fileType : fileTypes) {
if (!fileType.startsWith(DOT_STR)) {
fileType = DOT_STR + fileType;
}
if (fileName.endsWith(fileType)) {
flag = true;
fileEnd = fileType.length();
break;
}
}
// 文件类型不匹配
if (!flag) {
return false;
}
// 判断文件名中是否包含/ \
// 以及在去除文件后缀 + . 的情况下是否包含 .
if (fileName.contains("/") || fileName.contains("\\")
|| fileName.substring(0, fileName.length() - fileEnd).contains(".")) {
return false;
}
return true;
}
}
任意文件上传
导致该漏洞的原因在于代码作者没有对访客提交的数据进行检验或者过滤不严,可以直接提交修改过的数据绕过扩展名的检验
攻击者通过上传木马文件,直接得到WEBSHELL,危害等级超级高,现在的入侵中上传漏洞也是常见的漏洞
代码实现
此次上传文件存在四处问题
- 未校验文件格式(包含校验文件后缀 和 校验文件内容中的文件格式)
- 将上传的文件上传至容器能解析位置
- 文件未重命名,重命名后增加黑客攻击难度
- 将文件上传地址信息返回前端
/**
* 上传任意文件
* @param file 上传文件
* @return
*/
@PostMapping("/upload")
public JSONObject upload(@RequestParam("file") MultipartFile file) {
/**
* 此处文件上传存在四处安全问题
* 1. 未校验文件格式(包含校验文件后缀 和 校验文件内容中的文件格式)
* 2. 将上传的文件上传至容器能解析位置
* 3. 文件未重命名,重命名后增加黑客攻击难度
* 4. 将文件上传地址信息返回前端
*/
try {
FileCopyUtils.copy(file.getBytes(), new File("static/" + file.getOriginalFilename()));
} catch (IOException e) {
e.printStackTrace();
}
return JSONObject.parseObject("{\"code\":\"200\", \"msg\":\"文件上传成功\",\"data\":{\"filePath\":\"static/xx.xx\"}}");
}
修复建议
从校验上传文件类型上下手,方法有两种
- 校验文件格式后缀的合法性
- 校验文件内容二进制的文件类型标识判断文件类型的合法性
public class FortifyUtils {
private final static Map FILE_TYPE_MAP = new HashMap();
static {
// 加载文件类型
getAllFileType();
}
/**
* 验证上传文件的文件类型
* @param file 上传的文件
* @return
*/
public static String getFileHeader(MultipartFile file) {
String fileName = file.getOriginalFilename();
String fileType = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(Locale.ENGLISH);
if ("txt".equalsIgnoreCase(fileType) || "ppt".equalsIgnoreCase(fileType)
|| "tiff".equalsIgnoreCase(fileType) || "cebx".equalsIgnoreCase(fileType)
|| "3gp".equalsIgnoreCase(fileType) || "mpeg".equalsIgnoreCase(fileType)
|| "swf".equalsIgnoreCase(fileType) || "ceb".equalsIgnoreCase(fileType)) {
return fileType;
}
String value = null;
try (InputStream is = file.getInputStream()) {
byte[] b = new byte[4];
is.read(b, 0, b.length);
value = bytesToHexString(b);
} catch (IOException e) {
return null;
}
String result = null;
Iterator keyIter = FILE_TYPE_MAP.keySet().iterator();
while (keyIter.hasNext()) {
String key = keyIter.next();
if (!StringUtils.isEmpty(value)) {
if (key.toLowerCase(Locale.ENGLISH).startsWith(value.toLowerCase(Locale.ENGLISH))
|| value.toLowerCase(Locale.ENGLISH).startsWith(key.toLowerCase(Locale.ENGLISH))) {
result = FILE_TYPE_MAP.get(key);
break;
}
}
}
return result == null ? fileType : result;
}
/**
* 得到上传文件的文件头
* @param src 文件头字节数组
* @return
*/
private static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder();
if (null == src || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
private static void getAllFileType() {
FILE_TYPE_MAP.put("ffd8ffe000104a464946", "jpg");
FILE_TYPE_MAP.put("89504e470d0a1a0a0000", "png");
FILE_TYPE_MAP.put("47494638396126026f01", "gif");
FILE_TYPE_MAP.put("49492a00227105008037", "tif");
FILE_TYPE_MAP.put("424d228c010000000000", "bmp"); // 16色位图(bmp)
FILE_TYPE_MAP.put("424d8240090000000000", "bmp"); // 24位位图(bmp)
FILE_TYPE_MAP.put("424d8e1b030000000000", "bmp"); // 256色位图(bmp)
FILE_TYPE_MAP.put("41433130313500000000", "dwg");
FILE_TYPE_MAP.put("3c21444f435459504520", "html");
FILE_TYPE_MAP.put("3c21646f637479706520", "htm");
FILE_TYPE_MAP.put("48544d4c207b0d0a0942", "css");
FILE_TYPE_MAP.put("696b2e71623d696b2e71", "js");
FILE_TYPE_MAP.put("7b5c727466315c616e73", "rtf");
FILE_TYPE_MAP.put("38425053000100000000", "psd");
FILE_TYPE_MAP.put("46726f6d3a203d3f6762", "eml");
FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "doc"); // MS Excel、Word、Msi
FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "vsd");
FILE_TYPE_MAP.put("5374616E64617264204A", "mdb");
FILE_TYPE_MAP.put("255044462d312e350d0a", "pdf");
FILE_TYPE_MAP.put("2e524d46000000120001", "rmvb"); // rmvb、rm
FILE_TYPE_MAP.put("464c5601050000000900", "flv"); // flv、f4v
FILE_TYPE_MAP.put("00000020667479706d70", "mp4");
FILE_TYPE_MAP.put("49443303000000002176", "mp3");
FILE_TYPE_MAP.put("000001ba210001000180", "mpg");
FILE_TYPE_MAP.put("3026b2758e66cf11a6d9", "wmv"); // wmv、asf
FILE_TYPE_MAP.put("52494646e27807005741", "wav");
FILE_TYPE_MAP.put("52494646d07d60074156", "avi");
FILE_TYPE_MAP.put("4d546864000000060001", "mid");
FILE_TYPE_MAP.put("504b0304140000000800", "zip");
FILE_TYPE_MAP.put("526172211a0700cf9073", "rar");
FILE_TYPE_MAP.put("235468697320636f6e66", "ini");
FILE_TYPE_MAP.put("504b03040a0000000000", "jar");
FILE_TYPE_MAP.put("4d5a9000030000000400", "exe");
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
FILE_TYPE_MAP.put("4d616e69666573742d56", "mf");
FILE_TYPE_MAP.put("3c3f786d6c2076657273", "xml");
FILE_TYPE_MAP.put("494e5345525420494e54", "sql");
FILE_TYPE_MAP.put("7061636b616765207765", "java");
FILE_TYPE_MAP.put("406563686f206f66660d", "bat");
FILE_TYPE_MAP.put("1f8b0800000000000000", "gz");
FILE_TYPE_MAP.put("6c6f67346a2e726f6f74", "properties");
FILE_TYPE_MAP.put("cafebabe0000002e0041", "class");
FILE_TYPE_MAP.put("49545346030000006000", "chm");
FILE_TYPE_MAP.put("04000000010000001300", "mxp");
FILE_TYPE_MAP.put("504b0304140006000800", "docx");
FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "wps");// WPS(wps、et、dps)
FILE_TYPE_MAP.put("6431303a637265617465", "torrent");
FILE_TYPE_MAP.put("6D6F6F76", "mov");
FILE_TYPE_MAP.put("FF575043", "wpd");
FILE_TYPE_MAP.put("CFAD12FEC5FD746F", "dbx");
FILE_TYPE_MAP.put("2142444E", "pst");
FILE_TYPE_MAP.put("AC9EBD8F", "qdf");
FILE_TYPE_MAP.put("E3828596", "pwl");
FILE_TYPE_MAP.put("2E7261FD", "ram");
}
}
代码地址
https://github.com/huaweirookie/toalibaba/tree/master/security