--[[ 免费WAF 功能比较少 ]]-- local cpath = "/www/server/free_waf/" local jpath = cpath .. "rule/" local json = require "cjson" local ngx_match = ngx.re.find error_rule = nil function read_file(name) fbody = read_file_body(jpath .. name .. '.json') if fbody == nil then return {} end return json.decode(fbody) end function read_file_body(filename) fp = io.open(filename,'r') if fp == nil then return nil end fbody = fp:read("*a") fp:close() if fbody == '' then return nil end return fbody end function write_file(filename,body) fp = io.open(filename,'w') if fp == nil then return nil end fp:write(body) fp:flush() fp:close() return true end local config = json.decode(read_file_body(cpath .. 'config.json')) local site_config = json.decode(read_file_body(cpath .. 'site.json')) function is_ipaddr(client_ip) local cipn = split(client_ip,'.') if arrlen(cipn) < 4 then return false end for _,v in ipairs({1,2,3,4}) do local ipv = tonumber(cipn[v]) if ipv == nil then return false end if ipv > 255 or ipv < 0 then return false end end return true end function compare_ip_block(ips) if not ips then return false end if string.find(ips,':') then return false end ips = arrip(ips) if not is_max(ips,arrip("127.0.0.255")) then return false end if not is_min(ips,arrip("127.0.0.1")) then return false end return true end function get_client_ip() local client_ip = "unknown" if site_config[server_name] then if site_config[server_name]['cdn'] then for _,v in ipairs(site_config[server_name]['cdn_header']) do if request_header[v] ~= nil and request_header[v] ~= "" then local header_tmp = request_header[v] if type(header_tmp) == "table" then header_tmp = header_tmp[1] end client_ip = split_bylog(header_tmp,',')[1] if compare_ip_block(client_ip) then if tostring(ngx.var.remote_addr) == tostring(client_ip) then client_ip = ngx.var.remote_addr else client_ip = ngx.var.remote_addr end break end break; end end end end if type(client_ip) == 'table' then client_ip = "" end if (string.match(client_ip,"^%d+%.%d+%.%d+%.%d+$") == nil and string.match(client_ip,"^[%w:]+$") == nil) or client_ip == 'unknown' then client_ip = ngx.var.remote_addr if client_ip == nil then client_ip = "unknown" end end return client_ip end function split_bylog( str,reps ) local resultStrList = {} string.gsub(str,'[^'..reps..']+',function(w) table.insert(resultStrList,w) end) return resultStrList end function get_client_ip22() local client_ip = "unknown" if site_config[server_name] then if site_config[server_name]['cdn'] then for _,v in ipairs(site_config[server_name]['cdn_header']) do if request_header[v] ~= nil and request_header[v] ~= "" then client_ip = request_header[v] break; end end end end if (string.match(client_ip,"^%d+%.%d+%.%d+%.%d+$") == nil and string.match(client_ip,"^[%w:]+$") == nil) or client_ip == 'unknown' then client_ip = ngx.var.remote_addr if client_ip == nil then client_ip = "unknown" end end return client_ip end function split( str,reps ) local resultStrList = {} string.gsub(str,'[^'..reps..']+',function(w) table.insert(resultStrList,w) end) return resultStrList end function arrip(ipstr) if string.find(ipstr,':') then return ipstr end if ipstr == 'unknown' then return {0,0,0,0} end iparr = split(ipstr,'.') iparr[1] = tonumber(iparr[1]) iparr[2] = tonumber(iparr[2]) iparr[3] = tonumber(iparr[3]) iparr[4] = tonumber(iparr[4]) return iparr end function join(arr,e) result = '' length = arrlen(arr) for k,v in ipairs(arr) do if length == k then e = '' end result = result .. v .. e end return result end function arrlen(arr) if not arr then return 0 end count = 0 for _,v in ipairs(arr) do count = count + 1 end return count end function select_rule(rules) if not rules then return {} end new_rules = {} for i,v in ipairs(rules) do if v[1] == 1 then table.insert(new_rules,v[2]) end end return new_rules end function is_site_config(cname) if site_config[server_name] ~= nil then if cname == 'cc' then return site_config[server_name][cname]['open'] else return site_config[server_name][cname] end end return true end function get_boundary() local header = request_header["content-type"] if not header then return nil end if type(header) == "table" then header = header[1] end local m = string.match(header, ";%s*boundary=\"([^\"]+)\"") if m then return m end return string.match(header, ";%s*boundary=([^\",;]+)") end local get_html = read_file_body(config["reqfile_path"] .. '/' .. config["get"]["reqfile"]) local post_html = read_file_body(config["reqfile_path"] .. '/' .. config["post"]["reqfile"]) local cookie_html = read_file_body(config["reqfile_path"] .. '/' .. config["cookie"]["reqfile"]) local user_agent_html = read_file_body(config["reqfile_path"] .. '/' .. config["user-agent"]["reqfile"]) local other_html = read_file_body(config["reqfile_path"] .. '/' .. config["other"]["reqfile"]) local cnlist = json.decode(read_file_body(cpath .. '/rule/cn.json')) local scan_black_rules = read_file('scan_black') local ip_black_rules = read_file('ip_black') local ip_white_rules = read_file('ip_white') local url_white_rules = read_file('url_white') local url_black_rules = read_file('url_black') local user_agent_rules = select_rule(read_file('user_agent')) local post_rules = select_rule(read_file('post')) local cookie_rules = select_rule(read_file('cookie')) local args_rules = select_rule(read_file('args')) local url_rules = select_rule(read_file('url')) local head_white_rules = read_file('head_white') function is_min(ip1,ip2) n = 0 for _,v in ipairs({1,2,3,4}) do if ip1[v] == ip2[v] then n = n + 1 elseif ip1[v] > ip2[v] then break else return false end end return true end function is_max(ip1,ip2) n = 0 for _,v in ipairs({1,2,3,4}) do if ip1[v] == ip2[v] then n = n + 1 elseif ip1[v] < ip2[v] then break else return false end end return true end function compare_ip(ips) if ip == 'unknown' then return true end if string.find(ip,':') then return false end if not is_max(ipn,ips[2]) then return false end if not is_min(ipn,ips[1]) then return false end return true end function write_log(name,rule) local count,_ = ngx.shared.free_waf_drop_ip:get(ip) if count then ngx.shared.free_waf_drop_ip:incr(ip,1) else ngx.shared.free_waf_drop_ip:set(ip,1,retry_cycle) end if config['log'] ~= true or is_site_config('log') ~= true then return false end local method = ngx.req.get_method() if error_rule then rule = error_rule error_rule = nil end local logtmp = {ngx.localtime(),ip,method,request_uri,ngx.var.http_user_agent,name,rule} local logstr = json.encode(logtmp) .. "\n" local count,_ = ngx.shared.free_waf_drop_ip:get(ip) if count > retry and name ~= 'cc' then local safe_count,_ = ngx.shared.free_waf_drop_sum:get(ip) if not safe_count then ngx.shared.free_waf_drop_sum:set(ip,1,86400) safe_count = 1 else ngx.shared.free_waf_drop_sum:incr(ip,1) end local lock_time = retry_time * safe_count if lock_time > 86400 then lock_time = 86400 end logtmp = {ngx.localtime(),ip,method,request_uri,ngx.var.http_user_agent,name,retry_cycle .. '秒以内累计超过'..retry..'次以上非法请求,封锁'.. lock_time ..'秒'} logstr = logstr .. json.encode(logtmp) .. "\n" ngx.shared.free_waf_drop_ip:set(ip,retry+1,lock_time) write_drop_ip('inc',lock_time) end write_to_file(logstr) inc_log(name,rule) end function write_drop_ip(is_drop,drop_time) local filename = cpath .. 'drop_ip.log' local fp = io.open(filename,'ab') if fp == nil then return false end local logtmp = {os.time(),ip,server_name,request_uri,drop_time,is_drop} local logstr = json.encode(logtmp) .. "\n" fp:write(logstr) fp:flush() fp:close() return true end function inc_log(name,rule) local total_path = cpath .. 'total.json' local tbody = read_file_body(total_path) if not tbody then return false end local total = json.decode(tbody) if not total['sites'] then total['sites'] = {} end if not total['sites'][server_name] then total['sites'][server_name] = {} end if not total['sites'][server_name][name] then total['sites'][server_name][name] = 0 end if not total['rules'] then total['rules'] = {} end if not total['rules'][name] then total['rules'][name] = 0 end if not total['total'] then total['total'] = 0 end total['total'] = total['total'] + 1 total['sites'][server_name][name] = total['sites'][server_name][name] + 1 total['rules'][name] = total['rules'][name] + 1 local total_log = json.encode(total) if not total_log then return false end write_file(total_path,total_log) end function write_to_file(logstr) local filename = config["logs_path"] .. '/' .. server_name .. '_' .. ngx.today() .. '.log' local fp = io.open(filename,'ab') if fp == nil then return false end fp:write(logstr) fp:flush() fp:close() return true end function drop_abroad() if ip == 'unknown' then return false end if not config['drop_abroad']['open'] or not is_site_config('drop_abroad') then return false end for _,v in ipairs(cnlist) do if compare_ip(v) then return false end end ngx.exit(config['drop_abroad']['status']) return true end function drop() local count,_ = ngx.shared.free_waf_drop_ip:get(ip) if not count then return false end if count > retry then ngx.exit(config['cc']['status']) return true end return false end function cc() if not config['cc']['open'] or not site_cc then return false end local token = ngx.md5(ip .. '_' .. request_uri) local count,_ = ngx.shared.free_waf:get(token) if count then if count > limit then local safe_count,_ = ngx.shared.free_waf_drop_sum:get(ip) if not safe_count then ngx.shared.free_waf_drop_sum:set(ip,1,86400) safe_count = 1 else ngx.shared.free_waf_drop_sum:incr(ip,1) end local lock_time = (endtime * safe_count) if lock_time > 86400 then lock_time = 86400 end ngx.shared.free_waf_drop_ip:set(ip,retry+1,lock_time) write_log('cc',cycle..'秒内累计超过'..limit..'次请求,封锁' .. lock_time .. '秒') write_drop_ip('cc',lock_time) ngx.exit(config['cc']['status']) return true else ngx.shared.free_waf:incr(token,1) end else ngx.shared.free_waf:set(token,1,cycle) end return false end function cc2() if not config['cc']['open'] or not site_cc then return false end if not site_config[server_name] then return false end if not site_config[server_name]['cc']['increase'] then return false end if ngx_match(uri,"\\.(jpg|png|gif|css|js|swf|ts)$","isjo") then return false end sv,_ = ngx.shared.free_waf:get(ip) if sv == 666 then return false end local token2 = ngx.md5(method .. server_name .. tostring(request_header['user-agent']) .. tostring(request_header['host']) .. tostring(request_header['accept-language']) .. tostring(request_header['connection']) .. tostring(request_header['accept']) .. tostring(request_header['accept-encoding']) .. tostring(request_header['upgrade-insecure-requests']) .. tostring(request_header['cache-control'])) .. '_' .. 'cc2' local cc2_limit = limit * 3 local count,_ = ngx.shared.free_waf:get(token2) if count then if count > cc2_limit then local safe_count,_ = ngx.shared.free_waf_drop_sum:get(ip) if not safe_count then ngx.shared.free_waf_drop_sum:set(ip,1,86400) safe_count = 1 else ngx.shared.free_waf_drop_sum:incr(ip,1) end if safe_count > retry then local lock_time = (endtime * safe_count) if lock_time > 86400 then lock_time = 86400 end ngx.shared.free_waf_drop_ip:set(ip,retry+1,lock_time) write_log('cc',cycle..'秒内累计超过'..limit..'次请求,封锁' .. lock_time .. '秒') write_drop_ip('cc',lock_time) ngx.exit(config['cc']['status']) return true end security_verification() else ngx.shared.free_waf:incr(token2,1) end else ngx.shared.free_waf:safe_set(token2,1,cycle) end return false end function security_verification() if uri_request_args['btwaf'] then vn3,_ = ngx.shared.free_waf:get(ip) if tostring(vn3) == uri_request_args['btwaf'] then ngx.shared.free_waf:delete(ip) ngx.shared.free_waf:delete(ngx.md5(ip .. '_' .. uri)) ngx.shared.free_waf:set(ip,666,3600) return false end end math.randomseed(tostring(os.time()):reverse():sub(1, 6)) local n1 = math.random(0,20) local n2 = math.random(0,20) local n3 = n1 + n2 ngx.shared.free_waf:set(ip,n3,300) local vargs = '&btwaf=' sargs = string.gsub(request_uri,'.?btwaf=.*','') if not string.find(sargs,'?',1,true) then vargs = '?btwaf=' end ngx.header.charset = 'utf-8' local jsbody = string.format([[ ]],tostring(n1) .. ' + ' .. tostring(n2),sargs .. vargs) ngx.header.content_type = "text/html;charset=utf8" ngx.say(jsbody) ngx.exit(403) end function scan_black() if not config['scan']['open'] or not is_site_config('scan') then return false end if is_ngx_match(scan_black_rules['cookie'],request_header['cookie'],false) then write_log('scan','regular') ngx.exit(config['scan']['status']) return true end if is_ngx_match(scan_black_rules['args'],request_uri,false) then write_log('scan','regular') ngx.exit(config['scan']['status']) return true end for key,value in pairs(request_header) do if is_ngx_match(scan_black_rules['header'],key,false) then write_log('scan','regular') ngx.exit(config['scan']['status']) return true end end return false end function ip_black() for _,rule in ipairs(ip_black_rules) do if compare_ip(rule) then ngx.exit(config['cc']['status']) return true end end return false end function ip_white() for _,rule in ipairs(ip_white_rules) do if compare_ip(rule) then return true end end return false end function split2(input, delimiter) input = tostring(input) delimiter = tostring(delimiter) if (delimiter=='') then return false end local pos,arr = 0, {} for st,sp in function() return string.find(input, delimiter, pos, true) end do table.insert(arr, string.sub(input, pos, st - 1)) pos = sp + 1 end table.insert(arr, string.sub(input, pos)) return arr end function url_white() if ngx.var.document_root=='/www/server/phpmyadmin' then return true end if is_ngx_match(url_white_rules,request_uri,false) then url_data=split2(request_uri,'?') if not url_data then url_data=request_uri end if not url_data[1] then url_data=request_uri else url_data=url_data[1] end if ngx.re.match(url_data,'/\\.\\./') then return false end return true end if site_config[server_name] ~= nil then if is_ngx_match(site_config[server_name]['url_white'],request_uri,false) then url_data=split2(request_uri,'?') if not url_data then url_data=request_uri end if not url_data[1] then url_data=request_uri else url_data=url_data[1] end if ngx.re.match(url_data,'/\\.\\./') then return false end return true end end return false end function url_black() if is_ngx_match(url_black_rules,request_uri,false) then ngx.exit(config['get']['status']) return true end return false end function head() if method ~= 'HEAD' then return false end for _,v in ipairs(head_white_rules) do if ngx_match(uri,v,"isjo") then return false end end spiders = {'spider','bot'} for _,v in ipairs(spiders) do if ngx_match(request_header['user-agent'],v,"isjo") then return false end end write_log('head','禁止HEAD请求') ngx.shared.free_waf:set(ip,retry,endtime) write_drop_ip('head',endtime) ngx.exit(444) end function user_agent() if not config['user-agent']['open'] or not is_site_config('user-agent') then return false end if is_ngx_match(user_agent_rules,request_header['user-agent'],'user_agent') then write_log('user_agent','regular') return_html(config['user-agent']['status'],user_agent_html) return true end return false end function post() if not config['post']['open'] or not is_site_config('post') then return false end if method ~= "POST" then return false end content_length=tonumber(request_header['content-length']) max_len = 64 * 1024 if content_length > max_len then return false end if get_boundary() then return false end ngx.req.read_body() request_args = ngx.req.get_post_args() if not request_args then return false end if is_ngx_match(post_rules,request_args,'post') then write_log('post','regular') return_html(config['post']['status'],post_html) return true end return false end function disable_upload_ext(ext) if not ext then return false end ext = string.lower(ext) if is_key(site_config[server_name]['disable_upload_ext'],ext) then write_log('upload_ext','上传扩展名黑名单') return_html(config['other']['status'],other_html) return true end end function post_data() if method ~= "POST" then return false end content_length=tonumber(request_header['content-length']) if not content_length then return false end max_len = 256 * 1024 if content_length > max_len then return false end local boundary = get_boundary() if boundary then ngx.req.read_body() local data = ngx.req.get_body_data() if not data then return false end local tmp = ngx.re.match(data,[[filename=\"(.+)\.(.*)\"]]) if not tmp then return false end if not tmp[2] then return false end disable_upload_ext(tmp[2]) end return false end function cookie() if not config['cookie']['open'] or not is_site_config('cookie') then return false end if not request_header['cookie'] then return false end if type(request_header['cookie']) == "table" then return false end request_cookie = string.lower(request_header['cookie']) if is_ngx_match(cookie_rules,request_cookie,'cookie') then write_log('cookie','regular') return_html(config['cookie']['status'],cookie_html) return true end return false end function args() if not config['get']['open'] or not is_site_config('get') then return false end if is_ngx_match(args_rules,uri_request_args,'args') then write_log('args','regular') return_html(config['get']['status'],get_html) return true end return false end function url() if not config['get']['open'] or not is_site_config('get') then return false end --正则-- if is_ngx_match(url_rules,uri,'url') then write_log('url','regular') return_html(config['get']['status'],get_html) return true end return false end function php_path() if site_config[server_name] == nil then return false end for _,rule in ipairs(site_config[server_name]['disable_php_path']) do if ngx_match(uri,rule .. "/.*\\.php$","isjo") then write_log('php_path','regular') return_html(config['other']['status'],other_html) return true end end return false end function url_path() if site_config[server_name] == nil then return false end for _,rule in ipairs(site_config[server_name]['disable_path']) do if ngx_match(uri,rule,"isjo") then write_log('path','regular') return_html(config['other']['status'],other_html) return true end end return false end function url_ext() if site_config[server_name] == nil then return false end for _,rule in ipairs(site_config[server_name]['disable_ext']) do if ngx_match(uri,"\\."..rule.."$","isjo") then write_log('url_ext','regular') return_html(config['other']['status'],other_html) return true end end return false end function url_rule_ex() if site_config[server_name] == nil then return false end if method == "POST" and not request_args then content_length=tonumber(request_header['content-length']) max_len = 64 * 1024 request_args = nil if content_length < max_len then ngx.req.read_body() request_args = ngx.req.get_post_args() end end for _,rule in ipairs(site_config[server_name]['url_rule']) do if ngx_match(uri,rule[1],"isjo") then if is_ngx_match(rule[2],uri_request_args,false) then write_log('url_rule','regular') return_html(config['other']['status'],other_html) return true end if method == "POST" and request_args ~= nil then if is_ngx_match(rule[2],request_args,'post') then write_log('post','regular') return_html(config['other']['status'],other_html) return true end end end end return false end function url_tell() if site_config[server_name] == nil then return false end for _,rule in ipairs(site_config[server_name]['url_tell']) do if ngx_match(uri,rule[1],"isjo") then if uri_request_args[rule[2]] ~= rule[3] then write_log('url_tell','regular') return_html(config['other']['status'],other_html) return true end end end return false end function continue_key(key) key = tostring(key) if string.len(key) > 64 then return false end; local keys = {"content","contents","body","msg","file","files","img",""} for _,k in ipairs(keys) do if k == key then return false end; end return true; end function is_ngx_match(rules,sbody,rule_name) if rules == nil or sbody == nil then return false end if type(sbody) == "string" then sbody = {sbody} end if type(rules) == "string" then rules = {rules} end for k,body in pairs(sbody) do if continue_key(k) then for i,rule in ipairs(rules) do if site_config[server_name] and rule_name then local n = i - 1 for _,j in ipairs(site_config[server_name]['disable_rule'][rule_name]) do if n == j then rule = "" end end end if body and rule ~="" then if type(body) == "string" then if ngx_match(ngx.unescape_uri(body),rule,"isjo") then error_rule = rule .. ' >> ' .. k .. ':' .. body return true end end if type(k) == "string" then if ngx_match(ngx.unescape_uri(k),rule,"isjo") then error_rule = rule .. ' >> ' .. k return true end end end end end end return false end function is_key(keys,values) if keys == nil or values == nil then return false end if type(values) == "string" then values = {values} end if type(keys) == "string" then keys = {keys} end for _,value in pairs(values) do if type(value) == "boolean" or value == "" then return false end sval = ngx.unescape_uri(string.lower(ngx.unescape_uri(value))) for _,v in ipairs(keys) do if v == sval then return true end end end return false end function get_return_state(rstate,rmsg) result = {} result['status'] = rstate result['msg'] = rmsg return result end function get_btwaf_drop_ip() local data = ngx.shared.free_waf_drop_ip:get_keys(0) return data end function remove_btwaf_drop_ip() if not uri_request_args['ip'] or not is_ipaddr(uri_request_args['ip']) then return get_return_state(true,'格式错误') end ngx.shared.free_waf_drop_ip:delete(uri_request_args['ip']) return get_return_state(true,uri_request_args['ip'] .. '已解封') end function clean_btwaf_drop_ip() local data = get_btwaf_drop_ip() for _,value in ipairs(data) do ngx.shared.free_waf_drop_ip:delete(value) end return get_return_state(true,'已解封所有封锁IP') end function min_route() if ngx.var.remote_addr ~= '127.0.0.1' then return false end if uri == '/get_btwaf_drop_ip' then return_message(200,get_btwaf_drop_ip()) elseif uri == '/remove_btwaf_drop_ip' then return_message(200,remove_btwaf_drop_ip()) elseif uri == '/clean_btwaf_drop_ip' then return_message(200,clean_btwaf_drop_ip()) end end function return_message(status,msg) ngx.header.content_type = "application/json;" ngx.status = status ngx.say(json.encode(msg)) ngx.exit(status) end function return_html(status,html) ngx.header.content_type = "text/html" ngx.status = status ngx.say(html) ngx.exit(status) end function run_btwaf() server_name = ngx.var.server_name if not config['open'] or not is_site_config('open') then return false end error_rule = nil request_header = ngx.req.get_headers() method = ngx.req.get_method() ip = get_client_ip() ipn = arrip(ip) request_uri = ngx.var.request_uri uri = ngx.unescape_uri(ngx.var.uri) uri_request_args = ngx.req.get_uri_args() cycle = config['cc']['cycle'] endtime = config['cc']['endtime'] limit = config['cc']['limit'] retry = config['retry'] retry_time = config['retry_time'] retry_cycle = config['retry_cycle'] min_route() site_cc = is_site_config('cc') if site_config[server_name] and site_cc then cycle = site_config[server_name]['cc']['cycle'] endtime = site_config[server_name]['cc']['endtime'] limit = site_config[server_name]['cc']['limit'] end if site_config[server_name] then retry = site_config[server_name]['retry'] retry_time = site_config[server_name]['retry_time'] retry_cycle = site_config[server_name]['retry_cycle'] end if ip_white() then return true end ip_black() if url_white() then return true end url_black() drop() drop_abroad() cc() cc2() user_agent() url() args() cookie() scan_black() post() if site_config[server_name] then php_path() url_path() url_ext() url_rule_ex() url_tell() post_data() end end