Commit 849cac69 by edetmt

init waf

0 parents
- Nginx+Lua实现自定义WAF防护(Web application firewall)
- 源项目为 https://github.com/loveshell/ngx_lua_waf 只做些许更改
### nginx配置
nginx.conf
```
# lua_waf
lua_shared_dict limit 50m;
lua_shared_dict blackip 50m;
lua_package_path "/usr/local/nginx/conf/waf/?.lua";
init_by_lua_file /usr/local/nginx/conf/waf/init.lua;
access_by_lua_file /usr/local/nginx/conf/waf/access.lua;
```
CC攻击拦截
![输入图片说明](https://oscimg.oschina.net/oscnet/efaccc2866f958aa14a64426e1b2cf36066.jpg "在这里输入图片标题")
elk日志分析
![输入图片说明](https://images.gitee.com/uploads/images/2019/0221/204828_74e2e35f_747638.jpeg "在这里输入图片标题")
###########################
更新日志:
增加了whiteip cdip的功能,用以匹配ip段
121.29.53.0/24
120.55.146.0/24
增加config_set_ip_addr参数,用以指定获取源地址的方式:X_Forwarded_For X_real_ip[header] or ngx.var.remote_addr
config_set_ip_addr = "X_Forwarded_For"
增加cc.rule -- 针对不同域名
.*.abc.com|1/60 //匹配所有子域名
oa.abc.com|60/60
默认规则在config.lua里面配置[config_cc_rate]
增加black_ip_in_cache功能
命中一次cc攻击后,拉入black_ip_in_cache,缓存600s[config_black_ip_cache]
参数rulematch
rulematch(unescape(ARGS_DATA),rule,"jo") 修改
为 rulematch(unescape(ARGS_DATA),rule,"joi")
----------------------------------
i 大小写不敏感模式.
防止参数攻击(select注入) 绕过waf:
http://abc.com?app="sEleCt * fRom dual"
匹配模式不区分大小写
增加post_attack_check防止利用简单密码爆破,或者利用post参数列表插入非法参数
测试方法:
curl -H "Host:www.abc.com" -X POST -d "password=123456" http://www.abc.com/6666666666
curl -H "Host:yum.ops.net" -X POST -d "hj=select * FroM *" http://127.0.0.1:8088/script/install-dev.sh
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="zh-cn" />
<title>网站防火墙</title>
</head>
<body>
<h1 align="center"> 网站waf防火墙已拦截 </h1>
</body>
</html>
require 'config'
require 'init'
function waf_main()
if white_ip_check() then
elseif black_ip_check() then
elseif white_url_check() then
elseif user_agent_attack_check() then
elseif cc_attack_check() then
elseif cookie_attack_check() then
elseif url_attack_check() then
elseif url_args_attack_check() then
elseif post_attack_check() then
else
return
end
end
if config_waf_enable == "on" then
waf_main()
end
--WAF config file,enable = "on",disable = "off"
--waf status # waf 开关
config_waf_enable = "on"
--log dir
config_log_dir = "/usr/local/nginx/logs"
--rule setting
config_rule_dir = "/usr/local/nginx/conf/waf/rule-config"
--set ip addr from: X_Forwarded_For X_real_ip[header] or ngx.var.remote_addr
config_set_ip_addr = "X_Forwarded_For"
--enable/disable white url #是否开启url检测
config_white_url_check = "on"
--enable/disable white ip #是否开启IP白名单检测
config_white_ip_check = "on"
--enable/disable block ip #是否开启ip黑名单检测
config_black_ip_check = "on"
--black ip in cache time of xxx secondsa # 命中一次cc攻击后,拉入black_ip_in_cache,缓存600s
config_black_ip_cache = "600"
--enable/disable url filtering #是否开启url过滤
config_url_check = "on"
--enalbe/disable url args filtering #是否开启参数检测
config_url_args_check = "on"
--enable/disable user agent filtering #是否开启ua检测
config_user_agent_check = "on"
--enable/disable cookie deny filtering #是否开启cookie检测
config_cookie_check = "on"
--enable/disable cc filtering #启用cc防御
config_cc_check = "on"
--cc rate the xxx of xxx secondsa #允许单个ip60秒内只能访问10次
config_cc_rate = "10/60"
--enable/disable post filtering #是否开启post检测
config_post_check = "on"
--config waf output redirect/html # 选择跳转url或者html
config_waf_output = "html"
--if config_waf_output ,setting url
config_waf_redirect_url = "http://www.baidu.com"
config_output_html=[[
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="zh-cn" />
<title>网站waf防火墙</title>
</head>
<body>
<h1 align="center"> 网站waf防火墙 </h1>
<br/>
<h4 align="center">由于你访问的URL可能对网站造成安全威胁,访问被阻断</h4>
<br/><br/><br/><br/><br/>
<img src="https://oscimg.oschina.net/oscnet/c91b537ae2dcdabca9bab58fc3807f74259.jpg">
<hr/>
</body>
</html>
]]
--WAF Action
require 'config'
require 'lib'
--args
local rulematch = ngx.re.find
local unescape = ngx.unescape_uri
--Get whiteIP by iputils
function white_ip_check()
if config_white_ip_check == "on" then
local iputils = require("iputils")
-- iputils.enable_lrucache()
local IP_WHITE_RULE = get_rule('whiteip.rule')
local CLIENT_IP = get_client_ip()
whitelist = iputils.parse_cidrs(IP_WHITE_RULE)
if whitelist ~= nil then
if iputils.ip_in_cidrs(CLIENT_IP, whitelist) then
-- log_record('White_IP',ngx.var.request_uri,CLIENT_IP,"_")
return true
end
end
end
end
--allow white ip
function white_ip_check_old()
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 ~= "" and rulematch(WHITE_IP,rule,"jo") then
log_record('White_IP',ngx.var.request_uri,"_","_")
return true
end
end
end
end
end
--deny black ip
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()
local blackip = ngx.shared.blackip
local req,_ = blackip:get(BLACK_IP)
if req then
log_record('Blackip_in_cache',ngx.var.request_uri,"_","_")
waf_output() -- ngx.exit(403)
end
if IP_BLACK_RULE ~= nil then
for _,rule in pairs(IP_BLACK_RULE) do
if rule ~= "" and rulematch(BLACK_IP,rule,"jo") then
log_record('BlackList_IP',ngx.var.request_uri,"_","_")
ngx.exit(403)
return true
end
end
end
end
end
--allow white url
function white_url_check()
if config_white_url_check == "on" then
local URL_WHITE_RULES = get_rule('whiteurl.rule')
local REQ_URI = ngx.var.request_uri
if URL_WHITE_RULES ~= nil then
for _,rule in pairs(URL_WHITE_RULES) do
if rule ~= "" and rulematch(REQ_URI,rule,"jo") then
return true
end
end
end
end
end
function get_cc_v(SERVER_NAME)
local CC_RULES = get_rule('cc.rule')
local CCcount = tonumber(string.match(config_cc_rate,'(.*)/'))
local CCseconds = tonumber(string.match(config_cc_rate,'/(.*)'))
if CC_RULES ~= nil then
for _,rule in pairs(CC_RULES) do
server_name, err=string.match(rule,'(.*)|.*')
if server_name ~= nil and rulematch(SERVER_NAME,server_name,"jo") then
CCcount=string.match(rule,'.*|(.*)/')
CCseconds=string.match(rule,'.*|.*/(.*)')
break
end
end
end
return CCcount..'/'..CCseconds
end
--deny slow cc attack
function cc_attack_check()
if config_cc_check == "on" then
local ATTACK_URI=ngx.var.uri
local SERVER_NAME = ngx.var.http_host
local CLIENT_IP = get_client_ip()
if SERVER_NAME == nil then
SERVER_NAME="no_serverName"
end
local CC_TOKEN = CLIENT_IP..SERVER_NAME..ATTACK_URI
local limit = ngx.shared.limit
local blackip = ngx.shared.blackip
local req,_ = limit:get(CC_TOKEN)
CC_V = get_cc_v(SERVER_NAME)
CCcount=tonumber(string.match(CC_V,'(.*)/'))
CCseconds=tonumber(string.match(CC_V,'/(.*)'))
blackip_seconds=tonumber(config_black_ip_cache)
if req then
if req > CCcount then
log_record('CC_Attack',ATTACK_URI,CC_TOKEN,"-")
blackip:add(CLIENT_IP,1,blackip_seconds)
waf_output() -- ngx.exit(403)
else
limit:incr(CC_TOKEN,1)
end
else
limit:set(CC_TOKEN,1,CCseconds)
end
end
return false
end
--deny flood(1 second) cc attack
--deny cookie
function cookie_attack_check()
if config_cookie_check == "on" then
local COOKIE_RULES = get_rule('cookie.rule')
local USER_COOKIE = ngx.var.http_cookie
if USER_COOKIE ~= nil then
for _,rule in pairs(COOKIE_RULES) do
if rule ~="" and rulematch(USER_COOKIE,rule,"jo") then
log_record('Deny_Cookie',ngx.var.request_uri,"-",rule)
waf_output()
return true
end
end
end
end
return false
end
--deny url
function url_attack_check()
if config_url_check == "on" then
local URL_RULES = get_rule('url.rule')
local REQ_URI = ngx.var.request_uri
for _,rule in pairs(URL_RULES) do
if rule ~="" and rulematch(REQ_URI,rule,"jo") then
log_record('Deny_URL',REQ_URI,"-",rule)
waf_output()
return true
end
end
end
return false
end
--deny url args
function url_args_attack_check()
if config_url_args_check == "on" then
local ARGS_RULES = get_rule('args.rule')
for _,rule in pairs(ARGS_RULES) do
local REQ_ARGS = ngx.req.get_uri_args()
for key, val in pairs(REQ_ARGS) do
if type(val) == 'table' then
ARGS_DATA = table.concat(val, " ")
else
ARGS_DATA = val
end
if ARGS_DATA and type(ARGS_DATA) ~= "boolean" and rule ~="" and rulematch(unescape(ARGS_DATA),rule,"joi") then
log_record('Deny_URL_Args',ngx.var.request_uri,"-",rule)
waf_output()
return true
end
end
end
end
return false
end
--deny user agent
function user_agent_attack_check()
if config_user_agent_check == "on" then
local USER_AGENT_RULES = get_rule('useragent.rule')
local USER_AGENT = ngx.var.http_user_agent
if USER_AGENT ~= nil then
for _,rule in pairs(USER_AGENT_RULES) do
if rule ~="" and rulematch(USER_AGENT,rule,"jo") then
log_record('Deny_USER_AGENT',ngx.var.request_uri,"-",rule)
waf_output()
return true
end
end
end
end
return false
end
--deny post
function post_attack_check()
if config_post_check == "on" and ngx.var.request_method == "POST" then
local POST_RULES = get_rule('post.rule')
ngx.req.read_body()
local POST_ARGS,err = ngx.req.get_post_args()
for _,rule in pairs(POST_RULES) do
if POST_ARGS ~= nil then
for key, val in pairs(POST_ARGS) do
if type(val) == 'table' then
local f_val = {}
for _,item in pairs(val) do
if type(item) ~= "boolean" then
table.insert(f_val,item)
end
end
ARGS_DATA = table.concat(f_val, " ")
else
ARGS_DATA = val
end
if ARGS_DATA and type(ARGS_DATA) ~= "boolean" then
-- filter post args
if rulematch(unescape(ARGS_DATA),rule,"joi") then
log_record('Deny_Post_Args',ngx.var.request_uri,"POST",rule)
waf_output()
return true
end
-- filter post args key=var like password=123456
KV_ARGS = key.."="..ARGS_DATA
if rulematch(unescape(KV_ARGS),rule,"joi") then
log_record('Deny_Post_KV',ngx.var.request_uri,"POST",rule)
waf_output()
return true
end
end
end
end
end
end
return false
end
local ipairs, tonumber, tostring, type = ipairs, tonumber, tostring, type
local bit = require("bit")
local lshift = bit.lshift
local band = bit.band
local bor = bit.bor
local xor = bit.bxor
local byte = string.byte
local str_find = string.find
local str_sub = string.sub
local lrucache = nil
local _M = {
_VERSION = '0.3.0',
}
local mt = { __index = _M }
-- Precompute binary subnet masks...
local bin_masks = {}
for i=0,32 do
bin_masks[tostring(i)] = lshift((2^i)-1, 32-i)
end
-- ... and their inverted counterparts
local bin_inverted_masks = {}
for i=0,32 do
local i = tostring(i)
bin_inverted_masks[i] = xor(bin_masks[i], bin_masks["32"])
end
local log_err
if ngx then
log_err = function(...)
ngx.log(ngx.ERR, ...)
end
else
log_err = function(...)
print(...)
end
end
local function split_octets(input)
local pos = 0
local prev = 0
local octs = {}
for i=1, 4 do
pos = str_find(input, ".", prev, true)
if pos then
if i == 4 then
-- Should not have a match after 4 octets
return nil, "Invalid IP"
end
octs[i] = str_sub(input, prev, pos-1)
elseif i == 4 then
-- Last octet, get everything to the end
octs[i] = str_sub(input, prev, -1)
break
else
return nil, "Invalid IP"
end
prev = pos +1
end
return octs
end
local function unsign(bin)
if bin < 0 then
return 4294967296 + bin
end
return bin
end
local function ip2bin(ip)
if lrucache then
local get = lrucache:get(ip)
if get then
return get[1], get[2]
end
end
if type(ip) ~= "string" then
return nil, "IP must be a string"
end
local octets = split_octets(ip)
if not octets or #octets ~= 4 then
return nil, "Invalid IP"
end
-- Return the binary representation of an IP and a table of binary octets
local bin_octets = {}
local bin_ip = 0
for i,octet in ipairs(octets) do
local bin_octet = tonumber(octet)
if not bin_octet or bin_octet < 0 or bin_octet > 255 then
return nil, "Invalid octet: "..tostring(octet)
end
bin_octets[i] = bin_octet
bin_ip = bor(lshift(bin_octet, 8*(4-i) ), bin_ip)
end
bin_ip = unsign(bin_ip)
if lrucache then
lrucache:set(ip, {bin_ip, bin_octets})
end
return bin_ip, bin_octets
end
_M.ip2bin = ip2bin
local function split_cidr(input)
local pos = str_find(input, "/", 0, true)
if not pos then
return {input}
end
return {str_sub(input, 1, pos-1), str_sub(input, pos+1, -1)}
end
local function parse_cidr(cidr)
local mask_split = split_cidr(cidr, '/')
local net = mask_split[1]
local mask = mask_split[2] or "32"
local mask_num = tonumber(mask)
if not mask_num or (mask_num > 32 or mask_num < 0) then
return nil, "Invalid prefix: /"..tostring(mask)
end
local bin_net, err = ip2bin(net) -- Convert IP to binary
if not bin_net then
return nil, err
end
local bin_mask = bin_masks[mask] -- Get masks
local bin_inv_mask = bin_inverted_masks[mask]
local lower = band(bin_net, bin_mask) -- Network address
local upper = bor(lower, bin_inv_mask) -- Broadcast address
return unsign(lower), unsign(upper)
end
_M.parse_cidr = parse_cidr
local function parse_cidrs(cidrs)
local out = {}
local i = 1
for _,cidr in ipairs(cidrs) do
local lower, upper = parse_cidr(cidr)
if not lower then
log_err("Error parsing '", cidr, "': ", upper)
else
out[i] = {lower, upper}
i = i+1
end
end
return out
end
_M.parse_cidrs = parse_cidrs
local function ip_in_cidrs(ip, cidrs)
local bin_ip, bin_octets = ip2bin(ip)
if not bin_ip then
return nil, bin_octets
end
for _,cidr in ipairs(cidrs) do
if bin_ip >= cidr[1] and bin_ip <= cidr[2] then
return true
end
end
return false
end
_M.ip_in_cidrs = ip_in_cidrs
local function binip_in_cidrs(bin_ip_ngx, cidrs)
if 4 ~= #bin_ip_ngx then
return false, "invalid IP address"
end
local bin_ip = 0
for i=1,4 do
bin_ip = bor(lshift(bin_ip, 8), byte(bin_ip_ngx, i))
end
bin_ip = unsign(bin_ip)
for _,cidr in ipairs(cidrs) do
if bin_ip >= cidr[1] and bin_ip <= cidr[2] then
return true
end
end
return false
end
_M.binip_in_cidrs = binip_in_cidrs
return _M
--waf core lib
require 'config'
--Get the client IP
function get_client_ip()
local SET_IP_FROM = config_set_ip_addr
if SET_IP_FROM == "X_Forwarded_For" then
CLIENT_IP = ngx.req.get_headers()["X_Forwarded_For"]
end
if SET_IP_FROM == "X_real_ip" then
CLIENT_IP = ngx.req.get_headers()["X_real_ip"]
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
--Get the client user agent
function get_user_agent()
USER_AGENT = ngx.var.http_user_agent
if USER_AGENT == nil then
USER_AGENT = "unknown"
end
return USER_AGENT
end
--Get WAF rule
function get_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
if line ~= nil and line ~= "" then
table.insert(RULE_TABLE,line)
end
end
RULE_FILE:close()
return(RULE_TABLE)
end
--WAF log record for json,(use logstash codec => json)
function log_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 = {
ip_from = config_set_ip_addr,
client_ip = CLIENT_IP,
local_time = LOCAL_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 LOG_NAME = LOG_PATH..'/'.."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
--WAF return
function waf_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
\.\./
\:\$
\$\{
select.+(from|limit)
(?:(union(.*?)select))
having|rongjitest
sleep\((\s*)(\d*)(\s*)\)
benchmark\((.*)\,(.*)\)
base64_decode\(
(?:from\W+information_schema\W)
(?:(?:current_)user|database|schema|connection_id)\s*\(
(?:etc\/\W*passwd)
into(\s+)+(?:dump|out)file\s*
group\s+by.+\(
xwork.MethodAccessor
(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\(
xwork\.MethodAccessor
(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/
java\.lang
\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[
\<(iframe|script|body|img|layer|div|meta|style|base|object|input)
(onmouseover|onerror|onload)\=
File mode changed
abc.com|20/60
oa.abc.com|6/60
\.\./
\:\$
\$\{
select.+(from|limit)
(?:(union(.*?)select))
having|rongjitest
sleep\((\s*)(\d*)(\s*)\)
benchmark\((.*)\,(.*)\)
base64_decode\(
(?:from\W+information_schema\W)
(?:(?:current_)user|database|schema|connection_id)\s*\(
(?:etc\/\W*passwd)
into(\s+)+(?:dump|out)file\s*
group\s+by.+\(
xwork.MethodAccessor
(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\(
xwork\.MethodAccessor
(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/
java\.lang
\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[
password=123456$
select.+(from|limit)
(?:(union(.*?)select))
having|rongjitest
sleep\((\s*)(\d*)(\s*)\)
benchmark\((.*)\,(.*)\)
base64_decode\(
(?:from\W+information_schema\W)
(?:(?:current_)user|database|schema|connection_id)\s*\(
(?:etc\/\W*passwd)
into(\s+)+(?:dump|out)file\s*
group\s+by.+\(
xwork.MethodAccessor
(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\(
xwork\.MethodAccessor
(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/
java\.lang
\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[
\<(iframe|script|body|img|layer|div|meta|style|base|object|input)
(onmouseover|onerror|onload)\=
\.(htaccess|bash_history)
\.(bak|inc|old|mdb|sql|backup|java|class|tgz|gz|tar|zip)$
(phpmyadmin|jmx-console|admin-console|jmxinvokerservlet)
java\.lang
\.svn\/
/(attachments|upimg|images|css|uploadfiles|html|uploads|templets|static|template|data|inc|forumdata|upload|includes|cache|avatar)/(\\w+).(php|jsp)
(HTTrack|harvest|audit|dirbuster|pangolin|nmap|sqln|-scan|hydra|Parser|libwww|BBBike|sqlmap|w3af|owasp|Nikto|fimap|havij|PycURL|zmeu|BabyKrokodil|netsparker|httperf|bench)
10.0.0.0/8
172.16.0.0/16
192.168.0.0/16
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!