Web 安全漏洞之 SQL 注入

ThinkJS發表於2018-10-29

什麼是 SQL 注入

魯迅

“有人的地方就有江湖,有資料庫存在的地方就可能存在 SQL 注入漏洞。”

在所有漏洞型別中,SQL 注入可是說是危害最大最受大家關注的漏洞。簡單說來,SQL 注入是通過在使用者可控引數中注入SQL語法,破壞原有SQL結構,達到編寫程式時意料之外結果的攻擊行為。還是以 ThinkJS 為例,假設我們寫了如下一個介面(實際情況肯定不會這麼寫的):

// user.js
module.exports = class extends think.Controller {
  async loginAction() {
    const { username, password } = this.post();
    const user = await this.model().query(
      `SELECT * FROM user WHERE name = "${username}" AND password= "${password}"`
    );

    if (think.isEmpty(user)) {
      return this.fail();
    }
    return this.success(user);
  }
}
複製程式碼

當使用者提交的 usernameadmin"; -- 的話,最終執行的 SQL 語句就會變成

SELECT * FROM user WHERE name = "admin"; --" AND password= "111"
複製程式碼

最終攻擊者就可以成功登入 admin 賬號了,這就是最簡單的 SQL 注入了。從上面這個簡單示例中,我們發現漏洞成因可以歸結為以下兩個原因疊加造成的:

  1. 程式編寫者在處理應用程式和資料庫互動時,使用字串拼接的方式構造SQL語句。
  2. 未對使用者可控引數進行足夠的過濾便將引數內容拼接進入到SQL語句中。

SQL隱碼攻擊根據攻擊者獲取資料的方式分為回顯注入報錯注入以及盲注。剛才演示的直接從返回結果中獲取資料則為回顯注入,當然也可以通過 MySQL 執行的報錯結果中嗅探到資料庫的結構和內容,這就是報錯注入。盲注則是根據資料庫執行的延時等操作來判斷是否接近正確值,簡單的說來有點像是拿著聽診器試探保險箱的密碼的感覺。

不同的分類原則會有不同的分類,也有按照注入位置及方式不同進行分類分為POST注入GET注入cookie注入盲注延時注入搜尋注入base64注入等。不過大家都支援分類形式不同,原理還是一致的,這裡就不一一細說了。

SQL 注入的危害

如果網站存在 SQL 注入漏洞,相當於將資料庫直接暴露在攻擊者面前,可想而知危害會有多大了。攻擊者利用 SQL 注入漏洞能實現以下攻擊:

  1. 跳過賬戶許可權驗證達到越權
  2. 獲取資料庫關鍵資訊從而進行脫庫
  3. 在特別情況下還可以修改資料庫內容或者插入內容到資料庫,如果資料庫許可權分配存在問題,或者資料庫本身存在缺陷,那麼攻擊者可以通過SQL隱碼攻擊漏洞直接獲取webshell或者伺服器系統許可權。

防禦方法

資料校驗

從文章開頭可以看到,其實漏洞的主要原因還是沒有對使用者輸入的資料進行過濾,所以對來自使用者的資料(GET, POST, cookie 等)最好做到以下兩種過濾校驗

  1. 檢查輸入的資料是否具有所期望的資料格式。這種在引數是數字的時候特別有效,如果攻擊者選擇在引數中插入內容的話則會被轉換成 NaN 導致攻擊失敗。在 ThinkJS 中我們提供了強大的 Logic 功能可以方便的對資料進行格式校驗。
  2. 使用資料庫特定的敏感字元轉義函式把使用者提交上來的非數字資料進行轉義。在 ThinkJS 中封裝了 escapeString() 方法可以對敏感字元進行轉義,其原理則和 PHP 的 mysql_escape_string() 方法是一致的。

檢查輸入資料格式在 ThinkJS 中還能防止另外一種非通用 SQL 安全問題。文章開頭的示例程式碼我們在實際的應用中一般會這麼寫:

// user.js
module.exports = class extends think.Controller {
  async loginAction() {
    const { username, password } = this.post();
    const user = await this.model('user').where({
      name: username,
      password
    }).find();

    if (think.isEmpty(user)) {
      return this.fail();
    }
    return this.success(user);
  }
}
複製程式碼

當我們構造如 name=admin&password[]=!%3D&password[]= 的請求引數時,最終執行的 Model 語句就會變成

this.model('user').where({name: 'admin', password: ['!=', '']});
複製程式碼

由於 HTTP 請求的自動合併陣列的特性造成了我們的 SQL 語句並非是我們想要的效果。雖然說框架本身已經針對這種情況進行了處理,當使用者輸入引數被認為是 SQL 運算子時則會將關鍵字增加空格,從而將其變成普通字串避免這個問題。不過這種方法會有 一定的損傷,畢竟當真的要傳這幾個運算子的情況的時候接收到的資料和請求的不一樣還是有點懵逼的。所以最好還是在 Logic 層對資料進行完善的校驗將問題前置比較好。

除了資料校驗,也可以選擇使用資料庫的儲存過程和預定義指標等特性來抽象數庫訪問,使使用者不能直接訪問資料表和檢視。但這個辦法又有別的影響。

via: SQL隱碼攻擊

許可權限制

嚴格限制Web應用的資料庫的操作許可權,給此使用者提供僅僅能夠滿足其工作的最低許可權,從而最大限度的減少注入攻擊對資料庫的危害。**請記住永遠不要使用超級使用者或所有者帳號去連線資料庫!**當資料庫被攻擊時將損傷限制在當前表的範圍是比較明智的選擇。通過許可權限制可以防止攻擊者獲取資料庫其它資訊,甚至利用資料庫執行 Shell 命令等操作。

日誌處理

當資料庫操作失敗的時候,儘量不要將原始錯誤日誌返回,比如型別錯誤、欄位不匹配等,把程式碼裡的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤資訊進行 SQL 注入。除此之外,在允許的情況下,使用程式碼或資料庫系統儲存查詢日誌也是一個好辦法。顯然,日誌並不能防止任何攻擊,但定期審計資料庫執行日誌可以跟蹤是否存在應用程式正常邏輯之外的 SQL 語句執行。日誌本身沒用,要查閱其中包含的資訊才行。畢竟,更多的資訊總比沒有要好。

後記

綜上所說之後,大家可能覺得 SQL 資料校驗會比較麻煩,其實在 ThinkJS 中已經將關鍵字處理類的方法已經整合,使用程式提供的 ORM 方法進行 SQL 構造會比自己寫 SQL 語句拼接來的更方便,同時也能提高專案程式碼複用,減少潛在的風險。如果對 ThinkJS 預設的 think-model 不喜歡的話,也可以使用其它第三方的 ORM 框架,例如 think-sequelize

參考資料: