目前,很多WEB服务都具备了能够防御单IP发起的CC、扫描、采集等恶意行为的工具或模块,如Nginx的HttpLimitReqModule模块,Apache的mod_evasive模块,而且OWASP规则自身也包含了针对单IP发起的DOS攻击防护,但通过在云计算行业的多年工作经验中发现,为了避免单IP的恶意行为被拦截,黑客甚至会直接在运营商处租赁1个,或多个C的IP段,然后使用C段内的IP循环发起恶意行为,降低了单个IP单位时间内的访问频率,导致上述防御手段无法触发。
针对上述情况,本文将讲述个人所研究出的四种方案,用于应对C段IP地址发起的恶意行为,并讲述对应的防御思路、根据防御思路所编写的具体规则以及方案的优缺点。其中方案1不可用,不可用原因是ModSecurity自身机制导致,但方案1确实为最优方案,其中也描写了发现其不可用原因的过程。
如果网站并发较小,可直接采用方案二,反之则建议采用方案三或方案四,如果自身或公司内部具备开发能力,则建议直接采用方案四。
在进行下述方案时,首先需要定义防御阈值,用于设定指定时间内,达到指定的访问次数时,将C段IP封停的时间,而在OWASP规则中,已经包含了对应的的规则,我们只需进行简单的配置即可。
在crs-setup.conf文件中,找到ID为900260、900700的两条规则,并取消注释:(注意,一旦取消该两条规则,OWASP规则中针对单IP的DOS防御功能也会启动,但由于防御C段IP的规则也会自动对单IP进行防御,因此可将OWASP规则中单IP的DOS防御功能关闭,减少规则匹配次数,详细做法为修改REQUEST-912-DOS-PROTECTION.conf文件名称,直接添加".bak"后缀即可,或直接删除该文件)
SecAction \ "id:900260,\ phase:1,\ nolog,\ pass,\ t:none,\ setvar:'tx.static_extensions=/.jpg/ /.jpeg/ /.png/ /.gif/ /.js/ /.css/ /.ico/ /.svg/ /.webp/'" SecAction \ "id:900700,\ phase:1,\ nolog,\ pass,\ t:none,\ setvar:'tx.dos_burst_time_slice=30',\ setvar:'tx.dos_counter_threshold=5',\ setvar:'tx.dos_block_timeout=600'"
ID为900260的规则定义了静态资源的后缀名,在判断是否是CC攻击或采集行为时,此类静态资源的访问不在计数范围内。
ID为900700的规则定义了防御阈值,即如果在60秒内(tx.dos_burst_time_slice),IP地址的访问频率达到了100次(tx.dos_counter_threshold),就被判定为一次攻击嫌疑,而一旦将IP地址封禁,600秒后(tx.dos_block_timeout)才会解封。可根据自身需求更改上述参数值。
a、通过正则匹配方式,得出客户端IP的C段地址并记录;
b、在全局集合中创建变量,以得出的C段地址进行命名,用于保存该C段IP的访问次数总和,与此同时,由于是判断指定时间内的访问次数是否超过阈值,因此设置该变量的销毁时间为设置的频率时间,即tx.dos_burst_time_slice;
c、当客户端访问WEB服务时,判断对应的C段访问次数是否大于设定的阈值,如果不大于,则访问继续,同时该C段访问次数+1;如果大于,则阻断此次访问,同时修改该变量销毁时间为封停时间,即tx.dos_block_timeout,时间一到,该变量自动销毁,此时该C段IP即可自动解封,恢复对WEB服务的正常访问。
#通过正则获取客户端IP的C段地址,然后在ip集合中创建名为ip_c_msg的变量,将其值设置为IP的C段地址,如,客户端IP为192.168.11.2,则ip_c_msg变量的值为"192.168.11." SecRule REMOTE_ADDR "^((25[0-5]"2[0-4]\d"[01]?\d\d?)\.){3}" "id:10000,nolog,pass,phase:1,capture,setvar:ip.ip_c_msg=%{TX.0}" #判断全局集合中,记录该IP所在C段的访问次数的变量是否超过了设定的阈值 #如果超过,则阻断此次访问,同时将变量的销毁时间设置为封禁时间,否则,将继续执行下方规则 SecRule GLOBAL:%{ip.ip_c_msg} "@ge %{tx.dos_counter_threshold}" "id:10001,drop,log,phase:1,expirevar:global.%{ip.ip_c_msg}=%{tx.dos_block_timeout}" #判断此次访问是否是静态资源,如果不是的话,执行第二条,将全局集合中,记录该IP地址所在C段的访问次数+1,同时设置该变量销毁时间为计数周期%{tx.dos_burst_time_slice},如,客户端IP为192.168.11.2,则global集合中会创建名为"192.168.11."的变量,同时其数值+1 SecRule REQUEST_BASENAME ".*?(\.[a-z0-9]{1,10})?$" "id:10002,phase:5,t:none,t:lowercase,nolog,pass,capture,setvar:tx.extension=/%{TX.1}/,chain" SecRule TX:EXTENSION "!@within %{tx.static_extensions}" "setvar:global.%{ip.ip_c_msg}=+1,expirevar:global.%{ip.ip_c_msg}=%{tx.dos_burst_time_slice}"
优点:
a、规则少,相应的匹配次数少,大并发情况下不会消耗服务器过多资源;
b、仅通过ModSecurity即可进行防御,无需引入第三方插件或工具。
缺点:
该方案有一个致命缺点:无法起到防御效果……原因为ModSecurity自身机制所致,%{}是ModSecurity定义的用于获取指定变量的值的操作符,但是该操作符若出现在规则中的特定位置时将不会执行,如下所示(加粗部分):
SecRule GLOBAL:%{ip.ip_c_msg}"@ge %{tx.dos_counter_threshold}" "id:10001,drop,log,phase:1,expirevar:global.%{ip.ip_c_msg}=%{tx.dos_block_timeout}"
上述规则,旨在对集合中对应C段IP地址的访问次数进行判断,GLOBAL:%{ip.ip_c_msg},个人原本设想的是,ModSecurity会先通过%{}操作符获取ip.ip_c_msg的值,即"192.168.11.",然后再去获取GLOBAL集合中,名称为"192.168.11."变量的值,用于后续判断,但实际此时%{}并不执行,ModSecurity会直接去GLOBAL集合中,获取名称为"%{ip.ip_c_msg}"的变量的值,但集合中根本不存在此变量,因此永远不会触发拦截操作。
针对上述情况,本人又想了另外一种方式,即在IP集合中额外设置一个固定名称的变量,然后将GLOBAL:%{ip.ip_c_msg}的值赋给新的变量,然后直接判断新定义的固定变量的值,如setvar:ip.ip_c_count=%{global.%{ip.ip_c_msg}},但实际测试后发现,此方案也行不通,原因仍然为%{}操作符导致,上述赋值的语句中,个人原本设想,ModSecurity会先获取ip.ip_c_msg的值,即"192.168.11.",然后再去获取GLOBAL集合中,名称为"192.168.11."变量的值,但实际测试发现,%{global.%{ip.ip_c_msg}}的最终结果为"}",个人猜测,ModSecurity应该是通过正则去获取变量名称,而没有判断变量中是否存在多个%{}操作符,因此导致,对于%{global.%{ip.ip_c_msg}},加粗部分为ModSecurity实际匹配到的%{}操作符的开始与结尾,中间部分作为一个整体被当做变量名称,但实际并不存在名称为"global.%{ip.ip_c_msg"这一变量,因此此部分取值为空,而由于后方还有一个"}"符号,因此ip.ip_c_count的值,最终会被赋予为"}"。
如若想解决上述问题,则需要对其源代码进行更改,可惜,本人不会C语言,因此方案1暂时被毙。
该方案防御思路与方案1大体相同,但由于受制于ModSecurity自身的机制,因此调整规则思路,直接对集合中的变量进行循环判断,通过遍历的方式,找出当前IP所处C段对应变量的值,然后再进行判断。
#通过正则获取客户端IP的C段地址,然后在ip集合中创建名为ip_c_msg的变量,将其值设置为IP的C段地址,如,客户端IP为192.168.11.2,则ip_c_msg变量的值为"192.168.11." SecRule REMOTE_ADDR "^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}" \ "id:10000,nolog,pass,phase:1,capture,setvar:ip.ip_c_msg=%{TX.0}" #判断IP集合中的DOS_BLOCK_C的值是否为1,此变量代表该IP所处C段是否触发了阈值 #1代表触发,如果触发,则拦截该IP下的访问,该变量是通过id为10003的规则进行赋值 #与此同时,设置变量DOS_BLOCK_C_FLAG,用于标志是否已触发过拦截 #0代表未触发,1代表触发,同时设置变量在%{tx.dos_block_timeout}时间后自动销毁 #用于保证在拦截期间,一个IP地址在封禁时间内只会记录一次日志,避免日志堆积 SecRule IP:DOS_BLOCK_C "@eq 1" "id:10001,phase:1,drop,log,chain,msg:'Detect Dos Attack from %{ip.ip_c_msg}0/24'" SecRule &IP:DOS_BLOCK_C_FLAG "@eq 0" "setvar:ip.dos_block_c_flag=1,expirevar:ip.dos_block_c_flag=%{tx.dos_block_timeout}" #判断IP集合中的DOS_BLOCK_C的值是否为1,此变量代表该IP所处C段是否触发了阈值 #1代表触发,如果触发,则拦截该IP下的访问,该变量是通过id为10003的规则进行赋值 #与规则10001互补,用于避免日志堆积 SecRule IP:DOS_BLOCK_C "@eq 1" "id:10004,phase:1,drop,nolog" #判断此次访问是否是静态资源,如果不是的话,执行第二条规则 #将全局集合中,记录该IP地址所在C段的访问次数+1 #同时设置该变量销毁时间为计数周期%{tx.dos_burst_time_slice} #如,客户端IP为192.168.11.2,则global集合中会创建名为"ip_c_192.168.11."的变量 #同时其数值+1 SecRule REQUEST_BASENAME ".*?(\.[a-z0-9]{1,10})?$" \ "phase:5,id:10002,t:none,t:lowercase,nolog,pass,capture,setvar:tx.extension=/%{TX.1}/,chain" SecRule TX:EXTENSION "!@within %{tx.static_extensions}" "setvar:'global.ip_c_%{ip.ip_c_msg}=+1',expirevar:global.ip_c_%{ip.ip_c_msg}=%{tx.dos_burst_time_slice}" #遍历GLOBAL集合中,以ip_c为开头的所有变量的值,如果发现大于设定的阈值 #则通过第二条规则,获取出变量名称中的C段名称,然后将对应的ip_c变量的销毁时间,改为封禁时间%{tx.dos_block_timeout} #再通过第三条规则判断是否与当前客户端的C段IP相同,如果相同,则设置ip.dos_block_c变量为1 #同时设置ip.dos_block_c变量销毁时间为封禁时间%{tx.dos_block_timeout} SecRule GLOBAL:/^ip_c/ "@ge %{tx.dos_counter_threshold}" "phase:5,id:10003,pass,nolog,chain" SecRule MATCHED_VAR_NAME "((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}" "capture,setvar:tx.extension=%{TX.0},chain,expirevar:global.ip_c_%{TX.0}=%{tx.dos_block_timeout}" SecRule TX:EXTENSION "@streq %{ip.ip_c_msg}" "setvar:ip.dos_block_c=1,expirevar:ip.dos_block_c=%{tx.dos_block_timeout}"
优点:
仅通过ModSecurity即可进行防御,无需引入第三方插件或工具。
缺点:
a、服务器资源消耗增高。由于是采用遍历的方式,因此每次访问都会对集合中的内容进行遍历,虽然遍历的过程放在了phase5,即日志记录阶段,不会影响网站的响应速度,但会增加服务器的资源消耗;
b、拦截的C段,该C段下的其他IP,只有在访问成功一次之后才会拦截,即,假设192.168.11.2触发拦截机制,导致192.168.11段IP被封,但是当192.168.11.3访问服务器时,第一次访问并不会被拦截,而是从第二次开始访问才会拦截,原因是遍历的过程放在了日志记录阶段,因此只有访问成功一次,进入日志记录阶段时,才会将该C段下的此次访问IP进行拦截,如果要解决此情况,将规则10003的phase改为1即可,但是此举会将遍历的过程置于网站访问过程中,大并发情况下反而可能会影响网站响应速度,同时服务器资源消耗也不会有过多降低,得不偿失。除此之外,即便当192.168.11.3已经被封,但是该IP更换客户端进行访问时,仍会在访问成功一次后才会拦截,原因是拦截标志存放于ip集合中,而ip集合是基于IP+客户端创建的,因此会导致某一IP即使已经被封,换一个客户端进行访问时,会重新生成一个ip集合,而此时该ip集合中并没有设置拦截标志,依然会访问成功一次。
该方案是对方案二的改造,旨在减少大并发情况下,遍历导致的服务器资源消耗增加,具体做法为采用ModSecurity+Lua+ipset+iptables,在遍历过程中,直接将要拦截的C段IP通过ipset进行拦截,然后再将此C段IP的信息从集合中删除,减少遍历次数,以此来达到降低服务器资源消耗的目的。
#通过正则获取客户端IP的C段地址,然后在ip集合中创建名为ip_c_msg的变量,将其值设置为IP的C段地址,如,客户端IP为192.168.11.2,则ip_c_msg变量的值为"192.168.11." SecRule REMOTE_ADDR "^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}" \ "id:10000,nolog,pass,phase:1,capture,setvar:ip.ip_c_msg=%{TX.0}" #判断此次访问是否是静态资源,如果不是的话,执行第二条规则 #将全局集合中,记录该IP地址所在C段的访问次数+1 #同时设置该变量销毁时间为计数周期%{tx.dos_burst_time_slice} #如,客户端IP为192.168.11.2,则global集合中会创建名为"ip_c_192.168.11."的变量 #同时其数值+1 SecRule REQUEST_BASENAME ".*?(\.[a-z0-9]{1,10})?$" \ "phase:5,id:10002,t:none,t:lowercase,nolog,pass,capture,setvar:tx.extension=/%{TX.1}/,chain" SecRule TX:EXTENSION "!@within %{tx.static_extensions}" "setvar:'global.ip_c_%{ip.ip_c_msg}=+1',expirevar:global.ip_c_%{ip.ip_c_msg}=%{tx.dos_burst_time_slice}" #遍历GLOBAL集合中,以ip_c为开头的所有变量的值,如果发现大于设定的阈值 #则通过第二条规则,获取出变量名称中包含的C段信息后,直接销毁集合中对应的变量 #再通过Lua脚本,调用ipset命令,将C段IP地址通过iptables进行封禁 SecRule GLOBAL:/^ip_c/ "@ge %{tx.dos_counter_threshold}" "phase:5,id:10003,pass,log,capture,msg:'Detect Dos Attack from %{TX.0}0/24',chain" SecRule MATCHED_VAR_NAME "((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}" "capture,setvar:!global.ip_c_%{TX.0},exec:/tmp/ms_c_dos.lua"
Lua脚本内容如下:
function main() local ip = m.getvar("REMOTE_ADDR"); local rt= {}; local p='%.'; string.gsub(ip, '[^'..p..']+', function(w) table.insert(rt, w) end ); if #rt == 4 then local ip_c=rt[1].."."..rt[2].."."..rt[3]..".0/24"; os.execute("sudo ipset add list_c_dos "..ip_c); end return nil; end
使用该方案需要额外安装ipset-service,详细教程可参见最后第七章第4小节。
由于ipset命令需要调用系统内核,因此普通用户无法执行ipset命令,因此还需赋予对应用户相应的root权限,操作流程如下所示,而具体生产环境中要根据实际的运行用户进行授权,如宝塔由于设置了WEB服务是以www用户运行,因此将下方命令中的daemon更换为www即可:
#/etc/sudoers默认为只读状态,需要先设置为可写 chmod 640 /etc/sudoers #通过vi进行编辑 vi /etc/sudoers #找到root ALL=(ALL) ALL所在,在下方添加以下内容 daemon ALL=(ALL) NOPASSWD: /usr/sbin/ipset #将/etc/sudoers重置为只读状态 chmod 440 /etc/sudoers
优点:
降低遍历过程中导致的服务器资源消耗增加的情况。
缺点:
a、需要额外安装ipset-service;
b、需要赋予相关用户执行ipset的权限,该举措可能存在隐性未知安全风险。
该方案是对方案三的改造,旨在解决方案三中,必须赋予相关系统账户执行ipset权限所可能带来的未知安全隐患,具体思路为在服务器内额外搭建一个HTTP服务,单独用于执行ipset命令,同时通过设置只允许本地访问+密钥等安全策略,确保该服务的安全性。
#通过正则获取客户端IP的C段地址,然后在ip集合中创建名为ip_c_msg的变量,将其值设置为IP的C段地址,如,客户端IP为192.168.11.2,则ip_c_msg变量的值为"192.168.11." SecRule REMOTE_ADDR "^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}" \ "id:10000,nolog,pass,phase:1,capture,setvar:ip.ip_c_msg=%{TX.0}" #判断此次访问是否是静态资源,如果不是的话,执行第二条规则 #将全局集合中,记录该IP地址所在C段的访问次数+1 #同时设置该变量销毁时间为计数周期%{tx.dos_burst_time_slice} #如,客户端IP为192.168.11.2,则global集合中会创建名为"ip_c_192.168.11."的变量 #同时其数值+1 SecRule REQUEST_BASENAME ".*?(\.[a-z0-9]{1,10})?$" \ "phase:5,id:10002,t:none,t:lowercase,nolog,pass,capture,setvar:tx.extension=/%{TX.1}/,chain" SecRule TX:EXTENSION "!@within %{tx.static_extensions}" "setvar:'global.ip_c_%{ip.ip_c_msg}=+1',expirevar:global.ip_c_%{ip.ip_c_msg}=%{tx.dos_burst_time_slice}" #遍历GLOBAL集合中,以ip_c为开头的所有变量的值,如果发现大于设定的阈值 #则通过第二条规则,获取出变量名称中包含的C段信息后,直接销毁集合中对应的变量 #再通过Lua脚本,访问HTTP服务,令其调用ipset命令,将C段IP地址通过iptables进行封禁 SecRule GLOBAL:/^ip_c/ "@ge %{tx.dos_counter_threshold}" "phase:5,id:10003,pass,log,capture,msg:'Detect Dos Attack from %{TX.0}0/24',chain" SecRule MATCHED_VAR_NAME "((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}" "capture,setvar:!global.ip_c_%{TX.0},exec:/tmp/ms_c_dos.lua"
Lua脚本内容如下:
function main() local ip = m.getvar("REMOTE_ADDR"); local rt= {}; local p='%.'; string.gsub(ip, '[^'..p..']+', function(w) table.insert(rt, w) end ); if #rt == 4 then local ip_c=rt[1].."."..rt[2].."."..rt[3]..".0/24"; os.execute("curl http://localhost:端口/forbidden.php?ip_c="..ip_c.."&auth=123456"); end return nil; end
使用该方案需要额外安装ipset-service,详细教程可参见最后第七章第4小节。
优点:
a、安全性较方案三略微提高;
b、可在额外搭建的HTTP服务中开发新的功能便于日常维护操作,如在HTTP服务中开发封禁IP的记录、查询、解封等功能,避免每次操作均需登录服务器。
缺点:
需要开发额外的HTTP服务。
1.上述方案中的具体规则,建议放置在OWASP规则中REQUEST-901-INITIALIZATION.conf文件的末尾;
2.上述方案中的具体规则,如ID与现有规则的ID冲突,自行修改即可;
3.crs-setup.conf文件中SecCollectionTimeout参数的值,需大于等于tx.dos_block_timeout参数的值;
4.方案三与方案四需要安装与配置ipset-service,教程如下:
#安装ipset服务 yum install ipset-service #设置开启自动启动 systemctl enable ipset #添加名称为list_c_dos的集合,用于保存封禁的IP,同时设置封禁时间为3600秒,封禁时间可按需更改 ipset create list_c_dos hash:ip timeout 3600 #保存ipset配置,避免重启后list_c_dos集合自ipset中丢失 service ipset save #将以下内容复制到/etc/sysconfig/iptables,禁止集合内的IP访问80端口 -A INPUT -m set --match-set list_c_dos src -p tcp --dport 80 -j DROP #重启iptables service iptables restart #向list_c_dos集合中添加IP的命令demo #ipset add list_c_dos 192.168.142.0/24
5.上述所有方案均不适用于ModSecurity V3以上版本。受限于ModSecurity团队的开发规划,上述方案规则中使用的部分指令在ModSecurity V3以上版本中暂不支持,如expirevar,该指令将变量配置为在指定时间段(以秒为单位)后销毁,如上述规则中,设定是60秒内访问次数达到100则封禁对应的C段IP,而60秒的这个时间限制是通过expirevar进行设置,但由于此指令在V3以上版本中暂不支持,因此最终的结果是,IP地址的访问次数将永久保存在内存中,一直进行累加操作且永不过期,而一旦总访问次数超过100,IP也会被永久封禁,无法达到封禁指定时间的目的,最终则会形成所有IP均会被拦截的情况,除非重启或重载WEB服务。
最新更新(2024年4月7日):除方案1外,上述其他方案在ModSecurity V3.0.11及以上版本可用,原因是V3.0.11及以上版本支持了expirevar指令。