├── wafconf ├── whiteurl ├── white-user-agent ├── user-agent ├── url ├── cookie ├── post └── args ├── config.lua ├── waf.lua ├── README.md └── init.lua /wafconf/whiteurl: -------------------------------------------------------------------------------- 1 | ^/123/$ 2 | -------------------------------------------------------------------------------- /wafconf/white-user-agent: -------------------------------------------------------------------------------- 1 | (baidu) 2 | -------------------------------------------------------------------------------- /wafconf/user-agent: -------------------------------------------------------------------------------- 1 | (HTTrack|harvest|audit|dirbuster|pangolin|nmap|sqln|-scan|hydra|Parser|libwww|BBBike|sqlmap|w3af|owasp|Nikto|fimap|havij|PycURL|zmeu|BabyKrokodil|netsparker|httperf|bench| SF/) 2 | -------------------------------------------------------------------------------- /wafconf/url: -------------------------------------------------------------------------------- 1 | \.(svn|git|htaccess|bash_history|DS_Store) 2 | \.(bak|inc|old|mdb|sql|backup|java|class)$ 3 | (vhost|bbs|host|wwwroot|www|site|root|hytop|flashfxp).*\.rar 4 | (phpmyadmin|jmx-console|jmxinvokerservlet) 5 | java\.lang 6 | /(attachments|upimg|images|css|uploadfiles|html|uploads|templets|static|template|data|inc|forumdata|upload|includes|cache|avatar)/(\w+).(php|jsp) 7 | -------------------------------------------------------------------------------- /wafconf/cookie: -------------------------------------------------------------------------------- 1 | \.\./ 2 | \:\$ 3 | \$\{ 4 | select.+(from|limit) 5 | (?:(union(.*?)select)) 6 | having|rongjitest 7 | sleep\((\s*)(\d*)(\s*)\) 8 | benchmark\((.*)\,(.*)\) 9 | base64_decode\( 10 | (?:from\W+information_schema\W) 11 | (?:(?:current_)user|database|schema|connection_id)\s*\( 12 | (?:etc\/\W*passwd) 13 | into(\s+)+(?:dump|out)file\s* 14 | group\s+by.+\( 15 | xwork.MethodAccessor 16 | (?: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)\( 17 | xwork\.MethodAccessor 18 | (gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/ 19 | java\.lang 20 | \$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[ 21 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | RulePath = "/usr/local/nginx/conf/waf/wafconf/" 2 | attacklog = "off" 3 | logdir = "/usr/local/nginx/logs/hack/" 4 | UrlDeny="on" 5 | Redirect="on" 6 | CookieMatch="on" 7 | PostMatch="on" 8 | whiteModule="off" 9 | whiteHostModule="off" 10 | black_fileExt={"php","jsp"} 11 | ipWhitelist={"127.0.0.1","172.16.1.0-172.16.1.255"} 12 | ipBlocklist={"127.0.0.1","172.16.1.0-172.16.1.255"} 13 | hostWhiteList = {"blog.whsir.com","down.whsir.com"} 14 | CCDeny="off" 15 | CCrate="100/60" 16 | html=[[ 17 | 18 | 19 | 20 | 21 |

503 Service Temporarily Unavailable

22 |
nginx
23 | 24 | 25 | ]] 26 | 27 | -------------------------------------------------------------------------------- /wafconf/post: -------------------------------------------------------------------------------- 1 | select.+(from|limit) 2 | (?:(union(.*?)select)) 3 | having|rongjitest 4 | sleep\((\s*)(\d*)(\s*)\) 5 | benchmark\((.*)\,(.*)\) 6 | base64_decode\( 7 | (?:from\W+information_schema\W) 8 | (?:(?:current_)user|database|schema|connection_id)\s*\( 9 | (?:etc\/\W*passwd) 10 | into(\s+)+(?:dump|out)file\s* 11 | group\s+by.+\( 12 | xwork.MethodAccessor 13 | (?: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)\( 14 | xwork\.MethodAccessor 15 | (gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/ 16 | java\.lang 17 | \$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[ 18 | \<(iframe|script|body|img|layer|div|meta|style|base|object|input) 19 | (onmouseover|onerror|onload)\= 20 | -------------------------------------------------------------------------------- /wafconf/args: -------------------------------------------------------------------------------- 1 | \.\./ 2 | \:\$ 3 | \$\{ 4 | select.+(from|limit) 5 | (?:(union(.*?)select)) 6 | having|rongjitest 7 | sleep\((\s*)(\d*)(\s*)\) 8 | benchmark\((.*)\,(.*)\) 9 | base64_decode\( 10 | (?:from\W+information_schema\W) 11 | (?:(?:current_)user|database|schema|connection_id)\s*\( 12 | (?:etc\/\W*passwd) 13 | into(\s+)+(?:dump|out)file\s* 14 | group\s+by.+\( 15 | xwork.MethodAccessor 16 | (?: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)\( 17 | xwork\.MethodAccessor 18 | (gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/ 19 | java\.lang 20 | \$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[ 21 | \<(iframe|script|body|img|layer|div|meta|style|base|object|input) 22 | (onmouseover|onerror|onload)\= 23 | -------------------------------------------------------------------------------- /waf.lua: -------------------------------------------------------------------------------- 1 | local content_length=tonumber(ngx.req.get_headers()['content-length']) 2 | local method=ngx.req.get_method() 3 | local ngxmatch=ngx.re.match 4 | if whiteip() then 5 | elseif whitehost() then 6 | elseif whiteua() then 7 | elseif blockip() then 8 | elseif denycc() then 9 | elseif ngx.var.http_Acunetix_Aspect then 10 | ngx.exit(444) 11 | elseif ngx.var.http_X_Scan_Memo then 12 | ngx.exit(444) 13 | elseif whiteurl() then 14 | elseif ua() then 15 | elseif url() then 16 | elseif args() then 17 | elseif cookie() then 18 | elseif PostCheck then 19 | if method=="POST" then 20 | local boundary = get_boundary() 21 | if boundary then 22 | local len = string.len 23 | local sock, err = ngx.req.socket() 24 | if not sock then 25 | return 26 | end 27 | ngx.req.init_body(128 * 1024) 28 | sock:settimeout(0) 29 | local content_length = nil 30 | content_length=tonumber(ngx.req.get_headers()['content-length']) 31 | local chunk_size = 4096 32 | if content_length < chunk_size then 33 | chunk_size = content_length 34 | end 35 | local size = 0 36 | while size < content_length do 37 | local data, err, partial = sock:receive(chunk_size) 38 | data = data or partial 39 | if not data then 40 | return 41 | end 42 | ngx.req.append_body(data) 43 | if body(data) then 44 | return true 45 | end 46 | size = size + len(data) 47 | local m = ngxmatch(data,[[Content-Disposition: form-data;(.+)filename="(.+)\\.(.*)"]],'ijo') 48 | if m then 49 | fileExtCheck(m[3]) 50 | filetranslate = true 51 | else 52 | if ngxmatch(data,"Content-Disposition:",'isjo') then 53 | filetranslate = false 54 | end 55 | if filetranslate==false then 56 | if body(data) then 57 | return true 58 | end 59 | end 60 | end 61 | local less = content_length - size 62 | if less < chunk_size then 63 | chunk_size = less 64 | end 65 | end 66 | ngx.req.finish_body() 67 | else 68 | ngx.req.read_body() 69 | local args = ngx.req.get_post_args() 70 | if not args then 71 | return 72 | end 73 | for key, val in pairs(args) do 74 | if type(val) == "table" then 75 | if type(val[1]) == "boolean" then 76 | return 77 | end 78 | data=table.concat(val, ", ") 79 | else 80 | data=val 81 | end 82 | if data and type(data) ~= "boolean" and body(data) then 83 | return true 84 | end 85 | end 86 | end 87 | end 88 | else 89 | return 90 | end 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) 2 | [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) 3 | 4 | 5 | 安装配置可参考:https://blog.whsir.com/post-4141.html 6 | 7 | 8 | ngx_lua_waf改版基于原[ngx_lua_waf](https://github.com/loveshell/ngx_lua_waf)作者二次更改,代码很简单,高性能和轻量级。 9 | 10 | 11 | 增加功能如下: 12 | 13 | 1、增加黑白名单网段IP限制,能力有限,网段不能写成172.16.1.0/24,只能写成172.16.1.0-172.16.1.255了。 14 | ipWhitelist={"127.0.0.1","172.16.1.0-172.16.1.255"} 15 | 16 | 2、增加User-Agent白名单,用来过滤蜘蛛的。 17 | 在wafconf文件夹下white-user-agent文件中添加 18 | 19 | 3、增加server_name白名单。 20 | 21 | 22 | 23 | 目前我的wlnmp一键包默认的nginx已经集成了此版本 24 | 25 | wlnmp一键包,支持lnmp环境通过yum方式一键快速部署:https://www.wlnmp.com/ 26 | 27 | 28 | ###用途: 29 | 30 | 防止sql注入,本地包含,部分溢出,fuzzing测试,xss,SSRF等web攻击 31 | 防止svn/备份之类文件泄漏 32 | 防止ApacheBench之类压力测试工具的攻击 33 | 屏蔽常见的扫描黑客工具,扫描器 34 | 屏蔽异常的网络请求 35 | 屏蔽图片附件类目录php执行权限 36 | 防止webshell上传 37 | 38 | ###推荐安装: 39 | 40 | 推荐使用lujit2.1做lua支持 41 | 42 | ngx_lua如果是0.9.2以上版本,建议正则过滤函数改为ngx.re.find,匹配效率会提高三倍左右。 43 | 44 | ###使用说明: 45 | 46 | nginx安装路径假设为:/usr/local/nginx/conf/ 47 | 48 | 把ngx_lua_waf下载到conf目录下,解压命名为waf 49 | 50 | 在nginx.conf的http段添加 51 | 52 | lua_package_path "/usr/local/nginx/conf/waf/?.lua"; 53 | lua_shared_dict limit 10m; 54 | init_by_lua_file /usr/local/nginx/conf/waf/init.lua; 55 | access_by_lua_file /usr/local/nginx/conf/waf/waf.lua; 56 | 57 | 配置config.lua里的waf规则目录(一般在waf/conf/目录下) 58 | 59 | RulePath = "/usr/local/nginx/conf/waf/wafconf/" 60 | 61 | 路径如有变动,需对应修改,然后重启nginx即可 62 | 63 | ###配置文件详细说明: 64 | 65 | RulePath = "/usr/local/nginx/conf/waf/wafconf/" 66 | --规则存放目录 67 | attacklog = "off" 68 | --是否开启攻击信息记录,需要配置logdir 69 | logdir = "/usr/local/nginx/logs/hack/" 70 | --log存储目录,该目录需要用户自己新建,切需要nginx用户的可写权限 71 | UrlDeny="on" 72 | --是否拦截url访问 73 | Redirect="on" 74 | --是否拦截后重定向 75 | CookieMatch = "on" 76 | --是否拦截cookie攻击 77 | postMatch = "on" 78 | --是否拦截post攻击 79 | whiteModule = "on" 80 | --是否开启URL白名单 81 | whiteHostModule="off" 82 | --是否开启主机(对应nginx里面的server_name)白名单 83 | black_fileExt={"php","jsp"} 84 | --填写可上传文件后缀类型 85 | ipWhitelist={"127.0.0.1","192.168.1.0-192.168.255.255"}} 86 | --ip白名单,多个ip用逗号分隔 87 | ipBlocklist={"1.0.0.1","2.0.0.0-2.0.0.255"} 88 | --ip黑名单,多个ip用逗号分隔 89 | hostWhiteList = {"blog.whsir.com","down.whsir.com"} 90 | --server_name白名单,多个用逗号分隔 91 | CCDeny="on" 92 | --是否开启拦截cc攻击(需要nginx.conf的http段增加lua_shared_dict limit 10m;) 93 | CCrate = "100/60" 94 | --设置cc攻击频率,单位为秒. 95 | --默认1分钟同一个IP只能请求同一个地址100次 96 | html=[[......]] 97 | --警告内容,可在中括号内自定义 98 | 备注:不要乱动双引号,区分大小写 99 | 100 | ###检查规则是否生效 101 | 102 | 部署完毕可以尝试如下命令: 103 | 104 | curl http://xxxx/test.php?id=../etc/passwd 105 | 返回"Please go away~~"字样,说明规则生效。 106 | 107 | 注意:默认,本机在白名单不过滤,可自行调整config.lua配置 108 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | require 'config' 2 | local match = string.match 3 | local ngxmatch=ngx.re.match 4 | local unescape=ngx.unescape_uri 5 | local get_headers = ngx.req.get_headers 6 | local optionIsOn = function (options) return options == "on" and true or false end 7 | logpath = logdir 8 | rulepath = RulePath 9 | UrlDeny = optionIsOn(UrlDeny) 10 | PostCheck = optionIsOn(PostMatch) 11 | CookieCheck = optionIsOn(CookieMatch) 12 | WhiteCheck = optionIsOn(whiteModule) 13 | WhiteHostCheck = optionIsOn(whiteHostModule) 14 | PathInfoFix = optionIsOn(PathInfoFix) 15 | attacklog = optionIsOn(attacklog) 16 | CCDeny = optionIsOn(CCDeny) 17 | Redirect=optionIsOn(Redirect) 18 | function getClientIp() 19 | IP = ngx.req.get_headers()["X-Real-IP"] 20 | if IP == nil then 21 | IP = ngx.var.remote_addr 22 | end 23 | if IP == nil then 24 | IP = "unknown" 25 | end 26 | return IP 27 | end 28 | function write(logfile,msg) 29 | local fd = io.open(logfile,"ab") 30 | if fd == nil then return end 31 | fd:write(msg) 32 | fd:flush() 33 | fd:close() 34 | end 35 | function log(method,url,data,ruletag) 36 | if attacklog then 37 | local realIp = getClientIp() 38 | local ua = ngx.var.http_user_agent 39 | local servername=ngx.var.server_name 40 | local time=ngx.localtime() 41 | if ua then 42 | line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..data.."\" \""..ua.."\" \""..ruletag.."\"\n" 43 | else 44 | line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..data.."\" - \""..ruletag.."\"\n" 45 | end 46 | local filename = logpath..'/'..servername.."_"..ngx.today().."_sec.log" 47 | write(filename,line) 48 | end 49 | end 50 | 51 | function ipToDecimal(ckip) 52 | local n = 4 53 | local decimalNum = 0 54 | local pos = 0 55 | for s, e in function() return string.find(ckip, '.', pos, true) end do 56 | n = n - 1 57 | decimalNum = decimalNum + string.sub(ckip, pos, s-1) * (256 ^ n) 58 | pos = e + 1 59 | if n == 1 then decimalNum = decimalNum + string.sub(ckip, pos, string.len(ckip)) end 60 | end 61 | return decimalNum 62 | end 63 | ------------------------------------规则读取函数------------------------------------------------------------------- 64 | function read_rule(var) 65 | file = io.open(rulepath..'/'..var,"r") 66 | if file==nil then 67 | return 68 | end 69 | t = {} 70 | for line in file:lines() do 71 | table.insert(t,line) 72 | end 73 | file:close() 74 | return(t) 75 | end 76 | 77 | urlrules=read_rule('url') 78 | argsrules=read_rule('args') 79 | uarules=read_rule('user-agent') 80 | whiteuarules=read_rule('white-user-agent') 81 | wturlrules=read_rule('whiteurl') 82 | postrules=read_rule('post') 83 | ckrules=read_rule('cookie') 84 | 85 | function say_html() 86 | if Redirect then 87 | ngx.header.content_type = "text/html" 88 | ngx.say(html) 89 | ngx.exit(200) 90 | end 91 | end 92 | 93 | function whiteurl() 94 | if WhiteCheck then 95 | if wturlrules ~=nil then 96 | for _,rule in pairs(wturlrules) do 97 | if ngxmatch(ngx.var.request_uri,rule,"isjo") then 98 | return true 99 | end 100 | end 101 | end 102 | end 103 | return false 104 | end 105 | 106 | function whitehost() 107 | if WhiteHostCheck then 108 | local items = Set(hostWhiteList) 109 | for host in pairs(items) do 110 | if ngxmatch(ngx.var.host, host, "isjo") then 111 | log('POST',ngx.var.request_uri,"-","white host".. host) 112 | return true 113 | end 114 | end 115 | end 116 | return false 117 | end 118 | 119 | function args() 120 | for _,rule in pairs(argsrules) do 121 | local args = ngx.req.get_uri_args() 122 | for key, val in pairs(args) do 123 | if type(val)=='table' then 124 | if val == false then 125 | data=table.concat(val, " ") 126 | end 127 | else 128 | data=val 129 | end 130 | if data and type(data) ~= "boolean" and rule ~="" and ngxmatch(unescape(data),rule,"isjo") then 131 | log('GET',ngx.var.request_uri,"-",rule) 132 | say_html() 133 | return true 134 | end 135 | end 136 | end 137 | return false 138 | end 139 | 140 | function url() 141 | if UrlDeny then 142 | for _,rule in pairs(urlrules) do 143 | if rule ~="" and ngxmatch(ngx.var.request_uri,rule,"isjo") then 144 | log('GET',ngx.var.request_uri,"-",rule) 145 | say_html() 146 | return true 147 | end 148 | end 149 | end 150 | return false 151 | end 152 | 153 | function ua() 154 | local ua = ngx.var.http_user_agent 155 | if ua ~= nil then 156 | for _,rule in pairs(uarules) do 157 | if rule ~="" and ngxmatch(ua,rule,"isjo") then 158 | log('UA',ngx.var.request_uri,"-",rule) 159 | say_html() 160 | return true 161 | end 162 | end 163 | end 164 | return false 165 | end 166 | 167 | function body(data) 168 | for _,rule in pairs(postrules) do 169 | if rule ~="" and data~="" and ngxmatch(unescape(data),rule,"isjo") then 170 | log('POST',ngx.var.request_uri,data,rule) 171 | say_html() 172 | return true 173 | end 174 | end 175 | return false 176 | end 177 | 178 | function cookie() 179 | local ck = ngx.var.http_cookie 180 | if CookieCheck and ck then 181 | for _,rule in pairs(ckrules) do 182 | if rule ~="" and ngxmatch(ck,rule,"isjo") then 183 | log('Cookie',ngx.var.request_uri,"-",rule) 184 | say_html() 185 | return true 186 | end 187 | end 188 | end 189 | return false 190 | end 191 | 192 | function denycc() 193 | if CCDeny then 194 | local uri=ngx.var.uri 195 | CCcount=tonumber(string.match(CCrate,'(.*)/')) 196 | CCseconds=tonumber(string.match(CCrate,'/(.*)')) 197 | local token = getClientIp()..uri 198 | local limit = ngx.shared.limit 199 | local req,_=limit:get(token) 200 | if req then 201 | if req > CCcount then 202 | ngx.exit(503) 203 | return true 204 | else 205 | limit:incr(token,1) 206 | end 207 | else 208 | limit:set(token,1,CCseconds) 209 | end 210 | end 211 | return false 212 | end 213 | 214 | function whiteua() 215 | local ua = ngx.var.http_user_agent 216 | if ua ~= nil then 217 | for _,rule in pairs(whiteuarules) do 218 | if rule ~="" and ngxmatch(ua,rule,"isjo") then 219 | return true 220 | end 221 | end 222 | end 223 | return false 224 | end 225 | 226 | function get_boundary() 227 | local header = get_headers()["content-type"] 228 | if not header then 229 | return nil 230 | end 231 | 232 | if type(header) == "table" then 233 | header = header[1] 234 | end 235 | 236 | local m = match(header, ";%s*boundary=\"([^\"]+)\"") 237 | if m then 238 | return m 239 | end 240 | 241 | return match(header, ";%s*boundary=([^\",;]+)") 242 | end 243 | 244 | function blockip() 245 | if next(ipBlocklist) ~= nil then 246 | local cIP = getClientIp() 247 | local numIP = 0 248 | if cIP ~= "unknown" then numIP = tonumber(ipToDecimal(cIP)) end 249 | for _,ip in pairs(ipBlocklist) do 250 | local s, e = string.find(ip, '-', 0, true) 251 | if s == nil and cIP == ip then 252 | ngx.exit(403) 253 | return true 254 | elseif s ~= nil then 255 | sIP = tonumber(ipToDecimal(string.sub(ip, 0, s - 1))) 256 | eIP = tonumber(ipToDecimal(string.sub(ip, e + 1, string.len(ip)))) 257 | if numIP >= sIP and numIP <= eIP then 258 | ngx.exit(403) 259 | return true 260 | end 261 | end 262 | end 263 | end 264 | return false 265 | end 266 | 267 | function fileExtCheck(ext) 268 | local items = Set(black_fileExt) 269 | ext=string.lower(ext) 270 | if ext then 271 | for rule in pairs(items) do 272 | if ngx.re.match(ext,rule,"isjo") then 273 | log('POST',ngx.var.request_uri,"-","file attack with ext "..ext) 274 | say_html() 275 | end 276 | end 277 | end 278 | return false 279 | end 280 | function Set (list) 281 | local set = {} 282 | for _, l in ipairs(list) do set[l] = true end 283 | return set 284 | end 285 | 286 | function whiteip() 287 | if next(ipWhitelist) ~= nil then 288 | local cIP = getClientIp() 289 | local numIP = 0 290 | if cIP ~= "unknown" then numIP = tonumber(ipToDecimal(cIP)) end 291 | for _,ip in pairs(ipWhitelist) do 292 | local s, e = string.find(ip, '-', 0, true) 293 | if s == nil and cIP == ip then 294 | return true 295 | elseif s ~= nil then 296 | sIP = tonumber(ipToDecimal(string.sub(ip, 0, s - 1))) 297 | eIP = tonumber(ipToDecimal(string.sub(ip, e + 1, string.len(ip)))) 298 | if numIP >= sIP and numIP <= eIP then 299 | return true 300 | end 301 | end 302 | end 303 | end 304 | return false 305 | end 306 | 307 | --------------------------------------------------------------------------------