漏洞示例代码(代码上下文就不展示了,只看查询数据库操作的类)
public String jdbc_sqli_vul(@RequestParam("username") String username) { StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); if (!con.isClosed()) System.out.println("Connect to database successfully."); // sqli vuln code Statement statement = con.createStatement(); String sql = "select * from users where username = '" + username + "'"; logger.info(sql); ResultSet rs = statement.executeQuery(sql); while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); } rs.close(); con.close(); } catch (ClassNotFoundException e) { logger.error("Sorry,can`t find the Driver!"); } catch (SQLException e) { logger.error(e.toString()); } return result.toString(); }
直接看sql语句
String sql = "select * from users where username = '" + username + "'";
没有做任何的过滤就只是吧username拼接了一下,一定存在SQL注入的,简单测试一下
预防的方法是 加上预处理方法 防止sql注入
重复代码就不看了 直接看预处理部分
... String sql = "select * from users where username = ?"; PreparedStatement st = con.prepareStatement(sql); st.setString(1, username); logger.info(st.toString()); // sql after prepare statement ResultSet rs = st.executeQuery(); ...
在sql语句的构造中 直接加上一个问号当作占位符。st.setString(1, username);
将占位符替换为我们传入的值 然后执行sql语句
相同代码不再展示
...... String sql = "select * from users where username = '" + username + "'"; PreparedStatement st = con.prepareStatement(sql); ......
虽然使用了预处理 但是没有使用占位符 我们依旧可以在username处 传入sql语句 达到我们的目的
漏洞位置:
ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java
跟踪Db.update函数
继续跟踪MAIN.update
一直跟踪update到如下的方法
可以看到这里对sql没有进行任何的过滤
所以可以直接导致sql注入
public static String reflect(String xss) { return xss; } //反射型XSS public String store(String xss, HttpServletResponse response) { Cookie cookie = new Cookie("xss", xss); response.addCookie(cookie); return "Set param into cookie"; } //存储型XSS public String show(@CookieValue("xss") String xss) { return xss; } //将cookie中的XSS展示到页面中
这里的存储型XSS是存储到cookie中 正常网站的存储型XSS一般都是存储到数据库中
反射型XSS图示:
存储型XSS图示:
漏洞存在处:
ofcms-admin\ofcms-api\src\main\java\com\ofsoft\cms\api\v1\CommentApi.java
save方法中 将值传入params中 并且添加评论者的ip地址之后 直接保存到数据库中
通过跟踪Db._getSqlPara_
方法和Db._update_
方法 并未发现其对评论者的评论进行任何过滤
payload:<script>alert(1)</script>
写入一个替换特殊符号的方法
public static String safe(String xss) { return encode(xss); } private static String encode(String origin) { origin = StringUtils.replace(origin, "&", "&"); origin = StringUtils.replace(origin, "<", "<"); origin = StringUtils.replace(origin, ">", ">"); origin = StringUtils.replace(origin, "\"", """); origin = StringUtils.replace(origin, "'", "'"); origin = StringUtils.replace(origin, "/", "/"); return origin; //将各个符号进行替换 } }
替换符号只是一种方法
或者可以检测不合法的符号,如果字段里存在不合法的符号就返回此字段不合法。
public static String safe(String xss) { if (code(xss)=="false"){ System.out.println("参数不合法"); } } private static String code(String origin){ if (origin.contains(""+'&')||origin.contains(""+'<')||origin.contains(""+'>')||origin.contains(""+'&')||origin.contains(""+'"')){ return "false"; } return "true"; }
这只是这个想法的简单实现,对特殊符号的过滤都没写全
public String singleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { if (file.isEmpty()) { // 赋值给uploadStatus.html里的动态参数message redirectAttributes.addFlashAttribute("message", "Please select a file to upload"); return "redirect:/file/status"; } try { // Get the file and save it somewhere byte[] bytes = file.getBytes(); Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename()); Files.write(path, bytes); redirectAttributes.addFlashAttribute("message", "You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'"); } catch (IOException e) { redirectAttributes.addFlashAttribute("message", "upload failed"); logger.error(e.toString()); } return "redirect:/file/status"; }
可以看到就是正常上传
可以上传任何文件 没有任何过滤之类的
判断文件后缀是否为白名单 文件类型是否在黑名单中(或者在文件类型中也做一个白名单)
public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception { if (multifile.isEmpty()) { return "Please select a file to upload"; } String fileName = multifile.getOriginalFilename(); String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名 String mimeType = multifile.getContentType(); // 获取MIME类型 String filePath = UPLOADED_FOLDER + fileName; File excelFile = convert(multifile); // 判断文件后缀名是否在白名单内 校验1 String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"}; boolean suffixFlag = false; for (String white_suffix : picSuffixList) { if (Suffix.toLowerCase().equals(white_suffix)) { //转为小写 和白名单中的后缀进行对比 suffixFlag = true; break; } } if (!suffixFlag) { logger.error("[-] Suffix error: " + Suffix); deleteFile(filePath); return "Upload failed. Illeagl picture."; } // 判断MIME类型是否在黑名单内 校验2 String[] mimeTypeBlackList = { "text/html", "text/javascript", "application/javascript", "application/ecmascript", "text/xml", "application/xml" }; for (String blackMimeType : mimeTypeBlackList) { // 用contains是为了防止text/html;charset=UTF-8绕过 if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) { logger.error("[-] Mime type error: " + mimeType); deleteFile(filePath); return "Upload failed. Illeagl picture."; } } // 判断文件内容是否是图片 校验3 boolean isImageFlag = isImage(excelFile); deleteFile(randomFilePath); if (!isImageFlag) { logger.error("[-] File is not Image"); deleteFile(filePath); return "Upload failed. Illeagl picture."; } try { // Get the file and save it somewhere byte[] bytes = multifile.getBytes(); Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename()); Files.write(path, bytes); } catch (IOException e) { logger.error(e.toString()); deleteFile(filePath); return "Upload failed"; } logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType); logger.info("[+] Successfully uploaded {}", filePath); return String.format("You successfully uploaded '%s'", filePath); } private void deleteFile(String filePath) { File delFile = new File(filePath); if(delFile.isFile() && delFile.exists()) { if (delFile.delete()) { logger.info("[+] " + filePath + " delete successfully!"); return; } } logger.info(filePath + " delete failed!"); } /** * 为了使用ImageIO.read() * * 不建议使用transferTo,因为原始的MultipartFile会被覆盖 * https://stackoverflow.com/questions/24339990/how-to-convert-a-multipart-file-to-file */ private File convert(MultipartFile multiFile) throws Exception { String fileName = multiFile.getOriginalFilename(); String suffix = fileName.substring(fileName.lastIndexOf(".")); UUID uuid = Generators.timeBasedGenerator().generate(); randomFilePath = UPLOADED_FOLDER + uuid + suffix; // 随机生成一个同后缀名的文件 File convFile = new File(randomFilePath); boolean ret = convFile.createNewFile(); if (!ret) { return null; } FileOutputStream fos = new FileOutputStream(convFile); fos.write(multiFile.getBytes()); fos.close(); return convFile; } /** * Check if the file is a picture. */ private static boolean isImage(File file) throws IOException { BufferedImage bi = ImageIO.read(file); //读取图片内容 return bi != null; } }
这一串代码很完美的解决了文件上传产生的各种问题
文件被使用UUID库随机常见一个名字 可以防止恶意文件上传被连接访问
1.通过suffixFlag来判断文件的后缀名是否处于白名单中
2.然后使用SecurityUtil.replaceSpecialStr
方法处理一下mimeType
,将非0-9a-zA-Z/-.的字符替换为空,然后和黑名单类型做对比
3.通过isImage
方法 判断文件是否为图片文件
以上者三个校验 有一个无法通过就会将文件直接删除。
XXE:XML External Entity 即外部实体从安全角度理解成XML External Entity attack 外部实体注入攻击。可以导致读取任意文件或SSRF、端口探测、DoS拒绝服务攻击、执行系统命令、攻击内部网站等。
public String DocumentBuilderVuln01(HttpServletRequest request) { try { String body = WebUtils.getRequestBody(request); logger.info(body); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(body); InputSource is = new InputSource(sr); Document document = db.parse(is); // parse xml // 遍历xml节点name和value StringBuilder buf = new StringBuilder(); NodeList rootNodeList = document.getChildNodes(); for (int i = 0; i < rootNodeList.getLength(); i++) { Node rootNode = rootNodeList.item(i); NodeList child = rootNode.getChildNodes(); for (int j = 0; j < child.getLength(); j++) { Node node = child.item(j); buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent())); } } sr.close(); return buf.toString(); } catch (Exception e) { logger.error(e.toString()); return EXCEPT; } }
可以看到,没有进行过滤和防护,直接解析我们传入的XML文件,导致XXE
payload:
<?xml version="1.0" encoding="UTF-8"?> <book id="1"> <name>Good Job</name> <author>ol4three</author> <year>2021</year> <price>100.00</price> </book>
public String DocumentBuilderSec(HttpServletRequest request) { try { String body = WebUtils.getRequestBody(request); logger.info(body); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(body); InputSource is = new InputSource(sr); db.parse(is); // parse xml sr.close(); } catch (Exception e) { logger.error(e.toString()); return EXCEPT; } return "DocumentBuilder xxe security code"; }
serFeature是关键,设置了过后,再解析xml时会直接报错
因为我对XXE了解不是很深刻 所以对于此漏洞的审计也是比较浅薄
对XXE有兴趣的师傅可以移步
https://www.freebuf.com/articles/web/318984.html
https://blog.spoock.com/2018/10/23/java-xxe/
public String getImage(String filepath) throws IOException { return getImgBase64(filepath); } private String getImgBase64(String imgFile) throws IOException { logger.info("Working directory: " + System.getProperty("user.dir")); logger.info("File path: " + imgFile); File f = new File(imgFile); if (f.exists() && !f.isDirectory()) { byte[] data = Files.readAllBytes(Paths.get(imgFile)); return new String(Base64.encodeBase64(data)); } else { return "File doesn't exist or is not a file."; } }
对路径参数没有进行任何过滤。简单的判断传入的路径参数存不存在,然后将文件内容base64加密返回。
public String getImageSec(String filepath) throws IOException { if (SecurityUtil.pathFilter(filepath) == null) { logger.info("Illegal file path: " + filepath); return "Bad boy. Illegal file path."; } return getImgBase64(filepath); } public static String pathFilter(String filepath) { String temp = filepath; // use while to sovle multi urlencode while (temp.indexOf('%') != -1) { try { temp = URLDecoder.decode(temp, "utf-8"); } catch (UnsupportedEncodingException e) { logger.info("Unsupported encoding exception: " + filepath); return null; } catch (Exception e) { logger.info(e.toString()); return null; } } if (temp.contains("..") || temp.charAt(0) == '/') { return null; //对url传入的参数进行判断 } return filepath; }
可以看到pathFilter
方法对URL传入的路径参数进行判断 ,但是判断并不完全,只是一个示例,应该根据实际情况进行改变,比如系统 Linux或者Windows又或者是业务逻辑情况,进行自定义的更改。
如果是Linux系统应该禁用/ , ..
temp.contains("..") || temp.contains("/")
Windows系统应该禁用: , .. , c , d , e, /
防止切换不同盘符进行文件读取
temp.contains("..") || temp.contains("/") || temp.contains(":") ||temp.charAt(0) == "c" ...