預防SQL隱碼攻擊筆記

禿桔子發表於2019-06-18

SQL隱碼攻擊如何預防?

本文參考自owasp,重點是提供清晰,簡單,可操作的指導,以防止應用程式中的SQL隱碼攻擊漏洞。不幸的是,SQL隱碼攻擊攻擊很常見,這是由於兩個因素:

  1. SQL隱碼攻擊漏洞的顯著流行
  2. 目標的吸引力(即資料庫通常包含應用程式的所有有趣/關鍵資料)。

發生瞭如此多的成功SQL隱碼攻擊有點可恥,因為在程式碼中避免SQL隱碼攻擊漏洞非常簡單。

當軟體開發人員建立包含使用者提供的輸入的動態資料庫查詢時,會引入SQL隱碼攻擊漏洞。為了避免SQL隱碼攻擊缺陷很簡單。開發人員需要:

a)停止編寫動態查詢; 

b)防止使用者提供的包含惡意SQL的輸入影響所執行查詢的邏輯。

 

本文提供了一組通過避免這兩個問題來防止SQL隱碼攻擊漏洞的簡單技術。這些技術幾乎可以與任何型別的資料庫一起使用。還有其他型別的資料庫,如XML資料庫,可能有類似的問題(例如,XPath和XQuery注入),這些技術也可用於保護它們。

主要防禦:

  • 選項1:使用準備好的語句(帶引數化查詢)
  • 選項2:使用儲存過程
  • 選項3:白名單輸入驗證
  • 選項4:轉義所有使用者提供的輸入

額外防禦:

  • 另外:強制執行最低許可權
  • 另外:執行白名單輸入驗證作為輔助防禦

不安全的例子:

SQL隱碼攻擊漏洞通常如下所示:

以下(Java)示例是UNSAFE,並允許攻擊者將程式碼注入將由資料庫執行的查詢中。簡單地附加到查詢的未經驗證的“customerName”引數允許攻擊者注入他們想要的任何SQL程式碼。不幸的是,這種訪問資料庫的方法太常見了。

String query = "SELECT account_balance FROM user_data WHERE user_name = " 
             + request.getParameter("customerName");
try {
    Statement statement = connection.createStatement( ... );
    ResultSet results = statement.executeQuery( query );
}
...
 

主要防禦

防禦選項1:準備好的語句(帶引數化查詢)

使用帶有變數繫結的預準備語句(也就是引數化查詢)是所有開發人員應該首先學習如何編寫資料庫查詢的方法。它們比動態查詢更易於編寫,更易於理解。引數化查詢強制開發人員首先定義所有SQL程式碼,然後將每個引數傳遞給查詢。這種編碼風格允許資料庫區分程式碼和資料,無論提供什麼使用者輸入。

準備好的語句可確保攻擊者無法更改查詢的意圖,即使攻擊者插入了SQL命令也是如此。在下面的安全示例中,如果攻擊者輸入的是userID tom' or '1'='1,則引數化查詢不會受到攻擊,而是會查詢與字串完全匹配的使用者名稱tom' or '1'='1

特定語言的建議:

  • Java EE - PreparedStatement()與繫結變數一起使用
  • .NET - 使用引數化查詢,如繫結變數SqlCommand()OleDbCommand()使用繫結變數
  • PHP - 將PDO與強型別引數化查詢一起使用(使用bindParam())
  • Hibernate - createQuery()與繫結變數一起使用(在Hibernate中稱為命名引數)
  • SQLite - 用於sqlite3_prepare()建立語句物件

在極少數情況下,準備好的陳述會損害績效。遇到這種情況時,最好是a)強烈驗證所有資料或b)使用特定於資料庫供應商的轉義例程來轉義所有使用者提供的輸入,如下所述,而不是使用預準備語句。

安全JavaSQL語句示例

以下程式碼示例使用PreparedStatementJava的引數化查詢實現來執行相同的資料庫查詢。

// 一定要驗證
String custname = request.getParameter("customerName"); 
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname); 
ResultSet results = pstmt.executeQuery( );
 

安全C#.NET SQL語句示例

使用.NET,它更加直接。查詢的建立和執行不會更改。您所要做的就是使用Parameters.Add()此處所示呼叫將引數傳遞給查詢

String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
try {
  OleDbCommand command = new OleDbCommand(query, connection);
  command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text));
  OleDbDataReader reader = command.ExecuteReader();
  //
} catch (OleDbException se) {
  // error handling
} 

 

 

我們已經在Java和.NET中展示了示例,但實際上所有其他語言(包括Cold Fusion和Classic ASP)都支援引數化查詢介面。甚至SQL抽象層,如Hibernate查詢語言(HQL)也有相同型別的注入問題(我們稱之為HQL注入)。HQL也支援引數化查詢,因此我們可以避免這個問題:

Hibernate查詢語言(HQL)準備語句(命名引數)示例

//First is an unsafe HQL Statement
Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'");
//Here is a safe version of the same query using named parameters
Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid");
safeHQLQuery.setParameter("productid", userSuppliedParameter);

開發人員傾向於喜歡Prepared Statement方法,因為所有SQL程式碼都保留在應用程式中。這使您的應用程式相對資料庫獨立。

防禦選項2:儲存過程

SQL隱碼攻擊並不總是安全的儲存過程。但是,某些標準儲存過程程式設計結構與安全實現時使用引數化查詢具有相同的效果,這是大多數儲存過程語言的標準。

它們要求開發人員只使用自動引數化的引數構建SQL語句,除非開發人員在很大程度上超出了標準。預準備語句和儲存過程之間的區別在於,儲存過程的SQL程式碼已定義並儲存在資料庫本身中,然後從應用程式中呼叫。這兩種技術在防止SQL隱碼攻擊方面具有相同的效果,因此您的組織應該選擇哪種方法對您最有意義。

注意:'安全實現'意味著儲存過程不包含任何不安全的動態SQL生成。開發人員通常不會在儲存過程中生成動態SQL。但是,它可以做到,但應該避免。如果無法避免,則儲存過程必須使用輸入驗證或本文所述的正確轉義,以確保不能使用所有使用者提供的儲存過程輸入將SQL程式碼注入動態生成的查詢中。審計人員應始終在SQL Server儲存過程中查詢sp_execute,execute或exec的用法。類似的審計指南對於其他供應商的類似功能是必要的。

在某些情況下,儲存過程可能會增加風險。例如,MS SQL伺服器上,你有3個主要的預設角色:db_datareaderdb_datawriterdb_owner在儲存過程開始使用之前,DBA會根據要求為webservice的使用者提供db_datareader或db_datawriter許可權。但是,儲存過程需要執行許可權,預設情況下該角色不可用。使用者管理已集中在一些設定,但僅限於這3個角色,導致所有Web應用程式在db_owner許可權下執行,因此儲存過程可以正常工作。當然,這意味著如果伺服器遭到破壞,攻擊者擁有資料庫的完全許可權,以前他們可能只具有讀訪問許可權。

安全Java儲存過程示例

以下程式碼示例使用CallableStatementJava的儲存過程介面實現來執行相同的資料庫查詢。sp_getAccountBalance儲存過程將在資料庫中被預先定義和執行相同的功能與上述定義的查詢。

// This should REALLY be validated
String custname = request.getParameter("customerName"); 
try {
  CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
  cs.setString(1, custname);
  ResultSet results = cs.executeQuery();      
  // … result set handling 
} catch (SQLException se) {           
  // … logging and error handling
}
 

安全的VB .NET儲存過程示例

以下程式碼示例使用SqlCommand.NET的儲存過程介面實現來執行相同的資料庫查詢。sp_getAccountBalance儲存過程將在資料庫中被預先定義和執行相同的功能與上述定義的查詢。

 
Try
   Dim command As SqlCommand = new SqlCommand("sp_getAccountBalance", connection)
   command.CommandType = CommandType.StoredProcedure
   command.Parameters.Add(new SqlParameter("@CustomerName", CustomerName.Text))
   Dim reader As SqlDataReader = command.ExecuteReader()
   '...
 Catch se As SqlException 
   'error handling
 End Try
 

防禦選項3:白名單輸入驗證

SQL查詢的各個部分不是使用繫結變數的合法位置,例如表或列的名稱,以及排序順序指示符(ASC或DESC)。在這種情況下,輸入驗證或查詢重新設計是最合適的防禦。對於表或列的名稱,理想情況下,這些值來自程式碼,而不是來自使用者引數。

但是,如果使用者引數值用於使表名和列名不同,則應將引數值對映到合法/預期的表或列名,以確保未經驗證的使用者輸入不會在查詢中結束。請注意,這是設計不佳的症狀,如果時間允許,應考慮完全重寫。

以下是表名驗證的示例。

String tableName;
switch(PARAM):
  case "Value1": tableName = "fooTable";
                 break;
  case "Value2": tableName = "barTable";
                 break;
  ...
  default      : throw new InputValidationException("unexpected value provided" 
                                                  + " for table name");

 

tableName然後可以直接附加到SQL查詢,因為它是目前已知的是在此查詢表名的法律和預期值之一。請記住,通用表驗證功能可能會導致資料丟失,因為表名用於不期望它們的查詢中。

對於像排序順序這樣簡單的東西,最好將使用者提供的輸入轉換為布林值,然後使用該布林值選擇要附加到查詢的安全值。這是動態查詢建立中非常標準的需求。

例如:

public String someMethod(boolean sortOrder) {
 String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");`
 ...

 

 

任何時候使用者輸入都可以轉換為非String,如日期,數字,布林值,列舉型別等,然後將其附加到查詢中,或用於選擇要追加到查詢的值,這可以確保它是這樣做是安全的。

在所有情況下,也建議將輸入驗證作為輔助防禦,即使使用繫結變數,如本文後面所述。有關如何實施強白名單輸入驗證的更多技術在輸入驗證備忘單中進行了描述

防禦選項4:轉義所有使用者提供的輸入

當上述任何一種方法都不可行時,該技術僅應作為最後的手段使用。輸入驗證可能是一個更好的選擇,因為與其他防禦相比,這種方法很脆弱,我們不能保證它會在所有情況下阻止所有SQL隱碼攻擊。

此技術是在將使用者輸入放入查詢之前將其轉義。它的實現在資料庫方面非常具體。通常只建議在實現輸入驗證時不會降低遺留程式碼的成本效益。應該使用引數化查詢,儲存過程或某種為您構建查詢的物件關係對映器(ORM)來構建或重寫從頭開始構建的應用程式或需要低風險容忍度的應用程式。

這種技術就是這樣的。每個DBMS都支援一種或多種特定於某些查詢的字元轉義方案。如果您使用正在使用的資料庫的正確轉義方案轉義所有使用者提供的輸入,則DBMS不會將該輸入與開發人員編寫的SQL程式碼混淆,從而避免任何可能的SQL隱碼攻擊漏洞。

要專門為資料庫編碼器查詢javadoc,請單擊Codec左側類。有很多編解碼器實現。兩個特定於資料庫的編解碼器是OracleCodec,和MySQLCodec

只需All Known Implementing Classes:在Interface Codec頁面頂部單擊其名稱即可

目前,ESAPI目前擁有以下資料庫編碼器:

  • MySQL(支援ANSI和本機模式)

資料庫編碼器即將推出:

  • SQL Server
  • PostgreSQL的

如果您的資料庫編碼器丟失,請告訴我們。

特定於資料庫的轉義詳細資訊

如果您想構建自己的轉義例程,以下是我們為ESAPI編碼器開發的每個資料庫的轉義細節:

  • SQL Server
  • DB2
轉義動態查詢

使用ESAPI資料庫編解碼器非常簡單。Oracle示例如下所示:

ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );

 

因此,如果您在程式碼中生成了一個現有的動態查詢,該查詢將轉到Oracle,如下所示:

String query = "SELECT user_id FROM user_data WHERE user_name = '" 
              + req.getParameter("userID")
              + "' and user_password = '" + req.getParameter("pwd") +"'";
try {
    Statement statement = connection.createStatement( … );
    ResultSet results = statement.executeQuery( query );
}
 

你會重寫第一行看起來像這樣:

Codec ORACLE_CODEC = new OracleCodec();
String query = "SELECT user_id FROM user_data WHERE user_name = '" 
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("userID")) 
+ "' and user_password = '"
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("pwd")) +"'";
 

無論輸入是什麼,它現在都可以安全地進行SQL隱碼攻擊。

為了獲得最大的程式碼可讀性,您還可以構建自己的程式碼OracleEncoder

Encoder oe = new OracleEncoder();
String query = "SELECT user_id FROM user_data WHERE user_name = '" 
+ oe.encode( req.getParameter("userID")) + "' and user_password = '" 
+ oe.encode( req.getParameter("pwd")) +"'";
 

使用這種型別的解決方案,您只需要將每個使用者提供的引數包裝成一個ESAPI.encoder().encodeForOracle( )呼叫或者您命名為呼叫的任何內容,您就可以完成。

在Like子句中轉義萬用字元

LIKE關鍵字允許進行文字掃描搜尋。在Oracle中,下劃線_字元僅匹配一個字元,而&符號%用於匹配任何字元的零次或多次出現。必須在LIKE子句條件中轉義這些字元。

例如:

SELECT name FROM emp WHERE id LIKE '%/_%' ESCAPE '/';

SELECT name FROM emp WHERE id LIKE '%\%%' ESCAPE '\';

MySQL轉義

MySQL支援兩種轉義模式:

  1. ANSI_QUOTES SQL模式,以及這個關閉的模式,我們稱之為
  2. MySQL 模式。

ANSI SQL模式:'使用''(兩個單一刻度)簡單編碼所有(單個刻度)字元

MySQL 模式,執行以下操作:

NUL (0x00) --> \0  [This is a zero, not the letter O]
BS  (0x08) --> \b
TAB (0x09) --> \t
LF  (0x0a) --> \n
CR  (0x0d) --> \r
SUB (0x1a) --> \Z
"   (0x22) --> \"
%   (0x25) --> \%
'   (0x27) --> \'
\   (0x5c) --> \\
_   (0x5f) --> \_ 
all other non-alphanumeric characters with ASCII values 
less than 256  --> \c where 'c' is the original non-alphanumeric character.

 

SQL Server轉義

我們還沒有實現SQL Server轉義例程,但是下面有很好的指標和連結到描述如何防止SQL伺服器上的SQL隱碼攻擊的文章,請參見此處

DB2轉義

此資訊基於DB2 WebQuery特殊字元以及Oracle JDBC DB2驅動程式中的一些資訊

有關幾個DB2 Universal驅動程式之間差異的資訊

十六進位制編碼所有輸入

轉義的一個特殊情況是對從使用者接收的整個字串進行十六進位制編碼的過程(這可以看作是轉義每個字元)。Web應用程式應在將使用者輸入包含在SQL語句中之前對其進行十六進位制編碼。SQL語句應該考慮到這一事實,並相應地比較資料。

例如,如果我們必須查詢匹配sessionID的記錄,並且使用者將字串abc123作為會話ID傳送,則select語句將為:

SELECT ... FROM session WHERE hex_encode(sessionID) = '616263313233'

 

 

hex_encode應該由所使用的資料庫的特定工具替換。字串606162313233是從使用者接收的字串的十六進位制編碼版本(它是使用者資料的ASCII / UTF-8程式碼的十六進位制值的序列)。

如果攻擊者要傳輸包含單引號字元的字串,然後嘗試注入SQL程式碼,則構造的SQL語句將只顯示如下:

... WHERE hex_encode ( ... ) = '2720 ... '

 

27是單引號的ASCII程式碼(十六進位制),它與字串中的任何其他字元一樣只是十六進位制編碼。產生的SQL只能包含數字數字和字母af,從來沒有任何特殊字元,它可能會使SQL隱碼攻擊。

在PHP中轉義SQLi

使用預準備語句和引數化查詢。這些是由資料庫伺服器與任何引數分開傳送和解析的SQL語句。這樣攻擊者就無法注入惡意SQL。

你基本上有兩個選擇來實現這個目標:

  1. 使用PDO(適用於任何支援的資料庫驅動程式):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
    // do something with $row
}
  1. 使用MySQLi(用於MySQL):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // do something with $row
}

 

 

PDO是通用選項。如果您要連線到MySQL以外的資料庫,則可以引用特定於驅動程式的第二個選項(例如,對於PostgreSQL,請使用pg_prepare()和pg_execute())。

額外的防禦

除了採用四種主要防禦之一外,我們還建議採用所有這些額外的防禦措施,以便提供深度防禦。這些額外的防禦是:

  • 最低許可權
  • 白名單輸入驗證

最低許可權

為了最大限度地減少成功SQL隱碼攻擊的潛在損害,您應該最小化分配給環境中每個資料庫帳戶的許可權。不要為您的應用程式帳戶分配DBA或管理員型別訪問許可權。我們知道這很容易,當你這樣做時,一切都“有效”,但這是非常危險的。

從頭開始確定您的應用程式帳戶需要哪些訪問許可權,而不是試圖找出您需要帶走的訪問許可權。確保僅需要讀訪問許可權的帳戶才被授予對他們需要訪問的表的讀訪問許可權。

如果帳戶只需要訪問表的某些部分,請考慮建立一個檢視,以限制對該部分資料的訪問,併為帳戶分配帳戶訪問許可權,而不是基礎表。很少,如果有的話,授予對資料庫帳戶的建立或刪除訪問許可權。

如果您採用的策略是在任何地方使用儲存過程,並且不允許應用程式帳戶直接執行自己的查詢,那麼將這些帳戶限制為只能執行所需的儲存過程。不要直接向資料庫中的表授予任何許可權。

SQL隱碼攻擊不是對資料庫資料的唯一威脅。攻擊者可以簡單地將引數值從它們所呈現的合法值之一更改為未經授權的值,但應用程式本身可能被授權訪問。因此,儘量減少授予應用程式的許可權將降低此類未經授權的訪問嘗試的可能性,即使攻擊者沒有嘗試將SQL隱碼攻擊用作其漏洞利用的一部分。

在您使用它時,您應該最小化DBMS執行的作業系統帳戶的許可權。不要以root使用者身份或系統執行DBMS!大多數DBMS都是開箱即用的,具有非常強大的系統帳戶。例如,預設情況下,MySQL在Windows上作為系統執行!使用受限制的許可權將DBMS的OS帳戶更改為更合適的帳戶。

多個DB使用者

Web應用程式的設計者不僅應避免在Web應用程式中使用相同的所有者/管理員帳戶來連線到資料庫。不同的DB使用者可以用於不同的Web應用程式。

通常,需要訪問資料庫的每個單獨的Web應用程式都可以具有指定的資料庫使用者帳戶,Web應用程式將使用該帳戶連線到資料庫。這樣,應用程式的設計者可以在訪問控制中具有良好的粒度,從而儘可能地減少特權。然後,每個資料庫使用者都可以選擇訪問它所需的內容,並根據需要進行寫訪問。

例如,登入頁面需要對錶的使用者名稱和密碼欄位進行讀訪問,但不能對任何表單進行寫訪問(無插入,更新或刪除)。但是,註冊頁面當然需要對該表的插入許可權; 只有當這些Web應用程式使用不同的DB使用者連線到資料庫時,才能強制執行此限制。

檢視

通過限制對錶的特定欄位或表的連線的讀訪問,可以使用SQL檢視進一步增加訪問的粒度。它可能具有額外的好處:例如,假設系統需要(可能由於某些特定的法律要求)來儲存使用者的密碼,而不是鹽漬的密碼。

設計師可以使用檢視來彌補這種限制; 撤消對錶的所有訪問(來自除所有者/管理員之外的所有資料庫使用者)並建立一個輸出密碼欄位的雜湊而不是欄位本身的檢視。任何成功竊取資料庫資訊的SQL隱碼攻擊都將被限制為竊取密碼的雜湊值(甚至可能是鍵控雜湊值),因為任何Web應用程式的資料庫使用者都無權訪問表本身。

 

 

相關文章