WAF代码剖析之初识openresty
为什么会有这一系列的文章?
自从这几年信息安全的大力发展,信息安全的建设是逐步发展起来,作为甲方安全工程师,一个人的安全部,使用开源的WAF部署防御攻击,仿佛是件很平常的事情,但是开源的坏处就是没有人能够及时提供技术支撑,出现问题就只能自己维护,我相信维护WAF的代码和规则是一件很耗费精力的事情,还不如用商用的香。如果没安全预算的朋友,不妨跟我一起走向WAF开发的世界。
之前对WAF维护和加载新功能,只是单纯在openresty的access阶段进行处理,没有整体去了解整个流程是怎么运作的,接下来的文章我会针对于openresty处理阶段,WAF代码编写等内容进行说明,可能有些内容会显得啰嗦,因为这是我对于技术细节思考的结果。
为什么openresty能够成为WAF开发的核心选择?对于业务系统来说,一般放在最外层就是nginx,作为负载均衡,转发流量,并且官方也提供modsecurity规则用于WAF防御攻击,但是用过的人也知道,代码是用C写的,可写性差并且防御规则不容易维护,最重要的一点是性能损耗,我之前写过一篇WAF性能测试报告,虽然可能存在场景偏差,但是nginx性能不如openresty这是可以明显看得到。而且有一点重要的是,openresty本质还是nginx,但是加上了lua脚本语言进行嵌入,性能处理提高和能够编写复杂处理场景。
开源社区流行的主要是这些WAF:jxwaf,openstar,ngx_lua_waf(排名不分先后),我这边主要会以jxwaf的代码进行剖析,当然也会找其他WAF的代码进行思路碰撞和学习。
废话不多说,let‘s go~~
openresty处理流程
openresty有11个处理流程阶段,如下图所示
具体阶段作用域和功能作用如下表,来源地址:https://openresty-reference.readthedocs.io/en/latest/Directives/
阶段 |
作用域(nginx.conf) |
功能作用 |
---|---|---|
init_by_lua* |
http |
初始化 nginx 和预加载 lua(nginx 启动和 reload 时执行) |
init_worker_by_lua* |
http |
每个工作进程(worker_processes)被创建时执行,用于启动一些定时任务, |
比如心跳检查,后端服务的健康检查,定时拉取服务器配置等;| | ssl_certificate_by_lua* | server | 对HTTPS请求的处理 | | set_by_lua* | server, server if, location, location if | 流程分支处理判断变量初始化 | | rewrite_by_lua* | http, server, location, location if | 转发、重定向、缓存等功能 | | access_by_lua* | http, server, location, location if | 内容处理(WAF规则处理) | | content_by_lua* | location, location if | 内容生成,相当于response | | balancer_by_lua* | upstream | 负载均衡 | | header_filter_by_lua* | http, server, location, location if | 对响应头(headers)进行处理 | | body_filter_by_lua* | http, server, location, location if | 对响应体(body)进行处理 | | log_by_lua* | http, server, location, location if | 日志记录 |
为了让大家更加直观认识这些阶段,我拿jxwaf的nginx配置文件进行说明,当然不懂的朋友就要下去自我补习。
http {
include mime.types;
default_type application/octet-stream;
client_body_buffer_size 100m;
client_max_body_size 10m;
sendfile on;
#tcp_nopush on;
resolver 114.114.114.114;
resolver_timeout 5s;
#keepalive_timeout 0;
keepalive_timeout 65;
lua_ssl_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt;
lua_ssl_verify_depth 3;
lua_shared_dict limit_req 100m;
lua_shared_dict limit_req_count 100m;
lua_shared_dict limit_attack_ip 100m;
lua_shared_dict limit_bot 100m;
lua_shared_dict waf_common_conf 100m;
lua_shared_dict black_attack_ip 100m;
init_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/init.lua;
init_worker_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/init_worker.lua;
rewrite_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/rewrite.lua;
access_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/access.lua;
header_filter_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/header_filter.lua;
#body_filter_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/body_filter.lua;
log_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/log.lua;
rewrite_by_lua_no_postpone on;
#gzip on;
upstream jxwaf {
server www.jxwaf.com;
balancer_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/balancer.lua;
}
lua_code_cache on;
server {
listen 80;
server_name localhost;
set $proxy_pass_https_flag "false";
location / {
#root html;
# index index.html index.htm;
if ($proxy_pass_https_flag = "true"){
proxy_pass https://jxwaf;
}
if ($proxy_pass_https_flag = "false"){
proxy_pass http://jxwaf;
}
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate full_chain.pem;
ssl_certificate_key private.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!R";
ssl_prefer_server_ciphers on;
ssl_certificate_by_lua_file /opt/jxwaf/lualib/resty/jxwaf/ssl.lua;
set $proxy_pass_https_flag "false";
location / {
root html;
index index.html index.htm;
if ($proxy_pass_https_flag = "true"){
proxy_pass https://jxwaf;
}
if ($proxy_pass_https_flag = "false"){
proxy_pass http://jxwaf;
}
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
对openresty自带属性进行说明:
lua_ssl_trusted_certificate/etc/pki/tls/certs/ca-bundle.crt;
CA bundle是包含根证书和中间证书的文件。使用temsock:sslhandshake方法以PEM格式指定具有受信任CA证书的文件路径,该证书路径用于验证SSL / TLS服务器的证书。
lua_ssl_verify_depth3;
设置服务器证书链中的验证深度。证书链就是Root CA签发二级Intermediate CA,二级Intermediate CA可以签发三级Intermediate CA,也可以直接签发用户证书。从Root CA到用户证书之间构成了一个信任链:信任Root CA,就应该信任它所信任的二级Intermediate CA,从而就应该信任三级Intermediate CA直至信任用户证书。验证深度设置为3就是说如果验证Root CA下面的第三级的证书是否可信。
lua_shared_dict limit_req 100m;
声明一块共享内存区域,里面放着是变量字典数据,在nginx启动的时候,就是读取字典变量的数据。这样的好处是比如说有很多检测规则,SQL注入、XSS、命令注入等规则可以预先放进内存里面,openresty就可以直接去内存数据,而不是进来一个数据包就open一下文件然后close一下,减少对磁盘读写操作。
rewrite_by_lua_no_postpone on;
是否让“rewrite_by_lua”在rewrite阶段的最后执行,默认值是off, 即“rewrite_by_lua”里的Lua代码将在其他Nginx rewrite功能模块之后执行。
lua_code_cache on;
参考这里:https://linux.ci/201.html,在开发调试过程中,可以把它给关闭掉,因为可以实时更新,不需要重启nginx,在生产就要开启,不然性能损耗就非常大。
数了一下openresty11个执行阶段jxwaf就用了8个阶段,接下来讲述这8个阶段的代码将会是本系列的重点。
安装配置
根据目前jxwaf需要的openresty版本为1.15.8.3,使用centos7系统进行安装:
yum install -y readline-devel pcre pcre-devel openssl openssl-devel gcc curl GeoIP-devel wget wget https://openresty.org/download/openresty-1.15.8.3.tar.gz tar -xvf openresty-1.15.8.3.tar.gz cd openresty-1.15.8.3 ./configure -j2 make -j2 make install
或者单纯windows系统就可以下载Windows的openresty版本
https://openresty.org/download/openresty-1.15.8.3-win64.zip
开发WAF是采用LUA编程语言,LUA还属于小众语言,没有像pycharm这种完整的IDE,LUA IDE的选择可以看自己需要,可以使用vscode+插件进行开发。
lua插件,用于语法高亮和语法说明
Lua Debug用于调试和运行lua代码
按F5启动调试,可以看到lua运行成功的代码
测试
nginx.conf配置文件,我把lua_code_cache给关闭,这样做是因为在测试代码的时候,不用reload nginx,nginx会直接热更新,然后我在content_by_lua_file阶段调用test.lua文件输出内容
worker_processes 1;
events {
worker_connections 1024;
}
http {
lua_code_cache off;
server {
location /test {
default_type 'text/plain';
content_by_lua_file 'D:/waf/test.lua';
}
}
}
#test.lualocal name = "Anonymous"
ngx.say("Hello, ", name, "!")
ngx.say("test")
效果如下
- Silverlight:Dependency Property(依赖属性)学习笔记
- Silverlight:利用异步加载Xap实现自定义loading效果
- Docker容器学习梳理--手动制作系统镜像
- 怎样裁剪图片的局部
- vb中实现最佳按钮效果
- silverlight:wcf双工通讯学习笔记
- Docker容器学习梳理--web管理工具DockerUI部署记录
- Docker容器学习梳理-容器硬盘热扩容
- 检测到Loaderlock的问题
- 权威报告预测比特币在2018年“王位”不保
- Linux下FTP环境部署梳理(vsftpd和proftpd)
- Silverlight如何与JS相互调用
- Docker容器学习梳理--私有仓库Registry使用
- 从插件重构看如何提升测试质量与效率
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 策略模式
- updatedb命令
- Vue路由懒加载
- 简单谈谈我的Android屏幕适配之路
- Js中Symbol对象
- 责任链模式
- Android Studio升级到3.0后遇到的坑
- Android原生项目集成React Native的方法
- Android编程之光线传感器用法详解
- Android Studio 3.0 新功能全面解析和旧项目适配问题
- Android开发中使用外部应用获取SD卡状态的方法
- Android编程使用光线传感器获取光线强弱的方法【LightSensorManager封装类】
- Android开发中的重力传感器用法实例详解
- 腾讯云TKE-Ingress案例: Nginx-Ingress 实现grpc转发
- 机器人软件中间层 yarp-Yet Another Robot Platforms