├── 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 | [](https://github.com/996icu/996.ICU/blob/master/LICENSE)
2 | [](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 |
--------------------------------------------------------------------------------