從2023 OWASP掃出的TOP10攻擊手法中Injection排名來到第三名,而且還是跟XSS攻擊合併才來到第三名的位置。

看來是大家都已經知道怎麼防範了XD,但SQL injection還是長年在OWASP的攻擊排行TOP10,還是個值得關注的攻擊手法。

What is SQL injection?

SQL injection俗稱駭客的填字遊戲,來看看它在OWASP Top Ten 2023 – The Complete Guide是怎麼定義的?

Injection attacks exploit vulnerabilities in input validation and inadequate data handling. Attackers inject data such as SQL queries, code snippets, or commands into web application forms or URLs. They allow adversaries to access sensitive data and manipulate an application’s behavior.

  • 意味著靠非法的輸入程式片段到資料處理程序中,進而取得敏感訊息或使用不改擁有的權限操作應用程式。 讓我們用更簡單直接的方式來說明如何預防SQL injection:

    不要相信使用者任何輸入的東西!

SQL injection 攻擊手法

根據零基礎資安系列(四)-認識注入攻擊(Injection Attack所提及有兩大攻擊手法:

  1. Blindfolded Injection 可以用程式腳本來達成無差別進行攻擊,先寫好各種不同的injection攻擊範例,然後去Try各個網站
  2. Data Analysis then injection 先分析網站弱點再進行攻擊

而且綜合使用效果絕佳,利用Open Source 的弱點掃描工具對許多目標網站進行掃描,再看哪個網站最容易攻擊成功,再搭配上面兩個攻擊手法~

SQL injection 攻擊方式

正常的登入

既然是SQL injection,那一定得用SQL語法來進行進攻,SQL簡單來說就是跟資料庫溝通的語言,像我今天可能要登入校務系統撈取學生資料,我就會先透過網站進行登入,這個網站需要輸入信箱與密碼,借一下巴哈姆特電玩資訊站的登入畫面示範XD,我們在第一排帳號欄位輸入andy@gmail.com,第二排密碼欄位輸入123,假設帳號密碼皆正確,就代表登入成功!

在程式的背後是用以下SQL語句透過網頁伺服器向資料庫請求學生資料

SELECT * from students where student_email='andy@gmail.com' and password="123"
  • 他的意思代表:我要從系統中取得學生信箱為andy@gmail.com與密碼為123的學生全部個人資料。

有沒有發現端倪,他把第一個欄位填入的資料帶入到student_email的等式中,並把密碼帶入到password的等式中。看起來很簡單直關的語句,那駭客要怎麼透過SQL injection進行攻擊呢,他只要設法將SQL語句變成下面這樣:

SQL injection的登入

SELECT * from students where student_email='' or 1=1 --' and password="123"

他的意思代表:我要取得系統中所有學生的個人資料。

要完成SQL injection攻擊非常簡單,駭客只在第一排帳號欄位輸入’ or 1=1–’,第二排密碼欄位輸入123,所以在登入頁面的輸入就改成下圖這樣。

如果伺服器端都沒有實作SQL injection防禦,就真的出事了,甚至連andy@gmail.com都不用輸入就可以完成一次SQL injection。

SQL injection的登入攻擊原理解說

來解釋一下駭客做了什麼,他讓學生姓名的查詢變成空值,有些SQL會擋空值,但這不要緊,他只是要讓student_name=‘‘變成完整的語句,重點是在後頭 or 1=1–or 1=1– 表示或者等於1=1,1=1 這個等式小學生都知道必定正確,1=1 直接把where條件語句廢掉, - - 代表把後面的密碼查詢語句用註解的方式來廢掉。駭客更可以用這個登入頁漏洞玩出各種不同的SQL攻擊花樣,像是撈出所有的用戶當初註冊網站所使用的重要資料(密碼、身分證字號、手機、住址等…)。

SQL查詢漏洞只是小case,如果是SQL寫入資料的操作漏洞,那可就緊張囉老哥。像是巴哈姆特的發文功能,更可以直接冒充其他會員的身份進行發文。軟體工程師要怎麼避免被SQL injection呢?讓我們繼續看下去。

SQL injection 防禦方式

避免 SQL 注入 這網站有提到如何避免SQL injection,我們來看看他怎麼說:

  1. 嚴格限制 Web 應用的資料庫的操作許可權,給此使用者提供僅僅能夠滿足其工作的最低許可權,從而最大限度的減少注入攻擊對資料庫的危害。

  2. 檢查輸入的資料是否具有所期望的資料格式,嚴格限制變數的型別,例如使用 regexp 套件進行一些匹配處理,或者使用 strconv 套件對字串轉化成其他基本型別的資料進行判斷。

  3. 對進入資料庫的特殊字元(’"\尖括號&*;等)進行轉義處理,或編碼轉換。Go 的text/template套件裡面的 HTMLEscapeString 函式可以對字串進行轉義處理。

  4. 所有的查詢語句建議使用資料庫提供的參數化查詢介面,參數化的語句使用參數而不是將使用者輸入變數嵌入到 SQL 語句中,即不要直接拼接 SQL 語句。例如使用database/sql裡面的查詢函式 Prepare 和Query,或者Exec(query string, args …interface{})。

  5. 在應用釋出之前建議使用專業的 SQL 注入檢測工具進行檢測,以及時修補被發現的 SQL 注入漏洞。網上有很多這方面的開源工具,例如 sqlmap、SQLninja 等。

  6. 避免網站顯示出 SQL 錯誤資訊,比如型別錯誤、欄位不匹配等,把程式碼裡的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤資訊進行 SQL 注入。

恩恩,他網站只有提到理論,讓我們17來一步一步實作~

SQL injection 防禦實作

1. 嚴格限制 Web 應用的資料庫的操作許可權,給此使用者提供僅僅能夠滿足其工作的最低許可權,從而最大限度的減少注入攻擊對資料庫的危害。

以MySQL為例子,MySQL可以用command line登入,-u為user,-p為password,當你輸入玩-p後他會再另外要求你打密碼,root預設密碼好像是’ ‘或是’root’。

mysql -u root -p

之後你可以對現有的帳號進行給予權限,向我這邊有一個username叫andy,我給予她我資料庫的所有權限

GRANT ALL PRIVILEGES ON `your_database`.* TO `andy`@`%`   

但如果他只需要查詢的功能,我可以只給他讀取的權限

GRANT SELECT ON your_database.* TO 'andy'@'%';

這樣就算初步的完成Database權限控管了。

2.檢查輸入的資料是否具有所期望的資料格式,嚴格限制變數的型別,例如使用 regexp 套件進行一些匹配處理,或者使用 strconv 套件對字串轉化成其他基本型別的資料進行判斷。

  • regex套件 他的意思是指定使用正規表達式來驗證使用者所輸入的東西,再golang中一般都會搭配validator套件來使用

使用validator+regex來過濾輸入的文字

  • example/main.go 如果是HTTP server先綁定一個驗證器,當他每次發送request時都要先經過我的middleware並驗證
import (
"github.com/go-playground/validator/v10"
"github.com/gin-gonic/gin/binding"
)
// 註冊Validator Func
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
	v.RegisterValidation("userpasd", middleware.UserPasd)
}
  • example/app/middleware/validator.go middleware意味著在真正進入商業邏輯前我要對他進行request資料是否有誤。
package middleware

import (
	"github.com/go-playground/validator/v10"
	"regexp"
)

func UserPasd(field validator.FieldLevel) bool {
	// 右邊表達式為開頭必為大寫且長度最小為五最大為十:^[A-Z]\w{4,10}$
	// 下面表達式為
	if match, _ := regexp.MatchString(`^[a-zA-Z0-9_!@]+$`, field.Field().String()); match {
		return true
	}
	return false
}

我後來自己發明了很屌的專門防sql injection的正規表達式,這東西把可能發生sql injection的字串跟註解行為都列進去,這東西只可意會不可褻完焉

^(.*(?:\b[Aa][Nn][Dd]\b|\b[Oo][Rr]\b|\b[Ss][Ee][Ll][Ee][Cc][Tt]\b|\b[Ww][Hh][Ee][Rr][Ee]\b|\b[Hh][Aa][Vv][Ii][Nn][Gg]\b|\b[Uu][Nn][Ii][Oo][Nn]\b)|.*\d+=\d+|.*[a-zA-Z]+=[a-zA-Z]+|.*--|\s)+.*$

總之他過濾了:

  1. SQL中的關鍵字,包括 AND、OR、SELECT、WHERE、HAVING、UNION,不區分大小寫,該方法禁止掉了SQL敏感關鍵字。
  2. *\d+=\d+:這部分匹配了形如數字=數字 的模式,表示一個等式,該方法禁止掉了數字等式。
  3. *[a-zA-Z]+=[a-zA-Z]+:這部分匹配了形如 字母=字母 的模式,該方法禁止掉了字母等式。
  4. .*–:這部分匹配了SQL中的註釋,該方法禁止掉了註解。
  5. \s:這部分匹配了空白字符,該方法禁止掉了空白鍵輸入。
  • example/app/models/sql_injection_model.go

要驗證request傳進來的哪個欄位就在此定義,向我要驗證Name,我就在Name中綁定剛剛寫的的userpasd驗證。

type SQLinjectionStudent struct {
	Id             int    `json:"Id" gorm:"primaryKey;uniqueIndex;autoIncrement;column:id"`
	Name           string `json:"Name" binding:"required,userpasd"`
}
  • 以下為檔案結構
.
├── README.md
├── go.mod
├── go.sum
├── app
│   ├── models
│   │   └── sql_injection_model.go
│   ├── middleware
│   │   └── validator.go
│   ├── controller
│   └── services
└── main.go
  • strconv套件 以最簡單的例子來說,當request中有"–“之類的奇怪字元,又剛好我們程式中輸入只接收純數字,我們就可以把字串轉為數字型別,使用 strconv.Atoi(userIDInput) 來驗證字串能否轉換為整數,如果不成功就可以合理推斷他可能帶有惡意字串。
import (
    "database/sql"
    "fmt"
    "strconv"
)

func getUserByID(userIDInput string, db *sql.DB) (string, error) {
    // 將用戶輸入的字串轉換為整數
    userID, err := strconv.Atoi(userIDInput)
    if err != nil {
        // 如果無法轉換為整數,返回錯誤
        return "", fmt.Errorf("Invalid user ID: %s", userIDInput)
    }

    // 使用參數化查詢,防止 SQL 注入攻擊
    query := "SELECT username FROM users WHERE id = ?"
    var username string
    err = db.QueryRow(query, userID).Scan(&username)
    if err != nil {
        return "", err
    }
    return username, nil
}

3. 對進入資料庫的特殊字元(’"\尖括號&*;等)進行轉義處理,或編碼轉換。Go 的text/template套件裡面的 HTMLEscapeString 函式可以對字串進行轉義處理。

總之讓輸 入只能有大小寫英文、數字、驚嘆號、底線、小老鼠。特殊符號包括空白鍵都不能有。

// OWASP ZAP SQLinjection Testing
	userApi.POST("/SQLinJSON", func(c *gin.Context) {
		var specStudents model.SQLinjectionStudent
		var students []model.Student
		if err := c.ShouldBindJSON(&specStudents); err != nil {
			c.JSON(http.StatusNotAcceptable, err.Error())
			return
		}
		// 對輸入進行 HTML 轉義
		escapedName := template.HTMLEscapeString(specStudents.Name)
		fmt.Println(escapedName)
		database.DB.Where("name = ?", escapedName).Find(&students)

		c.JSON(http.StatusOK,gin.H{
			"students":students,
			},
		)
	})
  • 使用**template.HTMLEscapeString可防止HTML相關的攻擊,在SQLinjection中主要會阻擋”與’,以下是他實際會阻擋的內容。**
  • < 轉義為 &lt;
  • > 轉義為 &gt;
  • & 轉義為 &amp;
  • " 轉義為 &quot;
  • ' 轉義為 &#39;
  • 主要透過下面這段來達成的。
import "html/template"
// 對輸入進行 HTML 轉義
escapedName := template.HTMLEscapeString(specStudents.Name)
fmt.Println(escapedName)

4. 所有的查詢語句建議使用資料庫提供的參數化查詢介面,參數化的語句使用參數而不是將使用者輸入變數嵌入到 SQL 語句中,即不要直接拼接 SQL 語句。例如使用database/sql裡面的查詢函式 Prepare 和Query,或者Exec(query string, args …interface{})。

最經典的就是古早的PHP用法。

query := "SELECT COUNT(*) FROM users WHERE username='" + username + "' AND password='" +  password + "'"

在golang可以用 ? 的方式將參數帶入,這樣就能用參數來防sql injection。

func login(username string, password string, db *sql.DB) bool {
    // 使用參數化查詢,將參數作為傳遞給 SQL 語句的參數
    query := "SELECT COUNT(*) FROM users WHERE username=? AND password=?"
    var count int
    err := db.QueryRow(query, username, password).Scan(&count)
    if err != nil {
        // 處理錯誤
        return false
    }
    return count > 0
}

5. 在應用釋出之前建議使用專業的 SQL 注入檢測工具進行檢測,以及時修補被發現的 SQL 注入漏洞。網上有很多這方面的開源工具,例如 sqlmap、SQLninja 等。

這項目最經典的就是OWASP,它內建已經寫好的駭客行為腳本,你只要用他的腳本跑,他就會幫你模擬駭客進行攻擊,包刮sql injection,而且他會嘗試各種不同的sql injection攻擊,產品上限前進行滲透測試是相當重要的一環。

6. 避免網站顯示出 SQL 錯誤資訊,比如型別錯誤、欄位不匹配等,把程式碼裡的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤資訊進行 SQL 注入。

這個是很多程式設計師會懶得做,但相當重要的一環,千萬不要把錯誤完整的展現給使用者,因為這可能會透漏你資料庫哪邊有漏洞可以進行攻擊,你可以另外自訂客製化錯誤碼,再回傳給前端,讓駭客無法根據錯誤碼進行攻擊。

重點回顧

  1. SQL injection是什麼?
  2. SQL injection如何攻擊。
  3. SQL injection如何防禦。

總結

永遠不要相信使用者輸入的任何東西,永遠也不要相信前端傳過來的東西,更不要相信惹女朋友生氣後,她說沒有生氣,不是真的沒生氣…((誤,總之後端工程師能信的只有自己!