Mybatis下的SQL注入漏洞原理及防護方法解析
之前我一直認為 Mybatis 框架下已經(jīng)實現(xiàn)預編譯機制,很多東西都封裝好了,應該基本上不會再有 SQL 注入問題了。近期在滲透中發(fā)現(xiàn),在實際項目中,即使使用了 Mybatis 框架,但仍然有可能因為編碼人員安全意識不足而導致 SQL 注入問題。出現(xiàn)情況還真不少,因此有了這篇文章。
一、前言
之前我一直認為 Mybatis 框架下已經(jīng)實現(xiàn)預編譯機制,很多東西都封裝好了,應該基本上不會再有 SQL 注入問題了。近期在滲透中發(fā)現(xiàn),在實際項目中,即使使用了 Mybatis 框架,但仍然有可能因為編碼人員安全意識不足而導致 SQL 注入問題。出現(xiàn)情況還真不少,因此有了這篇文章。
二、SQL 注入漏洞原理
1、概述
SQL 注入(SQL Injection)是發(fā)生在 Web 程序中數(shù)據(jù)庫層的安全漏洞,是網(wǎng)站存在最多也是最簡單的漏洞。主要原因是程序?qū)τ脩糨斎霐?shù)據(jù)的合法性沒有判斷和處理,導致攻擊者可以在 Web 應用程序中事先定義好的 SQL 語句中添加額外的 SQL 語句,在管理員不知情的情況下實現(xiàn)非法操作,以此來實現(xiàn)欺騙數(shù)據(jù)庫服務器執(zhí)行非授權的任意查詢,從而進一步獲取到數(shù)據(jù)信息。
簡單地說,就是通過在用戶可控參數(shù)中注入 SQL 語法,破壞原有 SQL 結(jié)構(gòu),達到編寫程序時意料之外結(jié)果的攻擊行為。其成因可以歸結(jié)為如下原因造成的:
- 程序編寫者在處理應用程序和數(shù)據(jù)庫交互時,使用字符串拼接的方式構(gòu)造 SQL 語句。
- 且未對用戶可控參數(shù)進行足夠的過濾。
2、漏洞復現(xiàn)
下面使用DVWA靶場來進行演示,網(wǎng)站架構(gòu)為PHP,我們重點關注漏洞原理即可。
該頁面提供了一個簡單的查詢功能,可以根據(jù)前端輸入的用戶ID來查詢對應的用戶信息。如圖,輸入 1
,返回了對應 admin 用戶的信息。
查看該頁面的源代碼:
<?php if( isset( $_REQUEST[ 'Submit' ] ) ) { // Get input $id = $_REQUEST[ 'id' ]; // Check database $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } mysqli_close($GLOBALS["___mysqli_ston"]); } ?>
進行代碼審計可以發(fā)現(xiàn),程序?qū)⑶岸溯斎氲?id
參數(shù)未加任何處理,直接拼接在了 SQL 語句中,那么此時就導致了SQL注入漏洞。
若此時攻擊者輸入的用戶ID為 1' or 1='1
,則程序拼接后執(zhí)行的 SQL 語句變成了:
SELECT first_name, last_name FROM users WHERE user_id = '1' or 1='1';
可見,攻擊者通過單引號 '
閉合了數(shù)據(jù)庫查詢語句,并且在查詢條件之后構(gòu)造了“或 1=1”,即“或真”的邏輯,導致查詢出了全部用戶的數(shù)據(jù)。
如果攻擊者可以任意替代提交的字符串,就可以利用 SQL 注入漏洞改變原有 SQL 語句的含義,進而執(zhí)行任意 SQL 命令,入侵數(shù)據(jù)庫進行脫庫、刪庫,甚至通過數(shù)據(jù)庫提權獲取系統(tǒng)權限,造成不可估量的損失。(SQL注入的場景類型非常之多,攻擊手法、繞過姿勢也非常多,本文不作重點討論)
3、修復建議
一般來說,防御 SQL 注入的最佳方式就是使用預編譯語句(其他防御方法還有很多,本文不作重點討論),綁定變量。例如:
String sql = "SELECT * FROM user_table WHERE username = ?"; PreparedStatement pstmt = connection.prepareStatement(sql); pstmt.setString(1, "zxd"); ResultSet results = pstmt.executeQuery();
使用預編譯的 SQL 語句,SQL 語句的語義不會發(fā)生改變。在 SQL 語句中,變量用占位符 ?
表示,攻擊者無法改變 SQL 的結(jié)構(gòu)。
三、Mybatis 框架簡介
1、參數(shù)符號的兩種方式
Mybatis 的 SQL 語句可以基于注解的方式寫在類方法上面,更多的是以 xml 的方式寫到 xml 文件。Mybatis 中 SQL 語句需要我們自己手動編寫或者用 generator 自動生成。編寫 xml 文件時,MyBatis 支持兩種參數(shù)符號,#{}
和 ${}
。
#{}
使用預編譯,通過 PreparedStatement 和占位符來實現(xiàn),會把參數(shù)部分用一個占位符?
替代,而后注入的參數(shù)將不會再進行 SQL 編譯,而是當作字符串處理。可以有效避免 SQL 注入漏洞。${}
表示使用拼接字符串,將接受到參數(shù)的內(nèi)容不加任何修飾符拼接在 SQL 中。易導致 SQL 注入漏洞。
兩者的區(qū)別如下:
#{}
為參數(shù)占位符?
,即 SQL 預編譯。${}
為字符串替換,即 SQL 拼接。#{}
是“動態(tài)解析->預編譯->執(zhí)行”的過程。${}
是“動態(tài)解析->編譯->執(zhí)行”的過程。#{}
的變量替換是在 DBMS 中。${}
的變量替換是在 DBMS 外。- 變量替換后,
#{}
對應的變量自動加上引號。變量替換后,${}
對應的變量不會加上引號。
2、漏洞復現(xiàn)
下面以一個查詢場景進行簡單演示,數(shù)據(jù)庫表 user_table 的表數(shù)據(jù)如下:
若沒有采用 JDBC 的預編譯模式,查詢 SQL 寫為:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username = '${username}' </select>
這種寫法就產(chǎn)生了 SQL 語句的動態(tài)拼接,這樣格式的參數(shù)會直接參與 SQL 語句的編譯,從而不能避免SQL注入攻擊。
若此時攻擊者提交的參數(shù)值為 zxd' or 1='1
,如下圖,利用 SQL 注入漏洞,成功查詢了所有用戶數(shù)據(jù)。
因此,應用 Mybatis 框架 SQL語句的安全寫法(即 JDBC 預編譯模式):
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username = #{username}</select>
可見,此時采用 JDBC 預編譯模式,即使攻擊者嘗試 SQL 注入攻擊,也只會將參數(shù)整體作為字符串處理,有效避免了 SQL 注入問題。
四、Mybatis 框架下的 SQL 注入問題及防護方法
還是以上節(jié)的查詢場景舉例,Mybatis 框架下易產(chǎn)生 SQL 注入漏洞的情況主要有以下三種:
1、模糊查詢
在模糊查詢場景下,考慮安全編碼規(guī)范,使用 #{}
傳入?yún)?shù):
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username = #{username} </select>
在這種情況下使用 #{}
程序會報錯:
于是很多安全經(jīng)驗不足的程序員就把 #{}
號改成了 ${}
,如果應用層代碼沒有對用戶輸入的內(nèi)容做處理勢必會產(chǎn)生SQL注入漏洞。
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username like '%${username}%' </select>
若此時攻擊者提交的參數(shù)值為 zxd' or 1=1#
,如下圖,利用 SQL 注入漏洞,成功查詢了所有用戶數(shù)據(jù)。
因此,安全的寫法應當使用 CONCAT 函數(shù)連接通配符:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username like concat('%',#{username},'%') </select>
2、帶有 IN 謂詞的查詢
在 IN 關鍵字之后使用 #{}
查詢多個參數(shù):
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username in (#{usernames}) </select>
正常提交查詢參數(shù) 'zxd','hhh'
,因為預編譯機制,系統(tǒng)將我們輸入的字符當作了一個字符串,因此查詢結(jié)果為空,不能滿足業(yè)務功能需求。
于是很多安全經(jīng)驗不足的程序員就把 #{}
號改成了 ${}
:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table where username in (${usernames}) </select>
攻擊者提交參數(shù)值 'hhh') or 1=1#
,利用 SQL 注入漏洞,成功查詢了所有用戶數(shù)據(jù)。
因此,此種情況下,安全的做法應當使用 foreach 標簽:
<select id="getUserFromList" resultType="user.NewUserDO"> select * from user_table where username in <foreach collection="list" item="username" open="(" separator="," close=")"> #{username} </foreach> </select>
3、帶有動態(tài)排序功能的查詢
動態(tài)排序功能,需要在 ORDER BY 之后傳入?yún)?shù),考慮安全編碼規(guī)范,使用 #{}
傳入?yún)?shù):
<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table order by #{column} limit 0,1 </select>
提交參數(shù) username
根據(jù)用戶名字段排序。但因為預編譯機制,系統(tǒng)將我們輸入的字符當作了一個字符串,根據(jù)字符串排序是不生效的,不能滿足業(yè)務功能需求。(根據(jù)用戶名字段排序,此時正常應返回 root
用戶)
于是很多安全經(jīng)驗不足的程序員就把 #{}
號改成了 ${}
:
<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUserDO"> select * from user_table order by ${column} limit 0,1 </select>
攻擊者提交參數(shù)值 username#
,利用 SQL 注入漏洞,成功查詢了所有用戶數(shù)據(jù)。
因此,此種情況下,安全的做法應當在 Java 代碼層面來進行解決。可以設置一個字段值的白名單,僅允許用戶傳入白名單內(nèi)的字段。
String sort = request.getParameter("sort"); String[] sortWhiteList = {"id", "username", "password"}; if(!Arrays.asList(sortWhiteList).contains(sort)){ sort = "id"; }
或者僅允許用戶傳入索引值,代碼再將索引值映射成對應字段。
String sort = request.getParameter("sort"); switch(sort){ case "1": sort = "id"; break; case "2": sort = "username"; break; case "3": sort = "password"; break; default: sort = "id"; break; }
需要注意的是在 mybatis-generator 自動生成的 SQL 語句中,ORDER BY 使用的也是 ${}
,而 LIKE 和 IN 沒有問題。
到此這篇關于Mybatis下的SQL注入漏洞原理及防護方法的文章就介紹到這了,更多相關Mybatis SQL注入漏洞原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何從eureka獲取服務的ip和端口號進行Http的調(diào)用
這篇文章主要介紹了如何從eureka獲取服務的ip和端口號進行Http的調(diào)用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03spring cloud中啟動Eureka Server的方法
本篇文章主要介紹了spring cloud中啟動Eureka Server的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01Spring 實現(xiàn)excel及pdf導出表格示例
本篇文章主要介紹了Spring 實現(xiàn)excel及pdf導出表格示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03springboot基于Redis發(fā)布訂閱集群下WebSocket的解決方案
這篇文章主要介紹了springboot基于Redis發(fā)布訂閱集群下WebSocket的解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01