├── README.md ├── access.lua ├── config.lua ├── init.lua ├── other ├── 20180503174007.png ├── 20180503174354.png ├── WAF.png ├── nginx.conf ├── socket.lua ├── usertime.c ├── usertime.so └── 编译命令.txt ├── rules ├── args.rule ├── blackip.rule ├── cookie.rule ├── frequency.rule ├── post.rule ├── re.txt ├── url.rule ├── useragent.rule ├── whiteip.rule └── whiteurl.rule ├── util.lua └── waf.lua /README.md: -------------------------------------------------------------------------------- 1 | # Nginx-Lua-WAF概述 2 | Nginx-Lua-WAF是一款基于Nginx的使用Lua语言开发的灵活高效的Web应用层防火墙。Lua语言的灵活和不亚于C语言的执行效率,保证了在做为网站应用层防火墙时,功能和性能之间完美的平衡,整个防火墙系统对性能的消耗几乎可以忽略不计。 3 | 4 | Nginx-Lua-WAF完全可以放心在生产环境中使用,整个防火墙仅由5个lua程序文件组成,逻辑清晰,代码简单易懂,并配有较为详细的注释。整个代码全部读完也不会超过半个小时。强列建议先看下代码,相信你能很容易看懂。 5 | 6 | 本项目推荐使用由春哥(章亦春)维护的基于Nginx和LuaJIT的Web平台OpenResty作为Web服务器。OpenResty可以看作是在Nginx中添加了Lua的支持,并集成了常用的各类Lua库。当然,也可以手动编译Nginx,在编译中添加lua-nginx-module。 7 | 8 | # 主要特性 9 | 防火墙只是一个框架,核心是rule规则文件,源码中规则文件仅供参考,在实际的使用过程中,接合自己的业务特点,可以灵活开关各项功能,以及增添各种规则。 10 | - 支持对特定站点特定IP和特定URL组合的访问频率控制,即可以通过配置的百分比控制返回真实数据或预先配置的JSON字符串,该功能通常用于希望控制访问频率的接口,不希望客户端高频访问,以优雅的方式减少服务端不必要的性能开销 11 | - Nginx工作于web服务器模式,可以有多个不同的站点,仅需要配置hostname就可以对不同的站点应用不同的规则,或者使用全局的规则 12 | - 规则使用正则匹配,灵活性高 13 | - 支持IP白名单、IP黑名单、UserAgent、URL白名单、URL、Cookie、请求参数、POST级别的过滤,每个功能均有独立开关,可以自由启用需要的过滤功能,并且在规则层面都是可以基于站点的 14 | - 支持对CC攻击的防护 15 | - 完整的日志记录功能,JSON格式日志,方便后期通过ELK集中管理 16 | - 匹配规则后,支持回显html字符串、跳转到指定URL和不处理三种模式,其不设置为不处理后,仅记录日志,并不真正执行拦截动作,方便在生产环境中调试,不影响业务 17 | - 安装、部署和维护非常简单 18 | - 重载规则不会中断正常业务 19 | - 跨平台,Nginx可以运行在哪里,WAF就可以运行在哪里 20 | 21 | # 性能测试 22 | Nginx-Lua-WAF拥有非常高的性能,在虚拟机中测试结果如下: 23 | - 系统:CentOS Linux release 7.3.1611 (Core) 24 | - 内核:3.10.0-514.el7.x86_64 25 | - 内存:1G 26 | - CPU:1核心 Intel(R) Core(TM) i7-4600U CPU @ 2.10GHz 27 | - 测试命令:ab -n 10000 -c 50 http://127.0.0.1/index.html 28 | 29 | ### 关闭waf时测试 30 | 每秒处理14806次请求,处理单个请求平均3毫秒 31 | 32 | ![20180503174007.png](https://raw.githubusercontent.com/ddonline/nginx-lua-waf/master/other/20180503174007.png) 33 | ### 开启waf时测试 34 | (开启所有功能,因为有cc检测,将cc阈值设置为20000/60防止压测时被拦截) 35 | 每秒处理9581次请求,处理单个请求平均5毫秒 36 | 37 | ![20180503174354.png](https://raw.githubusercontent.com/ddonline/nginx-lua-waf/master/other/20180503174354.png) 38 | ##### 可以看出启用waf后,Nginx性能依然非常高,近10k次的处理能力,能够满足任何业务场景的需要 39 | 40 | # Nginx-Lua-WAF处理流程 41 | ![WAF.png](https://raw.githubusercontent.com/ddonline/nginx-lua-waf/master/other/WAF.png) 42 | 43 | # 安装部署 44 | ## 以CentOS 7为例 45 | ### 编译安装openresty 46 | 47 | 从[openresty](http://openresty.org/cn/download.html)官方下载最新版本的源码包。 48 | 49 | 01、编译安装openresty: 50 | 51 | ```bash 52 | #安装工具 53 | yum -y install wget 54 | #准备编译环境 55 | yum -y install gcc 56 | #准备依赖包 57 | yum -y install install perl openssl openssl-devel 58 | #下载并解压源码包 59 | wget https://openresty.org/download/openresty-1.13.6.1.tar.gz 60 | tar zxf openresty-1.13.6.1.tar.gz 61 | #编译安装 62 | cd openresty-1.13.6.1 63 | ./configure 64 | make 65 | make install 66 | #默认openresty会安装到/usr/local/openresty目录 67 | #nginx配置文件位置:/usr/local/openresty/nginx/conf/nginx.conf 68 | #nginx站点目录:/usr/local/openresty/nginx/html 69 | #nginx可执行文件位置:/usr/local/openresty/nginx/sbin/nginx 70 | #后续工作 71 | #临时关闭selinux 72 | setenforce 0 73 | #开启防火墙 74 | #开启80端口的两种方式 75 | firewall-cmd --permanent --zone=public --add-port=80/tcp 76 | firewall-cmd --permanent --zone=public --add-service=http 77 | firewall-cmd --reload #重载防火墙,使配置生效 78 | #启动nginx 79 | /usr/local/openresty/nginx/sbin/nginx -t #检查配置文件语法是否正确 80 | /usr/local/openresty/nginx/sbin/nginx #启动nginx 81 | ``` 82 | 83 | 02、编译模块usertime.so 84 | 85 | ```bash 86 | #usertime.c文件位于nginx-lua-waf/other/usertime.c 87 | #编译好的usertime.so位于nginx-lua-waf/other/usertime.so 88 | #如果平台和我的不一样建议自己编译usertime.so模块 89 | #安装依赖包 90 | yum -y install lua-devel 91 | #编译模块usertime.so 92 | cc -g -O2 -Wall -fPIC --shared usertime.c -o usertime.so 93 | #将模块复制到lualib目录 94 | cp usertime.so /usr/local/openresty/lualib 95 | #如果是直接使用我编译的usertime.so,复制到lualib后需要授予可执行权限 96 | chmod a+x /usr/local/openresty/lualib/usertime.so 97 | ``` 98 | 99 | 03、部署Nginx-Lua-WAF 100 | 101 | ``` 102 | #安装工具 103 | yum -y install unzip 104 | #下载Nginx-Lua-WAF 105 | wget https://github.com/ddonline/nginx-lua-waf/archive/master.zip 106 | #解压缩 107 | unzip master.zip #解压后得到文件夹nginx-lua-waf-master 108 | #对文件夹重命名 109 | mv nginx-lua-waf-master nginx-lua-waf 110 | #将nginx-lua-waf文件夹复制到nginx/conf目录下 111 | cp -r nginx-lua-waf /usr/local/openresty/nginx/conf 112 | 113 | #在nginx.conf中添加配置 114 | vi /usr/local/openresty/nginx/conf/nginx.conf 115 | 在http级别添加以下内容: 116 | #nginx-lua-waf配置 117 | lua_package_path "/usr/local/openresty/nginx/conf/nginx-lua-waf/?.lua;"; 118 | lua_shared_dict limit 100m; 119 | #开启lua代码缓存功能 120 | lua_code_cache on; 121 | lua_regex_cache_max_entries 4096; 122 | init_by_lua_file /usr/local/openresty/nginx/conf/nginx-lua-waf/init.lua; 123 | access_by_lua_file /usr/local/openresty/nginx/conf/nginx-lua-waf/access.lua; 124 | 在server级别修改server_name: 125 | #在每个vhost中(server级别)定义server_name时,建议设置一个以上的主机名,默认第一个将做为规则中的主机区别标志,例如 126 | server_name api api.test.com; 127 | 128 | #修改config.lua中日志文件和规则文件路径和其它需要配置的项目 129 | vi /usr/local/openresty/nginx/conf/nginx-lua-waf/config.lua 130 | config_log_dir = "/var/log/nginx", 131 | config_rule_dir = "/usr/local/openresty/nginx/conf/nginx-lua-waf/rules", 132 | #修改日志目录权限,使nginx对目录可写 133 | mkdir -p /var/log/nginx/ 134 | chmod o+w /var/log/nginx/ 135 | #重载nginx使配置生效 136 | /usr/local/openresty/nginx/sbin/nginx -t #检查配置文件语法是否正确 137 | /usr/local/openresty/nginx/sbin/nginx -s reload #重载nginx 138 | #测试waf是否工作正常 139 | curl http://127.0.0.1/test.zip 140 | #若返回 "规则过滤测试" 字样,则说明waf已生效,url.rule中定义有规则阻止下载zip文件,此时/var/log/nginx/目录中应有类似2018-05-04_waf.log的JSON格式日志文件 141 | #若返回 404 说明waf未生效 142 | ``` 143 | 144 | # 使用中注意事项 145 | - waf配置文件:nginx-lua-waf/config.lua,各项配置均有注释说明 146 | - 使用前请检查过滤规则是否符合自己实际情况,根据实际增删条目,防止误伤 147 | - 规则文件除frequency.rule外全部为正则表达式,除frequency.rule、whiteip.rule、blackip.rule、whiteurl.rule外全部不区分大小写 148 | - 规则文件中以"--"开头的为注释内容,除最后一行外,不能留有空行,且结尾字符应为LF 149 | - 在用于生产环境时,可先将模式设置为jinghuashuiyue并检查拦截日志,确认有无误伤,该模式仅记录日志,不实际进行拦截(对IP黑名单和CC攻击过滤不适用,详见处理流程图) 150 | - 更新规则文件后,使用reload命令(/usr/local/openresty/nginx/sbin/nginx -s reload)使用配置生效,该命令不会中断服务,不建议使用restart 151 | - 部署过程中对openresty的安装使用的是默认选项,如果需要自定义,可以参考我的博文:[编译Nginx(OpenResty)支持Lua扩展](http://pdf.us/2018/03/19/742.html) 152 | 153 | 154 | # 致谢 155 | 156 | 1. 感谢春哥开源的[openresty](https://openresty.org) 157 | 2. 开源项目:https://github.com/xsec-lab/x-waf 158 | 3. 开源项目:https://github.com/tluolovembtan/jsh_waf 159 | 4. 开源项目:https://github.com/loveshell/ngx_lua_waf 160 | -------------------------------------------------------------------------------- /access.lua: -------------------------------------------------------------------------------- 1 | waf.check() -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 说明: 3 | 1、nginx配置:在http级别添加以下内容 4 | #nginx-lua-waf配置 5 | lua_package_path "/usr/local/openresty/nginx/conf/nginx-lua-waf/?.lua;"; 6 | lua_shared_dict limit 100m; 7 | #开启lua代码缓存功能 8 | lua_code_cache on; 9 | lua_regex_cache_max_entries 4096; 10 | init_by_lua_file /usr/local/openresty/nginx/conf/nginx-lua-waf/init.lua; 11 | access_by_lua_file /usr/local/openresty/nginx/conf/nginx-lua-waf/access.lua; 12 | 2、vhost配置:server_name第一个名称为规则主机限制时的关键词 13 | server_name api api.test.com; 14 | 3、规则格式: 15 | 所有规则均可添加主机限制,如api-index.html 这会仅限制api站点的访问 16 | 如果不添加,则为全局限制 17 | 4、规则文件一行一条规则 18 | 5、规则文件可以使用"--"进行注释 19 | 6、规则文件需要LF行结尾 否则会发生问题 20 | 7、性能: 21 | ab -n 10000 -c 50 http://127.0.0.1/index.html 22 | 关闭waf Requests per second: 14806 [#/sec] (mean) 23 | 打开waf Requests per second: 9581 [#/sec] (mean) 24 | 8、日志存放文件夹需要有写放权限 chmod o+w /var/log/nginx/ 25 | ]] 26 | -- enable = "on", disable = "off" 27 | 28 | local _M = { 29 | -- 防火墙开关 30 | config_waf_enable = "on", 31 | -- 日志文件存放目录 结尾不带/ 32 | config_log_dir = "/var/log/nginx", 33 | -- 规则文件存放目录 结尾不带/ 34 | config_rule_dir = "/usr/local/openresty/nginx/conf/nginx-lua-waf/rules", 35 | 36 | --01假数据时不进行后续流程,非正则匹配 37 | -- 频率控制开关 38 | frequency_control_check = "on", 39 | -- 频率控制中返回空数据内容 40 | frequency_text = [[{"status":"ok"}]], 41 | 42 | --02直接通过不进行后续流程 43 | -- IP白名单开关 44 | config_white_ip_check = "on", 45 | 46 | --03返回403 47 | -- IP黑名单开关 48 | config_black_ip_check = "on", 49 | 50 | --04WAF处理:跳转/html/仅日志 51 | -- UserAgent过滤开关 52 | config_user_agent_check = "on", 53 | 54 | --05直接通过不进行后续流程 55 | -- URL白名单开关 56 | config_white_url_check = "on", 57 | 58 | --06WAF处理:跳转/html/仅日志 59 | -- URL过滤开关 60 | config_url_check = "on", 61 | 62 | --07返回403记录limit 63 | -- CC攻击过滤开关 64 | config_cc_check = "on", 65 | -- 设置CC攻击检测依据 攻击阈值/检测时间段 66 | config_cc_rate = "3000/60", 67 | 68 | --08WAF处理:跳转/html/仅日志 69 | -- Cookie过滤开关 70 | config_cookie_check = "on", 71 | 72 | --09WAF处理:跳转/html/仅日志 73 | -- ARGS请求参数过滤开关 74 | config_url_args_check = "on", 75 | 76 | --10WAF处理:跳转/html/仅日志 77 | -- POST过滤开关 78 | config_post_check = "on", 79 | -- 处理方式 redirect/html/jinghuashuiyue jinghuashuiyue只记录日志 80 | config_waf_model = "html", 81 | -- 当配置为redirect时跳转到的URL 82 | config_waf_redirect_url = "http://www.baidu.com", 83 | -- bad_guys过期时间 84 | -- config_expire_time = 600, 85 | -- 当配置为html时 显示的内容 86 | config_output_html = [[ 87 | 88 | 89 | 90 | LEPEI WAF 91 | 92 | 93 |
94 |
95 |
96 |
97 | 您的IP为: %s 98 |
99 |
100 | 已触发WAF规则 101 |
102 |
103 | 实际使用请修改此提示信息 104 |
105 |
106 |
107 |
108 | 109 | 110 | ]], 111 | } 112 | 113 | return _M -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | waf = require("waf") 2 | waf_rules = waf.load_rules() -------------------------------------------------------------------------------- /other/20180503174007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonline/nginx-lua-waf/d374f66511806224eeb8c83b97e9679ccabe4ac1/other/20180503174007.png -------------------------------------------------------------------------------- /other/20180503174354.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonline/nginx-lua-waf/d374f66511806224eeb8c83b97e9679ccabe4ac1/other/20180503174354.png -------------------------------------------------------------------------------- /other/WAF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonline/nginx-lua-waf/d374f66511806224eeb8c83b97e9679ccabe4ac1/other/WAF.png -------------------------------------------------------------------------------- /other/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | #user nobody; 3 | worker_processes 1; 4 | 5 | #error_log logs/error.log; 6 | #error_log logs/error.log notice; 7 | #error_log logs/error.log info; 8 | 9 | #pid logs/nginx.pid; 10 | 11 | 12 | events { 13 | worker_connections 1024; 14 | } 15 | 16 | 17 | http { 18 | include mime.types; 19 | default_type application/octet-stream; 20 | 21 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 22 | # '$status $body_bytes_sent "$http_referer" ' 23 | # '"$http_user_agent" "$http_x_forwarded_for"'; 24 | 25 | #access_log logs/access.log main; 26 | 27 | #nginx-lua-waf配置 28 | lua_package_path "/usr/local/openresty/nginx/conf/nginx-lua-waf/?.lua;"; 29 | lua_shared_dict limit 100m; 30 | #开启lua代码缓存功能 31 | lua_code_cache on; 32 | lua_regex_cache_max_entries 4096; 33 | init_by_lua_file /usr/local/openresty/nginx/conf/nginx-lua-waf/init.lua; 34 | access_by_lua_file /usr/local/openresty/nginx/conf/nginx-lua-waf/access.lua; 35 | 36 | sendfile on; 37 | #tcp_nopush on; 38 | 39 | #keepalive_timeout 0; 40 | keepalive_timeout 65; 41 | 42 | #gzip on; 43 | 44 | server { 45 | listen 80; 46 | server_name api api.test.com; 47 | 48 | #charset koi8-r; 49 | 50 | #access_log logs/host.access.log main; 51 | 52 | location / { 53 | root html; 54 | index index.html index.htm; 55 | } 56 | 57 | #error_page 404 /404.html; 58 | 59 | # redirect server error pages to the static page /50x.html 60 | # 61 | error_page 500 502 503 504 /50x.html; 62 | location = /50x.html { 63 | root html; 64 | } 65 | 66 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 67 | # 68 | #location ~ \.php$ { 69 | # proxy_pass http://127.0.0.1; 70 | #} 71 | 72 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 73 | # 74 | #location ~ \.php$ { 75 | # root html; 76 | # fastcgi_pass 127.0.0.1:9000; 77 | # fastcgi_index index.php; 78 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 79 | # include fastcgi_params; 80 | #} 81 | 82 | # deny access to .htaccess files, if Apache's document root 83 | # concurs with nginx's one 84 | # 85 | #location ~ /\.ht { 86 | # deny all; 87 | #} 88 | } 89 | 90 | 91 | # another virtual host using mix of IP-, name-, and port-based configuration 92 | # 93 | #server { 94 | # listen 8000; 95 | # listen somename:8080; 96 | # server_name somename alias another.alias; 97 | 98 | # location / { 99 | # root html; 100 | # index index.html index.htm; 101 | # } 102 | #} 103 | 104 | 105 | # HTTPS server 106 | # 107 | #server { 108 | # listen 443 ssl; 109 | # server_name localhost; 110 | 111 | # ssl_certificate cert.pem; 112 | # ssl_certificate_key cert.key; 113 | 114 | # ssl_session_cache shared:SSL:1m; 115 | # ssl_session_timeout 5m; 116 | 117 | # ssl_ciphers HIGH:!aNULL:!MD5; 118 | # ssl_prefer_server_ciphers on; 119 | 120 | # location / { 121 | # root html; 122 | # index index.html index.htm; 123 | # } 124 | #} 125 | 126 | } 127 | -------------------------------------------------------------------------------- /other/socket.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2013-2014 Jiale Zhi (calio), CloudFlare Inc. 2 | --require "luacov" 3 | 4 | local concat = table.concat 5 | local tcp = ngx.socket.tcp 6 | local udp = ngx.socket.udp 7 | local timer_at = ngx.timer.at 8 | local ngx_log = ngx.log 9 | local ngx_sleep = ngx.sleep 10 | local type = type 11 | local pairs = pairs 12 | local tostring = tostring 13 | local debug = ngx.config.debug 14 | 15 | local DEBUG = ngx.DEBUG 16 | local CRIT = ngx.CRIT 17 | 18 | local MAX_PORT = 65535 19 | 20 | 21 | -- table.new(narr, nrec) 22 | local succ, new_tab = pcall(require, "table.new") 23 | if not succ then 24 | new_tab = function () return {} end 25 | end 26 | 27 | local _M = new_tab(0, 5) 28 | 29 | local is_exiting 30 | 31 | if not ngx.config or not ngx.config.ngx_lua_version 32 | or ngx.config.ngx_lua_version < 9003 then 33 | 34 | is_exiting = function() return false end 35 | 36 | ngx_log(CRIT, "We strongly recommend you to update your ngx_lua module to " 37 | .. "0.9.3 or above. lua-resty-logger-socket will lose some log " 38 | .. "messages when Nginx reloads if it works with ngx_lua module " 39 | .. "below 0.9.3") 40 | else 41 | is_exiting = ngx.worker.exiting 42 | end 43 | 44 | 45 | _M._VERSION = '0.03' 46 | 47 | -- user config 48 | local flush_limit = 4096 -- 4KB 49 | local drop_limit = 1048576 -- 1MB 50 | local timeout = 1000 -- 1 sec 51 | local host 52 | local port 53 | local ssl = false 54 | local ssl_verify = true 55 | local sni_host 56 | local path 57 | local max_buffer_reuse = 10000 -- reuse buffer for at most 10000 58 | -- times 59 | local periodic_flush = nil 60 | local need_periodic_flush = nil 61 | local sock_type = 'tcp' 62 | 63 | -- internal variables 64 | local buffer_size = 0 65 | -- 2nd level buffer, it stores logs ready to be sent out 66 | local send_buffer = "" 67 | -- 1st level buffer, it stores incoming logs 68 | local log_buffer_data = new_tab(20000, 0) 69 | -- number of log lines in current 1st level buffer, starts from 0 70 | local log_buffer_index = 0 71 | 72 | local last_error 73 | 74 | local connecting 75 | local connected 76 | local exiting 77 | local retry_connect = 0 78 | local retry_send = 0 79 | local max_retry_times = 3 80 | local retry_interval = 100 -- 0.1s 81 | local pool_size = 10 82 | local flushing 83 | local logger_initted 84 | local counter = 0 85 | local ssl_session 86 | 87 | local function _write_error(msg) 88 | last_error = msg 89 | end 90 | 91 | local function _do_connect() 92 | local ok, err, sock 93 | 94 | if not connected then 95 | if (sock_type == 'udp') then 96 | sock, err = udp() 97 | else 98 | sock, err = tcp() 99 | end 100 | 101 | if not sock then 102 | _write_error(err) 103 | return nil, err 104 | end 105 | 106 | sock:settimeout(timeout) 107 | end 108 | 109 | -- "host"/"port" and "path" have already been checked in init() 110 | if host and port then 111 | if (sock_type == 'udp') then 112 | ok, err = sock:setpeername(host, port) 113 | else 114 | ok, err = sock:connect(host, port) 115 | end 116 | elseif path then 117 | ok, err = sock:connect("unix:" .. path) 118 | end 119 | 120 | if not ok then 121 | return nil, err 122 | end 123 | 124 | return sock 125 | end 126 | 127 | local function _do_handshake(sock) 128 | if not ssl then 129 | return sock 130 | end 131 | 132 | local session, err = sock:sslhandshake(ssl_session, sni_host or host, 133 | ssl_verify) 134 | if not session then 135 | return nil, err 136 | end 137 | 138 | ssl_session = session 139 | return sock 140 | end 141 | 142 | local function _connect() 143 | local err, sock 144 | 145 | if connecting then 146 | if debug then 147 | ngx_log(DEBUG, "previous connection not finished") 148 | end 149 | return nil, "previous connection not finished" 150 | end 151 | 152 | connected = false 153 | connecting = true 154 | 155 | retry_connect = 0 156 | 157 | while retry_connect <= max_retry_times do 158 | sock, err = _do_connect() 159 | 160 | if sock then 161 | sock, err = _do_handshake(sock) 162 | if sock then 163 | connected = true 164 | break 165 | end 166 | end 167 | 168 | if debug then 169 | ngx_log(DEBUG, "reconnect to the log server: ", err) 170 | end 171 | 172 | -- ngx.sleep time is in seconds 173 | if not exiting then 174 | ngx_sleep(retry_interval / 1000) 175 | end 176 | 177 | retry_connect = retry_connect + 1 178 | end 179 | 180 | connecting = false 181 | if not connected then 182 | return nil, "try to connect to the log server failed after " 183 | .. max_retry_times .. " retries: " .. err 184 | end 185 | 186 | return sock 187 | end 188 | 189 | local function _prepare_stream_buffer() 190 | local packet = concat(log_buffer_data, "", 1, log_buffer_index) 191 | send_buffer = send_buffer .. packet 192 | 193 | log_buffer_index = 0 194 | counter = counter + 1 195 | if counter > max_buffer_reuse then 196 | log_buffer_data = new_tab(20000, 0) 197 | counter = 0 198 | if debug then 199 | ngx_log(DEBUG, "log buffer reuse limit (" .. max_buffer_reuse 200 | .. ") reached, create a new \"log_buffer_data\"") 201 | end 202 | end 203 | end 204 | 205 | local function _do_flush() 206 | local ok, err, sock, bytes 207 | local packet = send_buffer 208 | 209 | sock, err = _connect() 210 | if not sock then 211 | return nil, err 212 | end 213 | 214 | bytes, err = sock:send(packet) 215 | if not bytes then 216 | -- "sock:send" always closes current connection on error 217 | return nil, err 218 | end 219 | 220 | if debug then 221 | ngx.update_time() 222 | ngx_log(DEBUG, ngx.now(), ":log flush:" .. bytes .. ":" .. packet) 223 | end 224 | 225 | if (sock_type ~= 'udp') then 226 | ok, err = sock:setkeepalive(0, pool_size) 227 | if not ok then 228 | return nil, err 229 | end 230 | end 231 | 232 | return bytes 233 | end 234 | 235 | local function _need_flush() 236 | if buffer_size > 0 then 237 | return true 238 | end 239 | 240 | return false 241 | end 242 | 243 | local function _flush_lock() 244 | if not flushing then 245 | if debug then 246 | ngx_log(DEBUG, "flush lock acquired") 247 | end 248 | flushing = true 249 | return true 250 | end 251 | return false 252 | end 253 | 254 | local function _flush_unlock() 255 | if debug then 256 | ngx_log(DEBUG, "flush lock released") 257 | end 258 | flushing = false 259 | end 260 | 261 | local function _flush() 262 | local err 263 | 264 | -- pre check 265 | if not _flush_lock() then 266 | if debug then 267 | ngx_log(DEBUG, "previous flush not finished") 268 | end 269 | -- do this later 270 | return true 271 | end 272 | 273 | if not _need_flush() then 274 | if debug then 275 | ngx_log(DEBUG, "no need to flush:", log_buffer_index) 276 | end 277 | _flush_unlock() 278 | return true 279 | end 280 | 281 | -- start flushing 282 | retry_send = 0 283 | if debug then 284 | ngx_log(DEBUG, "start flushing") 285 | end 286 | 287 | local bytes 288 | while retry_send <= max_retry_times do 289 | if log_buffer_index > 0 then 290 | _prepare_stream_buffer() 291 | end 292 | 293 | bytes, err = _do_flush() 294 | 295 | if bytes then 296 | break 297 | end 298 | 299 | if debug then 300 | ngx_log(DEBUG, "resend log messages to the log server: ", err) 301 | end 302 | 303 | -- ngx.sleep time is in seconds 304 | if not exiting then 305 | ngx_sleep(retry_interval / 1000) 306 | end 307 | 308 | retry_send = retry_send + 1 309 | end 310 | 311 | _flush_unlock() 312 | 313 | if not bytes then 314 | local err_msg = "try to send log messages to the log server " 315 | .. "failed after " .. max_retry_times .. " retries: " 316 | .. err 317 | _write_error(err_msg) 318 | return nil, err_msg 319 | else 320 | if debug then 321 | ngx_log(DEBUG, "send " .. bytes .. " bytes") 322 | end 323 | end 324 | 325 | buffer_size = buffer_size - #send_buffer 326 | send_buffer = "" 327 | 328 | return bytes 329 | end 330 | 331 | local function _periodic_flush(premature) 332 | if premature then 333 | exiting = true 334 | end 335 | 336 | if need_periodic_flush or exiting then 337 | -- no regular flush happened after periodic flush timer had been set 338 | if debug then 339 | ngx_log(DEBUG, "performing periodic flush") 340 | end 341 | _flush() 342 | else 343 | if debug then 344 | ngx_log(DEBUG, "no need to perform periodic flush: regular flush " 345 | .. "happened before") 346 | end 347 | need_periodic_flush = true 348 | end 349 | 350 | timer_at(periodic_flush, _periodic_flush) 351 | end 352 | 353 | local function _flush_buffer() 354 | local ok, err = timer_at(0, _flush) 355 | 356 | need_periodic_flush = false 357 | 358 | if not ok then 359 | _write_error(err) 360 | return nil, err 361 | end 362 | end 363 | 364 | local function _write_buffer(msg, len) 365 | log_buffer_index = log_buffer_index + 1 366 | log_buffer_data[log_buffer_index] = msg 367 | 368 | buffer_size = buffer_size + len 369 | 370 | 371 | return buffer_size 372 | end 373 | 374 | function _M.init(user_config) 375 | if (type(user_config) ~= "table") then 376 | return nil, "user_config must be a table" 377 | end 378 | 379 | for k, v in pairs(user_config) do 380 | if k == "host" then 381 | if type(v) ~= "string" then 382 | return nil, '"host" must be a string' 383 | end 384 | host = v 385 | elseif k == "port" then 386 | if type(v) ~= "number" then 387 | return nil, '"port" must be a number' 388 | end 389 | if v < 0 or v > MAX_PORT then 390 | return nil, ('"port" out of range 0~%s'):format(MAX_PORT) 391 | end 392 | port = v 393 | elseif k == "path" then 394 | if type(v) ~= "string" then 395 | return nil, '"path" must be a string' 396 | end 397 | path = v 398 | elseif k == "sock_type" then 399 | if type(v) ~= "string" then 400 | return nil, '"sock_type" must be a string' 401 | end 402 | if v ~= "tcp" and v ~= "udp" then 403 | return nil, '"sock_type" must be "tcp" or "udp"' 404 | end 405 | sock_type = v 406 | elseif k == "flush_limit" then 407 | if type(v) ~= "number" or v < 0 then 408 | return nil, 'invalid "flush_limit"' 409 | end 410 | flush_limit = v 411 | elseif k == "drop_limit" then 412 | if type(v) ~= "number" or v < 0 then 413 | return nil, 'invalid "drop_limit"' 414 | end 415 | drop_limit = v 416 | elseif k == "timeout" then 417 | if type(v) ~= "number" or v < 0 then 418 | return nil, 'invalid "timeout"' 419 | end 420 | timeout = v 421 | elseif k == "max_retry_times" then 422 | if type(v) ~= "number" or v < 0 then 423 | return nil, 'invalid "max_retry_times"' 424 | end 425 | max_retry_times = v 426 | elseif k == "retry_interval" then 427 | if type(v) ~= "number" or v < 0 then 428 | return nil, 'invalid "retry_interval"' 429 | end 430 | -- ngx.sleep time is in seconds 431 | retry_interval = v 432 | elseif k == "pool_size" then 433 | if type(v) ~= "number" or v < 0 then 434 | return nil, 'invalid "pool_size"' 435 | end 436 | pool_size = v 437 | elseif k == "max_buffer_reuse" then 438 | if type(v) ~= "number" or v < 0 then 439 | return nil, 'invalid "max_buffer_reuse"' 440 | end 441 | max_buffer_reuse = v 442 | elseif k == "periodic_flush" then 443 | if type(v) ~= "number" or v < 0 then 444 | return nil, 'invalid "periodic_flush"' 445 | end 446 | periodic_flush = v 447 | elseif k == "ssl" then 448 | if type(v) ~= "boolean" then 449 | return nil, '"ssl" must be a boolean value' 450 | end 451 | ssl = v 452 | elseif k == "ssl_verify" then 453 | if type(v) ~= "boolean" then 454 | return nil, '"ssl_verify" must be a boolean value' 455 | end 456 | ssl_verify = v 457 | elseif k == "sni_host" then 458 | if type(v) ~= "string" then 459 | return nil, '"sni_host" must be a string' 460 | end 461 | sni_host = v 462 | end 463 | end 464 | 465 | if not (host and port) and not path then 466 | return nil, "no logging server configured. \"host\"/\"port\" or " 467 | .. "\"path\" is required." 468 | end 469 | 470 | 471 | if (flush_limit >= drop_limit) then 472 | return nil, "\"flush_limit\" should be < \"drop_limit\"" 473 | end 474 | 475 | flushing = false 476 | exiting = false 477 | connecting = false 478 | 479 | connected = false 480 | retry_connect = 0 481 | retry_send = 0 482 | 483 | logger_initted = true 484 | 485 | if periodic_flush then 486 | if debug then 487 | ngx_log(DEBUG, "periodic flush enabled for every " 488 | .. periodic_flush .. " seconds") 489 | end 490 | need_periodic_flush = true 491 | timer_at(periodic_flush, _periodic_flush) 492 | end 493 | 494 | return logger_initted 495 | end 496 | 497 | function _M.log(msg) 498 | if not logger_initted then 499 | return nil, "not initialized" 500 | end 501 | 502 | local bytes 503 | 504 | if type(msg) ~= "string" then 505 | msg = tostring(msg) 506 | end 507 | 508 | local msg_len = #msg 509 | 510 | if (debug) then 511 | ngx.update_time() 512 | ngx_log(DEBUG, ngx.now(), ":log message length: " .. msg_len) 513 | end 514 | 515 | -- response of "_flush_buffer" is not checked, because it writes 516 | -- error buffer 517 | if (is_exiting()) then 518 | exiting = true 519 | _write_buffer(msg, msg_len) 520 | _flush_buffer() 521 | if (debug) then 522 | ngx_log(DEBUG, "Nginx worker is exiting") 523 | end 524 | bytes = 0 525 | elseif (msg_len + buffer_size < flush_limit) then 526 | _write_buffer(msg, msg_len) 527 | bytes = msg_len 528 | elseif (msg_len + buffer_size <= drop_limit) then 529 | _write_buffer(msg, msg_len) 530 | _flush_buffer() 531 | bytes = msg_len 532 | else 533 | _flush_buffer() 534 | if (debug) then 535 | ngx_log(DEBUG, "logger buffer is full, this log message will be " 536 | .. "dropped") 537 | end 538 | bytes = 0 539 | --- this log message doesn't fit in buffer, drop it 540 | end 541 | 542 | if last_error then 543 | local err = last_error 544 | last_error = nil 545 | return bytes, err 546 | end 547 | 548 | return bytes 549 | end 550 | 551 | function _M.initted() 552 | return logger_initted 553 | end 554 | 555 | _M.flush = _flush 556 | 557 | return _M 558 | 559 | -------------------------------------------------------------------------------- /other/usertime.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | static int getmicrosecond(lua_State *L) { 7 | struct timeval tv; 8 | gettimeofday(&tv,NULL); 9 | long microsecond = tv.tv_sec*1000000+tv.tv_usec; 10 | lua_pushnumber(L, microsecond); 11 | return 1; 12 | } 13 | static int getmillisecond(lua_State *L) { 14 | struct timeval tv; 15 | gettimeofday(&tv,NULL); 16 | long millisecond = (tv.tv_sec*1000000+tv.tv_usec)/1000; 17 | lua_pushnumber(L, millisecond); 18 | 19 | return 1; 20 | } 21 | static const struct luaL_Reg myLib[] = 22 | { 23 | {"getmillisecond", getmillisecond}, 24 | {"getmicrosecond", getmicrosecond}, 25 | { NULL, NULL } 26 | }; 27 | 28 | 29 | int luaopen_usertime(lua_State *L) { 30 | luaL_register(L, "FPCalc", myLib); 31 | return 1; 32 | } 33 | -------------------------------------------------------------------------------- /other/usertime.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonline/nginx-lua-waf/d374f66511806224eeb8c83b97e9679ccabe4ac1/other/usertime.so -------------------------------------------------------------------------------- /other/编译命令.txt: -------------------------------------------------------------------------------- 1 | cc -g -O2 -Wall -fPIC --shared usertime.c -o usertime.so -------------------------------------------------------------------------------- /rules/args.rule: -------------------------------------------------------------------------------- 1 | -- Args请求参数过滤 2 | -- args是转码过的,/index.html?a=3&bb=23检查的是值3或23 /index.html?23模式会被忽略 3 | -- 待匹配字符串示例:api-args 4 | -- 规则示例:^api-/text/index\.html 5 | -- 注意,如果不指定主机头api,将对所有主机生效 6 | -- 7 | \.\./ 8 | \:\$ 9 | \$\{ 10 | select.+(from|limit) 11 | (?:(union(.*?)select)) 12 | sleep\((\s*)(\d*)(\s*)\) 13 | benchmark\((.*)\,(.*)\) 14 | base64_decode\( 15 | (?:from\W+information_schema\W) 16 | (?:(?:current_)user|database|schema|connection_id)\s*\( 17 | (?:etc\/\W*passwd) 18 | into(\s+)+(?:dump|out)file\s* 19 | group\s+by.+\( 20 | xwork.MethodAccessor 21 | (?: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)\( 22 | xwork\.MethodAccessor 23 | (gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/ 24 | java\.lang 25 | \$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[ 26 | \<(iframe|script|body|img|layer|div|meta|style|base|object|input) 27 | (onmouseover|onerror|onload)\= -------------------------------------------------------------------------------- /rules/blackip.rule: -------------------------------------------------------------------------------- 1 | -- IP黑名单 2 | -- 待匹配字符串示例:api-192.168.158.1 3 | -- 规则示例:^api-192\.168\.158\.1$ 4 | -- 注意,如果不指定主机头api,将对所有主机生效 5 | -- -------------------------------------------------------------------------------- /rules/cookie.rule: -------------------------------------------------------------------------------- 1 | -- Cookie过滤 2 | -- 待匹配字符串示例:api-nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; 3 | -- kod_user_language=zh_CN; kod_user_online_version=check-at-1523267590; 4 | -- kod_name=admin; kod_token=3e016b80ce1e7349ff371324a1c0f996, 5 | -- client: 192.168.158.1, server: api, request: "GET /index.html HTTP/1.1", 6 | -- host: "192.168.158.139" 7 | -- 规则示例:^api-.*kod_name=admin; 8 | -- 注意,如果不指定主机头api,将对所有主机生效 9 | -- 10 | \.\./ 11 | select.+(from|limit) 12 | (?:(union(.*?)select)) 13 | sleep\((\s*)(\d*)(\s*)\) 14 | benchmark\((.*)\,(.*)\) 15 | base64_decode\( 16 | (?:from\W+information_schema\W) 17 | (?:(?:current_)user|database|schema|connection_id)\s*\( 18 | (?:etc\/\W*passwd) 19 | into(\s+)+(?:dump|out)file\s* 20 | group\s+by.+\( 21 | xwork.MethodAccessor 22 | (?: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)\( 23 | (gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/ 24 | java\.lang 25 | \$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[ 26 | \<(iframe|script|body|img|layer|div|meta|style|base|object|input) 27 | (onmouseover|onerror|onload)\= -------------------------------------------------------------------------------- /rules/frequency.rule: -------------------------------------------------------------------------------- 1 | -- 请求频率限制 2 | -- 真数据比例 3 => 30% 末位为返回真数据的百分比,范围0-9其中0为全部返回假数据 3 | -- 只要匹配规则,就返回200,即使url不存在 4 | -- 末尾两位不计入正则表达式,-5 5 | -- 待匹配字符串示例:api-192.168.158.1-/test/index.html 6 | -- 规则示例:api-192.168.158.1-/index.html-0 7 | -- 注意,本规则并非正则匹配,而是字符串比较 8 | -- -------------------------------------------------------------------------------- /rules/post.rule: -------------------------------------------------------------------------------- 1 | -- POST数据过滤 2 | -- multipart/form-data方式的数据,只要其中带有文件,则不检查;不带文件时key和value都会检查 3 | -- application/x-www-form-urlencoded二进制模式发送的文件会被检查;key:value仅检查value 4 | -- 待匹配字符串示例:api-argstxt 5 | -- 规则示例:^api-.*txt 6 | -- 注意,如果不指定主机头api,将对所有主机生效 7 | -- 8 | \.\./ 9 | select.+(from|limit) 10 | (?:(union(.*?)select)) 11 | sleep\((\s*)(\d*)(\s*)\) 12 | benchmark\((.*)\,(.*)\) 13 | base64_decode\( 14 | (?:from\W+information_schema\W) 15 | (?:(?:current_)user|database|schema|connection_id)\s*\( 16 | (?:etc\/\W*passwd) 17 | into(\s+)+(?:dump|out)file\s* 18 | group\s+by.+\( 19 | xwork.MethodAccessor 20 | (?: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)\( 21 | (gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/ 22 | java\.lang 23 | \$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[ 24 | \<(iframe|script|body|img|layer|div|meta|style|base|object|input) 25 | (onmouseover|onerror|onload)\= -------------------------------------------------------------------------------- /rules/re.txt: -------------------------------------------------------------------------------- 1 | 元字符: 2 | . 匹配除换行符以外的任意字符 3 | \w 匹配字母或数字或下划线 4 | \s 匹配任意的空白符 5 | \d 匹配数字 6 | \b 匹配单词的开始或结束 7 | ^ 匹配字符串的开始 8 | $ 匹配字符串的结束 9 | 10 | 限定符: 11 | * 重复零次或更多次 12 | + 重复一次或更多次 13 | ? 重复零次或一次 14 | {n} 重复n次 15 | {n,} 重复n次或更多次 16 | {n,m} 重复n到m次 17 | 18 | 反义词: 19 | \W 匹配任意不是字母,数字,下划线,汉字的字符 20 | \S 匹配任意不是空白符的字符 21 | \D 匹配任意非数字的字符 22 | \B 匹配不是单词开头或结束的位置 23 | [^x] 匹配除了x以外的任意字符 24 | [^aeiou] 匹配除了aeiou这几个字母以外的任意字符 25 | 26 | 示例: 27 | 用户名 [A-Za-z0-9_\-\u4e00-\u9fa5]+ 28 | 负整数 -[1-9]\d* 29 | 正整数 [1-9]\d* 30 | 31 | 其它: 32 | [xyz] 字符集合。 33 | [^xyz] 负值字符集合。 34 | [a-z] 字符范围,匹配指定范围内的任意字符。 35 | [^a-z] 负值字符范围,匹配任何不在指定范围内的任意字符。 36 | \b 匹配一个单词边界,也就是指单词和空格间的位置。 37 | \B 匹配非单词边界。 38 | \cx 匹配由x指明的控制字符。 39 | \d 匹配一个数字字符。等价于 [0-9]。 40 | \D 匹配一个非数字字符。等价于 [^0-9]。 41 | ? \f 匹配一个换页符。等价于 \x0c 和 \cL。 42 | \n 匹配一个换行符。等价于 \x0a 和 \cJ。 43 | \r 匹配一个回车符。等价于 \x0d 和 \cM。 44 | \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。 45 | \S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。 46 | \t 匹配一个制表符。等价于 \x09 和 \cI。 47 | \v 匹配一个垂直制表符。等价于 \x0b 和 \cK。 48 | \w 匹配包括下划线的任何单词字符。等价于’[A-Za-z0-9_]’。 49 | \W 匹配任何非单词字符。等价于 ’[^A-Za-z0-9_]’。 -------------------------------------------------------------------------------- /rules/url.rule: -------------------------------------------------------------------------------- 1 | -- Url过滤 2 | -- 待匹配字符串示例:api-/test/index.html 3 | -- 规则示例:^api-/text/index\.html 4 | -- 注意,如果不指定主机头api,将对所有主机生效 5 | -- 6 | \.(git|svn|htaccess|bash_history) 7 | \.(bak|inc|old|mdb|sql|backup|java|class|tgz|gz|tar|zip|rar)$ 8 | (phpmyadmin|jmx-console|admin-console|jmxinvokerservlet) 9 | java\.lang 10 | /(attachments|upimg|images|css|uploadfiles|html|uploads|templets|static|template|data|inc|forumdata|upload|includes|cache|avatar)/(.*)\.(php|jsp) 11 | \.(svn|git|sql|bak)/ -------------------------------------------------------------------------------- /rules/useragent.rule: -------------------------------------------------------------------------------- 1 | -- 浏览器UserAgent过滤 2 | -- 待匹配字符串示例:api-Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0 3 | -- 规则示例:^api-.*Firefox 4 | -- 注意,如果不指定主机头api,将对所有主机生效 5 | -- 6 | (ApacheBench|HTTrack|harvest|audit|dirbuster|pangolin|nmap|sqln|-scan|hydra|Parser|libwww|BBBike|sqlmap|w3af|owasp|Nikto|fimap|havij|PycURL|zmeu|BabyKrokodil|netsparker|httperf|bench) -------------------------------------------------------------------------------- /rules/whiteip.rule: -------------------------------------------------------------------------------- 1 | -- IP白名单 2 | -- 待匹配字符串示例:api-192.168.158.1 3 | -- 规则示例:^api-192\.168\.158\.1$ 4 | -- 注意,如果不指定主机头api,将对所有主机生效 5 | -- -------------------------------------------------------------------------------- /rules/whiteurl.rule: -------------------------------------------------------------------------------- 1 | -- Url白名单 2 | -- 待匹配字符串示例:api-/test/index.html 3 | -- 规则示例:^api-/text/index\.html 4 | -- 注意,如果不指定主机头api,将对所有主机生效 5 | -- -------------------------------------------------------------------------------- /util.lua: -------------------------------------------------------------------------------- 1 | -- 将日志发送到远端需要启用socket 2 | -- local logger = require "socket" 3 | local io = require("io") 4 | local cjson = require("cjson.safe") 5 | local string = require("string") 6 | local config = require("config") 7 | 8 | local _M = { 9 | version = "0.1", 10 | RULE_TABLE = {}, 11 | RULE_FILES = { 12 | "args.rule", 13 | "blackip.rule", 14 | "cookie.rule", 15 | "post.rule", 16 | "url.rule", 17 | "useragent.rule", 18 | "whiteip.rule", 19 | "whiteurl.rule", 20 | "frequency.rule" -- 新加 21 | } 22 | } 23 | 24 | -- 建立字典 规则类名称:规则文件路径 25 | function _M.get_rule_files(rules_path) 26 | local rule_files = {} 27 | for _, file in ipairs(_M.RULE_FILES) do 28 | if file ~= "" then 29 | local file_name = rules_path .. '/' .. file 30 | ngx.log(ngx.DEBUG, string.format("规则:%s, 文件路径:%s", file, file_name)) 31 | rule_files[file] = file_name 32 | end 33 | end 34 | return rule_files 35 | end 36 | 37 | -- 载入规则到本模块RULE_TABLE 38 | function _M.get_rules(rules_path) 39 | local rule_files = _M.get_rule_files(rules_path) 40 | if rule_files == {} then 41 | return nil 42 | end 43 | for rule_name, rule_file in pairs(rule_files) do 44 | local t_rule = {} 45 | --修改为按行读取规则文件 46 | local file_rule_name = io.open(rule_file,"r") 47 | if file_rule_name ~= nil then 48 | for line in file_rule_name:lines() do 49 | --在规则文件中可以使用lua模式的注释 50 | if string.sub( line, 1, 2 ) ~= "--" then 51 | -- string.gsub(s, "^%s*(.-)%s*$", "%1") 去除字符串s两端的空格 52 | table.insert(t_rule, (string.gsub(line, "^%s*(.-)%s*$", "%1"))) 53 | ngx.log(ngx.INFO, string.format("规则名称:%s, 值:%s", rule_name, line)) 54 | end 55 | end 56 | end 57 | file_rule_name:close() 58 | ngx.log(ngx.INFO, string.format("规则文件%s读取完毕!", rule_file)) 59 | _M.RULE_TABLE[rule_name] = t_rule 60 | end 61 | return (_M.RULE_TABLE) 62 | end 63 | 64 | -- 获取来访IP 65 | function _M.get_client_ip() 66 | local CLIENT_IP = ngx.req.get_headers()["X_real_ip"] 67 | if CLIENT_IP == nil then 68 | CLIENT_IP = ngx.req.get_headers()["X_Forwarded_For"] 69 | end 70 | if CLIENT_IP == nil then 71 | CLIENT_IP = ngx.var.remote_addr 72 | end 73 | if CLIENT_IP == nil then 74 | CLIENT_IP = "0.0.0.0" 75 | end 76 | -- 判断CLIENT_IP是否为table类型,table类型即获取到多个ip的情况 77 | if type(CLIENT_IP) == "table" then 78 | CLIENT_IP = table.concat(CLIENT_IP, ",") 79 | end 80 | if type(CLIENT_IP) ~= "string" then 81 | CLIENT_IP = "0.0.0.0" 82 | end 83 | return CLIENT_IP 84 | end 85 | 86 | -- 获取UserAgnet 87 | function _M.get_user_agent() 88 | local USER_AGENT = ngx.var.http_user_agent 89 | if USER_AGENT == nil then 90 | USER_AGENT = "unknown" 91 | end 92 | return USER_AGENT 93 | end 94 | 95 | -- 记录JSON格式日志 96 | function _M.log_record(config_log_dir, attack_type, url, data, ruletag) 97 | local log_path = config_log_dir 98 | local client_IP = _M.get_client_ip() 99 | local user_agent = _M.get_user_agent() 100 | local server_name = ngx.var.server_name 101 | local local_time = string.sub(ngx.localtime(),1,10).."T"..string.sub(ngx.localtime(),12,-1).."+08:00" 102 | local log_json_obj = { 103 | realip = client_IP, 104 | timestamp = local_time, 105 | server = server_name, 106 | agent = user_agent, 107 | attack_type = attack_type, 108 | urls = url, 109 | url = ngx.var.uri, 110 | postdata = data, 111 | ruletag = ruletag, 112 | } 113 | local log_line = cjson.encode(log_json_obj) 114 | -- log_line = string.gsub(log_line,"\\\"","") -- 去掉所有\" 115 | -- log_line = string.gsub(log_line,"\\","") -- 去掉所有\ 116 | local log_name = string.format("%s/%s_waf.log", log_path, ngx.today()) 117 | local file, err = io.open(log_name, "a+") 118 | if err ~= nil then ngx.log(ngx.DEBUG, "file err:" .. err) end 119 | if file == nil then 120 | return 121 | end 122 | file:write(string.format("%s\n", string.gsub(string.gsub(log_line,"\\\"",""),"\\",""))) 123 | file:flush() 124 | file:close() 125 | end 126 | 127 | -- 恶意访问处理函数 128 | -- 使用jinghuashuiyue模式时,仅记录ip到共享存储 129 | function _M.waf_output() 130 | if config.config_waf_model == "redirect" then 131 | ngx.redirect(config.config_waf_redirect_url, 301) 132 | elseif config.config_waf_model == "jinghuashuiyue" then 133 | -- 如果启用镜花水月,取消下面两行的注释 134 | -- local bad_guy_ip = _M.get_client_ip() 135 | --_M.set_bad_guys(bad_guy_ip, config.config_expire_time) 136 | return -- 如果启用镜花水月 请注释该行 137 | else 138 | ngx.header.content_type = "text/html" 139 | ngx.status = ngx.HTTP_FORBIDDEN 140 | ngx.say(string.format(config.config_output_html, _M.get_client_ip())) 141 | ngx.exit(ngx.status) 142 | end 143 | end 144 | 145 | --[[ 146 | -- 获取请求头中Host字段 147 | -- 镜花水月模式中使用 148 | function _M.get_server_host() 149 | local host = ngx.req.get_headers()["Host"] 150 | return host 151 | end 152 | 153 | -- 将IP存入共享存储gadGuys 154 | -- 镜花水月模式 155 | function _M.set_bad_guys(bad_guy_ip, expire_time) 156 | local badGuys = ngx.shared.badGuys 157 | local req, _ = badGuys:get(bad_guy_ip) 158 | if req then 159 | badGuys:incr(bad_guy_ip, 1) 160 | else 161 | badGuys:set(bad_guy_ip, 1, expire_time) 162 | end 163 | end 164 | ]] 165 | 166 | return _M 167 | -------------------------------------------------------------------------------- /waf.lua: -------------------------------------------------------------------------------- 1 | -- 高精度时间 2 | local utime = require "usertime" 3 | 4 | local rulematch = ngx.re.find 5 | local unescape = ngx.unescape_uri 6 | 7 | local config = require("config") 8 | local util = require("util") 9 | 10 | local _M = { 11 | RULES = {} 12 | } 13 | 14 | -- 载入规则到本模块RULES字典 15 | function _M.load_rules() 16 | _M.RULES = util.get_rules(config.config_rule_dir) 17 | for k, v in pairs(_M.RULES) 18 | do 19 | ngx.log(ngx.INFO, string.format("%s规则载入中...", k)) 20 | for kk, vv in pairs(v) 21 | do 22 | ngx.log(ngx.INFO, string.format("编号:%s, 规则:%s", kk, vv)) 23 | end 24 | end 25 | return _M.RULES 26 | end 27 | 28 | -- 获取RULES字典中指定类型规则列表 29 | function _M.get_rule(rule_file_name) 30 | return _M.RULES[rule_file_name] 31 | end 32 | 33 | -- 白名单IP检查 34 | -- 匹配字段式样:api-192.168.1.1 35 | function _M.white_ip_check() 36 | if config.config_white_ip_check == "on" then 37 | local IP_WHITE_RULE = _M.get_rule('whiteip.rule') 38 | local WHITE_IP = ngx.var.server_name.."-"..util.get_client_ip() 39 | if IP_WHITE_RULE ~= nil then 40 | for _, rule in pairs(IP_WHITE_RULE) do 41 | if rule ~= "" and rulematch(WHITE_IP, rule, "jo") then 42 | -- 为优化性能 白名单不记录日志 43 | -- util.log_record(config.config_log_dir, '白名单IP', ngx.var.request_uri, "_", "_") 44 | return true 45 | end 46 | end 47 | end 48 | end 49 | end 50 | 51 | -- 黑名单IP检查 52 | -- 匹配字段式样:api-192.168.1.1 53 | function _M.black_ip_check() 54 | if config.config_black_ip_check == "on" then 55 | local IP_BLACK_RULE = _M.get_rule('blackip.rule') 56 | local BLACK_IP = ngx.var.server_name.."-"..util.get_client_ip() 57 | if IP_BLACK_RULE ~= nil then 58 | for _, rule in pairs(IP_BLACK_RULE) do 59 | if rule ~= "" and rulematch(BLACK_IP, rule, "jo") then 60 | util.log_record(config.config_log_dir, '黑名单IP', ngx.var.request_uri, "_", rule) 61 | ngx.exit(403) 62 | return true 63 | end 64 | end 65 | end 66 | end 67 | end 68 | 69 | -- UserAgent检查 70 | -- 匹配字段式样:api-Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0 71 | function _M.user_agent_attack_check() 72 | if config.config_user_agent_check == "on" then 73 | local USER_AGENT = ngx.var.http_user_agent 74 | local USER_AGENT_RULES = _M.get_rule('useragent.rule') 75 | if USER_AGENT ~= nil then 76 | for _, rule in pairs(USER_AGENT_RULES) do 77 | if rule ~= "" and rulematch((ngx.var.server_name.."-"..USER_AGENT), rule, "joi") then 78 | util.log_record(config.config_log_dir, 'UserAgent受限', ngx.var.request_uri, "-", rule) 79 | util.waf_output() 80 | return true 81 | end 82 | end 83 | end 84 | end 85 | return false 86 | end 87 | 88 | -- 白名单URL 89 | -- 匹配字段式样:api-index.html 90 | function _M.white_url_check() 91 | if config.config_white_url_check == "on" then 92 | local URL_WHITE_RULES = _M.get_rule('whiteurl.rule') 93 | local REQ_URI = ngx.var.server_name.."-"..ngx.var.request_uri 94 | if URL_WHITE_RULES ~= nil then 95 | for _, rule in pairs(URL_WHITE_RULES) do 96 | if rule ~= "" and rulematch(REQ_URI, rule, "joi") then 97 | return true 98 | end 99 | end 100 | end 101 | end 102 | end 103 | 104 | -- URL检查 105 | -- 匹配字段式样:api-index.html 106 | function _M.url_attack_check() 107 | if config.config_url_check == "on" then 108 | local URL_RULES = _M.get_rule('url.rule') 109 | local REQ_URI = ngx.var.server_name.."-"..ngx.var.request_uri 110 | for _, rule in pairs(URL_RULES) do 111 | if rule ~= "" and rulematch(REQ_URI, rule, "joi") then 112 | util.log_record(config.config_log_dir, '非法URL', ngx.var.request_uri, "-", rule) 113 | util.waf_output() 114 | return true 115 | end 116 | end 117 | end 118 | return false 119 | end 120 | 121 | -- CC攻击 122 | -- 匹配字段式样:api-192.168.158.1-/index.html 123 | -- 使用共享存储limit 124 | function _M.cc_attack_check() 125 | if config.config_cc_check == "on" then 126 | -- 对ngx.var.request_uri限制长度为最大40字符 避免key太长 127 | local ATTACK_URI = string.sub(ngx.var.request_uri,1,40) 128 | local CC_TOKEN = ngx.var.server_name.."-"..util.get_client_ip() .."-"..ATTACK_URI 129 | local limit = ngx.shared.limit 130 | local CCcount = tonumber(string.match(config.config_cc_rate, '(.*)/')) 131 | local CCseconds = tonumber(string.match(config.config_cc_rate, '/(.*)')) 132 | local req, _ = limit:get(CC_TOKEN) 133 | -- 打印目标限制字符串 134 | -- ngx.log(ngx.ERR, "错误:" .. CC_TOKEN) 135 | if req then 136 | if req > CCcount then 137 | util.log_record(config.config_log_dir, 'CC攻击', ngx.var.request_uri, "-", "-") 138 | ngx.exit(403) 139 | else 140 | limit:incr(CC_TOKEN, 1) 141 | end 142 | else 143 | limit:set(CC_TOKEN, 1, CCseconds) 144 | end 145 | end 146 | return false 147 | end 148 | 149 | -- Cookie检查 150 | -- 匹配字段式样:api-nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; 151 | -- kod_user_language=zh_CN; kod_user_online_version=check-at-1523267590; kod_name=admin; 152 | -- kod_token=3e016b80ce1e7349ff371324a1c0f996, client: 192.168.158.1, server: api, 153 | -- request: "GET /index.html HTTP/1.1", host: "192.168.158.139" 154 | function _M.cookie_attack_check() 155 | if config.config_cookie_check == "on" then 156 | local COOKIE_RULES = _M.get_rule('cookie.rule') 157 | local USER_COOKIE = ngx.var.http_cookie 158 | if USER_COOKIE ~= nil then 159 | USER_COOKIE = ngx.var.server_name.."-"..USER_COOKIE 160 | for _, rule in pairs(COOKIE_RULES) do 161 | if rule ~= "" and rulematch(USER_COOKIE, rule, "joi") then 162 | util.log_record(config.config_log_dir, '非法Cookie', ngx.var.request_uri, "-", rule) 163 | util.waf_output() 164 | return true 165 | end 166 | end 167 | end 168 | end 169 | return false 170 | end 171 | 172 | -- 请求参数检查 173 | -- 匹配字段式样:api-3 174 | -- ?a 不检查,REQ_ARGS => table;ARGS_DATA => boolean 175 | -- ?a=3 检查的是3 176 | function _M.url_args_attack_check() 177 | if config.config_url_args_check == "on" then 178 | local ARGS_RULES = _M.get_rule('args.rule') 179 | for _, rule in pairs(ARGS_RULES) do 180 | local REQ_ARGS = ngx.req.get_uri_args() 181 | for key, val in pairs(REQ_ARGS) do 182 | local ARGS_DATA = {} 183 | if type(val) == 'table' then 184 | ARGS_DATA = table.concat(val, " ") 185 | else 186 | ARGS_DATA = val 187 | end 188 | if ARGS_DATA and type(ARGS_DATA) ~= "boolean" and rule ~= "" and rulematch(unescape(ngx.var.server_name.."-"..ARGS_DATA), rule, "joi") then 189 | util.log_record(config.config_log_dir, '参数非法', ngx.var.request_uri, "-", rule) 190 | util.waf_output() 191 | return true 192 | end 193 | end 194 | end 195 | end 196 | return false 197 | end 198 | 199 | -- POST检查 200 | -- 匹配字段式样:api-txt 201 | -- multipart/form-data方式的数据,只要其中带有文件,则不检查 202 | -- application/x-www-form-urlencoded二进制模式发送的文件会被检查 203 | -- 日志中的post_data并非完整的POST数据 204 | function _M.post_attack_check() 205 | if config.config_post_check == "on" and ngx.req.get_method() ~= "GET" then 206 | ngx.req.read_body() 207 | local POST_RULES = _M.get_rule('post.rule') 208 | local POST_ARGS = ngx.req.get_post_args() or {} 209 | -- 输出POST_ARGS内容 210 | -- for kk,vv in pairs(POST_ARGS) do 211 | -- ngx.log(ngx.ERR, "错误:"..kk) 212 | -- ngx.log(ngx.ERR, "错误:"..vv) 213 | -- end 214 | for _, rule in pairs(POST_RULES) do 215 | for k, v in pairs(POST_ARGS) do 216 | local post_data = "" 217 | if type(v) == "table" then 218 | -- 重构table,解决"invalid value (boolean) at index 1 in table for 'concat'" 219 | -- local tt={} 220 | -- for kk,vv in pairs(v) do 221 | -- if type(vv) ~= "boolean" then 222 | -- vv="-" 223 | -- end 224 | -- table.insert(tt,vv) 225 | -- end 226 | -- post_data = ngx.var.server_name.."-"..table.concat(tt, ",") 227 | -- 228 | -- 修复错误 2018.08.20 invalid value (boolean) at index 1 in table for 'concat' 229 | -- POST图片时 部分图片会触发 230 | if type(v[1]) == "boolean" then 231 | return false 232 | end 233 | post_data = ngx.var.server_name.."-"..table.concat(v, ", ") 234 | elseif type(v) == "boolean" then 235 | post_data = ngx.var.server_name.."-"..k 236 | else 237 | post_data = ngx.var.server_name.."-"..v 238 | end 239 | -- 选项s强制多行为一行对待,否则非第一行将不匹配 240 | if rule ~= "" and rulematch(post_data, rule, "jois") then 241 | util.log_record(config.config_log_dir, 'POST非法数据', ngx.var.request_uri, post_data, rule) 242 | util.waf_output() 243 | return true 244 | end 245 | end 246 | end 247 | end 248 | return false 249 | end 250 | 251 | --[[ 252 | -- 镜花水月模式 253 | -- 检查来访IP是否为标记过的恶意IP 254 | function _M.bad_guy_check() 255 | local client_ip = util.get_client_ip() 256 | local ret = false 257 | if client_ip ~= "" then 258 | ret = ngx.shared.badGuys.get(client_ip) 259 | if ret ~= nil and ret > 0 then 260 | ret = true 261 | end 262 | end 263 | return ret 264 | end 265 | 266 | -- 镜花水月模式 267 | -- 获取来访请求头中目标Host字段,并设置为target变量 268 | -- 如果该IP被标注为恶意IP 则改写target变量 使其路由到测试环境 269 | function _M.start_jingshuishuiyue() 270 | local host = util.get_server_host() -- 获取请求头中Host字段 271 | ngx.var.target = string.format("proxy_%s", host) 272 | if host and _M.bad_guy_check() then 273 | ngx.var.target = string.format("unreal_%s", host) 274 | end 275 | end 276 | ]] 277 | 278 | -- 加入频率控制函数 279 | -- 若返回假数据,将跳过后续检查流程 280 | function _M.frequency_control_check() 281 | if config.frequency_control_check == "on" then 282 | local FREQUENCY_RULE = _M.get_rule('frequency.rule') 283 | -- 目标 api-192.168.123.33-index.html 284 | local FREQUENCY_TAG = ngx.var.server_name.."-"..util.get_client_ip().."-"..ngx.var.request_uri 285 | if FREQUENCY_RULE ~= nil then 286 | for _, rule in pairs(FREQUENCY_RULE) do 287 | -- ngx.log(ngx.ERR, "错误1:FREQUENCY_TAG:" .. FREQUENCY_TAG) --api-192.168.158.1-/index.html 288 | -- ngx.log(ngx.ERR, "错误2:" .. rule) --api-192.168.158.1-/index.html-3 289 | -- ngx.log(ngx.ERR, "错误3:" .. string.sub(rule,-1,-1)) --3 290 | -- ngx.log(ngx.ERR, "错误4:" .. string.sub(rule,1,-3)) --api-192.168.158.1-/index.html 291 | if rule ~= "" and string.sub(rule,1,-3) == FREQUENCY_TAG then 292 | local microsecond = utime.getmillisecond() 293 | math.randomseed(tostring(microsecond):reverse():sub(1,12)) 294 | if math.random(0,9) >= tonumber(string.sub(rule,-1,-1)) then 295 | util.log_record(config.config_log_dir, '假数据', ngx.var.request_uri, "_", rule) 296 | ngx.header.content_type = "application/json" --text/html 297 | ngx.header.content_length = #config.frequency_text 298 | ngx.status = ngx.HTTP_OK 299 | ngx.say(config.frequency_text) -- 直接退出流程! 300 | -- ngx.log(ngx.ERR, "错误:假数据") 301 | ngx.exit(ngx.status) 302 | return true 303 | end 304 | end 305 | end 306 | end 307 | end 308 | return false 309 | end 310 | 311 | -- 执行检查 312 | -- CC处理方式:limit计数,记录日志,返回403 313 | -- IP黑名单处理方式:记录日志,返回403 314 | -- 其它方式:记录日志,调用waf_output函数处理 315 | function _M.check() 316 | if config.config_waf_enable ~= "on" then 317 | return 318 | end 319 | if _M.frequency_control_check() then 320 | -- if _M.white_ip_check() then 321 | elseif _M.white_ip_check() then 322 | elseif _M.black_ip_check() then 323 | elseif _M.user_agent_attack_check() then 324 | elseif _M.white_url_check() then 325 | elseif _M.url_attack_check() then 326 | elseif _M.cc_attack_check() then 327 | elseif _M.cookie_attack_check() then 328 | elseif _M.url_args_attack_check() then 329 | elseif _M.post_attack_check() then 330 | else 331 | return 332 | end 333 | end 334 | 335 | return _M 336 | --------------------------------------------------------------------------------