10年前,我就用 SQL注入漏洞黑了學校網站
阿新 • • 發佈:2021-01-07
> 我是風箏,公眾號「古時的風箏」,一個兼具深度與廣度的程式設計師鼓勵師,一個本打算寫詩卻寫起了程式碼的田園碼農!
文章會收錄在 [JavaNewBee](https://github.com/huzhicheng/JavaNewBee) 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裡面。
標題有點臭不要臉,有標題黨之嫌了,沒有黑,只是網站安全性做的太差,我一個初學者隨便就搞到了管理員許可權。
事情是這樣子的,在10年以前,某個月黑風高夜的夜裡,雖然這麼說有點暴露年齡了,但無所謂,畢竟我也才18而已。我開啟電腦,在瀏覽器中輸入我們高中學校的網址,頁面很熟悉,很簡陋,也沒什麼設計感,不過學校的網站從來都是這種風格,直到今天依然是這樣。
這次訪問與以往有些不同,因為我的目的很明確。作為學校的一員,理應為學校做些貢獻的,為學校官網找些安全漏洞也算貢獻的一種吧(強烈的求生欲)。
![](https://hexo.moonkite.cn/blog/4e2cf043fb9d41039f6d7ee0593450f8.jpeg)
之所以選擇學校的官網,一來是因為熟悉,先從熟悉的東西下手,一定錯不了。二來是因為之前使用網站的時候碰過到一些異常的頁面,直接就是異常堆疊直接丟擲來。正好那段時間對網路安全比較有興趣,在研究SQL 注入的時候正好想到在學校官網上碰到的問題好像就存在 SQL 注入的風險。於是,順理成章,我的第一個目標就鎖定了學校官網。
問題就出在一個大概這樣的搜尋頁面,真正的網站已經改版過好多次了,以前的頁面找不到了。
![](https://hexo.moonkite.cn/blog/image-20201229201637562.png)
當時我在上面隨便輸入了一些內容,裡面含有單引號,然後一點選搜尋,頁面就直接出現了異常提示,類似於下面這樣子的。別驚訝,當時很多網站都是這樣的,異常是直接丟擲來的,別說以前了,現在也不少。
![](https://hexo.moonkite.cn/blog/%E9%80%97%E5%8F%B7.png)
於是我用那時候剛學會的皮毛知識,加上兩個好用的工具,輕鬆拿到了資料庫的資料,其中就包括了管理員的賬號、密碼,密碼還是明文的,你說氣人不。
然後通過後臺管理員登入頁面進入了管理員後臺,當然了,只是進去看了看,什麼都沒碰,而且後臺也沒什麼重要資料,頂多就是一些通知、新聞等資料。
我是怎麼做到的呢,不說具體細節了,也確實沒什麼技術含量,而且時間太長也記不清了,後面就說一下 SQL 注入的原理和具體操作。
## 什麼是 SQL 注入
SQL注入,是發生於應用程式與資料庫層的安全漏洞。簡而言之,是在輸入的字串之中注入SQL指令,在設計不良的程式當中忽略了字元檢查,那麼這些注入進去的惡意指令就會被資料庫伺服器誤認為是正常的SQL指令而執行,因此遭到破壞或是入侵。
SQL 注入一般發生在使用者互動場景中,比如需要使用者自已輸入資訊的輸入框,或者下拉選擇選項的這種,如果不做好輸入內容的過濾,就很可能發生 SQL 注入。
![](https://hexo.moonkite.cn/blog/image-20201228221359222.png)
就拿這個登入介面來說,使用者名稱和密碼都是你要輸入的內容,點選登入按鈕之後,會把你輸入的值傳遞到服務端,服務端再到資料庫進行查詢。
假設後端的查詢語句是這樣的,不要在乎這是什麼語法,只是舉個例子。
```java
String sql = "select * from `user` where account={account} and password={password}";
```
正常的情況,比如 `account` 輸入的是一個電話號碼 `13001980988`,密碼是 `123456`,那拼接出的 SQL 語句就是
```sql
String sql = "select * from `user` where account='13001980988' and password='123456'";
```
然後,服務端通過資料庫連線執行這條語句:
```sql
select * from `user` where account='13001980988' and password='123456';
```
最後,資料庫正常返回符合條件的記錄,程式碼中再根據結果進行判斷,執行後面的邏輯。
### 不正常的輸入
SQL 注入就是通過不正常的輸入來獲取程式開發者意料之外的結果。
什麼是不正常的輸入呢?
比如我在使用者名稱輸入框中輸入的內容是這樣子的 :`13001980988' or 1=1 -- `,密碼輸入框隨便輸入什麼都無所謂,然後點選登入,傳輸給後端,後端拼接出來的結果就是這樣的:
```sql
String sql = "select * from `user` where account='13001980988' or 1=1 --' and password='123456'";
```
然後,服務端通過資料庫連線會執行下面這條語句:
```sql
select * from `user` where account='13001980988' or 1=1 --' and password='123456'
```
以 MySQL 為例, `--` 是 MySQL 中的註釋符號,上面的語句中 `--`後面的相當於是註釋內容了,所以最後實際執行的 SQL 語句是這樣的:
```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注入關鍵詞,有很多相關介紹。
![](https://hexo.moonkite.cn/blog/image-20201230210723770.png)
說了這麼多,這不都是 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 打印出來的日誌是這樣的:
```sql
select * from `user` where account=? and password=?
```
其實在框架底層,是 JDBC 中的 PreparedStatement 類在起作用,PreparedStatement 是我們很熟悉的 Statement 的子類,它的物件包含了編譯好的SQL語句。這種預編譯的方式不僅能提高安全性,而且在多次執行同一個SQL時,能夠提高效率。原因是SQL已編譯好,再次執行時無需再編譯。
不知道你有沒有寫過直接用 JDBC 操作資料庫的程式碼,反正大學老師就告訴我們要用佔位符去做資料庫查詢,而不是拼接 SQL 字串,因為用佔位符的方式安全。其實,MyBatis 的預編譯模式的底層實現就可以理解為下面這樣的。
```java
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)。
使用者表:
![](https://hexo.moonkite.cn/blog/image-20210104101249478.png)
新聞表:
![](https://hexo.moonkite.cn/blog/image-20210104101323777.png)
NewsMapper 類:
```java
public interface NewsMapper {
List selectNewsLikeTitle(@Param("keyword") String keyword);
}
```
實際的 SQL 語句,注意,正是用到了 `${keyword}`,才有了 SQL 注入的問題。
```xml
```
然後我寫了一個控制檯,接收輸入的引數,傳給 `selectNewsLikeTitle`,以便來嘗試 SQL 注入。
**程式碼已上傳至 github,連結放在了文末,需要的同學自取**
### 正常的輸入
新聞表就三條資料,我以 `Docker` 作為關鍵詞,正常情況下,應該是這樣的返回結果:
![](https://hexo.moonkite.cn/blog/image-20210104103501606.png)
藍色是我輸入的關鍵詞,後面跟著查詢語句。一個標準的模糊查詢語句,最後輸出的結果也沒問題。
```sql
select * from news where title like '%Docker%';
```
### SQL 注入
如果使用者都這麼守規矩就好了,但是真實情況往往並不是這樣的,有些人就是喜歡躲在陰暗的角落放著冷箭,大部分情況下是有利可圖,極少部分乾脆就只是為了滿足變態心理。
#### **1、查詢所有記錄**
有同學已經看出來了,輸入空值不就是查詢所有嗎?對的,沒錯,但真實情況下,前端或者 Controller、Service 層會做攔截,不允許查詢所有。
正常邏輯不允許,但是 SQL 注入就可以。我輸入下面這樣的條件引數,看看會出現什麼結果呢?
```
Docker' or 1=1 or 1='
```
結果出來了,三條資料全部查詢出來了。
![](https://hexo.moonkite.cn/blog/image-20210104104555343.png)
因為構造的 SQL 語句已經完全變味兒了,SQL 語句是這樣的,由於條件 `or 1=1` 的加持,導致任何記錄都符合條件。
```sql
select * from news where title like '%Docker' or 1=1 or 1='%';
```
通過新增`'`來保證條件中字串前後單引號的閉合。
還可以是這樣的條件
```
Docker' or 1=1 --
或者
Docker' or 1=1 #
```
因為`--`和`#`都是 MySQL 中的註釋符號,用它們來註釋掉關鍵注入後面的部分,最後構造出來的 SQL 語句是:
```sql
select * from news where title like '%Docker' or 1=1 -- %';
或
select * from news where title like '%Docker' or 1=1 #%';
```
所以,最後有效的部分就是註釋符號前面的部分,自然,查詢出來的就是所有的記錄。
```sql
select * from news where title like '%Docker' or 1=1
```
這種情況,其實保密資料沒有什麼洩漏,但是,它可能會拖垮資料庫,拋開 Redis 快取什麼的不談,假設僅有 MySQL 這一層,假設資料庫中有幾萬條、幾十萬條資料,黑客不斷製造這樣的模糊查詢,你的資料庫服務馬上就會掛掉。
#### **2、聯查其他表,危險行為**
把資料庫拖垮已經很不爽了,但是更嚴重的,是獲取資料。
我想要通過這條查詢語句把 `user`表的資料也套出來,你看著是不是就有點兒意思了。怎麼辦呢,通過 `union`就可以。
前提是我已經知道有 user 表的存在了,別問怎麼知道的,反正是已經知道了,而且黑客有很多辦法能猜到。
我構造這樣的引數:
```
Docker' union select * from `user` -- 註釋掉後面的內容
```
執行一下,出現這樣的提示:
![](https://hexo.moonkite.cn/blog/image-20210104113339354.png)
構造出來的 SQL 沒有問題,就是我們想要的。
```sql
select * from news where title like '%Docker' union select * from `user` -- 註釋掉後面的內容%';
```
但是這使用者體驗很好,給出了具體的異常。體驗好是對於攻擊者而言的,如果每次異常都把原始異常資訊丟擲,那能給攻擊者省不少事兒,就像下面這個異常。
```bash
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 表就有多少列了。
```sql
select * from news where title like '%Docker' union select 1,2 from `user`;
```
要構造這樣的語句,需要輸入的引數是:
```
Docker' union select 1,2 from user #
```
因為 news 表只有兩列,所以上面的引數可以成功執行。
![](https://hexo.moonkite.cn/blog/image-20210104132428774.png)
> ##### 盲注
>
> 大多數網站都不會將異常資訊直接返回的,當攻擊者拿不到即時的異常反饋時,就像是合著眼睛去猜,這種情況就叫做盲注。盲注是需要極大的耐心、高超的技術以及豐富的經驗的,所以說黑客真不是好當的。
當試探出 news 表的列數後,再去配合它篩選 user 表的列就可以了,user 表的列名其實也是靠各種猜測的,比如常規的命名 name、account、phone、mobile、password 等,或者根據你返回給前端的屬性對應著猜,比如返回的使用者名稱是 userName,那你資料庫中的欄位就很有可能是 user_name。
假設我猜 user 表有 phone 這個欄位,那我構造的引數就可以是下面這樣的:
```sql
Docker' union all select 1,phone from user # 註釋掉後面的內容 ,來確定news表有兩個欄位
```
最後執行下來,結果是這樣的,四條 user 記錄全出來了。
![](https://hexo.moonkite.cn/blog/image-20210104133357925.png)
或者我還確定 user 表中有 password 列,那就可以直接把 password 再取出來了,如果 password 再是明文的,那就熱鬧了。
有了使用者名稱和密碼,是不是就有點危險了。
#### **3、更危險的高許可權**
還有更危險的呢,假設你程式中資料庫連線用到的賬號是高許可權的,比如 root 賬號,有好多中小應用都這麼用,別驚訝。
發散思維想一想,這時候能幹嘛?
由於許可權夠高,那意味著可以執行任何合法的 SQL 語句了。在 MySQL 中有資料庫 `information_schema`,它儲存著當前資料庫例項中的很多重要資訊,而且其中的表結構都是公開透明的,那這樣一來呢,我們就可以通過這個資料庫掌握當前資料庫服務的幾乎所有內容了。
不僅如此,高許可權使用者還能通過 MySQL 的讀寫檔案功能實現更多的功能,比如配合 webshell 上傳木馬,獲取伺服器的控制權,從而實現脫褲(拖庫)。
當然了,說的輕鬆,實現起來就困難了,不過只要有漏洞,就會被利用。
## 一些工具
俗話說,工欲善其事,必先利其器。漏洞哪兒那麼容易挖,很多有價值的漏洞確實是厲害的黑客手工挖出來的。
為了方便的挖掘常見漏洞及利用漏洞,有很多網路安全專家開放出來的工具,可以讓我等小白簡單上手。比如我上學時候用到的「啊D注入工具」。可以用來掃碼注入點、SQL注入檢測、管理入口檢測等。
![](https://hexo.moonkite.cn/blog/watermark,image_d2F0ZXIvYmFpa2U5Mg==,g_7,xp_5,yp_5.jpeg)
還有比較專業的 SQL 注入工具「SQLMap」,它是一款命令列工具。SQLMap 提供了豐富的命令來幫我們發現漏洞、利用漏洞。
![img](https://hexo.moonkite.cn/blog/screenshot.png)
另外,如果你想學習 SQL 注入的一些基礎,可以直接整個靶場來玩玩兒。比如這個 Pikachu 就不錯。
https://github.com/zhuifengshaonianhanlu/pikachu
## 總結
本文並不是為了教各位如何完成 SQL注入,畢竟,我也沒這個實力。當然,製造漏洞的實力還是有的。
只是想說,在寫程式碼的時候一定要注意,稍有不慎就可能寫出有漏洞的 SQL,所以,儘量用成熟框架的標準寫法,不要圖省事自己拼 SQL。
不光是 SQL 這部分,其他的涉及到使用者互動的地方都要注入,比如使用者表單,有時候可能產生 xss 漏洞,還有檔案上傳的部分,別用戶上傳了木馬都不知道怎麼回事。
**願你的程式碼沒有 bug**。雖然這是不可能的。
文中的測試程式碼已放至倉庫:https://github.com/huzhicheng/SQL_Injection,有需要的同學自取。
***
**這位英俊瀟灑的少年,如果覺得還不錯的話,給個推薦可好!**
公眾號「古時的風箏」,Java 開發者,全棧工程師,bug 殺手,擅長解決問題。
一個兼具深度與廣度的程式設計師鼓勵師,本打算寫詩卻寫起了程式碼的田園碼農!堅持原創乾貨輸出,你可選擇現在就關注我,或者看看歷史文章再關注也不遲。長按二維碼關注,跟我一起變優秀!
![](https://img2020.cnblogs.com/blog/273364/202008/273364-20200807093211558-1258890269.jpg)