Open jinhailang opened 6 years ago
lua-resty-waf 是基于 OpenResty 开发的 WAF 项目,其核心的防护规则策略基本与 ModSecurity Core Rule 一致,但是具体实现有所不同。
OpenResty
下面主要分四块阐述其实现功能与原理:
系统配置分为两块系统基本配置和规则配置。
规则配置
系统默认提供了基础的防护规则集,规则集文件都在 rules/ 文件夹下,默认有九个文件:
rules/
11000_whitelist.json
20000_http_violation.json
21000_http_anomaly.json
35000_user_agent.json
40000_generic_attack.json
41000_sqli.json
42000_xss.json
90000_custom.json
99000_scoring.json
默认规则集的执行顺序也是从上到下的,注意文件的命名规律,后续添加自己的规则集的时候最好遵循这种规范。
规则配置的加载方式有三种:
1)系统启动时默认加载
waf.init 函数默认会去 package.path 前缀路径下的目录 rules/ 下加载数组 global_rulesets 指定的 .json 规则集配置文件。 global_rulesets 默认就是包括了 rules/ 目录下的所有文件名,也是基本的系统参数之一,可以通过 waf:set_option("global_rulesets", {}) 来指定,具体后面会说。
waf.init
package.path
global_rulesets
.json
waf:set_option("global_rulesets", {})
2)使用 load_secrules 加载
load_secrules
可以调用函数 load_secrules 函数从磁盘加载 ModSecurity SecRules 配置文件,参数就是文件所在的绝对路径。需要注意的是还需要调用函数 add_ruleset 将规则集名(文件名称)注册到系统,否则系统是识别不到的。
ModSecurity SecRules
add_ruleset
系统内部会按行将 ModSecurity 规则集文件转换(在 translate 包内)成 waf 对应的规则格式(json)。目前支持四种规则指令:
ModSecurity
translate
3)使用 add_ruleset_string 加载
add_ruleset_string
add_ruleset_string 可以直接加载规则集字符串(json)。 用户可以使用这种方式动态的加载自定义规则集合。
在每个阶段执行对应的规则集之前,都会先合并(merge)规则集,主要就是根据规则集名,当规则集名称一样时,用户自己添加的规则集优先级更高。
系统配置
waf.new 函数会初始化一个系统基础参数表,这些参数都可以通过函数 waf.set_option(参数名称, value) 来设置。 一些比较重要的参数说明:
waf.new
waf.set_option(参数名称, value)
_debug
false
_debug_log_level
ngx.INFO
_deny_status
403
_event_log_altered_only
DENY
DROP
true
_event_log_level
ngx.info
_event_log_request_*
arguments
body
headers
_event_log_target
error
_mode
INACTIVE
SIMULATE
action
exec
ACTIVE
_score_threshold
规则模块是 WAF 项目的核心,包括解析和执行两个部分,为了支持类似 ModSecurity 的防护规则,规则配置比较复杂,解析和执行逻辑就更复杂了。
规则解析
规则配置是按规则集(规则数组)的形式被读取的,规则集再分为多个阶段 --- access,header_filter 等。所有的规则集在解析时,会按阶段的维度,添加到对应阶段的规则集数组。
access
header_filter
需要注意的是,规则解析时会计算两个特殊的变量值:
rule.offset_nomatch
offset_nomatch
rule.offset_match
这两个变量值一般都是 1,即直接进入相邻的下个规则,但是使用 skip 或 CHAIN 都会改变这些值。他们都被放入 table 对象 rule,供规则执行时使用。
1
skip
CHAIN
rule
nondisrupt
disrupt
setvar
storage
initcol
redis
memcached
dict
sleep
ngx.sleep
status
rule_remove_id
data
ignore_rule
mode_update
col
key
inc
value
ACCEPT
nondisrupt.action.status
IGNORE
SCORE
anomaly_score
id
op_negated
operator
REGEX
table
REFIND
ngx.re.find
EQUALS
GREATER
LESS
EXISTS
pattern
CONTAINS
STR_EXISTS
STR_MATCH
PM
CIDR_MATCH
DETECT_SQLI
DETECT_XSS
VERIFY_CC
vars
下个规则位置 = 当前规则位置 + skip + 1
skip_after
type
REQUEST_HEADERS
METHOD
TX
ctx.storage["TX"]
URI_ARGS
QUERY_STRING
a=1&b=2
REQUEST_BODY
URI
REQUEST_URI
/a/b/c?a=1&b=2
COOKIES
cookies
REQUEST_ARGS
REMOTE_ADDR
HTTP_VERSION
SCORE_THRESHOLD
ARGS_COMBINED_SIZE
TIME
TIME_EPOCH
parse
2
specific
regex
keys
values
all
unconditional
opts
transform
uri_decode
lowercase
md5
nolog
logdata
%{value}
规则执行
规则是在 waf.exec 函数内执行的,每个阶段只会执行当前阶段对应的规则集,及规则集里面的规则。需要注意的是 CHAIN 规则的执行逻辑,这种类型的规则会组成一个规则链,规则链内的规则是 and 关系,即规则匹配失败,就会跳过当前整个规则链。规则链是怎么组成的呢?当遇到非 CHAIN 规则时,就会计算成一个规则链。
waf.exec
and
下面使用 C 代表 CHAIN 规则,X 代表非 CHAIN 规则,有如下规则集:
C
X
C C C X X C X
将生成两条规则链:
CCCX
CX
规则命中后,都会将命中(匹配成功)规则日志记录到日志输出缓存数组,除了 CHAIN 类型的规则,也就是说规则链命中后只会记录一条日志。 规则日志可以在阶段结束时输出,也可以在请求结束时,汇总一起输出。
日志是以 json 格式输出的,包括以下字段:
json
timestamp
client
remote_addr
method
uri
alerts
msg
match
uri_args
_event_log_request_arguments
request_headers
_event_log_request_headers
request_body
_event_log_request_body
ngx
_event_log_ngx_vars
ngx.var
日志对外输出方式由 _event_log_target 设置,有三种输出方式:
ngx.log
file
_event_log_target_path
socket
resty.log
log.socket
虽然,系统支持通过函数 load_secrules 直接加载 ModSecurity 规则集文件,但是,最好别这么干,因为自动转换过程交繁琐,不小心就容易出错。
赞👍
总结很到位。。这rule规则看得我头都晕了
lua-resty-waf 实践总结
lua-resty-waf 是基于
OpenResty
开发的 WAF 项目,其核心的防护规则策略基本与 ModSecurity Core Rule 一致,但是具体实现有所不同。下面主要分四块阐述其实现功能与原理:
配置模块
系统配置分为两块系统基本配置和规则配置。
系统默认提供了基础的防护规则集,规则集文件都在
rules/
文件夹下,默认有九个文件:11000_whitelist.json
20000_http_violation.json
违反 HTTP 协议防御21000_http_anomaly.json
异常 HTPP 请求防御35000_user_agent.json
user_agent 防御40000_generic_attack.json
一般攻击防御41000_sqli.json
SQL 注入防御42000_xss.json
XSS 攻击防御90000_custom.json
客户自定义防护规则99000_scoring.json
SCORE 阀值控制默认规则集的执行顺序也是从上到下的,注意文件的命名规律,后续添加自己的规则集的时候最好遵循这种规范。
规则配置的加载方式有三种:
1)系统启动时默认加载
waf.init
函数默认会去package.path
前缀路径下的目录rules/
下加载数组global_rulesets
指定的.json
规则集配置文件。global_rulesets
默认就是包括了rules/
目录下的所有文件名,也是基本的系统参数之一,可以通过waf:set_option("global_rulesets", {})
来指定,具体后面会说。2)使用
load_secrules
加载可以调用函数 load_secrules 函数从磁盘加载
ModSecurity SecRules
配置文件,参数就是文件所在的绝对路径。需要注意的是还需要调用函数add_ruleset
将规则集名(文件名称)注册到系统,否则系统是识别不到的。系统内部会按行将
ModSecurity
规则集文件转换(在translate
包内)成 waf 对应的规则格式(json)。目前支持四种规则指令:3)使用
add_ruleset_string
加载add_ruleset_string 可以直接加载规则集字符串(json)。 用户可以使用这种方式动态的加载自定义规则集合。
在每个阶段执行对应的规则集之前,都会先合并(merge)规则集,主要就是根据规则集名,当规则集名称一样时,用户自己添加的规则集优先级更高。
waf.new
函数会初始化一个系统基础参数表,这些参数都可以通过函数waf.set_option(参数名称, value)
来设置。 一些比较重要的参数说明:_debug
开启 debug 模式,将会打印更详细的日志,默认false
_debug_log_level
debug 模式的日志输出级别,默认ngx.INFO
_deny_status
请求被规则拒绝时返回的状态,默认403
_event_log_altered_only
是否只有当请求结束时(DENY
或DROP
)才对外输出日志数据,默认true
_event_log_level
设置日志输出级别,默认ngx.info
_event_log_request_*
可以指定对外日志输出arguments
、body
、headers
字段,默认都是false
_event_log_target
设置日志对外输出的方式,有三种方式可选(详见日志模块说明),默认error
_mode
系统运行模式,有三种可选值,默认值INACTIVE
SIMULATE
默认值,模拟模式,只会记录规则命中日志,不会执行规则action
INACTIVE
不执行规则引擎,即 不执行exec
函数ACTIVE
即正常模式_score_threshold
风险最大阀值,当大于该值时,请求将会被DENY
规则模块
规则模块是 WAF 项目的核心,包括解析和执行两个部分,为了支持类似 ModSecurity 的防护规则,规则配置比较复杂,解析和执行逻辑就更复杂了。
规则配置是按规则集(规则数组)的形式被读取的,规则集再分为多个阶段 ---
access
,header_filter
等。所有的规则集在解析时,会按阶段的维度,添加到对应阶段的规则集数组。需要注意的是,规则解析时会计算两个特殊的变量值:
rule.offset_nomatch
数值,当当前规则匹配失败时,规则遍历迭代器接下来要跳转的规则数,即:当前规则序数 +offset_nomatch
= 下条规则的序数rule.offset_match
数值,当当前规则匹配成功时,规则遍历迭代器接下来要跳转的规则数这两个变量值一般都是
1
,即直接进入相邻的下个规则,但是使用skip
或CHAIN
都会改变这些值。他们都被放入 table 对象rule
,供规则执行时使用。规则配置项
action
规则行为定义nondisrupt
map 数组,非破坏请求行为,定义命中当前规则后的数据行为,可与disrupt
配合使用,在disrupt
之前被执行action
指定具体的行为,有九个可选值:setvar
设置 K-V 值,默认将会存放到变量storage
(table 类型),可作为中间缓存,生存周期是当前请求(ngx.ctx)initcol
持久化存储,将指定的值做存放到redis
、memcached
或dict
sleep
调用ngx.sleep
status
设置当前请求被DENY
后,响应的 HTTP 状态rule_remove_id
临时(内存)移出data
(规则 ID) 对应的规则, 与ignore_rule
原理相同mode_update
更新_mode
(系统运行模式) 值data
上面 action 行为的参数值,动态数据类型,根据 action,可以是 map,字符串或者数值col
设置存放到storage
的 一维key
inc
累加,当value
为数字时,会将数值value
累加到对应的中间缓存值key
设置存储的二维key
value
设置存储的值,数值或字符串disrupt
字符串,定义具体防护方式,有六个可选值:ACCEPT
结束当前阶段,继续执行下一阶段,目前因为规则都集中在 Acess 阶段,可以认为直接通过(PASS) WafDENY
拒绝当前请求,默认返回 403,返回状态可以在nondisrupt.action.status
指定,但是不建议修改DROP
断开当前请求连接,特殊的 444 状态,Nginx 将直接断开连接,而不响应任何字节给客户端IGNORE
忽略该规则的本次命中,继续后面规则的校验SCORE
调整(加减)风险数值(anomaly_score
),只有当风险数值大于阀值(由配置_score_threshold
指定)请求才会被拒绝,与nondisrupt
配合使用CHAIN
规则链,与其他防护方式的规则组合使用,相当于后续规则的前置条件,类似 and 操作id
数值,唯一的标识当前规则op_negated
否定规则匹配结果,即对匹配结果取反operator
操作符,可选值:REGEX
正则匹配,如果待匹配项为table
,则会逐次匹配,一旦匹配成功就返回,下同REFIND
查找(ngx.re.find
)EQUALS
相等GREATER
大于LESS
小于EXISTS
在字符串数组pattern
内存在指定的字符串CONTAINS
在获取的字符串或字符串数组内包含指定的pattern
STR_EXISTS
在指定的字符串内存在STR_MATCH
字符串匹配PM
字符串匹配,可同时与组内所有子串进行匹配(Aho–Corasick 算法)CIDR_MATCH
IP 地址匹配,当前 IP 是否在pattern
IP 数组内DETECT_SQLI
SQL 攻击检查DETECT_XSS
XSS 攻击检查VERIFY_CC
验证信用卡号是否合法pattern
匹配值,字符串或字符串数组,可以是具体的值或者正则表达式,与待匹配值(根据下面的vars
计算所得)进行比较skip
数值,指跳过的规则个数(下个规则位置 = 当前规则位置 + skip + 1
)skip_after
数值,根据规则 id,直接跳转到对应规则vars
对象数组,定义待匹配值的获取方式type
定义数据源,可选值(部分):REQUEST_HEADERS
获取请求头,map 类型METHOD
获取请求方法,字符串类型,例如:GET,POST 等 HTTP 标准方法TX
获取中间缓存值(ctx.storage["TX"]
),map 类型URI_ARGS
获取请求参数(table),map 类型QUERY_STRING
获取请求参数,字符串,示例:a=1&b=2
REQUEST_BODY
获取请求 body 部分,map 或字符串类型URI
获取请求原始路径部分(ngx.var.uri),字符串REQUEST_URI
获取请求 URL,包括参数部分,字符串,示例:/a/b/c?a=1&b=2
COOKIES
请求cookies
(table),map 类型REQUEST_ARGS
对象(map)类型。包括URI_ARGS
,REQUEST_BODY
,COOKIES
三项值,但最终转化成一维 mapREMOTE_ADDR
获取请求端 IP 地址(remote_addr),字符串HTTP_VERSION
获取 HTTP 协议版本,数值,可选值:2.0, 1.0, 1.1SCORE_THRESHOLD
获取当前风险阀值,数值类型ARGS_COMBINED_SIZE
获取请求参数和请求 body 的字节大小,数值TIME
字符串,格式:“时:分:秒”TIME_EPOCH
获取当前时间戳,精确到秒,数值类型storage
是否跳过缓存,根据vars
的定义,重新计算待匹配值,存在(not nil)即为true
。因为请求处理过程数据源可能会修改数据源的某些值,导致缓存不一致的情况。需要注意的是这里的缓存,仅仅是存在当前请求内。parse
字符串数组,长度固定为2
,定义从数据源取出待匹配数据集的规则specific
取出参数指定的值regex
正则匹配,取出所有正则匹配参数的数据集keys
取出数据源中所有的key
,作为数据集values
取出数据源中所有的value
,作为数据集all
将整个数据源作为数据集1
,表示无意义unconditional
指示当前规则将一定会被命中opts
transform
字符串或字符串数组,对上述数据源进行转换的方式,有如下可选值(部分):uri_decode
uri 解码lowercase
转成小写md5
计算 md5nolog
不记录该条规则的命中日志logdata
设置规则命中日志字段logdata
的值,可以使用具体的值或者变量(%{value}
),实例:"logdata" : "%{TX.anomaly_score}"规则是在
waf.exec
函数内执行的,每个阶段只会执行当前阶段对应的规则集,及规则集里面的规则。需要注意的是CHAIN
规则的执行逻辑,这种类型的规则会组成一个规则链,规则链内的规则是and
关系,即规则匹配失败,就会跳过当前整个规则链。规则链是怎么组成的呢?当遇到非CHAIN
规则时,就会计算成一个规则链。下面使用
C
代表CHAIN
规则,X
代表非CHAIN
规则,有如下规则集:将生成两条规则链:
CCCX
CX
日志模块
规则命中后,都会将命中(匹配成功)规则日志记录到日志输出缓存数组,除了
CHAIN
类型的规则,也就是说规则链命中后只会记录一条日志。 规则日志可以在阶段结束时输出,也可以在请求结束时,汇总一起输出。日志是以
json
格式输出的,包括以下字段:timestamp
当前时间戳(秒)client
请求客户端地址(remote_addr
)method
请求方法uri
请求路径alerts
数组,规则命中记录id
命中的规则 idmsg
规则说明match
规则操作符(operator
)函数返回的第二个值,这个值非常灵活,可以是字符串,数字或者数组logdata
规则logdata
配置指定的输出项,比如当前阀值等id
唯一的标记当前请求 ID,首次调用函数waf.new
时随机生成id
唯一的标记当前请求 ID,首次调用函数waf.new
时随机生成uri_args
map 类型,请求参数;可选,由参数_event_log_request_arguments
控制request_headers
map 类型,请求头;可选,由参数_event_log_request_headers
控制request_body
map 或字符串 类型,请求体;可选,由参数_event_log_request_body
控制ngx
数组,可选,由参数_event_log_ngx_vars
指定的变量(ngx.var
)值日志对外输出方式由
_event_log_target
设置,有三种输出方式:error
直接使用ngx.log
输出file
输出到参数_event_log_target_path
指定的文件内socket
使用库resty.log
输出到指定的日志服务器,需要配置相关参数,初始化log.socket
客户端特别说明
虽然,系统支持通过函数
load_secrules
直接加载ModSecurity
规则集文件,但是,最好别这么干,因为自动转换过程交繁琐,不小心就容易出错。