运维如何使用Nginx+Lua编写WAF?

转载:赵班长-运维如何使用Nginx+Lua编写WAF?

引言

首先声明本人非安全从业人员,请专业人士不要吐(gao)槽(wo)。但是我相信很多运维人员和我一样,面临的困境就是:公司没有专业的安全工程师!不出问题,企业往往意识不到安全的重要性….。

那么怎么办?作为一名运维工程师,我们的知识范围连“背黑锅”这么专业的技术都有,安全我们也可以兼职一下的。这就是答案,我们要自己干,有总比没有好!不埋怨!我们不能改变环境,但是可以改变自己,来影响环境。

那么如何通过2天时间使用Nginx+Lua编写一个自己的WAF呢?

物料准备

- 《Lua程序设计》第二版

- 《WAF产品设计参考》:http://www.freebuf.com/tools/54221.html

WAF介绍

安全也是一个比较大的课题,不同视角、不同层级都会有不同的体系结构。那么今天的分享,我选择了一个特别贴近运维工程师实际工作的一个话题:如何使用Nginx+Lua来实现一个WAF。

WAF(Web Application Firewall),也就是Web应用防火墙。

一般企业用户都会选择使用防火墙作为企业安全的第一道防线,那么传统的网络防火墙墙只能够进行四层(OSI七层模型中的传输层)的防护,那么像SQL注入、XSS、网页挂马等安全问题却无法识别和解决,因为这些攻击是七层的(OSI 七层模型中的应用层),那么Web应用防火墙就顺势而生。因为有一个让我们胆战心惊的事实:“80端口是永远的后门”。(不管你怕不怕,反正我是怕了)

如果你觉得WAF比较陌生,没关系,作为非安全人员,这可以理解。那么我们先来亲近亲近,下面几个代码片段你一定非常的熟悉。我们经常会使用Nginx来做一些常规的安全防护:

Nginx实现的简单防护

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参数中是不可能有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可以在应用层进行针对请求头部的UA进行过滤和指定动作(返回403)、针对请求的URL进行过滤匹配和指定动作(返回404)和针对请求的参数进行过滤匹配和指定动作(返回403)。是的,Nginx非常的强大,帮了我们很大忙,你还可以给指定的URL再增加频率限制来防止CC攻击。但是!待我细细道来……。

痛点是什么?

 本文是介绍如何使用Nginx+Lua来实现一个Web应用防火墙,那么就像上面我们的例子,标准的Nginx可以实现很多的功能呢?为什么要自己造轮子?

 我是一个比较保守的运维者,虽然偶尔也会为了个人的技术提高而使用某些技术,但是决不会为了这些而不干正事!所以说如果要造轮子一定是为了解决运维痛点,“凡是不以解决问题为出发点的造轮子就是耍流氓”,那么痛点是什么:

Nginx不支持白名单:试想如果你要想这么设置,某一个IP不做防护(可能是你准备的一个漏洞扫描器,你当然不想被Nginx拦截);某一个URL不做防护(由于业务原因,有一些URL不能做CC防护)。

Nginx安全防护配置繁琐复杂:如果写一堆if else非常的繁琐,而且不能很直观的记录防护日志,比如我想单独把防护日志写成JSON的,把日志存入ELKStack中。

Nginx语法简单:想自定义一些逻辑进去,Nginx支持的简单高效的语法有点苍白无力。

注:上面这些痛点如果你脑洞大开其实有很多魔法可以让Nginx来实现我说的这些哦,有兴趣自己试试,但是实现起来还是比较费劲。

注:Nginx其实提供了非常丰富的全局变量,我们可以拿来进行相应的匹配和过滤,详细可见源码包中的./src/http/ngx_http_variables.c的源码文件中。

我要造轮子!

好的,经过几个日夜的思考,我醒悟了,既然Nginx实现这么费劲,那么我就看看有没有别的方案:

Modsecurity:是我看到的第一个方案并在线上测试很长一段时间,因为它太强了,强大到我Hold不住,而且当时只支持Apache,后来才支持Nginx,但是经过我的测试,Nginx编译上Modsecurity之后,性能下降太大,放弃。不过Modsecurity可以作为造轮子的一个设计图纸。

ngx_lua_waf:这个一个基于lua-nginx-module的web应用防火墙,作者是张会源(ID : kindle),微博:@神奇的魔法师:https://github.com/loveshell/


 好的,下面我们的目标就是使用Nginx+Lua来自己实现一个WAF,也就是当请求进来的时候经过我们的WAF进行检查,正常的请求,畅通无阻,非法请求关进小黑屋。至于Nginx+Lua相关的知识,大家可以自行搜索,本文使用Openresty来进行讲解。

先画一个图纸

作为一个非安全专业的运维人员,想搞一个WAF出来还是有点困难,不过胖子也是一口一口吃出来的,我们先画一个设计图纸,先具备基本功能,然后再慢慢完善。

那么WAF一句话描述,就是解析HTTP请求,然后做规则检测,做不同的防御动作,并将防御过程记录下来。通过这一句话,我们就可以知道编写一个WAF需要的四个基本模块:

l  解析HTTP请求:协议解析模块,openresty给我们提供了丰富的API。

l  规则检测:规则检测模块,当然还需要有规则库。

l  防御动作:动作模块,比如是直接拒绝还是返回某一个状态码,还是重定向到某个页面。

l  过程记录:日志记录模块,过防御日志记录下来,编译后期统计和分析。

同时我们还需要一个配置文件,可以称之为WAF的第五个模块,配置模块。

当然这个是一个非常简化的图纸了,如果你想真正开始开发一个产品,那么还是差很远。

 

开始造轮子

开始造轮子之前需要先设想一下,我们需要实现哪些功能。

配置文件

支持开启和关闭WAF,开启某项功能等

CC防护频率设置

规则库、日志记录的相关配置。

异常请求处理配置,是返回403还是调整到某个页面

IP白名单和黑名单

URL白名单

User Agent限制

URL限制

比如限制非安全访问、非安全下载等

URL参数限制

防止SQL注入、XSS

Cookie限制

防止SQL注入

POST限制

防止SQL注入、XSS

CC防护

防护输出

支持直接返回403

支持直接输出一个页面

支持直接调整到某个页面

 好的,现在如果你有开发经验,那么需要30-60分钟的时间,看一看Lua语言快速入门之后,我们就可以开始了。

配置模块

那么我们先从配置模块开始,要编写一个WAF,那么肯定要有配置文件,配置文件用来控制和管理WAF的某些行为和相关的访问路径。

第一个要做的是一个配置开关,让用户可以自定义是否全部打开、全部关闭WAF防护,是否开启或者关闭某些WAF功能。这个很有必要,尤其是我们刚上线的时候,我们需要的场景是WAF不进行防御处理,只记录日志,用来观察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=[[

网站防火墙

欢迎白帽子进行授权安全测试,安全漏洞请联系QQ:11111。

]]

配置文件,直接使用lua语法,没有设计单独的配置文件,这样的好处可以直接被lua其它代码require(include)进去。

基础库

由于我们后面需要获取用户的UA、用户的真实IP、规则库中的规则和通用的日志记录模块,所以我们把这些全部放在一个lib.lua文件里面,称之为基础库。

源码位置:https://github.com/unixhot/waf/blob/master/waf/lib.lua

--把配置文件include进来,require是Lua语言的写法。

require 'config'


--获取客户端的IP地址,如果有自定义的名称,就要根据实际情况调整了,暂时没有写到配置文件中。

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


--打开规则文件并读到一个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外,提供了两种方法,输出一个自定义页面或者调整到某个页面。

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

 

规则检测模块

真正干活的时候到了。代码很简单,易读性也好,大家可以根据需求自己增加。

源码位置: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白名单检测,如果发现规则库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黑名单检测,如果发现规则库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白名单,检测,如果发现规则库writeurl.rule有对应的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进行配对,一个IP访问相同的URL不能超过配置的次数。功能更强大的CC防护可以参考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函数

main函数看似简单,但是顺序很重要哦。

源码位置: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()

 生产上线

现在,我们的WAF开发完毕了,要开始生产上线了,需要提前准备好环境,如果你之前使用的是openresty那么不需要重新编译,如果是Nginx需要重新编译增加lua模块,具体的步骤可以参考github上的文档。

由于篇幅有限,这里把几个技巧总结一下。

 技巧一:不要一次性部署上线,先部署后,只记录日志,然后观察和调整规则,保证正常的请求不会被误防。

 技巧二:使用SaltStack管理规则库的更新。

 技巧三:使用ELKStack进行日志收集和分析,非常方便的可以在Kibana上做出一个漂亮的攻击统计的饼图。

有激情的下一步

 好的,我们现在拥有了一个“相对完善”的WAF,而且并部署上线,可以帮我们有效的防御一些攻击,那么下一步我们需要怎么办呢?这里我列举了一些路径,有必选和可选,任君喜好!

持续更新防护规则:(必选)规则更新也是安全技术的学习过程,不断的学习,不断将有效的规则增加到规则列表中,让规则库变得更加强大起来,不过前提最好是你Hold住,不要让一些莫名其妙的规则影响了正常的业务运行。

持续增加功能:(可选)既然选择了,就要坚定的走下去,或许你并不想自己造一个轮子,那么直接使用第三方的也是不错的选择。


并不明朗的展望

 我们设计和部署了WAF,或者说使用了第三方的WAF服务,是不是就万事大吉,高枕无忧了呢?事实是WAF并不是万能的,或者说世界上任何一款安全产品都无法提供100%的安全防护。让我想起曾经读过的一句话:

  凡是人设计的程序都存在人为因素,而任何一个人为因素都可以导致系统被入侵。

那么除了WAF自身的安全问题外,有一个悲伤的话题是WAF存在被绕过的风险。具体的资料大家可以通过网络搜索来获得,但不可否认的是WAF可以帮我们防御大部分的常规攻击,对于一些攻击者中的佼佼者,WAF并不能阻挡他们入侵的脚步,他们可以绕过WAF来实施攻击,而且还有更神秘的“社会工程学”!

总结:安全是相对的,没有100%的安全防护,但是做总比不做好,而且好很多很多。


感谢开源

好吧,我承认,目前这样的代码基本上就是一个Nginx自身防护的一个加强版,或者说只是一个思路,还有非常非常多的功能,需要我们继续开发,但是,我们有了!不是吗?不要做思想上的巨人,我们要做实践者,日积月累,最终可能就成为了一个产品。 

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容