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()函数,截图如下 sql1

注释’–‘后面需要有空格,使用#注释不需要,需要编码#为%23
http://localhost:8080/1.jsp?user=root’and%201=2%20union%20select%20user(),version()%23

去掉上面jsp的注释使用预编译,截图如下 sql2

此时不能执行恶意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抓数据包,此时还未在数据库把单引号转义为字符串 sql3

客户端预编译
在提交数据库query之前已经处理参数 sql4

客户端通过mysql-connector jar包com.mysql.jdbc.PreparedStatement类的setString方法,把单引号转义为普通字符串引号。 sql5

JDBC预编译的不足

order by
当使用如下语句执行查询时

String sql = "select host,user from mysql.user order by ?";

预编译会添加引号,导致最终查询结果不对

Query	select host,user from mysql.user order by 'host'

sql6

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 这行下断点。
可见插入的整个语句被单引号包裹起来,插入的引号被转义,最终是以字符串的形式插入查询的,无法成功注入。 sql7

小结

可见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/

Written on October 1, 2019