<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    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,如下圖:

    SQL注入漏洞

    攻擊者通過在密碼參數處輸入:'=0#即可使用SQL注入的方式改變查詢邏輯,繞過密碼認證并登陸系統,因此用于檢測用戶賬號密碼是否存在的SQL語句變成了:

    select id,username,password from sys_user where username = 'admin' and password = ''=0#'

    其中的password的值預期是傳入用戶密碼,但是實際上被攻擊者傳入了可改變查詢邏輯的SQL語句,將運算結果改變為true,從而攻擊者可以使用錯誤的用戶及密碼登陸系統,如下圖:

    SQL注入漏洞

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

    萬能密碼登陸注入原理圖解:

    SQL注入漏洞

    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:&quot;font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n    這話是20年前,我的新兵連長說的。\n</p>\n<p style=\"font-family:&quot;font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n    &emsp;&emsp;那是我們授銜后的第一個晚上,班長一臉神秘地說:“按照慣例,今天晚上肯定要緊急集合的,這是你們的‘成人禮’。”于是,熄燈哨音響過之后,我們都衣不解帶地躺在床上。班長為了所謂的班級榮譽,也默認了我們的做法。\n</p>\n<p style=\"font-family:&quot;font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n    &emsp;&emsp;果然,深夜一陣急促的哨音響起,我們迅速打起被包,沖到指定地點集合。大個子連長看著整齊的隊伍,說了句:“不錯,解散!”一個皆大歡喜的局面。我們都高高興興地回到宿舍,緊繃的神經一下子放松下來,排房里很快就響起了呼嚕聲。\n</p>\n<p align=\"center\" style=\"font-family:&quot;font-size:16px;background-color:#FFFFFF;\">\n    <img src=\"/res/images/20200419133156232.jpg\" alt=\"500\" />\n</p>\n<p style=\"font-family:&quot;font-size:16px;background-color:#FFFFFF;text-align:justify;\">\n    &emsp;&emsp;可是,令人沒有想到的是,睡夢中又一陣急促的哨音劃破夜空的寧靜——連長再次拉起了緊急集合。這一次,情況就完全不一樣了,毫無準備的我們,狼狽不堪,有的被包來不及打好,不得不用手抱住;有的找不到自己的鞋子,光腳站在地上,有的甚至連褲子都穿反了……\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,如下圖:

    SQL注入漏洞

    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,如下圖:

    SQL注入漏洞

    由于攻擊的Payload中包含了union、select、user()、version()、database()敏感關鍵字,大部分的WAF都能夠識別此類SQL注入。

    2.2 算數運算結果探測型攻擊測試

    但如果攻擊者將注入語句改為檢測語句:100001-1的時候頁面會輸出文章id100000的文章,由于id參數存在注入,數據庫最終查詢到的文章id100001-1也就是id100000的文章,請求:http://localhost:8000/modules/jdbc/article.jsp?id=100001-1,如下圖:

    SQL注入漏洞

    幾乎可以繞過99%WAF和大部分的RASP產品了,此類SQL注入攻擊屬于不具有攻擊性的探測性攻擊。

    2.3 數據庫函數型攻擊測試

    部分攻擊者使用了數據庫的一些特殊函數進行注入攻擊,可能會導致WAF無法識別,但是RASP具備特殊函數注入攻擊的精準檢測和防御能力。

    例如上述示例中攻擊者傳入的id參數值為:(100001-1)或者(100001)用于探測數據表中是否存在id值為100000的文章,請求:http://localhost:8000/modules/jdbc/article.jsp?id=(100001),如下圖:

    SQL注入漏洞

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

    SQL注入漏洞

    大多數數據庫支持使用()來包裹一個整數型的字段值,但是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注入攻擊的目的,如下圖:

    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,如下圖:

    SQL注入漏洞

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

    SQL注入漏洞

    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,如下圖:

    SQL注入漏洞

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

    SQL注入漏洞

    使用萬能密碼登陸成功:

    SQL注入漏洞

    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.Connectionjava.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 類繼承關系圖:

    SQL注入漏洞

    靈蜥內置了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注入原理:

    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注入攻擊,如圖:

    SQL注入漏洞

    RASP可以識別出參數id中的100001-1會在數據庫中做算術運算,因為會被RASP攔截,通常攻擊者非常喜歡使用這種方式來探測是否存在SQL注入,傳統的WAF因為根本識別這個參數的具體業務含義,從而無法識別此類SQL注入攻擊。

    5.1.4 RASP對JSON、Multipart請求的支持

    處理傳統的GET/POST參數傳遞以外,jsonmultipartweb service是目前后端開發最為常用的參數傳遞方式,標準的Web容器默認是不會解析這幾類請求參數的,而主流的MVC框架(如:Spring MVC)具備了解析這幾類請求參數的能力。

    示例 - Multipart請求攔截:

    SQL注入漏洞

    示例 - JSON請求攔截:

    SQL注入漏洞

    本文章首發在 網安wangan.com 網站上。

    上一篇 下一篇
    討論數量: 0
    只看當前版本


    暫無話題~
    亚洲 欧美 自拍 唯美 另类