前言:看到同乡学长说面试时候经常问 Java 方面的安全知识,于是买了本奇安信的 Java 审计书看了下,已经学完,补一下笔记。WEB 常见漏洞的原理基本是互通的,所以这个笔记不着重讲常见漏洞的原理,只记录一些审计时候要注意的函数和功能代码,以及可能与 PHP 不同的地方。
常见漏洞审计
1.SQL 注入漏洞
Java 执行 SQL 语句常见方式:
- 使用 JDBC 的 java.sql.Statement 执行 SQL 语句
- 使用 JDBC 的 java.sql.PreparedStatement 执行 SQL 语句
- 使用 Hibernate 执行 SQL 语句
- 使用 MyBatis 执行 SQL 语句
其中 Statement 需要通过拼接来执行语句且每次都要进行编译,若不对输入做过滤会导致 SQL 注入漏洞;而 PrepareStatement 进行预编译参数化查询能够有效防止 SQL 注入。通过搜索这两个类的调用并进行审计,从而检查出漏洞是否存在。
PreparedStatement 是使用占位符传入参数,因此在执行 order by 时会出错(具体原因不详细说明,自行百度),只能使用字符串拼接的方式,因此只能使用 Statement。
Java 预编译查询中不会对 % 和_进行转义处理,而 % 和_是 like 查询的通配符,不做相关过滤可能导致恶意模糊查询。
MyBatis 中的 #{} 在底层上使用 “?” 作为占位符来生成 PreparedStatement,是参数化查询预编译的机制,较安全,也因此在使用 order by、like、in 查询时会报错,需要转用 ${} 进行查询;${} 将传入的数据直接显示生成在 SQL 语句中,类似字符串拼接,因此不对输入进行检查、过滤将可能导致 SQL 注入。
审计总结:(留意 getParameter 函数来找到可控参数)
- Statement
- createStatement
- PreparedStatement
- like ‘%${
- in (${
- select
- update
- insert
2. 任意文件上传漏洞
常见文件上传方式:
通过文件流的方式上传
public String fileUpload(@RequestParam("file") CommonsMulpartFile file) throws IOException{ long startTime = System.currentTimeMillis(); System.out.println("fileName: "+file.getOriginalFilename()); try{ OutputStream os = new FileOutputStream("/tmp"+newDate().getTime()+file.getOriginalFilename()); InputStream is = file.getInputStream(); int temp; while((temp = is.read())!=(-1)) { os.write(temp); } os.flush(); os.close(); is.close(); } catch(FileNotFoundException e){ e.printStackTrace(); } return "/success"; }
通过 ServletFileUpload 方式上传
String realPath = this.getServletContext().getRealPath("/upload"); String tempPath = "/tmp"; File f = new File(realPath); if(!f.exists()&&!f.isDirectory()){ f.mkdir(); } File f1 = new File(tempPath); if(!f1.isDirectory)){ f1.mkdir(); } DiskFileUploadItemFactory factory = new DiskFileItemFactory(); factory.setRepository(f1); ServletFileUpload upload = new ServletFileUpload(factory); upload.setHeaderEncoding("UTF-8"); if(!ServletFileUpload.isMultipartContent(req)){ return; } List<FileItem> items = upload.parseRequest(req); for(FileItem item:items){ if(item.isFormField()){ String filedName = item.getFieldName(); String filedValue = item.getString("UTF-8"); } else{ String filedName = item.getName(); if(fileName == null || "".equals(fileName.trim())){ continue; } fileName = fileName.substring(fileName.lastIndexOf("/")+1); String filePath = realPath+"/"+fileName; InputStream in = item.getInputStream(); OutputStream out = new FileOutputStream(filePath); byte b[] = new byte[1024]; int len = -1; while((len=in.read(b)))!=-1){ out.write(b,0,len); } out.close(); in.close(); try{ Thread.sleep(3000); } catch(InterruptedException e){ e.printStackTrace(); } item.delete();
通过 MultipartFile 方式上传
public String handleFileUpload(@RequestParam("file") MultipartFile file){ if(file.isEmpty()){ return "请上传文件"; } //获取文件名 String fileName = file.getOriginalFilename(); String suffixName = fileName.substring(fileName.indexOf(".")); String filePath = "/tmp"; File dest = new File(filePath+fileName); if(!dest.getParentFile().exists()){ dest.getParentFile().mkdirs(); } try{ file.transferTo(dest); return "上传成功"; } catch(IllegalStateException e){ e.printStackTrace(); } catch(IOException e){ e.printStackTrace(); } return "上传失败"; }
审计总结:(检查前后端是否存在后缀过滤不全和读取后缀方式错误等)
- org.apache.commons.fileupload
- java.io.File
- MultipartFile
- RequestMethod
- MultipartHttpServletRequest
- CommonsMultipartResolver
3.XSS 漏洞
XXS 漏洞的产生必然存在相关的输入 / 输出:Java 输入通常使用 request.getParameter (param) 或 ${param} 输入信息。输出表现为前端的渲染,可通过定位前端中一些标识来找。
XSS 常见触发位置
JSP 表达式
- <%= 变量 %>
- <% out.println (变量); %>
- <% String msg = request.getParameter (‘变量’);%>
EL(Expression Language,表达式语言)
- <c:out> 标签:显示一个表达式的结果
- <c:if> 标签:判断表达式的值
- <c:forEach> 标签:迭代输出标签内部的内容
ModelAndView 类用来存储处理完成后的结果数据,以及显示该数据的视图,其前端 JSP 页面可以使用 ${参数} 的方法来获取值:
package com.dgr.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @RequestMapping("mvc") @Controller public class TestRequestMapping{ @RequestMapping(value="/getMessage") public ModelAndView getMessage(){ ModelAndView modelandview = new ModelAndView(); modelandview.setViewName("message"); modelandview.addObject("message","Hello World"); return modelandview; } }
ModelMap 类的使用
//Spring也提供了ModelMap类,此处用java.util.Map实现,可根据模型属性的具体类型自动生成模型属性的名称 public String testmethod(String someparam,ModelMap model){ //省略方法处理逻辑 //将数据放置到ModelMap类的model对象中,第二个参数可以是任何Java类型 Model.addAttribute("key",someparam); return "success"; }
Model 类的使用(Model 类是一个接口类,通过 attribute () 添加数据,存储的数据范围是 requestScope)
public String index1(Model model){ model.addAttribute("result","后台返回"); return "result"; }
审计总结:
- <%=
- ${
- <c:out
- <c:if
- <c:forEach
- ModelAndView
- ModelMap
- Model
- request.gerParameter
- request.setAttribute
- response.getWriter().print()
- response.getWriter().writer()
4. 目录穿越漏洞
目录穿越的本质是路径可控,一旦涉及文件的读取问题便会涉及 java.io.File 类,审计时优先查找 java.io.File 类的调用,判断 Paths、path、System.getProperty (“user.dir”) 等可能会用来构造路径的关键字
5.URL 跳转漏洞
检查用于 URL 重定向的方法:
- 通过 ModelAndView
- 通过返回 String
- 使用 sendRedirect
- 使用 RedirectAttributes
- 通过设置 Header
6. 命令执行漏洞
关注 Runtime.getRuntime.exec、ProcessBuilder.start;Java 的 Runtime.getRuntime.exec 和 ProcessBuilder.start 执行系统命令时,实际上并没有获得 UNIX 或 Linux shell 并在其中允许命令,因此要使用 UNIX/Linux 管道之类的功能,必须先通过 /bin/sh 调用一个 shell 程序
Java.lang.ProcessBuilder 类用于创建操作系统进程,每个 ProcessBuilder 实例管理一个进程属性集。start () 方法利用这些属性创建一个新的 Process 进程实例,从而利用 ProcessBuilder 执行命令:
ProcessBuilder pb = new ProcessBuilder("mycommand","myArg"); Process process = pb.start();
java.lang.Runtime 公共类中的 exec () 方法同样也可以执行系统命令:
- public Process exec(String command)
- public Process exec(String[] cmdarray)
- public Process exec(String[] cmdarray,String[] envp)
- public Process exec(String[] cmdarray,String[] envp,File dir)
- public Process exec(String command,String[] envp)
- public Process exec(String command,String[] envp,File dir)
7.XXE 漏洞
搜索常见的能够解析 XML 的方法:
- XMLReader
- SAXBuilder
- SAXReader
- SAXParserFactory
- Digester // 通常没有回显
- DocumentBuilderFactory
8.SSRF 漏洞
与 PHP 不同,在 Java 中 SSRF 仅支持 sun.net.www.protocol 下所有的协议:http、https、file、ftp、mailto、jar 及 netdoc 协议,因此不能像 PHP 一样使用 gopher 协议来扩展攻击面
在 Java 中可以通过 file 或 netdoc 协议进行列目录操作以读取更多敏感信息,对于无回显的文件读取可以利用 ftp 协议进行外带攻击(部分版本的 Java 即使使用 ftp 协议也无法读取多行文件
注意能够发起 HTTP 请求的类及函数:
- HttpURLConnection.getInputStream
- URLConnection.getInputStream
- HttpClient.execute
- OkHttpClient.newCall.execute
- Request.Get.execute
- Request.Post.execute
- [url].openStream () 方法
- ImageIO.read
若想支持 sun.net.www.protocol 中的所有协议则只能使用方法:URLConnection、URL
若发起的网络请求是带 HTTP 的,那么其将只支持 HTTP、HTTPS 协议:HttpURLConnection、HttpClient、OkHttpClient.newCall.execute
9.SpEL 表达式注入漏洞
SpEL 表达式语法:
- 使用量表达式语法:”#{‘Hello World’}”
- 使用 Java 代码 new/instanceof:Expression exp = parser.parseExpression (“new Spring (‘Hello World’)”);
- 使用 T (Type) 来表示 java.lang.Class 实例:parser.parseExpression (“T (Integer).MAX_VALUE”);
SpEL 中可以使用 #bean_id 来获取容器内的变量。同时,存在两个特殊的变量 #this、#root,分别用来表示使用当前的上下文和引用容器的 root 对象
SpEL 可以使用 T () 操作符声明特定的 Java 类型,一般用来访问 Java 类型中的静态属性或静态方法。括号中需要包含类名的全限定名,也就是包名加上类名。唯一例外的是 SpEL 内置了 java.lang 包下的类声明,例如 java.lang.String 可以通过 T (String) 访问而不需要使用全限定名
使用 new 可以直接在 SpEL 中创建实例,创建实例的类要通过全限定名进行访问:
ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("new.java.util.Date()"); Date value = (Date)exp.getValue(); System.out.println(value);
产生 SpEL 表达式注入的大前提是存在 SpEL 的相关库,因此可以针对以下关键字进行搜索,并跟踪参数是否可控:
- org.springframework.expression.spel.standard
- SpelExpressionParser
- parseExpression
- expression.getValue()
- expression.setValue()
SpEL 漏洞成因:Spring 为解析 SpEL 提供了两套接口,分别是 SimpleEvaluationContext 和 StandardEvaluationContext。SimpleEvaluationContext 仅支持 SpEL 语法的子集,抛弃了 Java 类型引用、构造函数及 beam 引用相对安全。而 StandardEvaluationContext 包含了 SpEL 所有功能,且在不指定 EvaluationContext 情况下默认采用 StandardEvaluationContext;很大一部分开发人员未对用户输入进行处理就直接通过解析引擎对 SpEL 继续解析
通过插入以下 POC 检查是否存在 SpEL 表达式注入:
- ${4*4}
- T(Thread).sleep(10000)
- T(java.lang.Runtime).getRuntime().exec(‘command’)
- T(java.lang.Runtime).getRuntime().exec(“nslookup baidu.com”)
- new java.lang.ProcessBuilder(“command”).start()
- new java.lang.ProcessBuilder({‘nslookup baidu.com’}).start()
- #this.getClass().forName(‘java.lang.Runtime’).getRuntime().exec(‘nslookup baidu.com’)
对于常见黑名单过滤可以利用两种方式构造 payload:利用反射与拆分关键字构造、利用 ScriptEngineManager 构造
10.Java 反序列化
审计关键字:
- ObjectInputStream.readObject
- ObjectInputStream.readUnshared
- XMLDecoder.readObject
- Yaml.load
- XStream.fromXML
- ObjectMapper.readValue
- JSON.parseObject
搜索关键字找到存在反序列化操作的文件时,考虑参数是否可控,以及引用的 Class Path 是否包含 Apache Commons Collections 等危险库(ysoserial 所支持的其他库亦可)。同时满足以上条件则通过 ysoserial 工具生成 payload 直接打。不支持危险库则尝试通过构造利用链接实现任意代码执行
11.SSTI 模板注入漏洞
Java 中常见模板引擎:XMLTemplate、Velocity、CommonTemplate、FreeMarker、Smarty4j、TemplateEngine 等,Velocity 使用较多
在 Velocity 中以 #来标识 Velocity 的脚本语句例如 #set、#if、#else、# 恩典、#foreach、#iinclude、#parse 等
#if($msg.img) <img src="$msg.imgs" border=0> #else <img src="qccp.jpg"> #end
$ 用来标识一个对象,一旦可以调用对象则有办法构造命令执行语句:$e.getClass ().forName (“java.lang.Runtime”).getMethod (“getRuntime”,null).invoke (null,null).exec ()
在 Velocity 模板注入中,如果无法进行命令执行,往往可以通过修改 Cookie 来进行特权升级,例如:$session.setAttribute (“IS_ADMIN”,”1″)
在漏洞不存在回显的情况且容器为 Tomcat7 时,可通过以下方法构造一个有回显的命令执行:
#set($str=$class.inspect("java.lang.String").type) #set($chr=$class.inspect("java.lang.Character").type) #set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami")) $ex.waitFor() #set($out=$ex.getInputStream()) #foreach($i in [1..$out.available()]) $str.valueOf($chr.toChars($out.read())) #end
审计关键字:搜索对于模板引擎的关键字,例如 velocity 对应的 org.apache.velocity
12. 整数溢出漏洞
java 中的 int 是 32 位有符号整数类型,其最大值是 0x7fffffff,最小值是 0x80000000,即 - 2147483648~2147483647 之间。当运算结果超出这个范围则发生了溢出,而且不会有任何异常抛出
13. 硬编码密码漏洞
硬编码密码指在系统中采用明文的形式存储密码(密码直接存储在源代码甚至是前端代码中),通常会导致严重的身份验证失败。
14. 不安全的随机数生成器
计算机产生的随机数具有周期性、可预测性,例如 java.util.Random 工具类没带参数构造函数生成的 Random 对象的种子默认是当前系统时间的毫秒数,故进入到 Random 类中查看其种子默认是当前的系统时间,只要种子一样,其输出的随机序列也是一样的