Sql注入之预编译-Java
本文介绍Java预编译,包括Jdbc预编译、框架Mybatis预编译。
Jdbc 预编译
实验环境介绍
tomcat+mysql
存在注入jsp代码如下
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.*" %>
<%@ page import="java.io.StringWriter" %>
<%@ page import="java.io.PrintWriter" %>
<style>
table {
border-collapse: collapse;
}
th, td {
border: 1px solid #C1DAD7;
font-size: 12px;
padding: 6px;
color: #4f6b72;
}
</style>
<%!
// 数据库驱动类名
public static final String CLASS_NAME = "com.mysql.jdbc.Driver";
// 数据库链接字符串
public static final String URL = "jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false";
// 数据库用户名
public static final String USERNAME = "root";
// 数据库密码
public static final String PASSWORD = "root";
Connection getConnection() throws SQLException, ClassNotFoundException {
Class.forName(CLASS_NAME);// 注册JDBC驱动类
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
%>
<%
String user = request.getParameter("user");
if (user != null) {
Connection connection = null;
try {
// 建立数据库连接
connection = getConnection();
// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select host,user from mysql.user where user = '" + user + "'";
//预编译
// String sql = "select host,user from mysql.user where user = ? ";
out.println("SQL:" + sql);
out.println("<hr/>");
// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);
// pstt.setObject(1, user);
// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery();
out.println("<table><tr>");
out.println("<th>主机</th>");
out.println("<th>用户</th>");
out.println("<tr/>");
// 输出SQL语句执行结果
while (rs.next()) {
out.println("<tr>");
// 获取SQL语句中查询的字段值
out.println("<td>" + rs.getObject("host") + "</td>");
out.println("<td>" + rs.getObject("user") + "</td>");
out.println("<tr/>");
}
out.println("</table>");
// 关闭查询结果
rs.close();
// 关闭预编译对象
pstt.close();
} catch (Exception e) {
// 输出异常信息到浏览器
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
out.println(sw);
} finally {
// 关闭数据库连接
connection.close();
}
}
%>
实验
请求
http://localhost:8080/1.jsp?user=root’and 1=2 union select user(),version() –%20
执行user(),version()函数,截图如下
注释’–‘后面需要有空格,使用#注释不需要,需要编码#为%23
http://localhost:8080/1.jsp?user=root’and%201=2%20union%20select%20user(),version()%23
去掉上面jsp的注释使用预编译,截图如下
此时不能执行恶意sql注入原因在于,预编译把设置的参数值作为字符串执行。具体如何把注入参数变为字符串,下面会提到。
JDBC两种预编译
JDBC预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmts为false时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmts为true时使用数据库服务器端预编译。
MYSQL官网 Connector/J 5.0.5中有如下内容
Important change: Due to a number of issues with the use of server-side prepared statements, Connector/J 5.0.5 has disabled their use by default. The disabling of server-side prepared statements does not affect the operation of the connector in any way.
To enable server-side prepared statements, add the following configuration property to your connector string:
useServerPrepStmts=true
The default value of this property is false (that is, Connector/J does not use server-side prepared statements).
默认mysql 5.0.5 useServerPrepStmts 参数为false,即5.0.5和之后版本默认使用客户端预编译。
数据库服务端预编译,设置useServerPrepStmts参数如下 jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=true
使用wireshark抓数据包,此时还未在数据库把单引号转义为字符串
客户端预编译
在提交数据库query之前已经处理参数
客户端通过mysql-connector jar包com.mysql.jdbc.PreparedStatement类的setString方法,把单引号转义为普通字符串引号。
JDBC预编译的不足
order by
当使用如下语句执行查询时
String sql = "select host,user from mysql.user order by ?";
预编译会添加引号,导致最终查询结果不对
Query select host,user from mysql.user order by 'host'
Mybatis
Mybatis介绍
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
实验代码地址
https://github.com/langligelang/dragonfly
Mybatis pom文件需要把spring-boot-starter-parent 版本改为2.3.4.RELEASE
实验
在PreparedStatementLogger类 invoke函数,return method.invoke 这行下断点。
可见插入的整个语句被单引号包裹起来,插入的引号被转义,最终是以字符串的形式插入查询的,无法成功注入。
小结
可见Mybatis的预编译处理和JDBC预编译类似
Mybatis依旧未解决order by 注入问题
网上有预编译安全编码规范,这里不一一贴出来。
参考
https://javasec.org/javase/JDBC/SqlInjection.html
https://github.com/langligelang/dragonfly
https://c0d3p1ut0s.github.io/MyBatis%E6%A1%86%E6%9E%B6%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84SQL%E6%B3%A8%E5%85%A5/