SQL注入漏洞
SQL注入漏洞
SQL注入是網絡攻擊中最為常見的攻擊方式,通過向服務器端發送惡意的SQL語句或SQL語句片段注入到服務器端的數據庫查詢邏輯中,改變原有的查詢邏輯,從而實現類惡意讀取服務器數據庫數據,攻擊者甚至可以利用數據庫內部函數或缺陷提升權限,從而獲取服務器權限。
1. 用戶后臺系統登陸注入
1.1 登陸位置注入測試
后臺登陸系統注入在前些年是非常常見的,我們通常會使用' or '1'='1之類的注入語句來構建一個查詢結果永為真的SQL,俗稱:萬能密碼。
示例 - 用戶登陸注入代碼:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.DriverManager" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%
// MYSQL sys_user示例表,測試時請先創建對應的數據庫和表
//
// CREATE TABLE `sys_user` (
// `id` int(9) unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
// `username` varchar(16) NOT NULL COMMENT '用戶名',
// `password` varchar(32) NOT NULL COMMENT '用戶密碼',
// `user_avatar` varchar(255) DEFAULT NULL COMMENT '用戶頭像',
// `register_time` datetime DEFAULT NULL COMMENT '注冊時間',
// PRIMARY KEY (`id`),
// UNIQUE KEY `idx_sys_user_username` (`username`) USING BTREE
// ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='系統用戶表'
//
// INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', '/res/images/avatar/default.png', '2020-05-05 17:21:27'), ('2', 'test', '123456', '/res/images/avatar/default.png', '2020-05-06 18:27:10'), ('3', 'root', '123456', '/res/images/avatar/default.png', '2020-05-06 18:28:27'), ('4', 'user', '123456', '/res/images/avatar/default.png', '2020-05-06 18:31:34'), ('5', 'rasp', '123456', '/res/images/avatar/default.png', '2020-05-06 18:32:08');
%>
<%
String sessionKey = "USER_INFO";
Object sessionUser = session.getAttribute(sessionKey);
// 退出登陸
if (sessionUser != null && "exit".equals(request.getParameter("action"))) {
session.removeAttribute(sessionKey);
out.println("<script>alert('再見!');location.reload();</script>");
return;
}
Map<String, String> userInfo = null;
// 檢查用戶是否已經登陸成功
if (sessionUser instanceof Map) {
userInfo = (Map<String, String>) sessionUser;
out.println("<p>歡迎回來:" + userInfo.get("username") + ",ID:" + userInfo.get("id") + " \r<a href='?action=exit'>退出登陸</a></p>");
return;
}
String username = request.getParameter("username");
String password = request.getParameter("password");
// 處理用戶登陸邏輯
if (username != null && password != null) {
userInfo = new HashMap<String, String>();
ResultSet rs = null;
Connection connection = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb-bbs", "root", "root");
String sql = "select id,username,password from sys_user where username = '" + username + "' and password = '" + password + "'";
System.out.println(sql);
rs = connection.prepareStatement(sql).executeQuery();
while (rs.next()) {
userInfo.put("id", rs.getString("id"));
userInfo.put("username", rs.getString("username"));
userInfo.put("password", rs.getString("password"));
}
// 檢查是否登陸成功
if (userInfo.size() > 0) {
// 設置用戶登陸信息
session.setAttribute(sessionKey, userInfo);
// 跳轉到登陸成功頁面
response.sendRedirect(request.getServletPath());
} else {
out.println("<script>alert('登陸失敗,賬號或密碼錯誤!');history.back(-1)</script>");
}
} catch (Exception e) {
out.println("<script>alert('登陸失敗,服務器異常!');history.back(-1)</script>");
} finally {
// 關閉數據庫連接
if (rs != null)
rs.close();
if (connection != null)
connection.close();
}
return;
}
%>
<html>
<head>
<title>Login Test</title>
</head>
<body>
<div style="margin: 30px;">
<form action="#" method="POST">
Username:<input type="text" name="username" value="admin"/><br/>
Password:<input type="text" name="password" value="'=0#"/><br/>
<input type="submit" value="登陸"/>
</form>
</div>
</body>
</html>
sys_user表結構如下:
| id | username | password | user_avatar | register_time |
|---|---|---|---|---|
| 1 | admin | 123456 | /res/images/avatar/default.png | 2020-05-05 17:21:27 |
| 2 | test | 123456 | /res/images/avatar/default.png | 2020-05-06 18:27:10 |
訪問示例中的后臺登陸地址:http://localhost:8000/modules/jdbc/login.jsp,如下圖:

攻擊者通過在密碼參數處輸入:'=0#即可使用SQL注入的方式改變查詢邏輯,繞過密碼認證并登陸系統,因此用于檢測用戶賬號密碼是否存在的SQL語句變成了:
select id,username,password from sys_user where username = 'admin' and password = ''=0#'
其中的password的值預期是傳入用戶密碼,但是實際上被攻擊者傳入了可改變查詢邏輯的SQL語句,將運算結果改變為true,從而攻擊者可以使用錯誤的用戶及密碼登陸系統,如下圖:

毫無疑問因為攻擊者輸入的信息足夠的短小簡潔,但是對于用戶網站系統來說卻有極強的殺傷性,絕大多數的WAF或者RASP產品都無法精準辨別'=0#的威脅性,無法正確做到精準防御。
萬能密碼登陸注入原理圖解:

2. 文章詳情頁注入
通常情況下在用戶系統發布文章后會在數據庫中產生一條記錄,并生成一個固定的文章ID,用戶瀏覽文章信息只需要傳入文章ID,即在后端通過文章ID查詢文章詳情信息。
示例 - 存在SQL注入的文章詳情代碼:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.DriverManager" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%
// MYSQL sys_article示例表,測試時請先創建對應的數據庫和表
// CREATE TABLE `sys_article` (
// `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '文章ID',
// `user_id` int(9) NOT NULL COMMENT '用戶ID',
// `title` varchar(100) NOT NULL COMMENT '標題',
// `author` varchar(16) NOT NULL COMMENT '作者',
// `content` longtext NOT NULL COMMENT '文章內容',
// `publish_date` datetime NOT NULL COMMENT '發布時間',
// `click_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文章點擊數量',
// PRIMARY KEY (`id`),
// KEY `index_title` (`title`) USING BTREE
// ) ENGINE=InnoDB AUTO_INCREMENT=100002 DEFAULT CHARSET=utf8 COMMENT='系統文章表';
//
// INSERT INTO `sys_article` VALUES ('100000', '1', '東部戰區陸軍:丟掉幻想,準備打仗!', 'admin', '<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;\">\n 中國人民解放軍東部戰區陸軍微信公眾號“人民前線”4月15日發布《丟掉幻想,準備打仗! 》,以下為文章全文:\n</p>\n<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;\">\n 文丨陳前線\n</p>\n<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;\">\n “丟掉幻想,準備斗爭!”\n</p>\n<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;\">\n 這是新中國成立前夕,毛主席發表的一篇文章標題。\n</p>\n<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;\">\n 毛主席曾說過: “我們愛好和平,但以斗爭求和平則和平存,以妥協求和平則和平亡。 ”\n</p>\n<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;text-align:center;\">\n <img src=\"/res/images/20200415203823695.jpg\" />\n</p>\n<p style=\"font-family:PingFangSC-Regular, 微軟雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;\">\n 放眼今日之中國,九州大地上熱潮迭涌。 在中國夢的指引下,華夏兒女投身祖國各項建設事業,追趕新時代發展的腳步。 中國在國際上的影響力顯著增強,“向東看”開始成為一股潮流。\n</p>', '2020-04-19 17:35:06', '4'), ('100001', '1', '面對戰爭,時刻準備著!', 'admin', '<p style=\"font-family:"font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n 這話是20年前,我的新兵連長說的。\n</p>\n<p style=\"font-family:"font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n   那是我們授銜后的第一個晚上,班長一臉神秘地說:“按照慣例,今天晚上肯定要緊急集合的,這是你們的‘成人禮’。”于是,熄燈哨音響過之后,我們都衣不解帶地躺在床上。班長為了所謂的班級榮譽,也默認了我們的做法。\n</p>\n<p style=\"font-family:"font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n   果然,深夜一陣急促的哨音響起,我們迅速打起被包,沖到指定地點集合。大個子連長看著整齊的隊伍,說了句:“不錯,解散!”一個皆大歡喜的局面。我們都高高興興地回到宿舍,緊繃的神經一下子放松下來,排房里很快就響起了呼嚕聲。\n</p>\n<p align=\"center\" style=\"font-family:"font-size:16px;background-color:#FFFFFF;\">\n <img src=\"/res/images/20200419133156232.jpg\" alt=\"500\" />\n</p>\n<p style=\"font-family:"font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n   可是,令人沒有想到的是,睡夢中又一陣急促的哨音劃破夜空的寧靜——連長再次拉起了緊急集合。這一次,情況就完全不一樣了,毫無準備的我們,狼狽不堪,有的被包來不及打好,不得不用手抱住;有的找不到自己的鞋子,光腳站在地上,有的甚至連褲子都穿反了……\n</p>', '2020-04-19 17:37:40', '17');
%>
<%
String id = request.getParameter("id");
Map<String, Object> articleInfo = new HashMap<String, Object>();
ResultSet rs = null;
Connection connection = null;
if (id != null) {
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb-bbs", "root", "root");
String sql = "select * from sys_article where id = " + id;
System.out.println(sql);
rs = connection.prepareStatement(sql).executeQuery();
while (rs.next()) {
articleInfo.put("id", rs.getInt("id"));
articleInfo.put("user_id", rs.getInt("user_id"));
articleInfo.put("title", rs.getString("title"));
articleInfo.put("author", rs.getString("author"));
articleInfo.put("content", rs.getString("content"));
articleInfo.put("publish_date", rs.getDate("publish_date"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 關閉數據庫連接
if (rs != null)
rs.close();
if (connection != null)
connection.close();
}
}
%>
<html>
<head>
<title><%=articleInfo.get("title")%></title>
</head>
<body>
<div style="margin: 30px;">
<h2 style="height: 30px; text-align: center;"><%=articleInfo.get("title")%></h2>
<p>作者:<%=articleInfo.get("author")%> - <%=articleInfo.get("publish_date")%></p>
<div style="border: 1px solid #C6C6C6;">
<%=articleInfo.get("content")%>
</div>
</div>
</body>
</html>
sys_article表結構如下:
| id | user_id | title | author | content | publish_date | click_count |
|---|---|---|---|---|---|---|
| 100000 | 1 | 東部戰區陸軍:丟掉幻想,準備打仗! | admin | 文章內容 | 2020-04-19 17:35:06 | 4 |
| 100001 | 1 | 面對戰爭,時刻準備著! | admin | 文章內容 | 2020-04-19 17:37:40 | 17 |
訪問示例程序并傳入參數id=100001后會顯示文章詳情,請求:http://localhost:8000/modules/jdbc/article.jsp?id=100001,如下圖:

2.1 union select類型的SQL注入攻擊測試
攻擊者在ID處構造并傳入惡意的SQL注入語句后,可以輕松的讀取出數據庫信息,如將請求中的id參數值改為100001 and 1=2 union select 1,2,user(),version(),database(),6,7,服務器端將會返回數據庫名稱、請求:http://localhost:8000/modules/jdbc/article.jsp?id=100001%20and%201=2%20union%20select%201,2,user(),version(),database(),6,7,如下圖:

由于攻擊的Payload中包含了union、select、user()、version()、database()敏感關鍵字,大部分的WAF都能夠識別此類SQL注入。
2.2 算數運算結果探測型攻擊測試
但如果攻擊者將注入語句改為檢測語句:100001-1的時候頁面會輸出文章id為100000的文章,由于id參數存在注入,數據庫最終查詢到的文章id為100001-1也就是id為100000的文章,請求:http://localhost:8000/modules/jdbc/article.jsp?id=100001-1,如下圖:

幾乎可以繞過99%的WAF和大部分的RASP產品了,此類SQL注入攻擊屬于不具有攻擊性的探測性攻擊。
2.3 數據庫函數型攻擊測試
部分攻擊者使用了數據庫的一些特殊函數進行注入攻擊,可能會導致WAF無法識別,但是RASP具備特殊函數注入攻擊的精準檢測和防御能力。
例如上述示例中攻擊者傳入的id參數值為:(100001-1)或者(100001)用于探測數據表中是否存在id值為100000的文章,請求:http://localhost:8000/modules/jdbc/article.jsp?id=(100001),如下圖:

或者傳入的id參數值為:(select 100000)來探測數據庫是否存在id值為100000的文章,請求:http://localhost:8000/modules/jdbc/article.jsp?id=(select%20100000),如下圖:

大多數數據庫支持使用()來包裹一個整數型的字段值,但是99%的WAF和極大多數的RASP產品是無法識別此類型的注入攻擊的。
3. SQL注入 - JSON傳參測試
示例 - 存在SQL注入漏洞的代碼示例(JSON傳參方式):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.DriverManager" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="com.alibaba.fastjson.JSON" %>
<%@ page import="org.apache.commons.io.IOUtils" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%
// MYSQL sys_user示例表,測試時請先創建對應的數據庫和表
//
// CREATE TABLE `sys_user` (
// `id` int(9) unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
// `username` varchar(16) NOT NULL COMMENT '用戶名',
// `password` varchar(32) NOT NULL COMMENT '用戶密碼',
// `user_avatar` varchar(255) DEFAULT NULL COMMENT '用戶頭像',
// `register_time` datetime DEFAULT NULL COMMENT '注冊時間',
// PRIMARY KEY (`id`),
// UNIQUE KEY `idx_sys_user_username` (`username`) USING BTREE
// ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='系統用戶表'
//
// INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', '/res/images/avatar/default.png', '2020-05-05 17:21:27'), ('2', 'test', '123456', '/res/images/avatar/default.png', '2020-05-06 18:27:10'), ('3', 'root', '123456', '/res/images/avatar/default.png', '2020-05-06 18:28:27'), ('4', 'user', '123456', '/res/images/avatar/default.png', '2020-05-06 18:31:34'), ('5', 'rasp', '123456', '/res/images/avatar/default.png', '2020-05-06 18:32:08');
%>
<%
String contentType = request.getContentType();
// 只接受JSON請求
if (contentType != null && contentType.toLowerCase().contains("application/json")) {
String content = IOUtils.toString(request.getInputStream());
JSONObject json = JSON.parseObject(content);
String username = json.getString("username");
String password = json.getString("password");
// 處理用戶登陸邏輯
if (username != null && password != null) {
ResultSet rs = null;
Connection connection = null;
Map<String, Object> userInfo = new HashMap<String, Object>();
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb-bbs", "root", "root");
String sql = "select * from sys_user where username = '" + username + "' and password = '" + password + "'";
System.out.println(sql);
rs = connection.prepareStatement(sql).executeQuery();
while (rs.next()) {
userInfo.put("id", rs.getString("id"));
userInfo.put("username", rs.getString("username"));
userInfo.put("password", rs.getString("password"));
userInfo.put("user_avatar", rs.getString("user_avatar"));
userInfo.put("register_time", rs.getDate("register_time"));
}
// 檢查是否登陸成功
if (userInfo.size() > 0) {
// 設置用戶登陸信息
out.println(JSON.toJSONString(userInfo));
} else {
out.println("<script>alert('登陸失敗,賬號或密碼錯誤!');history.back(-1)</script>");
}
} catch (Exception e) {
e.printStackTrace();
out.println("<script>alert('登陸失敗,服務器異常!');history.back(-1)</script>");
} finally {
// 關閉數據庫連接
if (rs != null)
rs.close();
if (connection != null)
connection.close();
}
}
}
%>
如果應用系統本身通過JSON格式傳參,傳統的WAF可能無法識別,如果后端將參數進行SQL語句的拼接,則將會導致SQL注入漏洞。攻擊者通過篡改JSON中對應參數的數據,達到SQL注入攻擊的目的,如下圖:

4. SQL注入 - Multipart傳參測試
示例 - 存在SQL注入漏洞的代碼示例(Multipart傳參方式):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.commons.fileupload.FileItemIterator" %>
<%@ page import="org.apache.commons.fileupload.FileItemStream" %>
<%@ page import="org.apache.commons.fileupload.servlet.ServletFileUpload" %>
<%@ page import="org.apache.commons.fileupload.util.Streams" %>
<%@ page import="java.io.File" %>
<%@ page import="java.io.FileOutputStream" %>
<%@ page import="org.apache.commons.fileupload.FileUploadException" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.sql.DriverManager" %>
<%@ page import="com.alibaba.fastjson.JSON" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.io.IOException" %>
<%
// MYSQL sys_user示例表,測試時請先創建對應的數據庫和表
//
// CREATE TABLE `sys_user` (
// `id` int(9) unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
// `username` varchar(16) NOT NULL COMMENT '用戶名',
// `password` varchar(32) NOT NULL COMMENT '用戶密碼',
// `user_avatar` varchar(255) DEFAULT NULL COMMENT '用戶頭像',
// `register_time` datetime DEFAULT NULL COMMENT '注冊時間',
// PRIMARY KEY (`id`),
// UNIQUE KEY `idx_sys_user_username` (`username`) USING BTREE
// ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='系統用戶表'
//
// INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', '/res/images/avatar/default.png', '2020-05-05 17:21:27'), ('2', 'test', '123456', '/res/images/avatar/default.png', '2020-05-06 18:27:10'), ('3', 'root', '123456', '/res/images/avatar/default.png', '2020-05-06 18:28:27'), ('4', 'user', '123456', '/res/images/avatar/default.png', '2020-05-06 18:31:34'), ('5', 'rasp', '123456', '/res/images/avatar/default.png', '2020-05-06 18:32:08');
%>
<%!
/**
* 解析Multipart請求中的參數
* @param request
* @return
*/
Map<String, String> parseMultipartContent(HttpServletRequest request) {
Map<String, String> dataMap = new HashMap<String, String>();
try {
ServletFileUpload fileUpload = new ServletFileUpload();
FileItemIterator fileItemIterator = fileUpload.getItemIterator(request);
while (fileItemIterator.hasNext()) {
FileItemStream fileItemStream = fileItemIterator.next();
if (fileItemStream.isFormField()) {
// 字段名稱
String fieldName = fileItemStream.getFieldName();
// 字段值
String fieldValue = Streams.asString(fileItemStream.openStream());
dataMap.put(fieldName, fieldValue);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return dataMap;
}
%>
<%
if (ServletFileUpload.isMultipartContent(request)) {
Map<String, String> dataMap = parseMultipartContent(request);
String username = dataMap.get("username");
String password = dataMap.get("password");
// 處理用戶登陸邏輯
if (username != null && password != null) {
ResultSet rs = null;
Connection connection = null;
Map<String, Object> userInfo = new HashMap<String, Object>();
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb-bbs", "root", "root");
String sql = "select * from sys_user where username = '" + username + "' and password = '" + password + "'";
System.out.println(sql);
rs = connection.prepareStatement(sql).executeQuery();
while (rs.next()) {
userInfo.put("id", rs.getString("id"));
userInfo.put("username", rs.getString("username"));
userInfo.put("password", rs.getString("password"));
userInfo.put("user_avatar", rs.getString("user_avatar"));
userInfo.put("register_time", rs.getDate("register_time"));
}
// 檢查是否登陸成功
if (userInfo.size() > 0) {
// 設置用戶登陸信息
out.println(JSON.toJSONString(userInfo));
} else {
out.println("<script>alert('登陸失敗,賬號或密碼錯誤!');history.back(-1)</script>");
}
} catch (Exception e) {
e.printStackTrace();
out.println("<script>alert('登陸失敗,服務器異常!');history.back(-1)</script>");
} finally {
// 關閉數據庫連接
if (rs != null)
rs.close();
if (connection != null)
connection.close();
}
}
} else {
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File upload</title>
</head>
<body>
<form action="" enctype="multipart/form-data" method="post">
<p>
Username: <input name="username" type="text" value="admin" /><br/>
Password: <input name="password" type="text" value="'=0#" />
</p>
<input name="submit" type="submit" value="Submit"/>
</form>
</body>
</html>
<%
}
%>
訪問示例中的后臺登陸地址:http://localhost:8000/modules/jdbc/multipart.jsp,如下圖:

提交萬能密碼'=0#即可繞過登陸驗證獲取到admin用戶信息:

Spring MVC Multipart請求解析示例
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import static org.javaweb.utils.HttpServletResponseUtils.responseHTML;
/**
* Creator: yz
* Date: 2020-05-04
*/
@Controller
@RequestMapping("/SQLInjection/")
public class SQLInjectionController {
@Resource
private JdbcTemplate jdbcTemplate;
@RequestMapping("/Login.php")
public void login(String username, String password, String action, String formType,
HttpServletRequest request, HttpServletResponse response,
HttpSession session) throws IOException {
String contentType = request.getContentType();
String sessionKey = "USER_INFO";
Object sessionUser = session.getAttribute(sessionKey);
PrintWriter out = response.getWriter();
// 退出登陸
if (sessionUser != null && "exit".equals(action)) {
session.removeAttribute(sessionKey);
response.sendRedirect(request.getServletPath() + (formType != null ? "?formType=" + formType : ""));
return;
}
Map<String, Object> userInfo = null;
// 檢查用戶是否已經登陸成功
if (sessionUser instanceof Map) {
userInfo = (Map<String, Object>) sessionUser;
responseHTML(response,
"<p>歡迎回來:" + userInfo.get("username") + ",ID:" +
userInfo.get("id") + " \r<a href='?action=exit" + (formType != null ? "&formType=" + formType : "") + "'>退出登陸</a></p>"
);
return;
}
// 處理用戶登陸邏輯
if (username != null && password != null) {
userInfo = new HashMap<String, Object>();
try {
String sql = "select id,username,password from sys_user where username = '" +
username + "' and password = '" + password + "'";
System.out.println(sql);
userInfo = jdbcTemplate.queryForMap(sql);
// 檢查是否登陸成功
if (userInfo.size() > 0) {
// 設置用戶登陸信息
session.setAttribute(sessionKey, userInfo);
String q = request.getQueryString();
// 跳轉到登陸成功頁面
response.sendRedirect(request.getServletPath() + (q != null ? "?" + q : ""));
} else {
responseHTML(response, "<script>alert('登陸失敗,賬號或密碼錯誤!');history.back(-1)</script>");
}
} catch (Exception e) {
responseHTML(response, "<script>alert('登陸失敗,服務器異常!');history.back(-1)</script>");
}
return;
}
String multipartReq = "";
// 如果傳入formType=multipart參數就輸出multipart表單,否則輸出普通的表單
if ("multipart".equals(formType)) {
multipartReq = " enctype=\"multipart/form-data\" ";
}
responseHTML(response, "<html>\n" +
"<head>\n" +
" <title>Login Test</title>\n" +
"</head>\n" +
"<body>\n" +
"<div style=\"margin: 30px;\">\n" +
" <form action=\"#\" " + multipartReq + " method=\"POST\">\n" +
" Username:<input type=\"text\" name=\"username\" value=\"admin\"/><br/>\n" +
" Password:<input type=\"text\" name=\"password\" value=\"'=0#\"/><br/>\n" +
" <input type=\"submit\" value=\"登陸\"/>\n" +
" </form>\n" +
"</div>\n" +
"</body>\n" +
"</html>");
out.flush();
out.close();
}
}
訪問示例中的后臺登陸地址:http://localhost:8000/SQLInjection/Login.php?formType=multipart,如下圖:

發送Multipart請求,登陸測試Spring MVC:

使用萬能密碼登陸成功:

5. SQL注入修復
為了避免SQL注入攻擊的產生,需要嚴格檢查請求參數的合法性或使用預編譯,請參考JDBC章節中的JDBC SQL注入防御方案。
5.1 RASP SQL注入防御
在Java中,所有的數據庫讀寫操作都需要使用JDBC驅動來實現,JDBC規范中定義了數據庫查詢的接口,不同的數據庫廠商通過實現JDBC定義的接口來實現數據庫的連接、查詢等操作。
RASP是基于行為的方式來實現SQL注入檢測的,如果請求的參數最終并沒有被數據庫執行,那么RASP的SQL注入檢測模塊根本就不會被觸發。
5.1.1 java.sql.Connection/Statement接口Hook
雖然每種數據庫的驅動包的類名都不一樣,但是它們都必須實現JDBC接口,所以我們可以利用這一特點,使用RASP Hook JDBC數據庫查詢的接口類:java.sql.Connection、java.sql.Statement。
例如Mysql的驅動包的實現數據庫連接的實現類是:com.mysql.jdbc.ConnectionImpl,該類實現了com.mysql.jdbc.MySQLConnection接口,而com.mysql.jdbc.MySQLConnection類是java.sql.Connection的子類,也就是說com.mysql.jdbc.ConnectionImpl接口必須實現java.sql.Connection定義的數據庫連接和查詢方法。
示例 - com.mysql.jdbc.ConnectionImpl 類繼承關系圖:

靈蜥內置了JDBC接口的Hook方法,如下:
示例 - 靈蜥Hook Connection接口代碼片段:
/**
* JDBC Connection 數據庫查詢Hook
* Creator: yz
* Date: 2019-07-23
*/
@RASPClassHook
public class ConnectionHook {
// 省略其他Hook方法
/**
* Hook java.sql.Connection接口的所有子類中的prepareStatement方法,且該方法的第一個參數必須是字符串
*/
@RASPMethodHook(
superClass = "java.sql.Connection", methodName = "prepareStatement",
methodArgsDesc = "^Ljava/lang/String;.*", methodDescRegexp = true
)
public static class ConnectionPrepareStatementHook extends RASPMethodAdvice {
@Override
public RASPHookResult<?> onMethodEnter() {
// 獲取prepareStatement的第一個參數值,也就是JDBC執行的SQL語句
String sql = (String) getArg(0);
// 分析執行的SQL語句是否合法,并返回檢測結果
return JdbcSqlQueryHookHandler.sqlQueryHook(sql, this);
}
}
}
當com.mysql.jdbc.ConnectionImpl類被JVM加載后會因為配置了RASP的Agent,該類的字節碼會傳遞到RASP的Agent處理,RASP經過分析后得出ConnectionImpl類符合RASP內置的ConnectionPrepareStatementHook類設置的Hook條件(父類名/方法名/方法參數完全匹配),那么RASP就會使用ASM動態生成防御代碼并插入到被Hook的方法中。
示例 - 未修改的ConnectionImpl類代碼片段:
package com.mysql.jdbc;
public class ConnectionImpl extends ConnectionPropertiesImpl implements MySQLConnection {
// 省略其他業務代碼
public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException {
return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
}
}
示例 - 經過RASP修改后的代碼片段:
package com.anbai.lingxe.agent;
import com.anbai.lingxe.loader.hooks.RASPHookHandlerType;
import com.anbai.lingxe.loader.hooks.RASPHookProxy;
import com.anbai.lingxe.loader.hooks.RASPHookResult;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class ConnectionImpl extends ConnectionPropertiesImpl implements MySQLConnection {
// 省略其他業務代碼
public PreparedStatement prepareStatement(String sql) throws SQLException {
// 生成Object數組對象,存儲方法參數值
Object[] parameters = new Object[]{sql};
// 生成try/catch
try {
// 調用RASP方法方法進入時檢測邏輯
RASPHookResult enterResult = RASPHookProxy.onMethodEnter(parameters, ...);
String HandlerType = enterResult.getRaspHookHandlerType().toString();
if (RASPHookHandlerType.REPLACE_OR_BLOCK.toString().equals(HandlerType)) {
// 如果RASP檢測結果需要阻斷或替換程序執行邏輯,return RASP返回結果中設置的返回值
return (PreparedStatement) enterResult.getReturnValue();
} else if (RASPHookHandlerType.THROW.toString().equals(HandlerType)) {
// 如果RASP檢測結果需要往外拋出異常,throw RASP返回結果中設置的異常對象
throw (Throwable) enterResult.getException();
}
// 執行程序原邏輯,創建PreparedStatement對象
PreparedStatement methodReturn = prepareStatement(sql, 1003, 1007);
// 調用RASP方法方法退出時檢測邏輯,同onMethodEnter,此處省略對應代碼
return methodReturn;
} catch (Throwable t) {
// 調用RASP方法方法異常退出時檢測邏輯,同onMethodEnter,此處省略對應代碼
}
}
}
增強后的ConnectionImpl類執行任何SQL語句都會被被RASP捕獲并檢測合法性,從而實現了徹底的SQL注入攻擊防御。
5.1.2 RASP SQL注入防御原理
RASP和WAF防御SQL注入能力有著本質上的區別,不管WAF如何的吹捧人工智能、機器學習它們都無法精確的識別SQL注入攻擊,因為在WAF層面根本就無法判定傳入的參數最終會不會拼接到SQL語句中,甚至連后端有沒有用數據庫都不知道,所以WAF的識別誤報率高也就是不可避免的了。
RASP與Web應用融為了一體,可以無視Https加密、無需手動解析Http請求參數,還可以直接獲取到數據庫最終執行的SQL語句,在SQL注入防御能力上有著得天獨厚的條件。
示例 - RASP防御SQL注入原理:

從上圖可以看出,RASP將Hook到的SQL語句做詞法解析,然后結合Http請求的參數做關聯分析,得出password參數直接導致了SQL詞法的語義變化,從而判定該SQL語句中包含了注入攻擊,RASP會立即阻止SQL查詢并阻斷Http請求。
RASP通過詞法解析可以得出SQL語句中使用的數據庫函數、表名稱、字段等關鍵信息,結合黑名單機制可以實現對敏感函數禁用,如:load_file/into outfile/xp_cmdshell等。
為了降低誤判和性能,RASP可以選擇性的不檢測長度低于3位的參數、不檢測數字型的參數等。
5.1.3 基于SQL詞法解析實現防御測試
靈蜥使用了SQL詞法解析來檢測SQL注入漏洞或攻擊,對于SQL注入的檢測能力精確而有效,比如可以輕松的識別出函數和算數運算類的SQL注入攻擊,如圖:

RASP可以識別出參數id中的100001-1會在數據庫中做算術運算,因為會被RASP攔截,通常攻擊者非常喜歡使用這種方式來探測是否存在SQL注入,傳統的WAF因為根本識別這個參數的具體業務含義,從而無法識別此類SQL注入攻擊。
5.1.4 RASP對JSON、Multipart請求的支持
處理傳統的GET/POST參數傳遞以外,json、multipart和web service是目前后端開發最為常用的參數傳遞方式,標準的Web容器默認是不會解析這幾類請求參數的,而主流的MVC框架(如:Spring MVC)具備了解析這幾類請求參數的能力。
示例 - Multipart請求攔截:

示例 - JSON請求攔截:

Java Web安全
推薦文章: