Java代码审计基础(一)
本文重点是让大家了解JAVA代码审计的基础,会以漏洞示例的方式介绍JAVA代码中常见Web漏洞的形成和针对的修复方案,文章是在国外网站上看到的,因为在接触JAVA代码审计,感觉挺高大上的文章,很适合对JAVA代码审计感兴趣的童鞋找感觉之用,就根据自己的理解翻译了过来,拿去某平台投稿,被鄙视说太基础,就只好发自己的Blog上了。
SQL注入
SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。
漏洞示例:
<%@page import="java.sql.*"%> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title> </title> </head> <body> <% String user = request.getParameter("user"); String pass = request.getParameter("pass"); Class.forName("com.mysql.jdbc.Driver"); Connection con = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root" ,""); Statement st= con.createStatement(); ResultSet rs=st.executeQuery("select * from users where username='"+user+"' and password='"+pass+"' limit 0,1"); if(rs.next()) { out.println("Login success"); } else { out.println("Login failed"); } %>
在上述代码中,开发者使用声明的类来创建一个SQL语句,并执行它来获取一个有效用户的用户名和密码。由于使用拼接SQL语句,并且没有做任何防注入的手段,导致存在SQL注入漏洞,可以绕过登录验证。
修复后的代码如下:
<%@page import="java.sql.*"%> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title></title> </head> <body> <% String user = request.getParameter("user"); String pass = request.getParameter("pass"); Class.forName("com.mysql.jdbc.Driver"); Connection con = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root" , ""); PreparedStatement ps=(PreparedStatement) con.prepareStatement("select * from users where username=? and password=? limit 0,1"); ps.setString(1,user); ps.setString(2,pass); ResultSet rs=ps.executeQuery(); if(rs.next()) { out.println("Login success"); } else { out.println("Login failed"); } %> </body>
修复后的代码使用了PreparedStatement预编译的方式,使用这种方式无需对传入的参数进行过滤等处理,因为由于PreparedStatement内置了字符过滤,因此是能够防止SQL注入的。
数据明文存储
11年底CSDN的数据在网上泄漏,据说库中的密码都是明文存储的,因此导致非常大的影响。如果使用了复杂的加密存储,即时数据泄漏,密码也有可能不会破解成功,下面看看JAVA代码中数据明文存储的设计缺陷是如何形成的。
漏洞示例:
<%@page import="java.sql.*"%> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title></title> </head> <body> <% String user=request.getParameter("user"); String pass=request.getParameter("pass"); Class.forName("com.mysql.jdbc.Driver"); Connection con=(Connection)DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root" ,""); PreparedStatement ps = (PreparedStatement) con.prepareStatement("insert into users_crypt values(?,?)"); ps.setString(1,user); ps.setString(1,pass); int res = ps.executeUpdate(); if(res>0) { out.println("Register success"); } else { out.println("Register failed"); } %> </body>
可以看到上面的代码对获取到的user和pass的没有做任何的加密处理,直接存储到数据库中,导致存在明文存储的缺陷。
修复后的代码如下:
<%@page import="java.util.Calendar"%> <%@page import="java.text.SimpleDateFormat"%> <%@page import="java.text.DateFormat"%> <%@page import="java.math.BigInteger"%> <%@page import="java.security.MessageDigest"%> <%@page import="java.sql.*"%> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title></title> </head> <body> <% String user=request.getParameter("user"); String pass=request.getParameter("pass"); DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Calendar cal = Calendar.getInstance(); String reg_time = (dateFormat.format(cal.getTime())).toString(); String original = pass+reg_time; String s = pass+reg_time; MessageDigest m = MessageDigest.getInstance("MD5"); m.update(s.getBytes(),0,s.length()); String calc_hash = new BigInteger(1,m.digest()).toString(16); if(calc_hash.length()<32) { calc_hash = "0"+calc_hash; } Class.forName("com.mysql.jdbc.Driver"); Connection con=(Connection)DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root" ,""); PreparedStatement ps = (PreparedStatement) con.prepareStatement("insert into users_crypt values(?,?,?)"); ps.setString(1,user); ps.setString(2,calc_hash); ps.setString(3,reg_time); int res = ps.executeUpdate(); if(res>0) { out.println("Register success"); } else { out.println("Register failed"); } %> </body> </html>
修复后的代码将用户注册的时候输入的密码和注册时间组合加密之后保存在数据库中,这样如果数据泄漏,不阅读源代码很难将密码破解成明文形式,大大降低了泄密之后的影响。
失效的会话管理
该漏洞主要是因为Web应用程序没有正确的执行会话管理,例如用户登陆前的会话Cookie和登录后的是一样的,另外一个例子是当用户点击退出的时候,Session不会失效。
示例漏洞代码:
<%@page import="java.sql.*"%> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title> </title> </head> <body> <% String user = request.getParameter("user"); String pass = request.getParameter("pass"); Class.forName("com.mysql.jdbc.Driver"); Connection con = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root" , ""); PreparedStatement ps=(PreparedStatement) con.prepareStatement("select * from users where username=? and password=? limit 0,1"); ps.setString(1,user); ps.setString(2,pass); ResultSet rs=ps.executeQuery(); if(rs.next()) { session.setAttribute("useracc", rs.getString("user")); out.println("Login success"); } else { out.println("Login failed"); } %> </body>
修复后的代码:
<%@page import="java.sql.*"%> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title> </title> </head> <body> <% String user = request.getParameter("user"); String pass = request.getParameter("pass"); Class.forName("com.mysql.jdbc.Driver"); Connection con = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/userdb", "root" , ""); PreparedStatement ps=(PreparedStatement) con.prepareStatement("select * from users where username=? and password=? limit 0,1"); ps.setString(1,user); ps.setString(2,pass); ResultSet rs=ps.executeQuery(); if(rs.next()) { session.invalidate(); request.getSession(true); session.setAttribute("useracc", rs.getString("user")); out.println("Login success"); } else { out.println("Login failed"); } %> </body>
以上修复后的代码中,用户在登录的时候,首先会让之前的session失效,然后又获取新的seesion。
XSS漏洞
Xss漏洞小伙伴们应该都比较熟悉了,攻击者可以向网页中注入恶意的JS或者HTML代码,有反射XSS、存储XSS、DOM XSS三种。
漏洞示例:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>XSS Vulnerable</title> </head> <body> <form action="xss-vuln.jsp" method="post"> Enter your name: <input type="text" name="name"><input type="submit"> </form> <% if(request.getMethod().equalsIgnoreCase("post")) { String name = request.getParameter("name"); if(!name.isEmpty()) { out.println("<br>Hi "+name+". How are you?"); } } %> </body> </html>
从上面漏洞代码中可以看到,对用户提交的name参数没有做任何的输入过滤和输出的编码,直接输出在HTML代码中,导致存在反射XSS漏洞。
修复后的代码:
<%@page import="org.apache.commons.lang.StringEscapeUtils"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> Patch <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>XSS Patched</title> </head> <body> <form action="xss-patch.jsp" method="post"> Enter your name: <input type="text" name="name"><input type="submit"> </form> <% if(request.getMethod().equalsIgnoreCase("post")) { String name = StringEscapeUtils.escapeHtml(request.getParameter("name")); if(!name.isEmpty()) { out.println("<br>Hi "+name+". How are you?"); } } %> </body>
上面修复后的代码对用户提交的name参数进行了HTML的编码处理,使用了StringEscapeUtils类的escapeHtml方法,该方法会自动对特殊符号进行HTML编码处理。该类是包含在 commons-lang-2.4.jar包中的。
越权漏洞
如果一个Web应用程序不正确检查用户是否被授权访问的特定的资源,就有可能导致产生越权漏洞。例如帐号A在登录的状态下,遍历访问请求中的ID就可以查看其它人的相关信息。
漏洞示例代码:
<%@page import="java.util.Enumeration"%> <%@ page import="java.sql.*" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Account Balance</title> </head> <body> <% int flag = 0; Enumeration e = session.getAttributeNames(); while (e.hasMoreElements()) { String name = (String) e.nextElement(); String value = session.getAttribute(name).toString(); if(name.equals("useracc") && !(value.isEmpty())) { flag = 1; break; } } if(flag == 1) { String accno = request.getParameter("accno"); Class.forName("com.mysql.jdbc.Driver"); Connection con = (Connection) DriverManager.getConnection("jdbc:mysql://localhost/mydb", "root", ""); PreparedStatement ps = (PreparedStatement) con.prepareStatement("select * from account_balance where accno=? limit 0,1"); ps.setString(1,accno); ResultSet rs = ps.executeQuery(); if(rs.next()) { String s = rs.getString("balance"); out.println("<h1>Welcome to your account</h1>"); out.println("<br>Account Number: "+session.getAttribute("useracc")); out.println("<br>Your current balance is: "+s); } else { out.println("Error: Contact administrator."); } } else { response.sendRedirect("login.jsp"); } %> </body> </html>
在上面的代码中,没有判断用户Session,导致通过修改accno的值就可以遍历返回的结果信息。
修复后的代码:
<%@page import="java.util.Enumeration"%> <%@ page import="java.sql.*" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Account Balance</title> </head> <body> <% int flag = 0; Enumeration e = session.getAttributeNames(); while (e.hasMoreElements()) { String name = (String) e.nextElement(); String value = session.getAttribute(name).toString(); if(name.equals("useracc") && !(value.isEmpty())) { flag = 1; break; } } if(flag == 1) { String sess_accno = session.getAttribute("useracc").toString(); String accno = request.getParameter("accno"); if(sess_accno.equals(accno)) { Class.forName("com.mysql.jdbc.Driver"); Connection con = (Connection) DriverManager.getConnection("jdbc:mysql://localhost/mydb", "root", ""); PreparedStatement ps = (PreparedStatement) con.prepareStatement("select * from account_balance where accno=? limit 0,1"); ps.setString(1,accno); /* This line will be better ps.setString(1,sess_accno); */ ResultSet rs = ps.executeQuery(); if(rs.next()) { String s = rs.getString("balance"); out.println("<h1>Welcome to your account</h1>"); out.println("<br>Account Number: "+session.getAttribute("useracc")); out.println("<br>Your current balance is: "+s); } else { out.println("Error: Contact administrator."); } } else { out.println("Unauthorized Access Detected"); } } else { response.sendRedirect("login.jsp"); } %> </body> </html>
上面修复后的代码,判断了用户的Session,在为True的情况下才能够查看返回的信息,因此当用户遍历accno的值来尝试获取返回结果时,会提示无权访问。