├── .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 | [![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 | 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 | --------------------------------------------------------------------------------