何謂資料隱碼(SQL injection)攻擊?程式設計師應如何預防?

這個sql injection問題是出自經濟部所屬事業機構 100年新進職員甄試試題,資訊管理、程式設計(pdf)的一題,這個問題ㄚ琪其實在設計PHP程式,搭配資料庫所設計的應用程式,都會面臨到這樣的難題,因為ㄚ琪本身對安全概念倒是沒有那麼的完整,所以有些程式為了求快,常會捨安全而不顧,不過如果你在開發一個重要的程式時,應該要謹慎這方面的問題,好了,我們先從中文維基的解釋來看吧。

SQL攻擊(SQL injection,中國大陸稱作SQL注入攻击,台灣稱作SQL资料隐码攻击),簡稱隱碼攻擊,是發生於應用程式之資料庫層的安全漏洞。簡而言之,是在輸入的字串之中夾帶SQL指令,在設計不良的程式當中忽略了檢查,那麼這些夾帶進去的指令就會被資料庫伺服器誤認為是正常的SQL指令而執行,因此遭到破壞。

有部份人認為SQL隱碼攻擊是只針對Microsoft SQL Server而來,但只要是支援批次處理SQL指令的資料庫伺服器,都有可能受到此種手法的攻擊。

原因

在應用程式中若有下列狀況,則可能應用程式正暴露在SQL Injection的高風險情況下:

  1. 在應用程式中使用字串聯結方式組合SQL指令。
  2. 在應用程式連結資料庫時使用權限過大的帳戶(例如很多開發人員都喜歡用sa(內建的最高權限的系統管理員帳戶)連接Microsoft SQL Server資料庫)。
  3. 在資料庫中開放了不必要但權力過大的功能(例如在Microsoft SQL Server資料庫中的xp_cmdshell延伸預存程式或是OLE Automation預存程式等)
  4. 太過於信任使用者所輸入的資料,未限制輸入的字元數,以及未對使用者輸入的資料做潛在指令的檢查。

作用原理

  1. SQL命令可查詢、插入、更新、刪除等,命令的串接。而以分號字元為不同命令的區別。(原本的作用是用於SubQuery或作為查詢、插入、更新、刪除……等的條件式)
  2. SQL命令對於傳入的字串參數是用單引號字元所包起來。〈但連續2個單引號字元,在SQL資料庫中,則視為字串中的一個單引號字元〉
  3. SQL命令中,可以夾帶註解〈連續2個減號字元——後的文字為註解,或「/*」與「*/」所包起來的文字為註解〉
  4. 因此,如果在組合SQL的命令字串時,未針對單引號字元作取代處理的話,將導致該字元變數在填入命令字串時,被惡意竄改原本的SQL語法的作用。

例子

某個網站的登入驗證的SQL查詢代碼為

strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"

惡意填入

userName = "' OR '1'='1";

passWord = "' OR '1'='1";

時,將導致原本的SQL字串被填為

strSQL = "SELECT * FROM users WHERE (name = '' OR '1'='1') and (pw = '' OR '1'='1');"

也就是實際上執行的SQL命令會變成下面這樣的

strSQL = "SELECT * FROM users;"

因此達到無帳號密碼,亦可登入網站。所以SQL隱碼攻擊被俗稱為駭客的填空遊戲。

sql injection可能造成的傷害

  1. 資料表中的資料外洩,例如個人機密資料,帳戶資料,密碼等。
  2. 資料結構被駭客探知,得以做進一步攻擊(例如SELECT * FROM sys.tables)。
  3. 資料庫伺服器被攻擊,系統管理員帳戶被竄改(例如ALTER LOGIN sa WITH PASSWORD=’xxxxxx’)。
  4. 取得系統較高權限後,有可能得以在網頁加入惡意連結以及XSS。
  5. 經由資料庫伺服器提供的作業系統支援,讓駭客得以修改或控制作業系統(例如xp_cmdshell “net stop iisadmin”可停止伺服器的IIS服務)。
  6. 破壞硬碟資料,癱瘓全系統(例如xp_cmdshell “FORMAT C:”)。

如何預防 SQL Injection 的攻擊

  1. 在設計應用程式時,完全使用參數化查詢(Parameterized Query)來設計資料存取功能。
  2. 在組合SQL字串時,先針對所傳入的參數作字元取代(將單引號字元取代為連續2個單引號字元)。
  3. 如果使用PHP開發網頁程式的話,亦可開啟PHP的魔術引號(Magic quote)功能(自動將所有的網頁傳入參數,將單引號字元取代為連續2個單引號字元)。
  4. 其他,使用其他更安全的方式連接SQL資料庫。例如已修正過SQL資料隱碼問題的資料庫連接元件,例如ASP.NET的SqlDataSource物件或是 LINQ to SQL。
  5. 使用SQL防資料隱碼系統。
  6. 應用系統中存取資料庫時,應明確定義存取資料庫的使用者權限
  7. 加強對用戶輸入資料的檢核與驗證
  8. 使用專業的程式碼弱點掃描工具來尋找應用系統所隱含的漏洞

但是在英文的維基中,有幾個章節中文維基中沒有,像是Forms and validity,ㄚ琪也不太懂在這裡的意思,但是從內容來看比較像是資料隱碼的分類說明,我們另闢一篇文章來說明。

這裡頭有提到原因、作用原理、可能造成的傷害以及避免的方法。但是ㄚ琪覺得對一個新手程式設計師而言,有一個協助自動檢查程式的機制或許會更好,不然誰會知道自己寫的程式沒問題,自我感覺良好的人,一定覺得沒問題吧,呵呵,就像我這樣。

查到PHP+MySQL环境下SQL Injection攻防总结,似乎是ㄚ琪開始注意寫PHP程式時,要注意SQL injection了,共勉之,另外這個部落格也有MySQL的最佳化,很可以看喔。

JDBC如何防範SQL隱碼攻擊(SQL Injection)

接下來我們引用恆逸教育訓練中心的範例來說明Java的程式如何防範sql injection,網址在https://www.uuu.com.tw/Public/content/article/20/20201221.htm

在MySQL伺服器建立了測試用的資料庫,且在其中建立users資料表格,並新增了兩筆測試用的使用者資料:

先示範一個無法防止SQL隱碼攻擊的測試程式如下:

package uuu.tips.test;
    import java.sql.*;
    import java.util.Scanner;
    import java.util.logging.*;

    public class TestUserLogin {
        private static final Logger tipsLogger = Logger.getLogger("UCOM Tips文章");
        public static void main(String[] args) {	
            String id, password;
            Scanner scanner = new Scanner(System.in);
            System.out.println("請輸入帳號:"); //John
            id = scanner.next();
            System.out.println("請輸入密碼:"); //asdf1234 
            password = scanner.next();

            final String sql = "SELECT id, password, name, gender, birthday, email " 
                    + "FROM users WHERE id='"
                    +id+"' AND password='"+password+"'";
            //資料庫元件皆使用try-with-resource語法確保在程式結束時會正常close
            try (	Connection connection = RDBConnection.getConnection();//2.建立連線
                    Statement stmt = connection.createStatement();//3.建立指令
                    ResultSet rs = stmt.executeQuery(sql); //4.執行指令
                ){
                //5.處理rs
                int i = 0;
                while(rs.next()) {		
                    System.out.println("登入"+ ++i +"位成功");
                    System.out.print("帳號: " + rs.getString("id"));
                    System.out.print("姓名: " + rs.getString("name"));
                    System.out.print("性別: " + rs.getString("gender"));
                    System.out.print("生日: " + rs.getString("birthday"));
                    System.out.println("Email: " + rs.getString("email"));
                }
                if(i<1)System.out.println("登入失敗,帳號或密碼不正確");
            } catch (SQLException e) {				
                tipsLogger.log(Level.SEVERE, "登入失敗", e);
            } catch (Exception e) {
                tipsLogger.log(Level.SEVERE, "登入功能發生錯誤", e);
            }			
        }
    }

第一次測試時使用正常的測試資料(帳號輸入John,密碼則輸入asdf1234)。結果如下:

第二次測試使用正常但錯誤的資料(帳號輸入john,密碼則輸入aaaa1234)。結果如下:

這次測試時使用特殊的資料(帳號輸入’OR’1’=’1,密碼則輸入’OR’1’=’1)。這看起來一定會發生登入失敗的帳號密碼,卻得到如下的結果:

因為當程式把這樣的輸入資料加入原來的SQL指令時,就讓整個WHERE子句變成一個恆真式,所以全部的使用者資料都被查出來了:

要避免這樣的攻擊方式,只要將SQL指令與使用者輸入的資料藉由PreparedStatement來分開處理即可。程式改寫如下:

package uuu.mod19.test;
    import java.sql.*;
    import java.util.Scanner;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    public class TestCustomerLogin2 {
        private static final Logger tipsLogger = Logger.getLogger("UCOM Tips文章");
        public static void main(String[] args) {	
            String id, password;
            Scanner scanner = new Scanner(System.in);
            System.out.println("請輸入帳號:"); //John
            id = scanner.next();
            System.out.println("請輸入密碼:"); //asdf1234 
            password = scanner.next();

            final String sql = "SELECT id, password, name, gender, birthday, email " 
                    + "FROM users WHERE id=? AND password=?";
            
            try (	Connection connection = RDBConnection.getConnection(); //2.建立連線
                    PreparedStatement pstmt = 
                                connection.prepareStatement(sql); //3.準備指令
                ){ 
                //執行前才傳入?對應的輸入值
                pstmt.setString(1, id);
                pstmt.setString(2, password);
                
                //4.執行指令並取回ResultSet rs。使用try-with-resource語法確保rs.close()
                try(ResultSet rs = pstmt.executeQuery();){ 
                    //5. 處理rs
                    int i = 0;
                    while(rs.next()) {		
                        System.out.println("登入"+ ++i +"位成功");
                        System.out.print("帳號: " + rs.getString("id"));
                        System.out.print("姓名: " + rs.getString("name"));
                        System.out.print("性別: " + rs.getString("gender"));
                        System.out.print("生日: " + rs.getString("birthday"));
                        System.out.println("Email: " + rs.getString("email"));
                    }
                    if(i<1)System.out.println("登入失敗,帳號或密碼不正確");
                }
            } catch (SQLException e) {				
                tipsLogger.log(Level.SEVERE, "登入失敗", e);
            } catch (Exception e) {
                tipsLogger.log(Level.SEVERE, "登入功能發生錯誤", e);
            }
        }
    }

第一次測試時使用正常的測試資料(帳號輸入John,密碼則輸入asdf1234)。結果如下:

第二次測試使用正常但錯誤的資料(帳號輸入john,密碼則輸入aaaa1234)。結果如下:

這次也用特殊資料(帳號輸入’OR’1’=’1,密碼則輸入’OR’1’=’1)。在PreparedStatement與SQL字串中用「?」暫時替代使用者輸入的資料,在執行前才傳入「?」的值,得到如下正確的結果:

如此一來,就可以確實防範SQL隱碼攻擊了。

另外提醒一下,如果你有在用Hostinger虛擬主機的話,他們採用的SQL是MariaDB,只不過在hPanel上還是會顯示MySQL,所以在操作SQL要注意一下。

※2022/05/08

分享一個sql injection攻擊的範例:《[ASP.NET] SQL Injection 攻擊範例》

延伸閱讀-更多高CP值的資訊類文章在這邊:

資料庫正規化
只有中壢人不知道的大型資料庫管理系統 MySQL 5 必考字串型態
C# List 定義及七種常用方法
MySQL 註解語法
MySQL 流程控制的迴圈
用G Suite協作平台Google Sites打造專業的賺錢網站在家工作

感謝你看到這裡,很快就可以離開了,但最好的獎勵行動就是按一下幫我分享或留言,感恩喔~

點我分享到Facebook

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *