描述
在對文件進行操作(讀,寫,刪除)的過程中,未對文件路徑進行有效的合法性校驗,導致文件被任意下載,上傳任意文件導致服務器被種植木馬,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<String, String> FILE_TYPE_MAP = new HashMap<String, String>();
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<String> 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