基于Nginx+Lua自建Web应用防火墙

优采云 发布时间: 2022-05-11 07:38

  基于Nginx+Lua自建Web应用防火墙

  

  

  读完需 8 分钟

  速读需 4 分钟

  

  简介

  对于信息类网站,总是会被各种不同目的的爬虫、采集器等不断的抓取或恶意访问,这些会让网站不堪重负,导致页面无法正常访问,极大的影响用户体验。针对此种情况,我们就需要对所有的访问来进行访问控制。

  此时Web应用防火墙(Web Application Firewall,简称 WAF)就可以助我们一臂之力,它可以为网站提供一站式安全防护。WAF可以有效识别Web业务流量的恶意特征,在对流量进行清洗和过滤后,将正常、安全的流量返回给服务器,避免网站服务器被恶意入侵导致服务器性能异常等问题,保障网站的业务安全和数据安全。

  Web应用防火墙主要功能如下:

  从WAF的定义及功能看,它的位置应该处于流量入口处。如果选用商业产品,多和CDN配合使用;如果自行开发,其位置应该在负载均衡Nginx上。结合lua可以进行二次扩展,实现个性化访问控制需求。

  分析

  在使用Nginx+lua实现个性化需求前,我们首先需要了解我们的网站的流量组成:

  1. 爬虫流量

  百度、bing、谷歌、360、一搜、神马、今日头条、采集器

  2. 异常流量

  单IP大流量访问、多IP大流量访问

  3. 恶意攻击

  DDos、CC、SQL注入、暴力破解等

  4. 正常流量

  5. 三方渠道大流量访问

  以上基本概括了我们网站的主要流量来源,这些流量我们可以从基础防护和动态防护两个层面展开。

  基础防护

  Nginx 不仅在负载均衡层面发挥着重要作用,其内置的一些基础模块,也可以在一定程度上做一些防护。

  1

  安全防护

  对于站点流量,我们可以主动分析客户端请求的特征,如user_agent、url、query_string ;结合业务特点,可以对其制定一些规则来进行主动防范,在应对异常流量时起到一定的防护作用。

  vim x.x.cn.conf# 在站点文件添加web安全限制,返回不同的状态码include conf.d/safe.conf;<br /># 安全规则文件vim safe.conf# 禁SQL注入 Block SQL injections set $block_sql_injections 0; if ($query_string ~ "union.*select.*(.*)") { set $block_sql_injections 1; } if ($request_uri ~* "select((/\*+/)|[+ ]+|(%20)+)") {set $block_sql_injections 1;}if ($request_uri ~* "union((/\*+/)|[+ ]+|(%20)+)") {set $block_sql_injections 1;}if ($request_uri ~* "order((/\*+/)|[+ ]+|(%20)+)by") {set $block_sql_injections 1;}#匹配"group/**/by", "group+by", "group by"if ($request_uri ~* "group((/\*+/)|[+ ]+|(%20)+)by") {set $block_sql_injections 1;}if ($block_sql_injections = 1) { return 444; } <br /># 禁掉文件注入 set $block_file_injections 0; if ($query_string ~ "[a-zA-Z0-9_]=http://") { set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=(..//?)+") { set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { set $block_file_injections 1; } if ($block_file_injections = 1) { return 444; } <br /># 禁掉溢出攻击 set $block_common_exploits 0; if ($query_string ~ "(|%3E)") { set $block_common_exploits 1; } if ($query_string ~ "GLOBALS(=|[|%[0-9A-Z]{0,2})") { set $block_common_exploits 1; } if ($query_string ~ "_REQUEST(=|[|%[0-9A-Z]{0,2})") { set $block_common_exploits 1; } if ($query_string ~ "proc/self/environ") { set $block_common_exploits 1; } if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|%3D)") { set $block_common_exploits 1; } if ($query_string ~ "base64_(en|de)code(.*)") { set $block_common_exploits 1; } if ($block_common_exploits = 1) { return 444; } <br /># 禁spam字段 set $block_spam 0; if ($query_string ~ "b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)b") { set $block_spam 1; } if ($query_string ~ "b(erections|hoodia|huronriveracres|impotence|levitra|libido)b") { set $block_spam 1; } if ($query_string ~ "b(ambien|bluespill|cialis|cocaine|ejaculation|erectile)b") { set $block_spam 1; } if ($query_string ~ "b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)b") { set $block_spam 1; } if ($block_spam = 1) { return 444; } <br /># 禁掉user-agents set $block_user_agents 0; <br />#禁止agent为空#if ($http_user_agent ~ ^$) { #set $block_user_agents 1; #} <br /># Don’t disable wget if you need it to run cron jobs! if ($http_user_agent ~ "Wget") { set $block_user_agents 1; } <br /># Disable Akeeba Remote Control 2.5 and earlier if ($http_user_agent ~ "Indy Library") { set $block_user_agents 1; } <br /># Common bandwidth hoggers and hacking tools. if ($http_user_agent ~ "libwww-perl") { set $block_user_agents 1; } if ($http_user_agent ~ "GetRight") { set $block_user_agents 1; } if ($http_user_agent ~ "GetWeb!") { set $block_user_agents 1; } if ($http_user_agent ~ "Go!Zilla") { set $block_user_agents 1; } if ($http_user_agent ~ "Download Demon") { set $block_user_agents 1; } if ($http_user_agent ~ "Go-Ahead-Got-It") { set $block_user_agents 1; } if ($http_user_agent ~ "TurnitinBot") { set $block_user_agents 1; } if ($http_user_agent ~ "GrabNet") { set $block_user_agents 1; } <br />if ($block_user_agents = 1) { return 444; } <br />#spiderset $spider '2';if ( $http_user_agent ~ .+Baiduspider.+ ){ set $spider '0';}if ( $http_user_agent ~ .+Googlebot.+){ set $spider '0';}if ( $http_user_agent ~ .+bingbot.+){ set $spider '0';}if ( $http_user_agent ~ .+JikeSpider.+){ set $spider '0';}if ( $http_user_agent ~ .+YoudaoBot.+){ set $spider '0';}if ( $http_user_agent ~ .+Sosospider.+){ set $spider '0';}if ( $http_user_agent ~ Yahoo!.+){ set $spider '0';}if ( $http_user_agent ~ Sogou.+){ set $spider '0';}if ( $http_user_agent ~ .+msnbot.+){ set $spider '0';}if ( $http_user_agent ~ .+YandexBot.+){ set $spider '0';}if ( $http_user_agent ~ .+Spider.+){ set $spider '0';}<br />if ( $http_user_agent ~ YisouSpider){ set $spider '1';}#if ( $http_user_agent ~ LBBROWSER){# set $spider '1';#}if ($spider = '1') { return 445;}

  通过分析客户端的user_agent、url、query_string 初步分析是否具备统一特征,并根据其行为返回不同的状态码:

  通过状态码,我们可以快速定位请求属于哪类安全范畴。

  2

  连接数、频率限制

  对于站点的访问连接数、访问频率,我们可以使用以下两个模块来做一些策略。此时可以对异常流量、恶意攻击起到一定的作用。

  限制每个已定义的 key 的连接数量,特别是来自单个 IP 地址的连接数量。

  限制请求的处理速率,特别是单一的IP地址的请求的处理速率。它基于漏桶算法进行限制。

  #针对url1访问频率每分100个limit_req_zone  $binary_remote_addr  zone=req_limit4:10m   rate=100r/m;<br />#针对url2访问频率每秒5个,burst 5个limit_req_zone  $binary_remote_addr  zone=req_limit3:10m   rate=5r/s;<br />#针对url3问频率每秒50个,burst 10个limit_req_zone  $binary_remote_addr  zone=req_limit2:10m   rate=50r/s;<br />#针对url4访问频率每分30个,burst 10个limit_req_zone  $binary_remote_addr  zone=req_limit1:10m   rate=30r/m;<br />

  对于频率的阈值需要结合站点的实际访问流量、峰值来具体设置。基于漏桶算法,可以对突发流量进行整形,避免单一IP或多IP的大流量请求压垮服务器。

  3

  map自定义变量

  map 指令通过使用 nginx 的内置变量创建自定义变量, 由 ngx_http_map_module 模块提供的,默认情况下安装 nginx 都会安装该模块。通过自定义变量来匹配某些特定规则,进行访问控制。

  我们可以通过map来设置白名单,不在白名单的IP将返回403。

  vim map.confmap $remote_addr $clientip { # 默认为false; default fase; # 精确匹配或正则匹配IP,则返回true 1.1.1.1 true; ~*12.12.3 true;}# 如果客户端ip为false 则返回403if( $clientip = 'false'){ return 403;}

  4

  小结

  基础防护在针对一些有规律的特征流量时,基于nginx基础模块做的一些工作。但对于一些动态流量的访问,这些规则就显得有些死板,无法满足需求。此时就行需要基于nginx+lua做一些个性化的需求。

  动态防护

  1

  策略分析

  基于WAF,结合日常流量的统计分析,我们主要想实现以下几方面:

  1. 黑白名单

  对于三方合作渠道的IP加入白名单,没有规则策略;

  通过分析日常流量,将异常行为的IP加到黑名单,前端直接返回403;

  2. 最大访问量

  对于不在白名单内的IP,每个IP的每天访问量在正常情况下应该是要有上限的,为避免IP过量访问我们需要应该进行限制;

  3. 人机验证

  (1)对于不在白名单内的IP,每个IP在一定周期内的访问量超限,此时需要跳转至验证码页进行人机验证;

  (2)如果验证码页验证次数超限,则认定为暴力破解,将IP进行封禁一段时间;

  (3)暴力破解的IP封禁超时后,重新解禁,再次访问将重新认证;

  4. 反查域名

  对于冒充搜索引擎试图跳过访问策略的请求,我们将进行域名反查;确定是否为真正的爬虫,若为搜索引擎则加入白名单。

  2

  实施规划

  1.openresty环境部署

  组件

  备注

  openresty

  nginx+lua

  lua-resty-redis

  lua连接redis

  redis

  存放客户端请求实时数据

  人机验证功能页

  由前端提供此页面

  相关组件的部署如下:

  # 0.基础依赖yum install -y GeoIP GeoIP-devel GeoIP-data libtool openssl openssl-devel # 1.创建用户groupadd openrestyuseradd -G operesty openresty -s /bin/nologin<br /># 2.准备源码包openresty-xxx.tar.gzpcre-xxx.tar.gz<br />tar -zxvf openresty-xxx.tar.gztar -zxvf pcre-xxx.tar.gz# 3.安装 LuaJITcd openresty-xxx/bundle/LuaJIT-xxxmake cleanmake make install<br /># 4.安装openrestycd openresty-xxx./configure --prefix=/usr/local/openresty --with-http_realip_module --with-pcre=../pcre-xxx --with-luajit --with-file-aio --with-http_sub_module --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_gzip_static_module --without-select_module --without-poll_module --with-http_geoip_modulemakemake install<br /># 5.lua-resty-redis模块安装wget https://github.com/openresty/lua-resty-redis/archive/master.zipunzip master.zipcd lua-resty-redis-master<br />#将lib拷贝到openresty安装目录下的lua文件夹内cp -rf lib /usr/local/openresty/conf/luacd /usr/local/openresty/conf/lua/libln -s redis.lua resty/redis.lua<br /># 6. 安装redisyum install redis -y/etc/init.d/redis start

  至此openresty的基础文件已经部署完毕,下一步需要加载lua脚本实现相关的策略配置。

  2.lua脚本规划

  统一将lua模块及相关脚本存放在`/usr/local/openresty/conf/lua`目录下,其中:

  lua |--lib | |-resty | | |-redis.lua | |-redis.lua #redis驱动 |--access | |-config.lua #统一配置文件 | |-access_init.lua #加载配置文件、获取客户端IP的方法 | |-access_ip.lua #黑白名单过滤 | |-access_veryfycode.lua #验证码

  规划完成后,我们就需要在oprneresty加载即可。

  vim nginx.conf# 在http区域内添加如下配置。<br />#加载lua配置初始化init_by_lua_file '/usr/local/openresty/nginx/conf/lua/access/access_init.lua';<br />#lua-resty-redislua_package_path "/usr/local/openresty/nginx/conf/lua/lib/resty/?.lua;;";<br />#黑白名单封禁ipaccess_by_lua_file '/usr/local/openresty/nginx/conf/lua/access/access_ip.lua';

  其中init_by_lua_file、access_by_lua_file 就是openresty执行流程中的不同阶段,我们根据访问流程可以在各阶段配置不同的访问策略。

  3.openresty执行流程

  

  如图openresty执行流程,在相应的阶段我们的策略如下:

  (1)init初始化阶段

  由于init阶段是流程的第一阶段,即nginx加载全局参数阶段,因此也需要首先加载我们的配置文件:

  # vim config.lua--waf统一配置文件<br />--ip白名单ipWhitelist={--"10.0.0.0-10.255.255.255",--神马搜索"42.156.0.0-42.156.255.255","42.120.0.0-42.120.255.255","106.11.0.0-106.11.255.255",--三方渠道"113.5.18.230-113.5.18.231","113.5.18.234",--内网"192.168.0.0-192.168.255.255",}<br />----ip黑名单ipBlocklist={"39.104.180.188","42.236.10.1-42.236.10.254",}

  以上配置文件中的客户端单个地址和地址段,都是通过access_init.lua来加载config.lua配置文件并由相关方法进行IP解析:

  # vim access_init.lua--此文件为需要在http段配置init_by_lua_file '/usr/local/nginx/lua/access/access_init.lua';--注意:由于连接reids无法在init阶段使用,因此验证码由单独的access_verifycode.lua文件使用;--封禁策略:--增加ip黑名单、白名单的ip段支持<br />package.path = "/usr/local/openresty/nginx/conf/lua/access/?.lua;/usr/local/openresty/nginx/conf/lua/lib/?.lua;"package.cpath = "/usr/local/openresty/nginx/conf/lua/?.so;/usr/local/openresty/nginx/conf/lua/lib/?.so;"<br />--加载配置文件require "config"<br />--获取客户端ipfunction getClientIp() IP = ngx.var.remote_addr if IP == nil then IP = "unknown" end return IPend<br /><br />function ipToDecimal(ckip) local n = 4 local decimalNum = 0 local pos = 0 for s, e in function() return string.find(ckip, '.', pos, true) end do n = n - 1 decimalNum = decimalNum + string.sub(ckip, pos, s-1) * (256 ^ n) pos = e + 1 if n == 1 then decimalNum = decimalNum + string.sub(ckip, pos, string.len(ckip)) end end return decimalNumend<br /># 白名单过滤function whiteip() if next(ipWhitelist) ~= nil then local cIP = getClientIp() local numIP = 0 if cIP ~= "unknown" then numIP = tonumber(ipToDecimal(cIP)) end for _,ip in pairs(ipWhitelist) do local s, e = string.find(ip, '-', 0, true) if s == nil and cIP == ip then return true elseif s ~= nil then sIP = tonumber(ipToDecimal(string.sub(ip, 0, s - 1))) eIP = tonumber(ipToDecimal(string.sub(ip, e + 1, string.len(ip)))) if numIP >= sIP and numIP = sIP and numIP = max_bind_count then should_bind = bind_reaseon.limit_bind elseif tonumber(bind_count) >= 1 then should_bind = bind_reaseon.robot end<br /> if not should_bind then if check_is_reading_list() then should_bind = bind_reaseon.robot end end end<br /> if not should_bind then if is_white == nil or (is_white ~= "wx" and is_white ~= "spider") then res, err = cache:incr(key_count_perday) if res == nil then res = 0 end if res == 1 then cache:expire(key_count_perday, 86400) end if res >= max_connect_count_perday then should_bind = bind_reaseon.limit_perday end end end<br /> return 1, should_bindend<br />local function check_visit_limit()<br /> local should_bind<br /> local redis = require "resty.redis" local cache = redis:new() cache:set_timeout(300000) local ok, err = cache:connect("192.168.3.129", 10005)<br /> if ok then ok, should_bind = check_access(cache) if ok then cache:set_keepalive(60000, 200) else cache:close() end else ngx.log(ngx.INFO, "failed to connect redis" .. tostring(err)) end<br /> if should_bind == bind_reaseon.limit_bind then ngx.exit(456) elseif should_bind == bind_reaseon.limit_perday then ngx.exit(457) elseif should_bind == bind_reaseon.robot then local source = ngx.encode_base64(ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.request_uri) -- 前端提供的验证码页 local dest = "http://authcode.xxx.cn/authcode.html" .. "?fromurl=" .. source        -- 触发策略,跳转到验证码页面 ngx.redirect(dest, 302) endend<br />local function doVerify() if whiteip() then elseif blockip() then else check_visit_limit() endend<br />doVerify()

  注意:人机验证依赖redis存储统计信息,同时也可以通过匹配客户端的IP来匹配,用于解封误封的客户端。

  总结

  经过长时间的流量分析、攻防实战,通过自建的WAF我们防住了大部分的恶意访问。正所谓“道高一尺,魔高一丈”,如今的盗采行为已经和常规访问无差别,通过一般的人机验证已经无法区分。过于严格的策略,则会“伤敌一千,自损八百”,因此我们还是要找到一个合适平衡点。

  

  

  你与世界

  只差一个

  公众号

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线