OpenResty學習筆記03:再探WAF

2023-05-10 18:00:22

一. 再談WAF

 
我們上一篇安裝的WAF來自另一位技術大神 趙舜東,花名 趙班長,一直從事自動化運維方面的架構設計工作。阿里雲MVP、華為雲MVP、中國SaltStack使用者組發起人 、新運維社群發起人。
雖然並非安全專業出身,但根據他的自述,邊學邊寫,只用了幾天的時間就將WAF寫出來了,並於2016年正式釋出到GitHub。
趙班長的WAF參考了Kindle大神寫的 ngx_lua_waf ,另一款基於 OpenResty 的非常優秀的開源WAF。
不得不向 章亦春(agentzh)、趙舜東(趙班長)和 Kindle 等所有開源前輩們致以最高的敬意,讓我等能有這麼多的學習和實踐資源,respect!
 

二. 瞭解擼啊(Lua)

 
在真正走近WAF之前,還是有必要對 Lua 這個指令碼語言進行一番瞭解,因為所有的業務邏輯都是基於 Lua 實現的。
推薦兩個關於Lua的學習資源:

  1. Lua基礎教學與實踐
  2. 菜鳥Lua教學
     

順便分享本人正在使用的Lua偵錯工具:網路硬碟連結
解壓後其中有兩個檔案:

  1. LuaForWindows_v5.1.4-46.exe:適用於windows環境的Lua編譯器。
  2. lua51.stx:適用於EditPlus環境的Lua語法高亮模板檔案。
     
    設定完成後,在EditPlus中進行Lua的編寫和測試還是很方便的:

     

三. 設定入口

 
我們回過頭,再來看看上一篇Nginx的組態檔中關於WAF參照的幾行關鍵程式碼:

lua_shared_dict    limit 50m;
lua_package_path   "/home/my/tools/waflib/?.lua";
init_by_lua_file   "/home/my/tools/waflib/init.lua";
access_by_lua_file "/home/my/tools/waflib/access.lua";

 
這四個指令都來自 lua-nginx-module,用來實現對 Nginx 的每個 Worker 執行緒中 Lua 環境的設定和定義。

1. lua_shared_dict

作用:宣告一個共用記憶體區域
層級:http
格式:lua_shared_dict <name> <size>

由於各 Worker 執行緒中的 Lua 環境相對是獨立的,無法共用資料,但在很多情況下,需要在不同的 Worker 之間共用資料,此時就可以通過該指令進行宣告。
此例中的 limit 就是宣告出來的一個共用變數,在 WAF 中的作用就是在 CC 攻擊檢測時判斷當前使用者的請求頻次是否超限進行攔截。

2. lua_package_path

作用:設定 Lua 指令碼檔案的搜尋路徑。;; 表示原始搜尋路徑。
層級:http
格式:lua_package_path <lua-style-path-str>

官網描述:設定由 set_by_Lua、content_by_Lua 等指定的指令碼使用的 Lua 模組搜尋路徑,它的預設值是LUA_PATH環境變數的內容或LUA的編譯預設值。
個人理解:在 Lua 中參照其他檔案時,比如 require 'config',就會將 config 替換掉路徑中的問號並嘗試查詢這個檔案並引入進來。
設定搜尋路徑引數時,還可以使用 $prefix 變數來表示當前 Nginx 的工作目錄,該目錄一般在啟動時通過 -p PATH 引數進行定義。
如果有多個搜尋路徑,可用 ; 分割。

3. init_by_lua_file

作用:指定初始化組態檔。
層級:http
格式:init_by_lua_file <path-to-lua-script-file>

個人理解:該初始化組態檔僅在 Nginx 啟動時執行一次,主要用於對全域性變數進行預載入或預處理。
但經過實踐,在對本篇的 WAF 進行設定時,刪除該指令並未產生任何影響。

4. access_by_lua_file

作用:請求存取階段處理。
層級:http, server, location, location if
格式:access_by_lua_file <path-to-lua-script-file>

個人理解:每個請求接入時,都會經過該檔案的處理,其作用類似於前置攔截器。
本篇的 WAF 在對請求進行檢查時,主要就是從這個檔案切入的。
 

四. 官方檔案支援

 
lua-nginx-module 模組的官方檔案:https://github.com/openresty/lua-nginx-module
 
其中最重要的兩個部分:

  1. 指令(Directives):https://github.com/openresty/lua-nginx-module#directives
  2. API(Nginx API for Lua):https://github.com/openresty/lua-nginx-module#nginx-api-for-lua
     
    在學習 OpenResty 之前,還是需要對指令和API進行詳細瞭解的。
     

五. 檔案說明

 
再來看一下 waflib 的目錄清單:

 

  1. access.lua:請求入口檔案;
  2. config.lua:WAF詳細組態檔;
  3. init.lua:WAF初始化檔案,包含IP白名單、黑名單、URL注入、CC攻擊等各種檢測函數;
  4. lib.lua:WAF函數庫檔案,包含獲取IP、獲取規則、寫入紀錄檔等各種通用函數;
  5. resty:一個來自 /usr/local/openresty/lualib/resty/ 的軟連線;
  6. rule-config:存放規則檔案目錄。
     

再進入到 rule-config 目錄,檢視其中的檔案清單:

 
以第一個 args.rule 規則檔案為例,我們檢視一下具體內容:

\.\./
\:\$
\$\{
select.+(from|limit)
(?:(union(.*?)select))
having|rongjitest
sleep\((\s*)(\d*)(\s*)\)
benchmark\((.*)\,(.*)\)
base64_decode\(
(?:from\W+information_schema\W)
(?:(?:current_)user|database|schema|connection_id)\s*\(
(?:etc\/\W*passwd)
into(\s+)+(?:dump|out)file\s*
group\s+by.+\(
xwork.MethodAccessor
(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\(
xwork\.MethodAccessor
(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/
java\.lang
\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[
\<(iframe|script|body|img|layer|div|meta|style|base|object|input)
(onmouseover|onerror|onload)\=

 
該檔案定義了 22 條檢測規則,一行一個,熟悉正則的話會很容易看懂。
我們也可以根據實際情況新增新的規則。
 

六. 參照關係

 

 
4 個Lua檔案和 8 個規則檔案,這12個檔案就是這個WAF的全部,你就說巧妙不巧妙!
 

七. 簡單優化

 
趙班長向我們展示了一個迷你WAF應該具備的基本能力,從程式碼上也能看出來,都是點到為止。
所以有必要進行一些簡單的優化。
本人優化之後的程式碼:點此下載
 
優化說明:

  1. 一些空值的判斷邏輯;
  2. 各檢測函數的跳出邏輯;
  3. 補全POST引數注入的檢測邏輯;
  4. 在lib.lua中增加了get_content_type函數,用來判斷請求型別;
     

八. 本篇總結

 
到這裡,就已經對 OpenResty 下的 WAF 從部署到使用有了全面瞭解了。
如果時間允許的話,真想繼續對 WAF 進行深層次的學習和客製化。