我是風箏,公眾號「古時的風箏」,一個兼具深度與廣度的程式員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 后端知識圖譜,從小白到大牛要走的路都在里面,
標題有點臭不要臉,有標題黨之嫌了,沒有黑,只是網站安全性做的太差,我一個初學者隨便就搞到了管理員權限,
事情是這樣子的,在10年以前,某個月黑風高夜的夜里,雖然這么說有點暴露年齡了,但無所謂,畢竟我也才18而已,我打開電腦,在瀏覽器中輸入我們高中學校的網址,頁面很熟悉,很簡陋,也沒什么設計感,不過學校的網站從來都是這種風格,直到今天依然是這樣,
這次訪問與以往有些不同,因為我的目的很明確,作為學校的一員,理應為學校做些貢獻的,為學校官網找些安全漏洞也算貢獻的一種吧(強烈的求生欲),

之所以選擇學校的官網,一來是因為熟悉,先從熟悉的東西下手,一定錯不了,二來是因為之前使用網站的時候碰過到一些例外的頁面,直接就是例外堆疊直接拋出來,正好那段時間對網路安全比較有興趣,在研究SQL 注入的時候正好想到在學校官網上碰到的問題好像就存在 SQL 注入的風險,于是,順理成章,我的第一個目標就鎖定了學校官網,
問題就出在一個大概這樣的搜索頁面,真正的網站已經改版過好多次了,以前的頁面找不到了,

當時我在上面隨便輸入了一些內容,里面含有單引號,然后一點擊搜索,頁面就直接出現了例外提示,類似于下面這樣子的,別驚訝,當時很多網站都是這樣的,例外是直接拋出來的,別說以前了,現在也不少,

于是我用那時候剛學會的皮毛知識,加上兩個好用的工具,輕松拿到了資料庫的資料,其中就包括了管理員的賬號、密碼,密碼還是明文的,你說氣人不,
然后通過后臺管理員登錄頁面進入了管理員后臺,當然了,只是進去看了看,什么都沒碰,而且后臺也沒什么重要資料,頂多就是一些通知、新聞等資料,
我是怎么做到的呢,不說具體細節了,也確實沒什么技術含量,而且時間太長也記不清了,后面就說一下 SQL 注入的原理和具體操作,
什么是 SQL 注入
SQL注入,是發生于應用程式與資料庫層的安全漏洞,簡而言之,是在輸入的字串之中注入SQL指令,在設計不良的程式當中忽略了字符檢查,那么這些注入進去的惡意指令就會被資料庫服務器誤認為是正常的SQL指令而運行,因此遭到破壞或是入侵,
SQL 注入一般發生在用戶互動場景中,比如需要用戶自已輸入資訊的輸入框,或者下拉選擇選項的這種,如果不做好輸入內容的過濾,就很可能發生 SQL 注入,

就拿這個登錄界面來說,用戶名和密碼都是你要輸入的內容,點擊登錄按鈕之后,會把你輸入的值傳遞到服務端,服務端再到資料庫進行查詢,
假設后端的查詢陳述句是這樣的,不要在乎這是什么語法,只是舉個例子,
String sql = "select * from `user` where account={account} and password={password}";
正常的情況,比如 account 輸入的是一個電話號碼 13001980988,密碼是 123456,那拼接出的 SQL 陳述句就是
String sql = "select * from `user` where account='13001980988' and password='123456'";
然后,服務端通過資料庫連接執行這條陳述句:
select * from `user` where account='13001980988' and password='123456';
最后,資料庫正常回傳符合條件的記錄,代碼中再根據結果進行判斷,執行后面的邏輯,
不正常的輸入
SQL 注入就是通過不正常的輸入來獲取程式開發者意料之外的結果,
什么是不正常的輸入呢?
比如我在用戶名輸入框中輸入的內容是這樣子的 :13001980988' or 1=1 -- ,密碼輸入框隨便輸入什么都無所謂,然后點擊登錄,傳輸給后端,后端拼接出來的結果就是這樣的:
String sql = "select * from `user` where account='13001980988' or 1=1 --' and password='123456'";
然后,服務端通過資料庫連接會執行下面這條陳述句:
select * from `user` where account='13001980988' or 1=1 --' and password='123456'
以 MySQL 為例, -- 是 MySQL 中的注釋符號,上面的陳述句中 --后面的相當于是注釋內容了,所以最后實際執行的 SQL 陳述句是這樣的:
select * from `user` where account='13001980988' or 1=1
于是,SQL 注入就這么發生了,顯然有了 or 1=1 這個條件,表中所有的記錄都符合條件,如果在用戶名輸入框中輸入的是 admin、administrator 等已知的后臺管理員賬號,那就可以用管理員賬號直接登錄系統了,
上面就是 SQL 注入的基本原理,
SQL 注入遍地都是的年代
在9、10年前,也就是在我小時候(對,這個詞好,小時候),那時候智能手機才剛剛出來,塞班系統還很貴,根本就買不起,用著功能機,30M的流量能用堅持一個月,聊天只靠 QQ 和 短信,微信才剛要問世,更別提什么 APP 了,根本就沒有,那時候,PC Web 才是根正苗紅的網路主宰,如果說要在網上干點兒什么的話,那必須要有一個配套的網站才可以,
互聯網還沒有發展的這么成熟,用的技術也比較原始,絕大多數的網站是用 PHP 寫的,還有很多用 ASP ,可能有些同學都不知道 ASP 是什么,它雖然也是微軟的,但是卻不是 ASP.NET,資料庫很多用的是 MySQL ,還有一部分用的是更原始的 Access,可能又觸到某些同學的盲區了,這不怪你沒見識,只怪你太年輕,
一些小公司啊、學校啊、政府部門網站啊、各種論壇啊等等,各種五花八門的網站,不像現在這樣,無論你用 PHP、Java 還是 Python,都有很多成熟的開發框架供你選擇,成熟的框架必然會減少漏洞和降低被攻擊的風險,但那時候沒有這么多框架供選擇,就比如很多學校會選擇用 ASP + Access 組合的架構來開發自己的學校官網、教務管理系統等,功能上比較簡單,但是全靠手工去寫,就說 SQL 查詢吧,從建立資料庫連接到拼接 SQL 陳述句,再到執行查詢處理查詢結果,全都要自己實作,并沒有什么 ORM 框架、資料庫連接池供選擇,由此就帶來了 SQL 注入的風險,
而且建網站,如果不想開發的話,有很多 CMS 框架,尤其 PHP 的很多,現在依舊使用廣泛的有 WordPress,當時國內的有 Discuz、DEDECMS 等一批傻瓜建站的 CMS 系統,由于代碼都是開源的,而且 WordPress 還支持插件,所以會有很多相關的漏洞爆出來,尤其在多年以前,有了漏洞,想拿下一個網站真是太容易了,即使漏洞已經公布并有了解決方案,但依然有好多網站不及時修補和升級,現在在 Google 中搜索相關的 SQL注入關鍵詞,有很多相關介紹,

說了這么多,這不都是 PHP 的代碼嗎?噓,只是碰巧而已,說明 PHP 市場大呀,畢竟PHP是最好的開發語言,那 Java 中就沒有了嗎,當然有啊,
MyBatis 中的 SQL注入風險
最近在看一些代碼,Spring Boot + MyBatis 的,偶然發現一個模糊查詢的方法的 SQL 陳述句中用到了 like '%${keyword}%'這樣的查詢條件,這一看就有 SQL 注入漏洞,
大家可能都了解,MyBatis 是可以解決 SQL 注入的問題的,一般我們在使用 MyBatis 的時候都會把 SQL 陳述句單獨的放到 xml 檔案中,在 SQL 陳述句中支持兩種格式的引數占位符,一種是 #{parameter},另一種是 ${parameter},在這兩種引數占位符中,#{parameter}是安全的,不存在SQL注入漏洞,而 ${parameter}是存在 SQL 注入漏洞的,
安全的占位符格式
#{parameter} 這種占位符會在 MySQL中進行預編譯,所以你觀察到 MyBatis 列印出來的日志是這樣的:
select * from `user` where account=? and password=?
其實在框架底層,是 JDBC 中的 PreparedStatement 類在起作用,PreparedStatement 是我們很熟悉的 Statement 的子類,它的物件包含了編譯好的SQL陳述句,這種預編譯的方式不僅能提高安全性,而且在多次執行同一個SQL時,能夠提高效率,原因是SQL已編譯好,再次執行時無需再編譯,
不知道你有沒有寫過直接用 JDBC 操作資料庫的代碼,反正大學老師就告訴我們要用占位符去做資料庫查詢,而不是拼接 SQL 字串,因為用占位符的方式安全,其實,MyBatis 的預編譯模式的底層實作就可以理解為下面這樣的,
Connection conn = getConn();//獲得連接
String sql = "select * from `user` where account=? and password=?"; //執行sql前會預編譯號該條陳述句
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "13001980988");
pstmt.setString(2, "123456");
ResultSet rs=pstmt.executeUpdate();
不安全的占位符格式
${parameter} 這種格式是不進行預編譯的,也就相當于字串的拼接,存在 SQL注入的問題,如果非要用這種方式,那需要在程式中對引數進行安全性校驗,強烈建議在使用 MyBatis 的程序中不要使用 ${parameter}這種格式的占位符,而要使用 #{parameter}這種格式,
來一波SQL注入
我模擬了一個 SQL 注入的場景,很簡單,就是一個模糊查詢的介面,根據用戶輸入的關鍵詞查詢,
資料庫中有兩張表,分別為用戶表(user)和 新聞表(news),
用戶表:

新聞表:

NewsMapper 類:
public interface NewsMapper {
List<News> selectNewsLikeTitle(@Param("keyword") String keyword);
}
實際的 SQL 陳述句,注意,正是用到了 ${keyword},才有了 SQL 注入的問題,
<select id="selectNewsLikeTitle" resultType="org.kite.purely.mybatis.entity.News">
select * from news where title like '%${keyword}%';
</select>
然后我寫了一個控制臺,接收輸入的引數,傳給 selectNewsLikeTitle,以便來嘗試 SQL 注入,
代碼已上傳至 github,鏈接放在了文末,需要的同學自取
正常的輸入
新聞表就三條資料,我以 Docker 作為關鍵詞,正常情況下,應該是這樣的回傳結果:

藍色是我輸入的關鍵詞,后面跟著查詢陳述句,一個標準的模糊查詢陳述句,最后輸出的結果也沒問題,
select * from news where title like '%Docker%';
SQL 注入
如果使用者都這么守規矩就好了,但是真實情況往往并不是這樣的,有些人就是喜歡躲在陰暗的角落放著冷箭,大部分情況下是有利可圖,極少部分干脆就只是為了滿足變態心理,
1、查詢所有記錄
有同學已經看出來了,輸入空值不就是查詢所有嗎?對的,沒錯,但真實情況下,前端或者 Controller、Service 層會做攔截,不允許查詢所有,
正常邏輯不允許,但是 SQL 注入就可以,我輸入下面這樣的條件引數,看看會出現什么結果呢?
Docker' or 1=1 or 1='
結果出來了,三條資料全部查詢出來了,

因為構造的 SQL 陳述句已經完全變味兒了,SQL 陳述句是這樣的,由于條件 or 1=1 的加持,導致任何記錄都符合條件,
select * from news where title like '%Docker' or 1=1 or 1='%';
通過添加'來保證條件中字串前后單引號的閉合,
還可以是這樣的條件
Docker' or 1=1 --
或者
Docker' or 1=1 #
因為--和#都是 MySQL 中的注釋符號,用它們來注釋掉關鍵注入后面的部分,最后構造出來的 SQL 陳述句是:
select * from news where title like '%Docker' or 1=1 -- %';
或
select * from news where title like '%Docker' or 1=1 #%';
所以,最后有效的部分就是注釋符號前面的部分,自然,查詢出來的就是所有的記錄,
select * from news where title like '%Docker' or 1=1
這種情況,其實保密資料沒有什么泄漏,但是,它可能會拖垮資料庫,拋開 Redis 快取什么的不談,假設僅有 MySQL 這一層,假設資料庫中有幾萬條、幾十萬條資料,黑客不斷制造這樣的模糊查詢,你的資料庫服務馬上就會掛掉,
2、聯查其他表,危險行為
把資料庫拖垮已經很不爽了,但是更嚴重的,是獲取資料,
我想要通過這條查詢陳述句把 user表的資料也套出來,你看著是不是就有點兒意思了,怎么辦呢,通過 union就可以,
前提是我已經知道有 user 表的存在了,別問怎么知道的,反正是已經知道了,而且黑客有很多辦法能猜到,
我構造這樣的引數:
Docker' union select * from `user` -- 注釋掉后面的內容
執行一下,出現這樣的提示:

構造出來的 SQL 沒有問題,就是我們想要的,
select * from news where title like '%Docker' union select * from `user` -- 注釋掉后面的內容%';
但是這用戶體驗很好,給出了具體的例外,體驗好是對于攻擊者而言的,如果每次例外都把原始例外資訊拋出,那能給攻擊者省不少事兒,就像下面這個例外,
Cause: java.sql.SQLException: The used SELECT statements have a different number of columns
這是因為 news 表和 user 表的列數不一致導致的,前后列數不一致,那這時候怎么辦呢?
構造出下面這樣的查詢陳述句可以試探出 news 表的列數,其中 select 1,2 from user中的 1,2表示假設 news 表有兩列,可以從 1 到 n,當嘗試到哪一個而不出錯或者正常回傳的時候,表示 news 表就有多少列了,
select * from news where title like '%Docker' union select 1,2 from `user`;
要構造這樣的陳述句,需要輸入的引數是:
Docker' union select 1,2 from user #
因為 news 表只有兩列,所以上面的引數可以成功執行,

盲注
大多數網站都不會將例外資訊直接回傳的,當攻擊者拿不到即時的例外反饋時,就像是合著眼睛去猜,這種情況就叫做盲注,盲注是需要極大的耐心、高超的技術以及豐富的經驗的,所以說黑客真不是好當的,
當試探出 news 表的列數后,再去配合它篩選 user 表的列就可以了,user 表的列名其實也是靠各種猜測的,比如常規的命名 name、account、phone、mobile、password 等,或者根據你回傳給前端的屬性對應著猜,比如回傳的用戶名是 userName,那你資料庫中的欄位就很有可能是 user_name,
假設我猜 user 表有 phone 這個欄位,那我構造的引數就可以是下面這樣的:
Docker' union all select 1,phone from user # 注釋掉后面的內容 ,來確定news表有兩個欄位
最后執行下來,結果是這樣的,四條 user 記錄全出來了,

或者我還確定 user 表中有 password 列,那就可以直接把 password 再取出來了,如果 password 再是明文的,那就熱鬧了,
有了用戶名和密碼,是不是就有點危險了,
3、更危險的高權限
還有更危險的呢,假設你程式中資料庫連接用到的賬號是高權限的,比如 root 賬號,有好多中小應用都這么用,別驚訝,
發散思維想一想,這時候能干嘛?
由于權限夠高,那意味著可以執行任何合法的 SQL 陳述句了,在 MySQL 中有資料庫 information_schema,它存盤著當前資料庫實體中的很多重要資訊,而且其中的表結構都是公開透明的,那這樣一來呢,我們就可以通過這個資料庫掌握當前資料庫服務的幾乎所有內容了,
不僅如此,高權限用戶還能通過 MySQL 的讀寫檔案功能實作更多的功能,比如配合 webshell 上傳木馬,獲取服務器的控制權,從而實作脫褲(拖庫),
當然了,說的輕松,實作起來就困難了,不過只要有漏洞,就會被利用,
一些工具
俗話說,工欲善其事,必先利其器,漏洞哪兒那么容易挖,很多有價值的漏洞確實是厲害的黑客手工挖出來的,
為了方便的挖掘常見漏洞及利用漏洞,有很多網路安全專家開放出來的工具,可以讓我等小白簡單上手,比如我上學時候用到的「啊D注入工具」,可以用來掃碼注入點、SQL注入檢測、管理入口檢測等,

還有比較專業的 SQL 注入工具「SQLMap」,它是一款命令列工具,SQLMap 提供了豐富的命令來幫我們發現漏洞、利用漏洞,

另外,如果你想學習 SQL 注入的一些基礎,可以直接整個靶場來玩玩兒,比如這個 Pikachu 就不錯,
https://github.com/zhuifengshaonianhanlu/pikachu
總結
本文并不是為了教各位如何完成 SQL注入,畢竟,我也沒這個實力,當然,制造漏洞的實力還是有的,
只是想說,在寫代碼的時候一定要注意,稍有不慎就可能寫出有漏洞的 SQL,所以,盡量用成熟框架的標準寫法,不要圖省事自己拼 SQL,
不光是 SQL 這部分,其他的涉及到用戶互動的地方都要注入,比如用戶表單,有時候可能產生 xss 漏洞,還有檔案上傳的部分,別用戶上傳了木馬都不知道怎么回事,
愿你的代碼沒有 bug,雖然這是不可能的,
文中的測驗代碼已放至倉庫:https://github.com/huzhicheng/SQL_Injection,有需要的同學自取,
這位英俊瀟灑的少年,如果覺得還不錯的話,給個推薦可好!
公眾號「古時的風箏」,Java 開發者,全堆疊工程師,bug 殺手,擅長解決問題,
一個兼具深度與廣度的程式員鼓勵師,本打算寫詩卻寫起了代碼的田園碼農!堅持原創干貨輸出,你可選擇現在就關注我,或者看看歷史文章再關注也不遲,長按二維碼關注,跟我一起變優秀!

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/246056.html
標籤:其他
上一篇:SQL Server解惑——為什么ORDER BY改變了變數的字串拼接結果
下一篇:Ambari 學習指南
