├── 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 | 
33 | ### 开启waf时测试
34 | (开启所有功能,因为有cc检测,将cc阈值设置为20000/60防止压测时被拦截)
35 | 每秒处理9581次请求,处理单个请求平均5毫秒
36 |
37 | 
38 | ##### 可以看出启用waf后,Nginx性能依然非常高,近10k次的处理能力,能够满足任何业务场景的需要
39 |
40 | # Nginx-Lua-WAF处理流程
41 | 
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 |
--------------------------------------------------------------------------------