一、前言
lua是一种轻量级脚本语言,具有高可扩展性特点,搭配nginx可以实现对http请求包过滤的效果,本文通过分析一个开源lua-waf项目学习nginx从攻击检测到请求拦截的过程
二、相关概念
2.1 nginx
01 http请求生命周期
nginx处理http请求的逻辑是先解析请求行、请求头,再处理http请求,最后将结果过滤返回给客户端
02 多阶段处理请求
处理http请求分为11个阶段,每个阶段调用一个或多个模块共同工作
名称 | 功能 | 相关模块 | 相关指令 | 注册函数 |
---|---|---|---|---|
POST_READ | 接受完请求头后的第一个阶段,还未做处理可以获取到原始请求数据,如:读取X-Real-IP 字段获得客户端ip |
ngx_http_realip_module | set_real_ip_from、real_ip_header、real_ip_recursive | √ |
SERVER_REWRITE | 处理server 块内location 块外的重写命令 |
ngx_http_rewrite_module | break、if、return、rewrite、set | √ |
FIND_CONFIG | 根据rewrite 后的uri 来匹配对应location |
ngx_http_core_module | location | / |
REWRITE | 处理location 块内的重写命令 |
ngx_http_rewrite_module | break、if、return、rewrite、set | √ |
POST_REWRITE | 检查上一阶段是否存在重写操作,存在则跳转至FIND_CONFIG 阶段重新走流程 |
/ | / | / |
PREACCESS | 限制客户端访问频率和访问数量 | http_limit_req_module、http_limit_conn_module | limit_req、limit_req_zone、limit_req_status、limit_conn、limit_conn_zone、limit_conn_status | √ |
ACCESS | 限制客户端访问,如:通过ip白名单、密码认证后才能访问 | ngx_http_access_module、ngx_http_auth_basic_module、ngx_http_auth_request_module | allow、deny、auth_basic、auth_basic_user_file、auth_request、auth_request_set | √ |
POST_ACCESS | 配合access 阶段实现satisfy 命令 |
/ | / | / |
PRECONTENT | 生成结果前的预处理阶段,主要处理try_files 和mirror 两个模块的命令 |
http_try_files_module、ngx_http_mirror_module | try_files、mirror | / |
CONTENT | 整理处理结果,生成原始http响应数据 | ngx_http_concat_module、ngx_http_random_index_module、ngx_http_index_module、ngx_http_autoindex_module、ngx_http_static_module | root、alias、index、autoindex、concat | √ |
LOG | 根据配置文件,生成日志 | ngx_http_log_module | log_format、access_log、error_log | √ |
03 filter输出过滤
CONTENT
阶段生成的数据存放在缓存中,经过filter处理后再发送给客户端,默认编译模块如下:
名称 | 功能 | 相关指令 |
---|---|---|
ngx_http_slice_filter_module | 用于nginx本地切片缓存待发送给客户端的大文件 | slice |
ngx_http_not_modified_filter_module | 处理If-Modified-Since ,判断客户端缓存文件时间与服务器本地文件最后修改时间是否相同,如果存在差异则返回最新文件内容,相同则返回304状态码 |
if_modified_since |
ngx_http_range_body_filter_module | 处理Range ,根据客户端请求返回指定数据 |
Range |
ngx_http_copy_filter_module | 处理HTTP请求体,根据需求重新复制输出链表中的某些节点 | / |
ngx_http_headers_filter_module | 处理http头部数据,如:添加任意名称请求头、响应头 | add_header、expires |
ngx_http_userid_filter_module | 设置cookie | userid、userid_name、userid_domain、userid_path、userid_expires |
ngx_http_gunzip_filter_module | 当客户端不支持gzip时,由nginx解压数据后再发送给客户端 | gunzip、gunzip_buffers |
ngx_http_addition_filter_module | 在响应之前或者之后追加文本内容,如:在网站底部追加一个js或css | add_before_body、add_after_body、addition_types |
ngx_http_sub_filter_module | 支持字符串替换 | sub_filter、sub_filter_once、sub_filter_types、sub_filter_last_modified |
ngx_http_charset_filter_module | 配置响应头Content-Type |
charset、charset_map、charset_types、override_charset、source_charset |
ngx_http_ssi_filter_module | 支持ssi服务端嵌入功能 | ssi、ssi_last_modified、ssi_min_file_chunk、ssi_silent_errors、ssi_types、ssi_value_length |
ngx_http_postpone_filter_module | 负责处理子请求和主请求数据的输出顺序 | / |
ngx_http_gzip_filter_module | 支持gzip压缩 | gzip、gzip_min_length、gzip_proxied、gzip_types |
ngx_http_range_header_filter_module | 处理range头 | / |
ngx_http_v2_filter_module | 支持http2协议 | http2_body_preread_size、http2_chunk_size、http2_idle_timeout、http2_max_concurrent_pushes、http2_max_concurrent_streams、http2_max_field_size、http2_max_header_size、http2_max_requests、http2_push、http2_push_preload、http2_recv_buffer_size、http2_recv_timeout |
ngx_http_chunked_filter_module | 支持chunked | / |
ngx_http_header_filter_module | 拼接响应头 | / |
ngx_http_write_filter_module | 发送http响应 | / |
2.2 lua_nginx_module
原生nginx不支持lua代码,需要手动添加三方模块Lua-Nginx-Module
,该模块提供了指令和api支持,使得lua代码可以在nginx不同阶段执行,常见的指令和api如下:
指令:
指令 | 阶段 | 范围 | 功能 |
---|---|---|---|
init_by_lua* | loading-config | http | nginx读取配置时执行,导入lua配置 |
set_by_lua* | rewrite | server、server if、location、location if | 设置lua变量 |
rewrite_by_lua* | rewrite tail | http、server、location、location if | 跳转、重定向相关 |
access_by_lua* | access tail | http、server、location、location if | 访问控制相关 |
content_by_lua* | content | location、location if | 内容生成阶段相关 |
header_filter_by_lua* | output-header-filter | http、server、location、location if | 修改http响应头数据 |
body_filter_by_lua* | output-body-filter | http、server、location、location if | 修改http响应体数据 |
log_by_lua* | log | http、server、location、location if | 日志相关 |
API:
API | 范围 | 功能 |
---|---|---|
ngx.req.raw_header | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua | 获取原始的http请求头 |
ngx.req.get_method | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、balancer_by_lua | 获取http请求方法,如:GET、POST |
ngx.req.get_uri_args | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、balancer_by_lua | 获取uri中全部的args,默认最大值100 |
ngx.req.get_body_data | rewrite_by_lua、access_by_lua、content_by_lua、log_by_lua | 从内存中获取http请求体 |
ngx.config.nginx_version | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、init_by_lua、init_worker_by_lua | 获取nginx版本 |
ngx.var.xxxx | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua | 调用nginx变量 |
ngx.status | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua | 获取http响应状态码 |
ngx.md5 | set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、balancer_by_lua、ssl_certificate_by_lua、ssl_session_fetch_by_lua、ssl_session_store_by_lua | 计算数据的md5哈希值 |
ngx.log | init_worker_by_lua、set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、balancer_by_lua | 日志操作 |
三、环境搭建
在nginx的环境下--add-module
添加lua模块,同时导入ngx_lua_waf项目
- centos 7.9
- nginx 1.20.1
- luajit 2.0.5
- lua-nginx-module 0.10.9
- ngx_devel_kit 0.3.0
- ngx_lua_waf 0.7.2
3.1 添加lua模块
01 准备lua环境
# 下载并编译
wget http://luajit.org/download/LuaJIT-2.0.5.tar.gz
tar -zxvf LuaJIT-2.0.5.tar.gz
cd LuaJIT-2.0.5
make install PREFIX=/usr/local/luajit
# 设置环境变量
vi /etc/profile
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.0
source /etc/profile
echo "/usr/local/luajit/lib" >> /etc/ld.so.conf
ldconfig
02 准备ndk模块
wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz
tar -zxvf v0.3.0.tar.gz
03 准备lnm模块
wget https://github.com/openresty/lua-nginx-module/archive/v0.10.9rc7.tar.gz
tar -zxvf v0.10.9rc7.tar.gz
04 添加lua模块
# 准备nginx安装包
wget http://nginx.org/download/nginx-1.20.1.tar.gz
tar -zxvf nginx-1.20.1.tar.gz
cd nginx-1.20.1
# 查看原始编译,在末尾处添加ndk、lnm文件路径
# --add-module=/waf/ngx_devel_kit-0.3.0
# --add-module=/waf/lua-nginx-module-0.10.9rc7
nginx -V
./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-compat --with-debug --with-file-aio --with-google_perftools_module --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_degradation_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module=dynamic --with-http_mp4_module --with-http_perl_module=dynamic --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_xslt_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-stream_ssl_preread_module --with-threads --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' --add-module=/waf/ngx_devel_kit-0.3.0 --add-module=/waf/lua-nginx-module-0.10.9rc7
systemctl stop nginx
# configure无法通过,补环境
error: SSL modules require the OpenSSL library
yum -y install openssl openssl-devel
error: the HTTP XSLT module requires the libxml2/libxslt libraries.
yum -y install libxslt-devel
error: the HTTP image filter module requires the GD library
yum -y install gd gd-devel
error: the Google perftools module requires the Google perftools library
yum -y install gperftools
# 编译
make
cd ./objs
# 备份原文件,拷贝新文件
which nginx
mv /usr/sbin/nginx /usr/sbin/nginx.old
cp nginx /usr/sbin/nginx
systemctl restart nginx
05 访问测试
# 测试使用content_by_lua响应http请求
vi /etc/nginx/nginx.conf
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
location /lua {
default_type 'text/html';
content_by_lua 'ngx.say("hello lua")';
}
nginx -s reload
curl 127.0.0.1/lua
3.2 加载lua-waf
# 下载文件
wget https://github.com/loveshell/ngx_lua_waf/archive/refs/tags/v0.7.2.tar.gz
tar -zxvf v0.7.2.tar.gz
cp -r ngx_lua_waf-0.7.2 /etc/nginx/conf.d/waf/
# 修改配置文件路径
vi /etc/nginx/conf.d/waf/config.lua
RulePath = "/etc/nginx/conf.d/waf/wafconf/"
attacklog = "on"
logdir = "/var/log/nginx/attacklog/"
# 写入nginx
vi /etc/nginx/nginx.conf
http {
...
# lua_waf
lua_package_path "/etc/nginx/conf.d/waf/?.lua";
lua_shared_dict limit 10m;
init_by_lua_file /etc/nginx/conf.d/waf/init.lua;
access_by_lua_file /etc/nginx/conf.d/waf/waf.lua;
}
nginx -s reload
3.3 测试
向网站发送攻击语句,被拦截返回403,配置成功
http://ip/?id=<script>alert(1)</script>
日志已记录攻击语句
四、过程分析
4.1 静态分析
ngx_lua_waf源文件由四大块组成:
- 配置文件:
config.lua
- 初始化文件:
init.lua
- 规则库文件:
wafconf
- 执行文件:
waf.lua
.
├── README.md
├── config.lua
├── init.lua
├── install.sh
├── waf.lua
└── wafconf
├── args
├── cookie
├── post
├── url
├── user-agent
└── whiteurl
在nginx配置文件中使用lua_ngx_module
模块提供的init_by_lua_file
指令和access_by_lua_file
指令添加项目,nginx在读取配置阶段会导入init.lua
配置、访问控制阶段会执行waf.lua
流程处理http请求包
http {
...
# lua_waf
lua_package_path "/etc/nginx/conf.d/waf/?.lua";
lua_shared_dict limit 10m;
init_by_lua_file /etc/nginx/conf.d/waf/init.lua;
access_by_lua_file /etc/nginx/conf.d/waf/waf.lua;
...
}
通过配置文件知道核心文件是waf.lua
,嵌入在nginx处理http请求11个阶段的access
阶段,此时刚与客户端建立tcp连接传输完http报文待nginx处理,lua代码可以调用api对请求包过滤实现访问控制,拒绝不符合规则的请求
01 配置文件
config.lua
配置具体检查项及拦截后的页面,如:CookieMatch
开启后会进入cookie检查流程、请求不符合规则则返回html
RulePath = "/usr/local/nginx/conf/waf/wafconf/"
attacklog = "on"
logdir = "/usr/local/nginx/logs/hack/"
UrlDeny="on"
Redirect="on"
CookieMatch="on"
postMatch="on"
whiteModule="on"
black_fileExt={"php","jsp"}
ipWhitelist={"127.0.0.1"}
ipBlocklist={"1.0.0.1"}
CCDeny="off"
CCrate="100/60"
html=[[ 403 ]]
02 初始化文件
init.lua
- 日志记录,格式为:客户端ip + 请求时间 + 拦截模块 + 服务器名 + url + '-' + ua + 拦截规则
function log(method,url,data,ruletag)
if attacklog then
local realIp = getClientIp()
local ua = ngx.var.http_user_agent
local servername=ngx.var.server_name
local time=ngx.localtime()
if ua then
line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..data.."\" \""..ua.."\" \""..ruletag.."\"\n"
else
line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..data.."\" - \""..ruletag.."\"\n"
end
local filename = logpath..'/'..servername.."_"..ngx.today().."_sec.log"
write(filename,line)
end
end
- 定义函数检查内容,如:
args()
函数,调用lua_ngx_module
模块提供的ngx.req.get_uri_args()
api获取args,再与规则库argsrules
做正则匹配,匹配成功则日志记录相关信息,最后say_html()
返回拦截后的页面
function args()
for _,rule in pairs(argsrules) do
local args = ngx.req.get_uri_args()
for key, val in pairs(args) do
if type(val)=='table' then
if val ~= false then
data=table.concat(val, " ")
end
else
data=val
end
if data and type(data) ~= "boolean" and rule ~="" and ngxmatch(unescape(data),rule,"isjo") then
log('GET',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
03 规则库文件
匹配恶意字符串的具体规则,如:args
中有注入、包含、ssrf等常见敏感字符
\.\./
\:\$
\$\{
select.+(from|limit)
(?:(union(.*?)select))
having|rongjitest
sleep\((\s*)(\d*)(\s*)\)
benchmark\((.*)\,(.*)\)
base64_decode\(
(?:from\W+information_schema\W)
(?:(?:current_)user|database|schema|connection_id)\s*\(
(?:etc\/\W*passwd)
into(\s+)+(?:dump|out)file\s*
group\s+by.+\(
xwork.MethodAccessor
(?: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)\(
xwork\.MethodAccessor
(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/
java\.lang
\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\[
\<(iframe|script|body|img|layer|div|meta|style|base|object|input)
(onmouseover|onerror|onload)\=
04 执行文件
调用init.lua
已定义的函数对http请求包做过滤处理,内容有:
- ip白名单
--[[
ngx.var.remote_addr api获取的ip值与config.lua中ipWhitelist做白名单匹配
]]
function whiteip()
if next(ipWhitelist) ~= nil then
for _,ip in pairs(ipWhitelist) do
if getClientIp()==ip then
return true
end
end
end
return false
end
- ip黑名单
--[[
ngx.var.remote_addr api获取的ip值与config.lua中ipBlocklist做黑名单匹配
]]
function blockip()
if next(ipBlocklist) ~= nil then
for _,ip in pairs(ipBlocklist) do
if getClientIp()==ip then
ngx.exit(403)
return true
end
end
end
return false
end
- 请求频率限制
--[[
ngx.shared.limit申请一个共享空间用于请求计数
每收到一个请求便会生成一个由$remote_addr和$uri组成的token,然后对token与limit数据进行做存在性匹配
不存在则将token写入共享空间,此时计数为1,过期时间为config.lua配置的CCrate值,初始值为60秒
存在则进行请求数量判断,当超过config.lua配置的CCrate参数值100次时返回503错误
没有超过阀值便会incr(token,1)新增1次请求数
]]
function denycc()
if CCDeny then
local uri=ngx.var.uri
CCcount=tonumber(string.match(CCrate,'(.*)/')) -- 100/
CCseconds=tonumber(string.match(CCrate,'/(.*)')) -- /60
local token = getClientIp()..uri -- 生成token 1.1.1.1/abc
local limit = ngx.shared.limit -- 初始化共享空间
local req,_=limit:get(token) -- 新token与共享空间做匹配
if req then -- 匹配成功,进入cc检测
if req > CCcount then -- 判断是否大于100
ngx.exit(503) -- 返回503
return true
else -- 存在token,但是没有达到100个数量
limit:incr(token,1) -- 请求数增加1
end
else -- 匹配失败,共享空间新增一个token,计数1,60秒过期时间
limit:set(token,1,CCseconds)
end
end
return false
end
- url白名单
--[[
ngx.var.uri api获取的uri与规则库提取的whiteurl做白名单匹配
]]
function whiteurl()
if WhiteCheck then
if wturlrules ~=nil then
for _,rule in pairs(wturlrules) do
if ngxmatch(ngx.var.uri,rule,"isjo") then
return true
end
end
end
end
return false
end
- ua检查
--[[
ngx.var.http_user_agent api获取的ua值与规则库提取的uarules做黑名单匹配
匹配成功时调用log()函数记录日志,并返回403页面
]]
function ua()
local ua = ngx.var.http_user_agent
if ua ~= nil then
for _,rule in pairs(uarules) do
if rule ~="" and ngxmatch(ua,rule,"isjo") then
log('UA',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
- url检查
--[[
ngx.var.request_uri api获取的url与规则库提取的urlrules做黑名单匹配
匹配成功时调用log()函数记录日志,并返回403页面
]]
function url()
if UrlDeny then
for _,rule in pairs(urlrules) do
if rule ~="" and ngxmatch(ngx.var.request_uri,rule,"isjo") then
log('GET',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
- args检查
--[[
ngx.req.get_uri_args() api获取url的args
判断args值是否为table类型,如果是则使用concat拼接数据
再将args的值与规则库提取的argsrules做黑名单匹配
匹配成功时调用log()函数记录日志,并返回403页面
]]
function args()
for _,rule in pairs(argsrules) do
local args = ngx.req.get_uri_args()
for key, val in pairs(args) do
if type(val)=='table' then
if val ~= false then
data=table.concat(val, " ")
end
else
data=val
end
if data and type(data) ~= "boolean" and rule ~="" and ngxmatch(unescape(data),rule,"isjo") then
log('GET',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
- cookie检查
--[[
ngx.var.http_cookie api获取的cookie与规则库提取的ckrules做黑名单匹配
匹配成功时调用log()函数记录日志,并返回403页面
]]
function cookie()
local ck = ngx.var.http_cookie
if CookieCheck and ck then
for _,rule in pairs(ckrules) do
if rule ~="" and ngxmatch(ck,rule,"isjo") then
log('Cookie',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
- body检查
post方法:
--[[
ngx.req.init_body api创建一个4k的缓存区,sock接收每个请求最大4k数据进行检查
正则匹配请求体数据,发现Content-Disposition: form-data时进入文件检查流程,对文件后缀名与规则库提取的black_fileExt做黑名单匹配
然后对请求体与规则库提取的postrules做黑名单匹配
匹配成功时调用log()函数记录日志,并返回403页面
]]
ngx.req.init_body(128 * 1024) -- buffer 128k
sock:settimeout(0)
local content_length = nil
content_length=tonumber(ngx.req.get_headers()['content-length']) -- 获取请求包总长度
local chunk_size = 4096
if content_length < chunk_size then
chunk_size = content_length
local data, err, partial = sock:receive(chunk_size) -- 默认最大取4k数据
data = data or partial
ngx.req.append_body(data)
local m = ngxmatch(data,[[Content-Disposition: form-data;(.+)filename="(.+)\\.(.*)"]],'ijo') -- 匹配form-data
if m then
fileExtCheck(m[3]) -- 匹配后缀名
filetranslate = true
else
if ngxmatch(data,"Content-Disposition:",'isjo') then
filetranslate = false
end
if filetranslate==false then
if body(data) then -- 调用body()检查请求体
return true
end
end
end
ngx.req.finish_body()
非post方法:
--[[
ngx.req.get_post_args api获取body部分的args
判断args值是否为table类型,如果是则使用concat拼接数据
再将args的值与规则库提取的postrules做黑名单匹配
匹配成功时调用log()函数记录日志,并返回403页面
]]
ngx.req.read_body()
local args = ngx.req.get_post_args()
if not args then
return
end
for key, val in pairs(args) do
if type(val) == "table" then
if type(val[1]) == "boolean" then
return
end
data=table.concat(val, ", ")
else
data=val
end
if data and type(data) ~= "boolean" and body(data) then
body(key)
end
end
04 小结
4.2 debug分析
开启debug模式,配置content_by_lua
指令处理请求
error_log /var/log/nginx/error.log debug;
http {
...
server{
...
location /test {
default_type 'text/html';
content_by_lua "ngx.say('ok')";
}
}
}
post方式向服务器发送a='/etc/passwd'
,返回403拦截
查看debug日志:
建立连接,nginx解析http请求
进入lua access阶段
进入lua_waf检查流程
检查流程结束,进入filter阶段,发送返回包
4.3 绕过分析
01 不规则的http请求包绕过
lua-waf基于nginx和ngx_lua_module,调用api获取原始http请求包,如果包内存在不符合RFC标准的数据则无法进入lua检查流程,如:body检查post请求时会先用正则匹配请求包是否包含Content-Disposition: form-data;
关键字,匹配成功才会进入下一步后缀检查流程
-- waf.lua
local m = ngxmatch(data,[[Content-Disposition: form-data;(.+)filename="(.+)\\.(.*)"]],'ijo')
if m then
fileExtCheck(m[3]) -- 匹配后缀名
filetranslate = true
当使用畸形的http请求时便绕过了waf检查
# Content-Disposition值大小写
Content-Disposition: form-datA; name="file"; filename="aaa.php"
Content-Type: image/png
# Content-Disposition值为空
Content-Disposition: ; name="file"; filename="aaa.php"
Content-Type: image/png
# Content-Disposition值多余数据
Content-Disposition: ~form-data; name="file"; filename="aaa.php"
Content-Type: image/png
02 不安全的api接口绕过
lua代码中用到了lua_ngx_module提供的ngx.req.get_uri_args()
、ngx.req.get_headers()
、ngx.req.get_post_args()
接口来获取http请求args和headers的值,默认配置下这三个接口最大接收前100个值,即超过100时不再接收,由此便存在绕过情况
当恶意args写入在100时,返回403被拦截
当恶意args写入在101时,lua代码对前100个参数进行检查无误后放行,便绕过了waf检查
搜索lua_ngx_module源码,发现默认值为100的方法还有:
ngx.decode_args
对args解码ngx.resp.get_headers
获取response头
lua_ngx_module 0.10.13版本已修复该问题,当超过最大数量限制时会返回一个truncated
字符串
Since v0.10.13, when the limit is exceeded, it will return a second value which is the string "truncated".
03 不设限的请求长度绕过
在进行body检查阶段,检测到post方法时会取请求的前4k数据进行检查,把恶意数据写入在4k之后便可以绕过waf
-- waf.lua
local chunk_size = 4096
if content_length < chunk_size then
chunk_size = content_length
local data, err, partial = sock:receive(chunk_size)
五、参考
- https://nginx.org/en/docs/
- https://blogs.vicsdf.com/article/4766
- https://www.aikaiyuan.com/12438.html
- https://iziyang.github.io/2020/04/12/5-nginx/
- https://roombox.xdf.cn/blog/nginx-eleven-phase/
- https://www.cnblogs.com/-wenli/p/13535173.html
- http://tengine.taobao.org/book/chapter_12.html#id8
- https://joychou.org/web/how-to-build-cloud-waf.html
- https://mp.weixin.qq.com/s/tGfoA75QoHZEXPrccQiYMg
- https://www.cnblogs.com/wangxusummer/p/4309007.html
- https://joychou.org/web/nginx-Lua-waf-general-bypass-method.html