一、前言

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_filesmirror两个模块的命令 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日志:

  1. 建立连接,nginx解析http请求

  2. 进入lua access阶段

  3. 进入lua_waf检查流程

  4. 检查流程结束,进入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)

五、参考

点击收藏 | 3 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖