├── .gitignore
├── LICENSE
├── README.md
├── area
├── GeoLite2-Country.mmdb
└── maxminddb.lua
├── config.lua
├── country_check.lua
├── init.lua
├── waf.lua
└── wafconf
├── BlockReferer
├── WhiteReferer
├── args
├── cookie
├── post
├── readme.txt
├── url
├── user-agent
├── white-user-agent
└── whiteurl
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Lua sources
2 | luac.out
3 |
4 | # luarocks build files
5 | *.src.rock
6 | *.zip
7 | *.tar.gz
8 |
9 | # Object files
10 | *.o
11 | *.os
12 | *.ko
13 | *.obj
14 | *.elf
15 |
16 | # Precompiled Headers
17 | *.gch
18 | *.pch
19 |
20 | # Libraries
21 | *.lib
22 | *.a
23 | *.la
24 | *.lo
25 | *.def
26 | *.exp
27 |
28 | # Shared objects (inc. Windows DLLs)
29 | *.dll
30 | *.so
31 | *.so.*
32 | *.dylib
33 |
34 | # Executables
35 | *.exe
36 | *.out
37 | *.app
38 | *.i*86
39 | *.x86_64
40 | *.hex
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 w38351479
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/996icu/996.ICU/blob/master/LICENSE)
2 | [](https://996.icu)
3 |
4 | ngx_lua_waf改版基于原[ngx_lua_waf](https://github.com/loveshell/ngx_lua_waf)作者二次更改,代码很简单,高性能和轻量级。
5 |
6 |
7 | **欢迎所有有兴趣的同学进行协同开发,在留言处@我**
8 | =========================================================
9 |
10 | ## 【**】正在二次开发中的功能
11 | 1、针对疑似机器人访问行为,浮层滑块验证,不再是单纯返回403,加入可以选功能图片验证码
12 | 2、蜘蛛验证(这个还不确定)
13 | 5、根据连续异常响应码分布,限制IP访问(拦截黑客针对不存在的URL地址发起的大量恶意访问)
14 | 7、可设置对某些特定URL地址(如管理员登录后台)指定只允许某些IP访问
15 | 8、基于日志服务,提供全量访问日志的攻击比例分析
16 |
17 |
18 | ## 【2020.06.X】
19 | 1、添加user-agent规则。支持如wordpress/pingback等常见CC变种型攻击防护
20 | 2、头部字段Referer限制,防止恶意请求或防盗链
21 | 3、HTTP/HTTPS协议请求方法限制(限制TRACE/TRACK/OPTIONS/PUT/PATCH/DELETE/CONNECT),不允许未知方法或空
22 | **【Bug修复】**
23 | 1、修复User-agent为空时直接绕过UA规则检查。要求强制有UA才可以访问
24 |
25 |
26 | ## 【2020.06.22】Bug修复,适用于此日期前的所有版本
27 | 1.自己的bug。
28 | * 描述:针对开启国家地域黑白名单时,出现内网或无法解析的IP采取直接放行,导致后面的规则不生效。
29 | 解决办法:
30 | 规则优先级应该放在最后
31 |
32 | 问题来源: https://github.com/loveshell/ngx_lua_waf/issues
33 | 2、waf 记录日志的bug及修复 #129:
34 | * 描述:当nginx 同一个server段的server_name 的list里有多个servername时,log函数只会匹配到第一个servername。并且,如果servername 出现通配符时,log函数会按照原样打入log。
35 | 解决办法(采纳意见):
36 | ngx.var.server_name
37 | --改为
38 | ngx.var.host
39 |
40 | 3、正则表达式有问题 #149 或 使用了ngx_lua_waf这个模块后,页面上传超过256M的文件,nginx会报400的错误。请问ngx_lua_waf对文件上传限制在哪儿进行修改? #123
41 | * 描述:POST匹配文件的表达式中表达式有误,看了很多这个项目的小伙伴后觉得还是不对
42 | 此处涉及多处修改和逻辑修改
43 |
44 | 4、通过URL传参时容易造成CC攻击误报
45 | * 描述:有些架构都需要访问index.php?id=xxx,具体资源有id来指定,那么index.php这个页面容易触发cc规则访问被deny,这样会造成大量的误报,有什么办法解决这个问题吗? #77
46 | 解决办法:
47 | 生成token时引入ngx.var.request_uri而不是单纯的uri,并且使用url安全的进行encode_base64url 数据编码方式
48 |
49 | 5、上传zip文件被post规则匹配到,导致403 #130
50 | * 描述:原因是 被 post 里面的 匹配到疑似攻击内容
51 | 解决办法:
52 | 对上传文件新增独立检查开关,而不是直接关闭post检查
53 |
54 | 6、发现用了waf开启Post功能,上传图片大的会上传不了,请问哪里取消图片容量限制? #115
55 | * 描述:原因是应该是nginx允许上传的参数不够大
56 | 解决办法:
57 | 调整您配置的两个参数,参数含义自己查,生产建议不高于100m。
58 | client_body_buffer_size 5m;
59 | client_max_body_size 512m;
60 |
61 | 其他自己发现的bug
62 | 7、匹配文件后缀时,采用match导致部分匹配,误拒绝POST上传
63 | if ngx.re.match(ext,rule,"isjo") then
64 | --改为
65 | if string.lower(rule) == ext then
66 |
67 | 8、上传文件时,上传js,py,html文件时日志变为cat文件,优化记录日记和错误拦截
68 | log("-","file attack with ext "..ext .. " rule: " .. rule)
69 | --改为
70 | log("-","file attack with ext. rule: " .. rule)
71 | --还有log函数,略..。
72 |
73 | **【新增功能】**
74 | 1、post文件上传时单独对文件内容检查设置一个小开关
75 | 2、上传文件的后缀黑名单改为允许上传的后缀白名单(因为未知的文件后缀数量太多,而且具有不确定性),并且对文件没有后缀的跳过次检查(ps你也可以强制改为必须有后缀,但感觉意义不大)
76 | 3、对上传成功的文件和,上传失败的,单独记录日志,便于查找
77 | **【修改nginx配置lua环境参数】**
78 | lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua;;";
79 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
80 | lua_shared_dict urllimit 10m;
81 | lua_shared_dict iplimit 10m;
82 | init_by_lua_file /usr/local/openresty/nginx/conf/waf/init.lua;
83 | access_by_lua_file /usr/local/openresty/nginx/conf/waf/waf.lua;
84 |
85 |
86 |
87 |
88 | ## 【2020.06.19】
89 | 1、国家级别的地域限制(黑白名单)。国家代码参考[ISO_3166-2](https://en.wikipedia.org/wiki/ISO_3166-2)。"GeoLite2-City.mmdb/GeoLite2-Country.mmdb"后期请自行更新
90 |
91 |
92 | ## 【2020.06.18】
93 | 1、获取客户端IP,支持代理,多级代理情况下只取最后一级
94 | 2、修改原来单一针对IP做cc检测。添加URL频率cc攻击检测,其次才是Ip 频率cc攻击检测(需要修改nginx配置,lua_shared_dict部分)
95 | 3、优化局部变量,减少高并发时变量覆盖
96 | 4、优化日志记录提醒
97 | 5、优化规则执行顺序
98 |
99 |
100 | ## 【2020.06.17】
101 | 1、增加黑白名单IP段掩码限制方法,例如:ipWhitelist={"127.0.0.1","192.168.1.0/24"}
102 |
103 |
104 |
105 | ## 【**】增加功能如下
106 | 1、增加黑白名单网段IP限制,例如:ipWhitelist={"127.0.0.1","172.16.1.0-172.16.1.255"}
107 | 2、增加User-Agent白名单,用来过滤蜘蛛的。在wafconf文件夹下white-user-agent文件中添加
108 | 3、增加server_name白名单。
109 |
110 |
111 |
112 |
113 | ### 初始功能:
114 |
115 | 防止sql注入,本地包含,部分溢出,fuzzing测试,xss,SSRF等web攻击
116 | 防止svn/备份之类文件泄漏
117 | 防止ApacheBench之类压力测试工具的攻击
118 | 屏蔽常见的扫描黑客工具,扫描器
119 | 屏蔽异常的网络请求
120 | 屏蔽图片附件类目录php执行权限
121 | 防止webshell上传
122 |
123 | ### 【1】环境推荐安装:
124 | 1.1)推荐使用lujit2.1做lua支持
125 | 1.2)ngx_lua如果是0.9.2以上版本,建议正则过滤函数改为ngx.re.find,匹配效率会提高三倍左右。
126 | 1.3)推荐直接使用openresty部署,而不是自己手动部署nginx+lua,下面安装示例使用“openresty/1.15.8.3”
127 | 1.4)推荐编译安装openresty时添加后端检查模块 “[nginx_upstream_check_module](https://github.com/yaoweibin/nginx_upstream_check_module)”,并添加模块参数“--with-http_geoip_module”
128 |
129 |
130 | ### 【2】安装使用说明:
131 | openresty安装路径假设为: /usr/local/openresty
132 | 2.1)下载文件:
133 | * 把ngx_lua_waf下载到/usr/local/openresty/nginx/conf/目录下,解压命名为waf
134 | * 确保lua_package_cpath配置中包含cjson.so(openrestym默认包含)
135 |
136 | 2.1.1)安装lua 库依赖 libmaxminddb 实现对 mmdb 的高效访问 (使用yum安装的,版本较低。yum install libmaxminddb-devel -y)
137 |
138 | wget https://github.com/maxmind/libmaxminddb/releases/download/1.4.2/libmaxminddb-1.4.2.tar.gz
139 | tar -zxvf libmaxminddb-1.4.2.tar.gz
140 | cd libmaxminddb-1.4.2
141 | ./configure
142 | make
143 | make check
144 | sudo make install
145 | echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
146 | sudo ldconfig
147 |
148 | 2.2)在nginx.conf的http段添加
149 |
150 | lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua;;";
151 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
152 | lua_shared_dict urllimit 10m;
153 | lua_shared_dict iplimit 10m;
154 | init_by_lua_file /usr/local/openresty/nginx/conf/waf/init.lua;
155 | access_by_lua_file /usr/local/openresty/nginx/conf/waf/waf.lua;
156 |
157 | 2.3)配置config.lua里的waf规则目录
158 |
159 | RulePath = "/usr/local/openresty/nginx/conf/waf/wafconf/"
160 |
161 | 路径如有变动,需对应修改,然后重启nginx即可
162 | 2.4)配置config.lua里的日志目录(该目录需要自己提前创建)
163 |
164 | logdir = "/usr/local/openresty/nginx/waflogs/"
165 |
166 | 2.5)配置文件详细说明:
167 |
168 | 其他参数说明直接卸载配置文件中了
169 |
170 | ### 【3】检查规则是否生效
171 | 部署完毕可以尝试如下命令:
172 |
173 | curl http://xxxx/test.php?id=/etc/passwd
174 | 返回"Please go away~~"字样,说明规则生效。
175 |
176 | 注意:默认,本机在白名单不过滤,可自行调整config.lua配置
177 |
178 |
179 |
180 |
181 |
182 | ## 【特别说明】
183 | 以上代码参考以下项目:
184 | > https://github.com/loveshell/ngx_lua_waf
185 | > https://github.com/whsir/ngx_lua_waf
186 | > https://github.com/oneinstack/ngx_lua_waf
187 | > https://github.com/taihedeveloper/ngx_lua_waf
188 | 感谢ngx_lua模块的开发者,感谢openresty的春哥!!!
189 |
190 |
191 | ## 【其他资源说明】
192 | * [GeoLite2-City.mmdb/GeoLite2-Country.mmdb](https://dev.maxmind.com/geoip/geoip2/geolite2/)
193 | * [maxminddb.lua](https://dev.maxmind.com/geoip/geoip2/downloadable/#MaxMind_APIs)
194 | * [libmaxminddb](https://github.com/maxmind/libmaxminddb/releases)
195 |
--------------------------------------------------------------------------------
/area/GeoLite2-Country.mmdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heartshare/ngx_lua_waf-2/b7a28810b2930fb68f34171dbbb9b6a3b5bef4cd/area/GeoLite2-Country.mmdb
--------------------------------------------------------------------------------
/area/maxminddb.lua:
--------------------------------------------------------------------------------
1 | -- Copyright (C) Anjia (anjia0532)
2 |
3 | local json = require('cjson')
4 | local json_encode = json.encode
5 | local json_decode = json.decode
6 |
7 | local ngx_log = ngx.log
8 | local ngx_ERR = ngx.ERR
9 | local ngx_CRIT = ngx.CRIT
10 | local ngx_INFO = ngx.INFO
11 |
12 |
13 | local ffi = require ('ffi')
14 | local ffi_new = ffi.new
15 | local ffi_str = ffi.string
16 | local ffi_cast = ffi.cast
17 | local ffi_gc = ffi.gc
18 | local C = ffi.C
19 |
20 | local _M ={}
21 | _M._VERSION = '1.3.1'
22 | local mt = { __index = _M }
23 |
24 | ffi.cdef[[
25 |
26 | typedef unsigned int mmdb_uint128_t __attribute__ ((__mode__(TI)));
27 |
28 | typedef struct MMDB_entry_s {
29 | struct MMDB_s *mmdb;
30 | uint32_t offset;
31 | } MMDB_entry_s;
32 |
33 | typedef struct MMDB_lookup_result_s {
34 | bool found_entry;
35 | MMDB_entry_s entry;
36 | uint16_t netmask;
37 | } MMDB_lookup_result_s;
38 |
39 | typedef struct MMDB_entry_data_s {
40 | bool has_data;
41 | union {
42 | uint32_t pointer;
43 | const char *utf8_string;
44 | double double_value;
45 | const uint8_t *bytes;
46 | uint16_t uint16;
47 | uint32_t uint32;
48 | int32_t int32;
49 | uint64_t uint64;
50 | mmdb_uint128_t uint128;
51 | bool boolean;
52 | float float_value;
53 | };
54 |
55 | uint32_t offset;
56 | uint32_t offset_to_next;
57 | uint32_t data_size;
58 | uint32_t type;
59 | } MMDB_entry_data_s;
60 |
61 | typedef struct MMDB_entry_data_list_s {
62 | MMDB_entry_data_s entry_data;
63 | struct MMDB_entry_data_list_s *next;
64 | } MMDB_entry_data_list_s;
65 |
66 | typedef struct MMDB_description_s {
67 | const char *language;
68 | const char *description;
69 | } MMDB_description_s;
70 |
71 | typedef struct MMDB_metadata_s {
72 | uint32_t node_count;
73 | uint16_t record_size;
74 | uint16_t ip_version;
75 | const char *database_type;
76 | struct {
77 | size_t count;
78 | const char **names;
79 | } languages;
80 | uint16_t binary_format_major_version;
81 | uint16_t binary_format_minor_version;
82 | uint64_t build_epoch;
83 | struct {
84 | size_t count;
85 | MMDB_description_s **descriptions;
86 | } description;
87 | } MMDB_metadata_s;
88 |
89 | typedef struct MMDB_ipv4_start_node_s {
90 | uint16_t netmask;
91 | uint32_t node_value;
92 | } MMDB_ipv4_start_node_s;
93 |
94 | typedef struct MMDB_s {
95 | uint32_t flags;
96 | const char *filename;
97 | ssize_t file_size;
98 | const uint8_t *file_content;
99 | const uint8_t *data_section;
100 | uint32_t data_section_size;
101 | const uint8_t *metadata_section;
102 | uint32_t metadata_section_size;
103 | uint16_t full_record_byte_size;
104 | uint16_t depth;
105 | MMDB_ipv4_start_node_s ipv4_start_node;
106 | MMDB_metadata_s metadata;
107 | } MMDB_s;
108 |
109 | typedef char * pchar;
110 |
111 | MMDB_lookup_result_s MMDB_lookup_string(MMDB_s *const mmdb, const char *const ipstr, int *const gai_error,int *const mmdb_error);
112 | int MMDB_open(const char *const filename, uint32_t flags, MMDB_s *const mmdb);
113 | void MMDB_close(MMDB_s *const mmdb);
114 | int MMDB_aget_value(MMDB_entry_s *const start, MMDB_entry_data_s *const entry_data, const char *const *const path);
115 | char *MMDB_strerror(int error_code);
116 | int MMDB_get_entry_data_list(MMDB_entry_s *start, MMDB_entry_data_list_s **const entry_data_list);
117 | void MMDB_free_entry_data_list(MMDB_entry_data_list_s *const entry_data_list);
118 |
119 | const char *gai_strerror(int errcode);
120 | ]]
121 |
122 | -- error codes
123 | -- https://github.com/maxmind/libmaxminddb/blob/master/include/maxminddb.h#L66
124 | local MMDB_SUCCESS = 0
125 | local MMDB_FILE_OPEN_ERROR = 1
126 | local MMDB_CORRUPT_SEARCH_TREE_ERROR = 2
127 | local MMDB_INVALID_METADATA_ERROR = 3
128 | local MMDB_IO_ERROR = 4
129 | local MMDB_OUT_OF_MEMORY_ERROR = 5
130 | local MMDB_UNKNOWN_DATABASE_FORMAT_ERROR = 6
131 | local MMDB_INVALID_DATA_ERROR = 7
132 | local MMDB_INVALID_LOOKUP_PATH_ERROR = 8
133 | local MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR = 9
134 | local MMDB_INVALID_NODE_NUMBER_ERROR = 10
135 | local MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR = 11
136 |
137 | -- data type
138 | -- https://github.com/maxmind/libmaxminddb/blob/master/include/maxminddb.h#L40
139 | local MMDB_DATA_TYPE_EXTENDED = 0
140 | local MMDB_DATA_TYPE_POINTER = 1
141 | local MMDB_DATA_TYPE_UTF8_STRING = 2
142 | local MMDB_DATA_TYPE_DOUBLE = 3
143 | local MMDB_DATA_TYPE_BYTES = 4
144 | local MMDB_DATA_TYPE_UINT16 = 5
145 | local MMDB_DATA_TYPE_UINT32 = 6
146 | local MMDB_DATA_TYPE_MAP = 7
147 | local MMDB_DATA_TYPE_INT32 = 8
148 | local MMDB_DATA_TYPE_UINT64 = 9
149 | local MMDB_DATA_TYPE_UINT128 = 10
150 | local MMDB_DATA_TYPE_ARRAY = 11
151 | local MMDB_DATA_TYPE_CONTAINER = 12
152 | local MMDB_DATA_TYPE_END_MARKER = 13
153 | local MMDB_DATA_TYPE_BOOLEAN = 14
154 | local MMDB_DATA_TYPE_FLOAT = 15
155 |
156 | -- you should install the libmaxminddb to your system
157 | local maxm = ffi.load('libmaxminddb')
158 | local mmdb = ffi_new('MMDB_s')
159 | local initted = false
160 | --https://github.com/maxmind/libmaxminddb
161 |
162 | local function mmdb_strerror(rc)
163 | return ffi_str(maxm.MMDB_strerror(rc))
164 | end
165 |
166 | local function gai_strerror(rc)
167 | return ffi_str(C.gai_strerror(rc))
168 | end
169 |
170 | function _M.init(dbfile)
171 | if not initted then
172 | local maxmind_ready = maxm.MMDB_open(dbfile,0,mmdb)
173 |
174 | if maxmind_ready ~= MMDB_SUCCESS then
175 | return nil, mmdb_strerror(maxmind_ready)
176 | end
177 |
178 | initted = true
179 |
180 | ffi_gc(mmdb, maxm.MMDB_close)
181 | end
182 | return initted
183 | end
184 |
185 | function _M.initted()
186 | return initted
187 | end
188 |
189 | -- https://github.com/maxmind/libmaxminddb/blob/master/src/maxminddb.c#L1938
190 | -- LOCAL MMDB_entry_data_list_s *dump_entry_data_list( FILE *stream, MMDB_entry_data_list_s *entry_data_list, int indent, int *status)
191 | local function _dump_entry_data_list(entry_data_list,status)
192 |
193 | if not entry_data_list then
194 | return nil,MMDB_INVALID_DATA_ERROR
195 | end
196 |
197 | local entry_data_item = entry_data_list[0].entry_data
198 | local data_type = entry_data_item.type
199 | local data_size = entry_data_item.data_size
200 | local result
201 |
202 | if data_type == MMDB_DATA_TYPE_MAP then
203 | result = {}
204 |
205 | local size = entry_data_item.data_size
206 |
207 | entry_data_list = entry_data_list[0].next
208 |
209 | while(size > 0 and entry_data_list)
210 | do
211 | entry_data_item = entry_data_list[0].entry_data
212 | data_type = entry_data_item.type
213 | data_size = entry_data_item.data_size
214 |
215 | if MMDB_DATA_TYPE_UTF8_STRING ~= data_type then
216 | return nil,MMDB_INVALID_DATA_ERROR
217 | end
218 |
219 | local key = ffi_str(entry_data_item.utf8_string,data_size)
220 |
221 | if not key then
222 | return nil,MMDB_OUT_OF_MEMORY_ERROR
223 | end
224 |
225 | local val
226 | entry_data_list = entry_data_list[0].next
227 | entry_data_list,status,val = _dump_entry_data_list(entry_data_list)
228 |
229 | if status ~= MMDB_SUCCESS then
230 | return nil,status
231 | end
232 |
233 | result[key] = val
234 |
235 | size = size -1
236 | end
237 |
238 |
239 | elseif entry_data_list[0].entry_data.type == MMDB_DATA_TYPE_ARRAY then
240 | local size = entry_data_list[0].entry_data.data_size
241 | result = {}
242 |
243 | entry_data_list = entry_data_list[0].next
244 |
245 | local i = 1
246 | while(i <= size and entry_data_list)
247 | do
248 | local val
249 | entry_data_list,status,val = _dump_entry_data_list(entry_data_list)
250 |
251 | if status ~= MMDB_SUCCESS then
252 | return nil,nil,val
253 | end
254 |
255 | result[i] = val
256 | i = i + 1
257 | end
258 |
259 |
260 | else
261 | entry_data_item = entry_data_list[0].entry_data
262 | data_type = entry_data_item.type
263 | data_size = entry_data_item.data_size
264 |
265 | local val
266 | -- string type "key":"val"
267 | -- other type "key":val
268 | -- default other type
269 | if data_type == MMDB_DATA_TYPE_UTF8_STRING then
270 | val = ffi_str(entry_data_item.utf8_string,data_size)
271 | if not val then
272 | status = MMDB_OUT_OF_MEMORY_ERROR
273 | return nil,status
274 | end
275 | elseif data_type == MMDB_DATA_TYPE_BYTES then
276 | val = ffi_str(ffi_cast('char * ',entry_data_item.bytes),data_size)
277 | if not val then
278 | status = MMDB_OUT_OF_MEMORY_ERROR
279 | return nil,status
280 | end
281 | elseif data_type == MMDB_DATA_TYPE_DOUBLE then
282 | val = entry_data_item.double_value
283 | elseif data_type == MMDB_DATA_TYPE_FLOAT then
284 | val = entry_data_item.float_value
285 | elseif data_type == MMDB_DATA_TYPE_UINT16 then
286 | val = entry_data_item.uint16
287 | elseif data_type == MMDB_DATA_TYPE_UINT32 then
288 | val = entry_data_item.uint32
289 | elseif data_type == MMDB_DATA_TYPE_BOOLEAN then
290 | val = entry_data_item.boolean
291 | elseif data_type == MMDB_DATA_TYPE_UINT64 then
292 | val = entry_data_item.uint64
293 | elseif data_type == MMDB_DATA_TYPE_INT32 then
294 | val = entry_data_item.int32
295 | else
296 | return nil,MMDB_INVALID_DATA_ERROR
297 | end
298 |
299 | result = val
300 | entry_data_list = entry_data_list[0].next
301 | end
302 |
303 | status = MMDB_SUCCESS
304 | return entry_data_list,status,result
305 | end
306 |
307 | function _M.lookup(ip)
308 |
309 | if not initted then
310 | return nil, "not initialized"
311 | end
312 |
313 | local gai_error = ffi_new('int[1]')
314 | local mmdb_error = ffi_new('int[1]')
315 |
316 | local result = maxm.MMDB_lookup_string(mmdb,ip,gai_error,mmdb_error)
317 |
318 | if mmdb_error[0] ~= MMDB_SUCCESS then
319 | return nil,'lookup failed: ' .. mmdb_strerror(mmdb_error[0])
320 | end
321 |
322 | if gai_error[0] ~= MMDB_SUCCESS then
323 | return nil,'lookup failed: ' .. gai_strerror(gai_error[0])
324 | end
325 |
326 | if true ~= result.found_entry then
327 | return nil,'not found'
328 | end
329 |
330 | local entry_data_list = ffi_cast('MMDB_entry_data_list_s **const',ffi_new("MMDB_entry_data_list_s"))
331 |
332 | local status = maxm.MMDB_get_entry_data_list(result.entry,entry_data_list)
333 |
334 | if status ~= MMDB_SUCCESS then
335 | return nil,'get entry data failed: ' .. mmdb_strerror(status)
336 | end
337 |
338 | local head = entry_data_list[0] -- Save so this can be passed to free fn.
339 | local _,status,result = _dump_entry_data_list(entry_data_list)
340 | maxm.MMDB_free_entry_data_list(head)
341 |
342 | if status ~= MMDB_SUCCESS then
343 | return nil,'dump entry data failed: ' .. mmdb_strerror(status)
344 | end
345 |
346 |
347 | return result
348 | end
349 |
350 | -- https://www.maxmind.com/en/geoip2-databases you should download the mmdb file from maxmind
351 |
352 | return _M;
353 |
--------------------------------------------------------------------------------
/config.lua:
--------------------------------------------------------------------------------
1 | RulePath = "/usr/local/openresty/nginx/conf/waf/wafconf/"
2 | attacklog = "on"
3 | logdir = "/usr/local/openresty/nginx/waflogs/"
4 |
5 |
6 | UrlDeny="on"
7 | --是否拦截url访问
8 |
9 | Redirect="on"
10 | --是否拦截后重定向
11 |
12 | CookieMatch="on"
13 | --是否拦截cookie攻击
14 |
15 | whiteModule="on"
16 | --是否开启URL白名单
17 |
18 | Referer="on"
19 | --是否开启防盗链黑白名单,在wafconf的WhiteReferer/BlockReferer配置,referer为空也不会限制
20 |
21 | BlockRequestMethod={"TRACE","TRACK","OPTIONS","PUT","PATCH","DELETE","CONNECT"}
22 | --HTTP/HTTPS协议请求方法限制(限制TRACE/TRACK/OPTIONS/PUT/PATCH/DELETE/CONNECT),不允许未知方法
23 |
24 |
25 | white_fileExt={"bmp","jpg","png","tif","gif",
26 | "wps","dps","et","doc","docx","ppt","pptx","xls","xlsx","csv","obt",
27 | "zip","rar","gz","tar","gzip","7z","tgz","tbz","bz2",
28 | "wav","mp3","wma","mmf","amr","aac","flac",
29 | "txt","pdf","yml","yaml","conf","log","rpm"}
30 | --填写可上传的文件后缀类型(不区分大小写)
31 | PostMatch="on"
32 | --是否拦截post攻击【所有post检查的总开关】
33 | FileContentCheck="on"
34 | --是否开启文件内容检查(严格对内容进行webshell/SQL注入等高危函数检查)【post中的小开关,只针对文件上传。开启的前提是PostMatch="on"】
35 |
36 |
37 | ipWhitelist={"127.0.0.1"}
38 | --ip白名单,多个ip用逗号分隔,
39 | --支持:
40 | --1)范围划分法 "192.168.0.70-192.168.0.99"
41 | --2)掩码划分法 "192.168.0.0/24"
42 | ipBlocklist={"1.0.0.1"}
43 | --ip黑名单,多个ip用逗号分隔
44 | --支持:
45 | ----1)范围划分法 "192.168.0.70-192.168.0.99"
46 | ----2)掩码划分法 "192.168.0.0/24"
47 |
48 | whiteHostModule="off"
49 | --是否开启主机(对应nginx里面的server_name)白名单
50 | hostWhiteList = {"blog.whsir.com"}
51 | --server_name白名单,多个用逗号分隔
52 |
53 | CCDeny="on"
54 | --是否开启拦截cc攻击(需要nginx.conf的http段增加lua_shared_dict limit 10m;)
55 | urlCCrate="2000/60"
56 | -- ip访问特定url频率(次/秒)
57 | ipCCrate="3000/60"
58 | -- 访问ip频次检测(次/秒),该值应该是urlCCrate的5-20倍左右
59 |
60 |
61 | CountryLimit="on"
62 | --否开启IP源的国家限制,黑白名单。国家代码参考[ISO_3166-2](https://en.wikipedia.org/wiki/ISO_3166-2)
63 | WhiteCountry={"CN"}
64 | --白名单国家,优先级低于"ipBlocklist"。针对内网或无法识别的IP统一进行放行。
65 | BlockCountry={}
66 | --黑名单国家,优先级低于"ipWhitelist"。
67 |
68 |
69 | html=[[
70 |
71 |
72 | 网站防火墙
73 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
网站防火墙
90 |
92 |
93 | 您的请求带有不合法参数,已被网站管理员设置拦截!
94 |
可能原因:您提交的内容包含危险的攻击请求
95 |
%s
96 |
如何解决:
97 |
98 | - 1)检查提交内容;
100 | - 2)如网站托管,请联系空间提供商;
101 | - 3)普通网站访客,请联系网站管理员;
102 |
103 |
104 |
105 |
106 | ]]
107 |
108 |
109 |
--------------------------------------------------------------------------------
/country_check.lua:
--------------------------------------------------------------------------------
1 | -- 根据ip地址获取国家
2 | function getCountry()
3 | local cjson=require "cjson"
4 | local geo=require "area/maxminddb"
5 | local now_ip = getClientIp()
6 | if not geo.initted() then
7 | geo.init("waf/area/GeoLite2-Country.mmdb")
8 | end
9 | local res,err=geo.lookup(now_ip)
10 |
11 | if not res then
12 | return "unknown_country"
13 | else
14 | return res["country"]["iso_code"]
15 | end
16 | end
17 |
18 | -- 国家白名单验证;如果开启国家验证,建议开启蜘蛛验证,并在参数内填写你允许的搜索引擎蜘蛛
19 | function country_white()
20 | if CountryLimit then
21 | if next(WhiteCountry) ~= nil then
22 | local country = getCountry()
23 | local items = Set(WhiteCountry)
24 | if country == "unknown_country" then
25 | return true
26 | end
27 | for country_iso in pairs(items) do
28 | if country == string.upper(country_iso) then
29 | return true
30 | end
31 | end
32 | return false
33 | end
34 | return false
35 | end
36 | return false
37 | end
38 |
39 | -- 国家黑名单验证
40 | function country_block()
41 | if CountryLimit then
42 | if next(BlockCountry) ~= nil then
43 | local country = getCountry()
44 | local items = Set(BlockCountry)
45 | for country_iso in pairs(items) do
46 | if country == string.upper(country_iso) then
47 | log("-","BlockCountry: ".. country)
48 | say_html("地域访问限制,请稍后再试")
49 | return true
50 | end
51 | end
52 | return false
53 | end
54 | return false
55 | end
56 | return false
57 | end
58 |
--------------------------------------------------------------------------------
/init.lua:
--------------------------------------------------------------------------------
1 | require 'config'
2 | require 'country_check'
3 | local b64 = require 'ngx.base64'
4 | local match = string.match
5 | local ngxmatch=ngx.re.match
6 | local unescape=ngx.unescape_uri
7 | local get_headers = ngx.req.get_headers
8 | local optionIsOn = function (options) return options == "on" and true or false end
9 | logpath = logdir
10 | rulepath = RulePath
11 | UrlDeny = optionIsOn(UrlDeny)
12 | PostCheck = optionIsOn(PostMatch)
13 | CookieCheck = optionIsOn(CookieMatch)
14 | WhiteCheck = optionIsOn(whiteModule)
15 | WhiteHostCheck = optionIsOn(whiteHostModule)
16 | PathInfoFix = optionIsOn(PathInfoFix)
17 | attacklog = optionIsOn(attacklog)
18 | CCDeny = optionIsOn(CCDeny)
19 | Redirect = optionIsOn(Redirect)
20 | CountryLimit = optionIsOn(CountryLimit)
21 | FileContentCheck = optionIsOn(FileContentCheck)
22 |
23 |
24 |
25 |
26 | --验证码
27 | function WafCaptcha()
28 | html_file = io.open("waf_captcha/waf-captcha.html","r")
29 | html_v = html_file:read("*a")
30 | say_html(html_v)
31 | end
32 |
33 |
34 | --获取客户端IP,支持代理
35 | function getClientIp()
36 | local headers = ngx.req.get_headers()
37 | local reip = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr
38 | if reip == nil then
39 | local reip = "unknown"
40 | end
41 | --检查返回的IP是否是多个值,如果是,只取最后一个
42 | if string.find(reip, ',') then
43 | local table_ip = split(reip,",")
44 | local table_len = table.getn(table_ip)
45 | local reip = table_ip[table_len]
46 | end
47 | return reip
48 | end
49 |
50 |
51 | function write(logfile,msg)
52 | local fd = io.open(logfile,"ab")
53 | if fd == nil then return end
54 | fd:write(msg)
55 | fd:flush()
56 | fd:close()
57 | end
58 |
59 | function log(data,ruletag)
60 | local request_method = ngx.req.get_method()
61 | local url = ngx.var.request_uri
62 | if attacklog then
63 | local realIp = getClientIp()
64 | local ua = ngx.var.http_user_agent
65 | --local servername=ngx.var.server_name
66 | local servername=ngx.var.host
67 | local time=ngx.localtime()
68 | local filename = logpath..'/'..servername.."_"..ngx.today().."_sec.log"
69 | if ua then
70 | local line = realIp.." ["..time.."] \""..request_method.." "..servername..url.."\" \""..data.."\" \""..ua.."\" \""..ruletag.."\"\n"
71 | write(filename,line)
72 | else
73 | local line = realIp.." ["..time.."] \""..request_method.." "..servername..url.."\" \""..data.."\" - \""..ruletag.."\"\n"
74 | write(filename,line)
75 | end
76 | end
77 | end
78 |
79 | --记录上传的文件
80 | function Filelog(logfilename,fn,finfo)
81 | local request_method = ngx.req.get_method()
82 | local url = ngx.var.request_uri
83 | if attacklog then
84 | local realIp = getClientIp()
85 | local ua = ngx.var.http_user_agent
86 | --local servername=ngx.var.server_name
87 | local servername=ngx.var.host
88 | local time=ngx.localtime()
89 | local filename = logpath..'/'..servername.."_"..ngx.today().."_"..logfilename..".log"
90 | if ua then
91 | local line = realIp.." ["..time.."] \""..request_method.." "..servername..url.."\" \""..fn.."\" \""..ua.."\" \""..finfo.."\"\n"
92 | write(filename,line)
93 | else
94 | local line = realIp.." ["..time.."] \""..request_method.." "..servername..url.."\" \""..fn.."\" - \""..finfo.."\"\n"
95 | write(filename,line)
96 | end
97 | end
98 | end
99 |
100 | function ipToDecimal(ckip)
101 | local n = 4
102 | local decimalNum = 0
103 | local pos = 0
104 | for s, e in function() return string.find(ckip, '.', pos, true) end do
105 | n = n - 1
106 | decimalNum = decimalNum + string.sub(ckip, pos, s-1) * (256 ^ n)
107 | pos = e + 1
108 | if n == 1 then decimalNum = decimalNum + string.sub(ckip, pos, string.len(ckip)) end
109 | end
110 | return decimalNum
111 | end
112 | ------------------------------------规则读取函数-------------------------------------------------------------------
113 | function read_rule(var)
114 | file = io.open(rulepath..'/'..var,"r")
115 | if file==nil then
116 | return
117 | end
118 | t = {}
119 | for line in file:lines() do
120 | table.insert(t,line)
121 | end
122 | file:close()
123 | return(t)
124 | end
125 |
126 | urlrules=read_rule('url')
127 | argsrules=read_rule('args')
128 | uarules=read_rule('user-agent')
129 | whiteuarules=read_rule('white-user-agent')
130 | wturlrules=read_rule('whiteurl')
131 | postrules=read_rule('post')
132 | ckrules=read_rule('cookie')
133 |
134 | function say_html(v)
135 | if not v then
136 | if Redirect then
137 | ngx.header.content_type = "text/html; charset=UTF-8"
138 | ngx.status = ngx.HTTP_FORBIDDEN
139 | ngx.say(html)
140 | ngx.exit(ngx.status)
141 | end
142 | else
143 | ngx.header.content_type = "text/html; charset=UTF-8"
144 | ngx.status = ngx.HTTP_FORBIDDEN
145 | ngx.say(say2_html(string.format(html,v)))
146 | ngx.exit(ngx.status)
147 | end
148 | end
149 |
150 | function say2_html(var)
151 | return var
152 | end
153 |
154 | function whiteurl()
155 | if WhiteCheck then
156 | if wturlrules ~=nil then
157 | for _,rule in pairs(wturlrules) do
158 | if ngxmatch(ngx.var.request_uri,rule,"isjo") then
159 | return true
160 | end
161 | end
162 | end
163 | end
164 | return false
165 | end
166 |
167 | function whitehost()
168 | if WhiteHostCheck then
169 | local items = Set(hostWhiteList)
170 | for host in pairs(items) do
171 | if ngxmatch(ngx.var.host, host, "isjo") then
172 | log("-","white host: ".. host)
173 | return true
174 | end
175 | end
176 | end
177 | return false
178 | end
179 |
180 | function args()
181 | for _,rule in pairs(argsrules) do
182 | if ngxmatch(unescape(ngx.var.request_uri),rule,"isjo") then
183 | log("-",rule)
184 | say_html("URL请求异常")
185 | return true
186 | end
187 | local args = ngx.req.get_uri_args()
188 | for key, val in pairs(args) do
189 | if type(val)=='table' then
190 | local t={}
191 | for k,v in pairs(val) do
192 | if v == true then
193 | v=""
194 | end
195 | table.insert(t,v)
196 | end
197 | data=table.concat(t, " ")
198 | else
199 | data=val
200 | end
201 | if data and type(data) ~= "boolean" and rule ~="" and ngxmatch(unescape(data),rule,"isjo") then
202 | log("-", "args in attack rules: " .. rule .. " data: " .. tostring(data))
203 | say_html("URL参数异常")
204 | return true
205 | end
206 | end
207 | end
208 | return false
209 | end
210 |
211 | function url()
212 | if UrlDeny then
213 | for _,rule in pairs(urlrules) do
214 | if rule ~="" and ngxmatch(ngx.var.request_uri,rule,"isjo") then
215 | log("-", "url in attack rules: " .. rule)
216 | say_html("URL拦截命中")
217 | return true
218 | end
219 | end
220 | end
221 | return false
222 | end
223 |
224 | function ua()
225 | local ua = ngx.var.http_user_agent
226 | if ua ~= nil then
227 | for _,rule in pairs(uarules) do
228 | if rule ~="" and ngxmatch(ua,rule,"isjo") then
229 | log("-", "ua in attack rules: " .. rule)
230 | say_html("UA拦截命中")
231 | return true
232 | end
233 | end
234 | end
235 | return false
236 | end
237 |
238 | --body内容检查
239 | function body(data)
240 | if not FileContentCheck then
241 | return false
242 | end
243 | for _,rule in pairs(postrules) do
244 | if rule ~="" and data~="" and ngxmatch(unescape(data),rule,"isjo") then
245 | log(data,rule)
246 | say_html("Body POST拦截命中")
247 | return true
248 | end
249 | end
250 | return false
251 | end
252 |
253 |
254 |
255 | function cookie()
256 | local ck = ngx.var.http_cookie
257 | if CookieCheck and ck then
258 | for _,rule in pairs(ckrules) do
259 | if rule ~="" and ngxmatch(ck,rule,"isjo") then
260 | log("-", "cookie in attack rules: " .. rule)
261 | say_html("Cookie异常,疑似攻击")
262 | return true
263 | end
264 | end
265 | end
266 | return false
267 | end
268 |
269 |
270 | --[[
271 | @comment cc攻击匹配
272 | @param
273 | @return
274 | ]]
275 | function denycc()
276 | if CCDeny then
277 | --local uri = ngx.var.uri
278 | --改用request_uri,并且进行base64,以防特殊符号出问题。解决使用URL传参导致触发CC异常
279 | --base64url是Base64编码的一种改进形式,它用“-”和“_”替代了“+”和“/”,编码后长度不是4的倍数时也不使用“=”填补,可以安全地用在URL 里。
280 | local uri = b64.encode_base64url(tostring(ngx.var.request_uri))
281 | local CCcount = tonumber(string.match(urlCCrate, "(.*)/"))
282 | local CCseconds = tonumber(string.match(urlCCrate, "/(.*)"))
283 | local ipCCcount = tonumber(string.match(ipCCrate, "(.*)/"))
284 | local ipCCseconds = tonumber(string.match(ipCCrate, "/(.*)"))
285 | local now_ip = getClientIp()
286 | local token = now_ip .. '.' ..uri
287 | local urllimit = ngx.shared.urllimit
288 | local iplimit = ngx.shared.iplimit
289 | local req, _ = urllimit:get(token)
290 | local ipreq, _ = iplimit:get(now_ip)
291 |
292 | if req then -- ip访问url频次检测
293 | if req > CCcount then
294 | log("-", "IP get url over times. ")
295 | say_html("IpURL频繁访问限制,请稍后再试")
296 | -- say_html(token)
297 | return true
298 | else
299 | urllimit:incr(token, 1)
300 | end
301 | else
302 | urllimit:set(token, 1, CCseconds)
303 | end
304 |
305 | if ipreq then -- 访问ip频次检测
306 | if ipreq > ipCCcount then
307 | log("-", "IP get host over times. ")
308 | say_html("IP频繁访问限制,请稍后再试")
309 | return true
310 | else
311 | iplimit:incr(now_ip, 1)
312 | end
313 | else
314 | iplimit:set(now_ip, 1, ipCCseconds)
315 | end
316 | end
317 |
318 | return false
319 | end
320 |
321 |
322 |
323 | function whiteua()
324 | local ua = ngx.var.http_user_agent
325 | if ua ~= nil then
326 | for _,rule in pairs(whiteuarules) do
327 | if rule ~="" and ngxmatch(ua,rule,"isjo") then
328 | return true
329 | end
330 | end
331 | end
332 | return false
333 | end
334 |
335 | function get_boundary()
336 | local header = get_headers()["content-type"]
337 | if not header then
338 | return nil
339 | end
340 |
341 | if type(header) == "table" then
342 | header = header[1]
343 | end
344 |
345 | local m = match(header, ";%s*boundary=\"([^\"]+)\"")
346 | if m then
347 | return m
348 | end
349 |
350 | return match(header, ";%s*boundary=([^\",;]+)")
351 | end
352 |
353 | --数字转换为八位二进制
354 | function byte2bin(n)
355 | local t = {}
356 | for i=7,0,-1 do
357 | t[#t+1] = math.floor(n / 2^i)
358 | n = n % 2^i
359 | end
360 | return table.concat(t)
361 | end
362 |
363 | --拼接IP每部分的二进制,返回IP完整的二进制
364 | function IP2bin(ip_s)
365 | local IP_p1,IP_p2,IP_p3,IP_p4=string.match(ip_s, "(%d+).(%d+).(%d+).(%d+)")
366 | ip_str = byte2bin(IP_p1)..byte2bin(IP_p2)..byte2bin(IP_p3)..byte2bin(IP_p4)
367 | return ip_str
368 | end
369 |
370 | --判断二进制IP是否在属于某网段
371 | function IpBelongToNetwork(bin_ip,bin_network,mask)
372 | if (string.sub(bin_ip,1,mask) == string.sub(bin_network,1,mask)) then
373 | return true
374 | else
375 | return false
376 | end
377 | end
378 |
379 | --字符串分割函数
380 | function split(str,delimiter)
381 | local dLen = string.len(delimiter)
382 | local newDeli = ''
383 | for i=1,dLen,1 do
384 | newDeli = newDeli .. "["..string.sub(delimiter,i,i).."]"
385 | end
386 | local locaStart,locaEnd = string.find(str,newDeli)
387 | local arr = {}
388 | local n = 1
389 | while locaStart ~= nil
390 | do
391 | if locaStart>0 then
392 | arr[n] = string.sub(str,1,locaStart-1)
393 | n = n + 1
394 | end
395 | str = string.sub(str,locaEnd+1,string.len(str))
396 | locaStart,locaEnd = string.find(str,newDeli)
397 | end
398 | if str ~= nil then
399 | arr[n] = str
400 | end
401 | return arr
402 | end
403 |
404 |
405 | function blockip()
406 | if next(ipBlocklist) ~= nil then
407 | local cIP = getClientIp()
408 | local numIP = 0
409 | if cIP ~= "unknown" then
410 | numIP = tonumber(ipToDecimal(cIP))
411 | end
412 | for _,ip in pairs(ipBlocklist) do
413 | local s, e = string.find(ip, '-', 0, true)
414 | local x, j = string.find(ip, '/', 0, true)
415 | --IP字符串中不存在"-"、"/"等划分网段标识
416 | if s == nil and x == nil and cIP == ip then
417 | ngx.exit(403)
418 | return true
419 | --范围划分法
420 | elseif s ~= nil then
421 | sIP = tonumber(ipToDecimal(string.sub(ip, 0, s - 1)))
422 | eIP = tonumber(ipToDecimal(string.sub(ip, e + 1, string.len(ip))))
423 | if numIP >= sIP and numIP <= eIP then
424 | ngx.exit(403)
425 | return true
426 | end
427 | --掩码划分法
428 | elseif x ~= nil then
429 | local ip_list = split(ip, "/")
430 | if IpBelongToNetwork(IP2bin(cIP),IP2bin(ip_list[1]),ip_list[2]) then
431 | ngx.exit(403)
432 | return true
433 | end
434 | end
435 | end
436 | end
437 | return false
438 | end
439 |
440 | --上传文件白名单后缀检查
441 | function fileExtCheck(ext,fn,finfo)
442 | local items = Set(white_fileExt)
443 | local ext = string.lower(ext)
444 | if ext then
445 | for rule in pairs(items) do
446 | if string.lower(rule) == ext then
447 | Filelog('UploadFile',fn,finfo)
448 | return true
449 | end
450 | end
451 | Filelog('UploadFileFailed',fn,finfo)
452 | say_html('该类型文件不允许上传:'..ext)
453 | end
454 | return false
455 | end
456 | function Set (list)
457 | local set = {}
458 | for _, l in ipairs(list) do set[l] = true end
459 | return set
460 | end
461 |
462 | function whiteip()
463 | if next(ipWhitelist) ~= nil then
464 | local cIP = getClientIp()
465 | local numIP = 0
466 | if cIP ~= "unknown" then
467 | numIP = tonumber(ipToDecimal(cIP))
468 | end
469 | for _,ip in pairs(ipWhitelist) do
470 | local s, e = string.find(ip, '-', 0, true)
471 | local x, j = string.find(ip, '/', 0, true)
472 | --IP字符串中不存在"-"、"/"等划分网段标识
473 | if s == nil and x == nil and cIP == ip then
474 | return true
475 | --范围划分法
476 | elseif s ~= nil then
477 | sIP = tonumber(ipToDecimal(string.sub(ip, 0, s - 1)))
478 | eIP = tonumber(ipToDecimal(string.sub(ip, e + 1, string.len(ip))))
479 | if numIP >= sIP and numIP <= eIP then
480 | return true
481 | end
482 | --掩码划分法
483 | elseif x ~= nil then
484 | local ip_list = split(ip, "/")
485 | if IpBelongToNetwork(IP2bin(cIP),IP2bin(ip_list[1]),ip_list[2]) then
486 | return true
487 | end
488 | end
489 | end
490 | end
491 | return false
492 | end
493 |
494 |
495 |
496 |
--------------------------------------------------------------------------------
/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 Block_RequestMethod() then
5 | elseif whiteip() then
6 | elseif whitehost() then
7 | elseif RefererLimit() then
8 | elseif whiteua() then
9 | elseif blockip() then
10 | elseif whiteurl() then
11 | elseif denycc() then
12 | elseif ngx.var.http_Acunetix_Aspect then
13 | ngx.exit(444)
14 | elseif ngx.var.http_X_Scan_Memo then
15 | ngx.exit(444)
16 | elseif ua() then
17 | elseif url() then
18 | elseif args() then
19 | elseif cookie() then
20 | elseif PostCheck then
21 | if method=="POST" then
22 | local boundary = get_boundary()
23 | if boundary then
24 | local len = string.len
25 | local sock, err = ngx.req.socket()
26 | if not sock then
27 | return
28 | end
29 | ngx.req.init_body(128 * 1024)
30 | sock:settimeout(0)
31 | local content_length = nil
32 | content_length=tonumber(ngx.req.get_headers()['content-length'])
33 | local content_type = nil
34 | content_type = ngx.req.get_headers()['content-type']
35 | local chunk_size = 4096
36 | if content_length < chunk_size then
37 | chunk_size = content_length
38 | end
39 | local size = 0
40 | while size < content_length do
41 | local data, err, partial = sock:receive(chunk_size)
42 | local data = data or partial
43 | if not data then
44 | return
45 | end
46 | ngx.req.append_body(data)
47 | --文件内容检查
48 | if body(data) then
49 | return true
50 | end
51 | size = size + len(data)
52 | --local m = ngxmatch(data,[[Content-Disposition: form-data;(.+)filename="(.+)\\.(.*)"]],'ijo')
53 | --单文件上传
54 | --local _,flname,lname = string.match(data,[[Content%-Disposition: form%-data;(.-)filename="([^\"]+)%.([^\"]-)".*]])
55 | --多文件上传i
56 | for _,flname,lname in string.gmatch(data,[[Content%-Disposition: form%-data;(.-)filename="([^\"]+)%.([^\"]-)"]],'ijo') do
57 | --文件后缀检查
58 | if lname then
59 | local fn = 'UploadFile: '..tostring(flname)..'.'..tostring(lname)
60 | fileExtCheck(lname,fn,'content-type: '..content_type)
61 | filetranslate = true
62 | end
63 | end
64 | if ngxmatch(data,"Content%-Disposition:",'isjo') then
65 | filetranslate = false
66 | end
67 | --文件内容检查
68 | if filetranslate==false then
69 | if body(data) then
70 | return true
71 | end
72 | end
73 | local less = content_length - size
74 | if less < chunk_size then
75 | chunk_size = less
76 | end
77 | end
78 | ngx.req.finish_body()
79 | else
80 | ngx.req.read_body()
81 | local args = ngx.req.get_post_args()
82 | if not args then
83 | return
84 | end
85 | for key, val in pairs(args) do
86 | if type(val) == "table" then
87 | if type(val[1]) == "boolean" then
88 | return
89 | end
90 | data=table.concat(val, ", ")
91 | else
92 | data=val
93 | end
94 | if data and type(data) ~= "boolean" and body(data) then
95 | --文件内容检查
96 | body(key)
97 | end
98 | end
99 | end
100 | end
101 | elseif country_white() then
102 | elseif country_block() then
103 | else
104 | return
105 | end
106 |
--------------------------------------------------------------------------------
/wafconf/BlockReferer:
--------------------------------------------------------------------------------
1 | --每行一个域名规则,支持正则
2 |
--------------------------------------------------------------------------------
/wafconf/WhiteReferer:
--------------------------------------------------------------------------------
1 | --每行一个域名规则,支持正则
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/readme.txt:
--------------------------------------------------------------------------------
1 | (1)user-agent的其他参考
2 | "FeedDemon" "JikeSpider" "Indy Library" "Alexa Toolbar" "AskTbFXTV" "AhrefsBot" "CrawlDaddy" "CoolpadWebkit" "Java" "Feedly"
3 | "Microsoft URL Control" "Swiftbot" "oBot" "jaunty" "Python-urllib" "lightDeckReports Bot" "YYSpider" "DigExt" "HttpClient" "MJ12bot"
4 | "heritrix" "EasouSpider" "Ezooms" "Baiduspider" "Googlebot" "Googlebot-Mobile"
5 |
6 |
--------------------------------------------------------------------------------
/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/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|pingback|wordpress| SF/)
2 |
--------------------------------------------------------------------------------
/wafconf/white-user-agent:
--------------------------------------------------------------------------------
1 | (baidu|Googlebot)
2 |
--------------------------------------------------------------------------------
/wafconf/whiteurl:
--------------------------------------------------------------------------------
1 | ^/123/$
2 |
--------------------------------------------------------------------------------