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

提交含有'(單引號)的注入語句測試:
http://localhost:8080/sql-injection.jsp?user=root‘

如果用戶屏蔽了異常信息的顯示我們就無法直接通過頁面信息確認是否是注入,但是我們可以通過后端響應的狀態碼來確定是否是注入點,如果返回的狀態碼為500,那么我們就可以初步的判定user參數存在注入了。
提交讀取Mysql用戶名和版本號注入語句測試:
http://localhost:8080/sql-injection.jsp?user=root‘ and 1=2 union select user(),version() –%20

這里使用了-- (--空格,空格可以使用%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 | admin@javaweb.org |
+----+----------+-------------------+
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 | admin@javaweb.org |
| 1 | 0 |
+----------+-------------------+
2 rows in set (3.06 sec)
SQL注入防御
既然我們學會了如何提交惡意的注入語句,那么我們到底應該如何去防御注入呢?通常情況下我們可以使用以下方式來防御SQL注入攻擊:
- 轉義用戶請求的參數值中的
'(單引號)、"(雙引號)。 - 限制用戶傳入的數據類型,如預期傳入的是數字,那么使用:
Integer.parseInt()/Long.parseLong等轉換成整型。 - 使用
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,當useServerPrepStmts為false時使用客戶端(驅動包內完成SQL轉義)預編譯,useServerPrepStmts為true時使用數據庫服務器端預編譯。
數據庫服務器端預編譯
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);
使用JDBC的PreparedStatement查詢數據包如下:

客戶端預編譯
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);
使用JDBC的PreparedStatement查詢數據包如下:

對應的Mysql客戶端驅動包預編譯代碼在com.mysql.jdbc.PreparedStatement類的setString方法,如下:

預編譯前的值為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注入。
Java Web安全