轉(zhuǎn)載:趙班長-運(yùn)維如何使用Nginx+Lua編寫WAF椭赋?
引言
首先聲明本人非安全從業(yè)人員,請專業(yè)人士不要吐(gao)槽(wo)或杠。但是我相信很多運(yùn)維人員和我一樣哪怔,面臨的困境就是:公司沒有專業(yè)的安全工程師!不出問題廷痘,企業(yè)往往意識不到安全的重要性….蔓涧。
那么怎么辦?作為一名運(yùn)維工程師笋额,我們的知識范圍連“背黑鍋”這么專業(yè)的技術(shù)都有,安全我們也可以兼職一下的篷扩。這就是答案兄猩,我們要自己干,有總比沒有好鉴未!不埋怨枢冤!我們不能改變環(huán)境,但是可以改變自己铜秆,來影響環(huán)境淹真。
那么如何通過2天時間使用Nginx+Lua編寫一個自己的WAF呢?
物料準(zhǔn)備
- 《Lua程序設(shè)計(jì)》第二版
- 《WAF產(chǎn)品設(shè)計(jì)參考》:http://www.freebuf.com/tools/54221.html
WAF介紹
安全也是一個比較大的課題连茧,不同視角核蘸、不同層級都會有不同的體系結(jié)構(gòu)巍糯。那么今天的分享,我選擇了一個特別貼近運(yùn)維工程師實(shí)際工作的一個話題:如何使用Nginx+Lua來實(shí)現(xiàn)一個WAF客扎。
WAF(Web Application Firewall)祟峦,也就是Web應(yīng)用防火墻。
一般企業(yè)用戶都會選擇使用防火墻作為企業(yè)安全的第一道防線徙鱼,那么傳統(tǒng)的網(wǎng)絡(luò)防火墻墻只能夠進(jìn)行四層(OSI七層模型中的傳輸層)的防護(hù)宅楞,那么像SQL注入、XSS袱吆、網(wǎng)頁掛馬等安全問題卻無法識別和解決厌衙,因?yàn)檫@些攻擊是七層的(OSI?七層模型中的應(yīng)用層),那么Web應(yīng)用防火墻就順勢而生绞绒。因?yàn)橛幸粋€讓我們膽戰(zhàn)心驚的事實(shí):“80端口是永遠(yuǎn)的后門”婶希。(不管你怕不怕,反正我是怕了)
如果你覺得WAF比較陌生处铛,沒關(guān)系饲趋,作為非安全人員,這可以理解撤蟆。那么我們先來親近親近奕塑,下面幾個代碼片段你一定非常的熟悉。我們經(jīng)常會使用Nginx來做一些常規(guī)的安全防護(hù):
Nginx實(shí)現(xiàn)的簡單防護(hù)
1.拒絕特定User Agent的訪問(想拿ab壓測我家肯,沒門(除非你修改下UA龄砰,冏))
#==Disable User Agents==
set $block_user_agents 0;
if ($http_user_agent ~ "Wget|ApacheBench|WebBench|TurnitinBot|libwww-perl"){
set $block_user_agents1;
}
if ($block_user_agents = 1) {
return 403;
}
2.拒絕訪問特定后綴的文件(一不小心備份一個tar包,可不能直接被用戶下載了去讨衣。)
#==Disable non-security Download===
location ~* "\.(sql|bak|inc|old|sh|zip|tgz|gz|tar)$"{
???? return 404;
}
3.SQL注入(我們的url參數(shù)中是不可能有select的)
#==Block SQL Injections
set $block_sql_injections 0;
if ($query_string ~ "union.*select.*\("){
?? set $block_sql_injections1;
}
if ( $block_sql_injections = 1){
?? return 403;
}
? 上面這三個例子换棚,我相信大家都比較熟悉了,我們使用Nginx可以在應(yīng)用層進(jìn)行針對請求頭部的UA進(jìn)行過濾和指定動作(返回403)反镇、針對請求的URL進(jìn)行過濾匹配和指定動作(返回404)和針對請求的參數(shù)進(jìn)行過濾匹配和指定動作(返回403)固蚤。是的,Nginx非常的強(qiáng)大歹茶,幫了我們很大忙夕玩,你還可以給指定的URL再增加頻率限制來防止CC攻擊。但是惊豺!待我細(xì)細(xì)道來……燎孟。
痛點(diǎn)是什么?
?本文是介紹如何使用Nginx+Lua來實(shí)現(xiàn)一個Web應(yīng)用防火墻尸昧,那么就像上面我們的例子揩页,標(biāo)準(zhǔn)的Nginx可以實(shí)現(xiàn)很多的功能呢?為什么要自己造輪子烹俗?
?我是一個比較保守的運(yùn)維者爆侣,雖然偶爾也會為了個人的技術(shù)提高而使用某些技術(shù)萍程,但是決不會為了這些而不干正事!所以說如果要造輪子一定是為了解決運(yùn)維痛點(diǎn)累提,“凡是不以解決問題為出發(fā)點(diǎn)的造輪子就是耍流氓”尘喝,那么痛點(diǎn)是什么:
Nginx不支持白名單:試想如果你要想這么設(shè)置,某一個IP不做防護(hù)(可能是你準(zhǔn)備的一個漏洞掃描器斋陪,你當(dāng)然不想被Nginx攔截)朽褪;某一個URL不做防護(hù)(由于業(yè)務(wù)原因,有一些URL不能做CC防護(hù))无虚。
Nginx安全防護(hù)配置繁瑣復(fù)雜:如果寫一堆if else非常的繁瑣缔赠,而且不能很直觀的記錄防護(hù)日志,比如我想單獨(dú)把防護(hù)日志寫成JSON的友题,把日志存入ELKStack中嗤堰。
Nginx語法簡單:想自定義一些邏輯進(jìn)去,Nginx支持的簡單高效的語法有點(diǎn)蒼白無力度宦。
注:上面這些痛點(diǎn)如果你腦洞大開其實(shí)有很多魔法可以讓Nginx來實(shí)現(xiàn)我說的這些哦踢匣,有興趣自己試試,但是實(shí)現(xiàn)起來還是比較費(fèi)勁戈抄。
注:Nginx其實(shí)提供了非常豐富的全局變量离唬,我們可以拿來進(jìn)行相應(yīng)的匹配和過濾,詳細(xì)可見源碼包中的./src/http/ngx_http_variables.c的源碼文件中划鸽。
我要造輪子输莺!
好的,經(jīng)過幾個日夜的思考裸诽,我醒悟了嫂用,既然Nginx實(shí)現(xiàn)這么費(fèi)勁,那么我就看看有沒有別的方案:
Modsecurity:是我看到的第一個方案并在線上測試很長一段時間丈冬,因?yàn)樗珡?qiáng)了嘱函,強(qiáng)大到我Hold不住,而且當(dāng)時只支持Apache埂蕊,后來才支持Nginx实夹,但是經(jīng)過我的測試,Nginx編譯上Modsecurity之后粒梦,性能下降太大,放棄荸实。不過Modsecurity可以作為造輪子的一個設(shè)計(jì)圖紙匀们。
ngx_lua_waf:這個一個基于lua-nginx-module的web應(yīng)用防火墻,作者是張會源(ID : kindle)准给,微博:@神奇的魔法師:https://github.com/loveshell/
?好的孝凌,下面我們的目標(biāo)就是使用Nginx+Lua來自己實(shí)現(xiàn)一個WAF供搀,也就是當(dāng)請求進(jìn)來的時候經(jīng)過我們的WAF進(jìn)行檢查宴偿,正常的請求,暢通無阻钟沛,非法請求關(guān)進(jìn)小黑屋。至于Nginx+Lua相關(guān)的知識局扶,大家可以自行搜索恨统,本文使用Openresty來進(jìn)行講解。
先畫一個圖紙
作為一個非安全專業(yè)的運(yùn)維人員三妈,想搞一個WAF出來還是有點(diǎn)困難畜埋,不過胖子也是一口一口吃出來的,我們先畫一個設(shè)計(jì)圖紙畴蒲,先具備基本功能悠鞍,然后再慢慢完善。
那么WAF一句話描述模燥,就是解析HTTP請求咖祭,然后做規(guī)則檢測,做不同的防御動作蔫骂,并將防御過程記錄下來么翰。通過這一句話,我們就可以知道編寫一個WAF需要的四個基本模塊:
l??解析HTTP請求:協(xié)議解析模塊纠吴,openresty給我們提供了豐富的API硬鞍。
l??規(guī)則檢測:規(guī)則檢測模塊,當(dāng)然還需要有規(guī)則庫戴已。
l??防御動作:動作模塊固该,比如是直接拒絕還是返回某一個狀態(tài)碼,還是重定向到某個頁面糖儡。
l??過程記錄:日志記錄模塊伐坏,過防御日志記錄下來,編譯后期統(tǒng)計(jì)和分析握联。
同時我們還需要一個配置文件桦沉,可以稱之為WAF的第五個模塊,配置模塊金闽。
當(dāng)然這個是一個非常簡化的圖紙了纯露,如果你想真正開始開發(fā)一個產(chǎn)品,那么還是差很遠(yuǎn)代芜。
?
開始造輪子
開始造輪子之前需要先設(shè)想一下埠褪,我們需要實(shí)現(xiàn)哪些功能。
配置文件
支持開啟和關(guān)閉WAF,開啟某項(xiàng)功能等
CC防護(hù)頻率設(shè)置
規(guī)則庫钞速、日志記錄的相關(guān)配置贷掖。
異常請求處理配置,是返回403還是調(diào)整到某個頁面
IP白名單和黑名單
URL白名單
User Agent限制
URL限制
比如限制非安全訪問渴语、非安全下載等
URL參數(shù)限制
防止SQL注入苹威、XSS
Cookie限制
防止SQL注入
POST限制
防止SQL注入、XSS
CC防護(hù)
防護(hù)輸出
支持直接返回403
支持直接輸出一個頁面
支持直接調(diào)整到某個頁面
?好的驾凶,現(xiàn)在如果你有開發(fā)經(jīng)驗(yàn)牙甫,那么需要30-60分鐘的時間,看一看Lua語言快速入門之后狭郑,我們就可以開始了腹暖。
配置模塊
那么我們先從配置模塊開始,要編寫一個WAF翰萨,那么肯定要有配置文件脏答,配置文件用來控制和管理WAF的某些行為和相關(guān)的訪問路徑。
第一個要做的是一個配置開關(guān)亩鬼,讓用戶可以自定義是否全部打開殖告、全部關(guān)閉WAF防護(hù),是否開啟或者關(guān)閉某些WAF功能雳锋。這個很有必要黄绩,尤其是我們剛上線的時候,我們需要的場景是WAF不進(jìn)行防御處理玷过,只記錄日志爽丹,用來觀察WAF都干了什么,是否有誤殺辛蚊。
源碼地址:https://github.com/unixhot/waf/blob/master/waf/config.lua
--WAF configfile,enable = "on",disable = "off"
--waf status
config_waf_enable= "on"
--log dir
config_log_dir ="/tmp/waf_logs"
--rule setting
config_rule_dir= "/usr/local/openresty/nginx/conf/waf/rule-config"
--enable/disablewhite url
config_white_url_check= "on"
--enable/disablewhite ip
config_white_ip_check= "on"
--enable/disableblock ip
config_black_ip_check= "on"
--enable/disableurl filtering
config_url_check= "on"
--enalbe/disableurl args filtering
config_url_args_check= "on"
--enable/disableuser agent filtering
config_user_agent_check= "on"
--enable/disablecookie deny filtering
config_cookie_check= "on"
--enable/disablecc filtering
config_cc_check= "on"
--cc rate thexxx of xxx seconds
config_cc_rate ="10/60"
--enable/disablepost filtering
config_post_check= "on"
--config wafoutput redirect/html
config_waf_output= "html"
--ifconfig_waf_output ,setting url
config_waf_redirect_url= "http://www.baidu.com"
config_output_html=[[
網(wǎng)站防火墻
歡迎白帽子進(jìn)行授權(quán)安全測試粤蝎,安全漏洞請聯(lián)系QQ:11111。
]]
配置文件袋马,直接使用lua語法初澎,沒有設(shè)計(jì)單獨(dú)的配置文件,這樣的好處可以直接被lua其它代碼require(include)進(jìn)去虑凛。
基礎(chǔ)庫
由于我們后面需要獲取用戶的UA碑宴、用戶的真實(shí)IP、規(guī)則庫中的規(guī)則和通用的日志記錄模塊桑谍,所以我們把這些全部放在一個lib.lua文件里面延柠,稱之為基礎(chǔ)庫。
源碼位置:https://github.com/unixhot/waf/blob/master/waf/lib.lua
--把配置文件include進(jìn)來锣披,require是Lua語言的寫法捕仔。
require 'config'
--獲取客戶端的IP地址匕积,如果有自定義的名稱,就要根據(jù)實(shí)際情況調(diào)整了榜跌,暫時沒有寫到配置文件中。
functionget_client_ip()
??? CLIENT_IP = ngx.req.get_headers()["My_Set_ip"]
??? if CLIENT_IP == nil then
??????? CLIENT_IP =ngx.req.get_headers()["X_Forwarded_For"]
??? end
??? if CLIENT_IP == nil then
??????? CLIENT_IP? = ngx.var.remote_addr
??? end
??? if CLIENT_IP == nil then
??????? CLIENT_IP? = "unknown"
??? end
??? return CLIENT_IP
end
--獲取客戶端的User Agent
functionget_user_agent()
??? USER_AGENT = ngx.var.http_user_agent
??? if USER_AGENT == nil then
?????? USER_AGENT = "unknown"
??? end
??? return USER_AGENT
end
--打開規(guī)則文件并讀到一個TABLE里面盅粪,返回這個TABLE钓葫。
functionget_rule(rulefilename)
??? local io = require 'io'
??? local RULE_PATH = config_rule_dir
??? local RULE_FILE =io.open(RULE_PATH..'/'..rulefilename,"r")
??? if RULE_FILE == nil then
??????? return
??? end
??? RULE_TABLE = {}
??? for line in RULE_FILE:lines() do
??????? table.insert(RULE_TABLE,line)
??? end
??? RULE_FILE:close()
??? return(RULE_TABLE)
end
--把日志寫成JSON的,這樣LogStash收集的時候直接codec => json票顾。
functionlog_record(method,url,data,ruletag)
??? local cjson = require("cjson")
??? local io = require 'io'
??? local LOG_PATH = config_log_dir
??? local CLIENT_IP = get_client_ip()
??? local USER_AGENT = get_user_agent()
??? local SERVER_NAME = ngx.var.server_name
??? local LOCAL_TIME = ngx.localtime()
??? local log_json_obj = {
???????????????? client_ip = CLIENT_IP,
???????????????? local_time = LOACL_TIME,
???????????????? server_name = SERVER_NAME,
???????????????? user_agent = USER_AGENT,
???????????????? attack_method = method,
???????????????? req_url = url,
???????????????? req_data = data,
???????????????? rule_tag = ruletag,
????????????? }
??? local LOG_LINE = cjson.encode(log_json_obj)
?????? ?local LOG_NAME = LOG_PATH..'/'..ngx.today().."_waf.log"
?????? ?local file = io.open(LOG_NAME,"a")
?????? ?if file == nil then
?????? ????return
?????? ?end
??? file:write(LOG_LINE.."\n")
?????? ?file:flush()
?????? ?file:close()
end
--除了需要直接返回403外础浮,提供了兩種方法,輸出一個自定義頁面或者調(diào)整到某個頁面奠骄。
functionwaf_output()
??? if config_waf_output =="redirect" then
??????? ngx.redirect(config_waf_redirect_url,301)
??? else
??????? ngx.header.content_type ="text/html"
??????? ngx.status = ngx.HTTP_FORBIDDEN
???????ngx.say(config_output_html)
??????? ngx.exit(ngx.status)
??? end
end
?
規(guī)則檢測模塊
真正干活的時候到了豆同。代碼很簡單,易讀性也好含鳞,大家可以根據(jù)需求自己增加影锈。
源碼位置:https://github.com/unixhot/waf/blob/master/waf/init.lua?
#access.lua
--WAF Action
require 'config'
require 'lib'
--args
local rulematch = ngx.re.find
local unescape = ngx.unescape_uri
--IP白名單檢測,如果發(fā)現(xiàn)規(guī)則庫whiteip.rule里面有這個IP蝉绷。直接返回鸭廷。
function white_ip_check()
????if config_white_ip_check == "on" then
???????local IP_WHITE_RULE = get_rule('whiteip.rule')
???????local WHITE_IP = get_client_ip()
???????if IP_WHITE_RULE ~= nil then
???????????for _,rule in pairs(IP_WHITE_RULE) do
??????????????? if rule ~= "" andrulematch(WHITE_IP,rule,"jo") then
???????????????? ???log_record('White_IP',ngx.var_request_uri,"_","_")
??????????????????? return true
??????????????? end
???????????end
???????end
???end
end
--IP黑名單檢測,如果發(fā)現(xiàn)規(guī)則庫blackip.rule有這個黑名單熔吗,直接返回403并返回辆床。(好的if和end啊。)
function black_ip_check()
????if config_black_ip_check == "on" then
???????local IP_BLACK_RULE = get_rule('blackip.rule')
???????local BLACK_IP = get_client_ip()
???????if IP_BLACK_RULE ~= nil then
???????????for _,rule in pairs(IP_BLACK_RULE) do
??????????????? if rule ~= "" andrulematch(BLACK_IP,rule,"jo") then
???????????????????log_record('BlackList_IP',ngx.var_request_uri,"_","_")
??????????????????? if config_waf_enable =="on" then
??????????????????????? ngx.exit(403)
??????????????????????? return true
??????????????????? end
??????????????? end
???????????end
???????end
???end
end
--URL白名單桅狠,檢測讼载,如果發(fā)現(xiàn)規(guī)則庫writeurl.rule有對應(yīng)的url直接返回。
function white_url_check()
???if config_white_url_check == "on" then
???????local URL_WHITE_RULES = get_rule('writeurl.rule')
???????local REQ_URI = ngx.var.request_uri
???????if URL_WHITE_RULES ~= nil then
???????????for _,rule in pairs(URL_WHITE_RULES) do
??????????????? if rule ~= "" andrulematch(REQ_URI,rule,"jo") then
??????????????????? return true
??????????????? end
???????????end
???????end
???end
end
--CC返回中跌,把IP和URL進(jìn)行配對咨堤,一個IP訪問相同的URL不能超過配置的次數(shù)。功能更強(qiáng)大的CC防護(hù)可以參考HttpGuard晒他,同樣是Nginx+lua吱型。很容易集成過來。https://github.com/centos-bz/HttpGuard
function cc_attack_check()
???if config_cc_check == "on" then
???????local ATTACK_URI=ngx.var.uri
???????local CC_TOKEN = get_client_ip()..ATTACK_URI
???????local limit = ngx.shared.limit
???????CCcount=tonumber(string.match(config_cc_rate,'(.*)/'))
???????CCseconds=tonumber(string.match(config_cc_rate,'/(.*)'))
???????local req,_ = limit:get(CC_TOKEN)
???????if req then
???????????if req > CCcount then
???????????????log_record('CC_Attack',ngx.var.request_uri,"-","-")
????????????? ifconfig_waf_enable == "on" then
??????????????????? ngx.exit(403)
????????????? end
???????????else
??????????????? limit:incr(CC_TOKEN,1)
? ??????????end
???????else
???????????limit:set(CC_TOKEN,1,CCseconds)
???????end
???end
???return false
end
###此處省略1000字###
main函數(shù)
main函數(shù)看似簡單陨仅,但是順序很重要哦津滞。
源碼位置:https://github.com/unixhot/waf/blob/master/waf/access.lua
require 'init'
function waf_main()
???if white_ip_check() then
???elseif black_ip_check() then
???elseif user_agent_attack_check() then
???elseif cc_attack_check() then
???elseif cookie_attack_check() then
???elseif white_url_check() then
???elseif url_attack_check() then
???elseif url_args_attack_check() then
???--elseif post_attack_check() then
???else
???????return
???end
end
waf_main()
?生產(chǎn)上線
現(xiàn)在,我們的WAF開發(fā)完畢了灼伤,要開始生產(chǎn)上線了触徐,需要提前準(zhǔn)備好環(huán)境,如果你之前使用的是openresty那么不需要重新編譯狐赡,如果是Nginx需要重新編譯增加lua模塊撞鹉,具體的步驟可以參考github上的文檔。
由于篇幅有限,這里把幾個技巧總結(jié)一下鸟雏。
?技巧一:不要一次性部署上線享郊,先部署后,只記錄日志孝鹊,然后觀察和調(diào)整規(guī)則炊琉,保證正常的請求不會被誤防。
?技巧二:使用SaltStack管理規(guī)則庫的更新又活。
?技巧三:使用ELKStack進(jìn)行日志收集和分析苔咪,非常方便的可以在Kibana上做出一個漂亮的攻擊統(tǒng)計(jì)的餅圖。
有激情的下一步
?好的柳骄,我們現(xiàn)在擁有了一個“相對完善”的WAF团赏,而且并部署上線,可以幫我們有效的防御一些攻擊耐薯,那么下一步我們需要怎么辦呢舔清?這里我列舉了一些路徑,有必選和可選可柿,任君喜好鸠踪!
持續(xù)更新防護(hù)規(guī)則:(必選)規(guī)則更新也是安全技術(shù)的學(xué)習(xí)過程,不斷的學(xué)習(xí)复斥,不斷將有效的規(guī)則增加到規(guī)則列表中营密,讓規(guī)則庫變得更加強(qiáng)大起來,不過前提最好是你Hold住目锭,不要讓一些莫名其妙的規(guī)則影響了正常的業(yè)務(wù)運(yùn)行评汰。
持續(xù)增加功能:(可選)既然選擇了,就要堅(jiān)定的走下去痢虹,或許你并不想自己造一個輪子被去,那么直接使用第三方的也是不錯的選擇。
并不明朗的展望
?我們設(shè)計(jì)和部署了WAF奖唯,或者說使用了第三方的WAF服務(wù)惨缆,是不是就萬事大吉,高枕無憂了呢丰捷?事實(shí)是WAF并不是萬能的坯墨,或者說世界上任何一款安全產(chǎn)品都無法提供100%的安全防護(hù)。讓我想起曾經(jīng)讀過的一句話:
? 凡是人設(shè)計(jì)的程序都存在人為因素病往,而任何一個人為因素都可以導(dǎo)致系統(tǒng)被入侵捣染。
那么除了WAF自身的安全問題外,有一個悲傷的話題是WAF存在被繞過的風(fēng)險停巷。具體的資料大家可以通過網(wǎng)絡(luò)搜索來獲得耍攘,但不可否認(rèn)的是WAF可以幫我們防御大部分的常規(guī)攻擊榕栏,對于一些攻擊者中的佼佼者,WAF并不能阻擋他們?nèi)肭值哪_步蕾各,他們可以繞過WAF來實(shí)施攻擊扒磁,而且還有更神秘的“社會工程學(xué)”!
總結(jié):安全是相對的示损,沒有100%的安全防護(hù)渗磅,但是做總比不做好,而且好很多很多检访。
感謝開源
好吧,我承認(rèn)仔掸,目前這樣的代碼基本上就是一個Nginx自身防護(hù)的一個加強(qiáng)版脆贵,或者說只是一個思路,還有非常非常多的功能起暮,需要我們繼續(xù)開發(fā)卖氨,但是,我們有了负懦!不是嗎筒捺?不要做思想上的巨人,我們要做實(shí)踐者纸厉,日積月累系吭,最終可能就成為了一個產(chǎn)品。?