防攻击的思路咱们都明白,好比限制IP啊,过滤攻击字符串啊,识别攻击指纹啦。但是要如何去实现它呢?用守护脚本吗?用PHP在外面包一层过滤?仍是直接加防火墙吗?这些都是防护手段。不过本文将要介绍的是直接经过nginx的普通模块和配置文件的组合来达到必定的防护效果。nginx
下面就是nginx的配置文件写法。git
if ($cookie_say != "hbnl"){ add_header Set-Cookie "say=hbnl"; rewrite .* "$scheme://$host$uri" redirect; }
让咱们看下这几行的意思,当cookie中say为空时,给一个设置cookie say为hbnl的302重定向包,若是访问者可以在第二个包中携带上cookie值,那么就能正常访问网站了,若是不能的话,那他永远活在了302中。你也能够测试一下,用CC攻击器或者webbench或者直接curl发包作测试,他们都活在了302世界中。github
固然,这么简单就能防住了?固然没有那么简单。web
仔细的你必定会发现配置文件这样写仍是有缺陷。若是攻击者设置cookie为say=hbnl(CC攻击器上就能够这么设置),那么这个防护就形同虚设了。shell
而后,咱们来看下这种方式的配置文件写法浏览器
if ($cookie_say != "hbnl$remote_addr"){ add_header Set-Cookie "say=hbnl$remote_addr"; rewrite .* "$scheme://$host$uri" redirect; }
这样的写法和前面的区别是,不一样IP的请求cookie值是不同的,好比IP是1.2.3.4,那么须要设置的cookie是say=hbnl1.2.3.4。因而攻击者便没法经过设置同样的cookie(好比CC攻击器)来绕过这种限制。你能够继续用CC攻击器来测试下,你会发现CC攻击器打出的流量已经所有进入302世界中。
不过你们也能感受到,这彷佛也不是一个万全之计,由于攻击者若是研究了网站的机制以后,总有办法测出并预先伪造cookie值的设置方法。由于咱们作差别化的数据源正是他们自己的一些信息(IP、user agent等)。攻击者花点时间也是能够作出专门针对网站的攻击脚本的。bash
那么要如何根据他们自身的信息得出他们又得出他们算不出的数值?
我想,聪明的你必定已经猜到了,用salt加散列。好比md5("opencdn$remote_addr"),虽然攻击者知道能够本身IP,可是他没法得知如何用他的IP来计算出这个散列,由于他是逆不出这个散列的。固然,若是你不放心的话,怕cmd5.com万一能查出来的话,能够加一些特殊字符,而后多散几回。
很惋惜,nginx默认是没法进行字符串散列的,因而咱们借助nginx_lua模块来进行实现。服务器
rewrite_by_lua ' local say = ngx.md5("opencdn" .. ngx.var.remote_addr) if (ngx.var.cookie_say ~= say) then ngx.header["Set-Cookie"] = "say=" .. say return ngx.redirect(ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.uri) end ';
经过这样的配置,攻击者便没法事先计算这个cookie中的say值,因而攻击流量(代理型CC和低级发包型CC)便在302地狱没法自拔了。
你们能够看到,除了借用了md5这个函数外,其余的逻辑和上面的写法是如出一辙的。所以若是能够的话,你彻底能够安装一个nginx的计算散列的第三方模块来完成,可能效率会更高一些。
这段配置是能够被放在任意的location里面,若是你的网站有对外提供API功能的话,建议API必定不能加入这段,由于API的调用也是没有浏览器行为的,会被当作攻击流量处理。而且,有些弱一点爬虫也会陷在302之中,这个须要注意。
同时,若是你以为set-cookie这个动做彷佛攻击者也有可能经过解析字符串模拟出来的话,你能够把上述的经过header来设置cookie的操做,变成经过高端大气的js完成,发回一个含有doument.cookie=...的文本便可。
那么,攻击是否是彻底被挡住了呢?只能说那些低级的攻击已经被挡住而来,若是攻击者必须花很大代价给每一个攻击器加上webkit模块来解析js和执行set-cookie才行,那么他也是能够逃脱302地狱的,在nginx看来,确实攻击流量和普通浏览流量是同样的。那么如何防护呢?下节会告诉你答案。cookie
不得不说,不少防CC的措施是直接在请求频率上作限制来实现的,可是,不少都存在着必定的问题。
那么是哪些问题呢?
首先,若是经过IP来限制请求频率,容易致使一些误杀,好比我一个地方出口IP就那么几个,而访问者一多的话,请求频率很容易到上限,那么那个地方的用户就都访问不了你的网站了。
因而你会说,我用SESSION来限制就有这个问题了。嗯,你的SESSION为攻击者敞开了一道大门。为何呢?看了上文的你可能已经大体知道了,由于就像那个“红包拿来”的扬声器同样,不少语言或者框架中的SESSION是可以伪造的。以PHP为例,你能够在浏览器中的cookie看到PHPSESSIONID,这个ID不一样的话,session也就不一样了,而后若是你杜撰一个PHPSESSIONID过去的话,你会发现,服务器也承认了这个ID,为这个ID初始化了一个会话。那么,攻击者只须要每次发完包就构造一个新的SESSIONID就能够很轻松地躲过这种在session上的请求次数限制。
那么咱们要如何来作这个请求频率的限制呢?
首先,咱们先要一个攻击者没法杜撰的sessionID,一种方式是用个池子记录下每次给出的ID,而后在请求来的时候进行查询,若是没有的话,就拒绝请求。这种方式咱们不推荐,首先一个网站已经有了session池,这样再作个无疑有些浪费,并且还须要进行池中的遍历比较查询,太消耗性能。咱们但愿的是一种能够无状态性的sessionID,能够吗?能够的。session
rewrite_by_lua ' local random = ngx.var.cookie_random if(random == nil) then random = math.random(999999) end local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random) if (ngx.var.cookie_token ~= token) then ngx.header["Set-Cookie"] = {"token=" .. token, "random=" .. random} return ngx.redirect(ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.uri) end ';
你们是否是以为好像有些眼熟?是的,这个就是上节的完美版的配置再加个随机数,为的是让同一个IP的用户也能有不一样的token。一样的,只要有nginx的第三方模块提供散列和随机数功能,这个配置也能够不用lua直接用纯配置文件完成。
有了这个token以后,至关于每一个访客有一个没法伪造的而且独一无二的token,这种状况下,进行请求限制才有意义。
因为有了token作铺垫,咱们能够不作什么白名单、黑名单,直接经过limit模块来完成。
http{ ... limit_req_zone $cookie_token zone=session_limit:3m rate=1r/s; }
而后咱们只须要在上面的token配置后面中加入
limit_req zone=session_limit burst=5;
因而,又是两行配置便让nginx在session层解决了请求频率的限制。不过彷佛仍是有缺陷,由于攻击者能够经过一直获取token来突破请求频率限制,若是能限制一个IP获取token的频率就更完美了。能够作到吗?能够。同时,咱们也要感谢一下Tengine,Tengine的limit模块能够支持多个变量。本文的全部的实验均在Tengine下完成。
http{ ... limit_req_zone $cookie_token zone=session_limit:3m rate=1r/s; limit_req_zone $binary_remote_addr $uri zone=auth_limit:3m rate=1r/m; }
location /{ limit_req zone=session_limit burst=5; rewrite_by_lua ' local random = ngx.var.cookie_random if (random == nil) then return ngx.redirect("/auth?url=" .. ngx.var.request_uri) end local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random) if (ngx.var.cookie_token ~= token) then return ngx.redirect("/auth?url=".. ngx.var.request_uri) end '; } location /auth { limit_req zone=auth_limit burst=1; if ($arg_url = "") { return 403; } access_by_lua ' local random = math.random(9999) local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random) if (ngx.var.cookie_token ~= token) then ngx.header["Set-Cookie"] = {"token=" .. token, "random=" .. random} return ngx.redirect(ngx.var.arg_url) end '; }
我想你们也应该已经猜到,这段配置文件的原理就是:把原本的发token的功能分离到一个auth页面,而后用limit对这个auth页面进行频率限制便可。这边的频率是1个IP每分钟受权1个token。固然,这个数量能够根据业务须要进行调整。
须要注意的是,这个auth部分我lua采用的是access_by_lua,缘由在于limit模块是在rewrite阶段后执行的,若是在rewrite阶段302的话,limit将会失效。所以,这段lua配置我不能保证能够用原生的配置文件实现,由于不知道如何用配置文件在rewrite阶段后进行302跳转,也求大牛可以指点一下啊。
固然,你若是还不知足于这种限制的话,想要作到某个IP若是一天到达上限超过几回以后就直接封IP的话,也是能够的,你能够用相似的思路再作个错误页面,而后到达上限以后不返回503而是跳转到那个错误页面,而后错误页面也作个请求次数限制,好比天天只能访问100次,那么当超过报错超过100次(请求错误页面100次)以后,那天这个IP就不能再访问这个网站了。
因而,经过这些配置咱们便实现了一个网站访问频率限制。不过,这样的配置也不是说能够彻底防止了攻击,只能说让攻击者的成本变高,让网站的扛攻击能力变强,固然,前提是nginx可以扛得住这些流量,而后带宽不被堵死。若是你家门被堵了,你还想开门营业,那真心没有办法了。
而后,作完流量上的防御,让咱们来看看对于扫描器之类的攻击的防护。
ngx_lua_waf模块: https://github.com/loveshell/ngx_lua_waf这个是一个不错的waf模块,这块咱们也无需重复造轮子。能够直接用这个模块来作防御,固然也彻底能够再配合limit模块,用上文的思路来作到一个封IP或者封session的效果。