JDBC SQL注入

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

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

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

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

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

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

    查询结果如下:

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

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

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

    查询结果如下:

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

    Java代码片段如下:

    1. // 获取用户传入的用户名
    2. String user = request.getParameter("user");
    3. // 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
    4. // 语句当中,从而导致了SQL注入漏洞。
    5. String sql = "select host,user from mysql.user where user = '" + user + "'";
    6. // 创建预编译对象
    7. PreparedStatement pstt = connection.prepareStatement(sql);
    8. // 执行SQL语句并获取返回结果对象
    9. 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:

    1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    2. <%@ page import="java.sql.*" %>
    3. <%@ page import="java.io.StringWriter" %>
    4. <%@ page import="java.io.PrintWriter" %>
    5. <style>
    6. table {
    7. border-collapse: collapse;
    8. }
    9. th, td {
    10. border: 1px solid #C1DAD7;
    11. font-size: 12px;
    12. padding: 6px;
    13. color: #4f6b72;
    14. }
    15. </style>
    16. <%!
    17. // 数据库驱动类名
    18. public static final String CLASS_NAME = "com.mysql.jdbc.Driver";
    19. // 数据库链接字符串
    20. 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";
    21. public static final String USERNAME = "root";
    22. // 数据库密码
    23. public static final String PASSWORD = "root";
    24. Connection getConnection() throws SQLException, ClassNotFoundException {
    25. Class.forName(CLASS_NAME);// 注册JDBC驱动类
    26. return DriverManager.getConnection(URL, USERNAME, PASSWORD);
    27. }
    28. %>
    29. <%
    30. String user = request.getParameter("user");
    31. if (user != null) {
    32. try {
    33. // 建立数据库连接
    34. connection = getConnection();
    35. // 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
    36. // 语句当中,从而导致了SQL注入漏洞。
    37. // String sql = "select host,user from mysql.user where user = ? ";
    38. String sql = "select host,user from mysql.user where user = '" + user + "'";
    39. out.println("SQL:" + sql);
    40. out.println("<hr/>");
    41. // 创建预编译对象
    42. PreparedStatement pstt = connection.prepareStatement(sql);
    43. // pstt.setObject(1, user);
    44. // 执行SQL语句并获取返回结果对象
    45. ResultSet rs = pstt.executeQuery();
    46. out.println("<table><tr>");
    47. out.println("<th>主机</th>");
    48. out.println("<th>用户</th>");
    49. out.println("<tr/>");
    50. // 输出SQL语句执行结果
    51. while (rs.next()) {
    52. out.println("<tr>");
    53. // 获取SQL语句中查询的字段值
    54. out.println("<td>" + rs.getObject("host") + "</td>");
    55. out.println("<td>" + rs.getObject("user") + "</td>");
    56. out.println("<tr/>");
    57. }
    58. out.println("</table>");
    59. // 关闭查询结果
    60. rs.close();
    61. // 关闭预编译对象
    62. pstt.close();
    63. } catch (Exception e) {
    64. // 输出异常信息到浏览器
    65. StringWriter sw = new StringWriter();
    66. e.printStackTrace(new PrintWriter(sw));
    67. out.println(sw);
    68. } finally {
    69. // 关闭数据库连接
    70. }
    71. }
    72. %>

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

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

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

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

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

    ‘ and 1=2 union select user(),version() —%20

    image-20191214234010523

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

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

    查询结果如下:

    1. mysql> select id, username, email from sys_user where id = 1;
    2. +----+----------+-------------------+
    3. | id | username | email |
    4. +----+----------+-------------------+
    5. | 1 | yzmm | admin@javaweb.org |
    6. +----+----------+-------------------+
    7. 1 row in set (0.01 sec)

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

    对应的程序代码片段:

    1. // 获取用户传入的用户ID
    2. String id = request.getParameter("id");
    3. // 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
    4. // 语句当中,从而导致了SQL注入漏洞。
    5. String sql = "select id, username, email from sys_user where id =" + id;
    6. // 创建预编译对象
    7. PreparedStatement pstt = connection.prepareStatement(sql);
    8. // 执行SQL语句并获取返回结果对象
    9. ResultSet rs = pstt.executeQuery();

    快速检测整型注入方式

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

    检测方式示例:

    1. id=2-1
    2. id=(2)
    3. id=(select 2 from dual)
    4. id=(select 2)

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

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

    执行结果如下:

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

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

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

    1. id=2 union select 1, sleep(3)

    查询结果如下:

    1. mysql> select username,email from sys_user where id=1 union select 1, sleep(3);
    2. +----------+-------------------+
    3. | username | email |
    4. +----------+-------------------+
    5. | yzmm | admin@javaweb.org |
    6. | 1 | 0 |
    7. +----------+-------------------+
    8. 2 rows in set (3.06 sec)

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

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

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

    示例代码片段:

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

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

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

    数据库服务器端预编译

    JDBC URL配置示例:

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

    代码片段:

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

    使用JDBCPreparedStatement查询数据包如下:

    客户端预编译

    JDBC URL配置示例:

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

    代码片段:

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

    使用JDBCPreparedStatement查询数据包如下:

    image-20191215011935278

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

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

    Mysql预编译

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

    prepare查询示例:

    1. prepare stmt from 'select host,user from mysql.user where user = ?';
    2. set @username='root';
    3. execute stmt using @username;
    1. mysql> prepare stmt from 'select host,user from mysql.user where user = ?';
    2. Query OK, 0 rows affected (0.00 sec)
    3. Statement prepared
    4. mysql> set @username='root';
    5. Query OK, 0 rows affected (0.00 sec)
    6. mysql> execute stmt using @username;
    7. +-----------+------+
    8. | host | user |
    9. +-----------+------+
    10. | localhost | root |
    11. 1 row in set (0.00 sec)

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