基于Nginx的第7层应用防火墙(Web Application Firewall)

一、前言

Web applications – yours, mine, everyone’s – are terribly insecure on average. We struggle to keep up with the security issues and need any help we can get to secure them.
IVAN RISTIĆ, CREATOR OF MODSECURITY

我们都希望自己创建的应用程序是安全的,不会轻易被破坏。但是,忽然而至的黑客攻击新闻,突然收到的安全通报,都在提醒我们安全无小事,挑战随时随地发生。随着扫描器、rootkit 和其他恶意工具的盛行,只需有一点点技术知识的人就可以很容易地开始入侵网站。虽然被入侵可能是不可避免的,但我们仍然应该采取一切预防措施,保护我们的应用程序和数据,降低被入侵的风险,减少被通报的次数。

ModSecurity 是一个确保应用程序安全的开源工具,全世界有超过一百万个网站在使用。它可以防止广泛的第7层攻击,如SQL注入(SQLi)、本地文件包含(LFI)和跨网站脚本(XSS),根据Akamai的数据,这些攻击在2017年第一季度共占已知第7层攻击的95%。

最新版本的 ModSecurity 3.0 在模块化架构方面有新的突破,可以在 NGINX 中原生运行。以前的版本只能在 Apache HTTP 服务器上运行。NGINX Plus 商用版本已经发布了 ModSecurity 3.0 动态模块,可以一键安装部署。但截至目前,NGINX Open Source 还没有预置的ModSecurity 动态模块。在这篇手册中,我们展示了如何创建一个 ModSecurity 3.0 的动态模块,以用于 NGINX Open Source。

在 NGINX 1.11.5 及以后的版本中,你可以编译单个动态模块,而不需要编译完整的 NGINX 二进制文件。在逐步介绍了编译过程后,我们将解释如何将 ModSecurity 动态模块加载到 NGINX 中,并运行一个基本测试以确保它在工作。

二、安装手册

1. 从官方仓库安装最新的 NGINX

参考官方手册配置 nginx: Linux packages 官方仓库地址并安装最新 Mainline 版本的 NGINX。

2. 准备编译环境

第一步是安装完成本教程中其余步骤所需的软件包。运行以下命令,由于各个发行版的Linux安装编译环境所需的包不一样,下面的命令都按照 Debian/Ubuntu 系统以及 RHEL/CentOS 分别列举。

Debian/Ubuntu

apt-get install -y apt-utils autoconf automake build-essential git libcurl4-openssl-dev libgeoip-dev liblmdb-dev libpcre++-dev libtool libxml2-dev libyajl-dev pkgconf wget zlib1g-dev

RHEL/CentOS

yum install gcc gcc-c++ make automake autoconf libtool pcre pcre-devel libxml2 libxml2-devel curl curl-devel httpd-devel

3. 下载并编译 ModSecurity 3.0 源代码

在安装了所需的前提包后,下一步是将 ModSecurity 编译为 NGINX 动态模块。在 ModSecurity 3.0 新的模块化架构中,libmodsecurity 是核心组件,包括所有的规则和功能。新的模块化架构中的第二个主要组件是一个连接器,它将 libmodsecurity 与它所运行的 Web 服务器连接起来。NGINX、Apache HTTP Server 和 IIS 都有单独的连接器适配 。我们将在下一节介绍 NGINX 连接器。

现在开始编译 libmodsecurity:

3.1. 克隆 GitHub 代码仓库

cd /usr/local/src
git clone https://github.com/SpiderLabs/ModSecurity
cd ModSecurity
git checkout v3.0.8

切换到最新的发布版本执行编译,本手册编写时最新的版本是v3.0.8。如果出现在服务器上无法下载源码,可以先在本地通过 git 命令下载,然后再 tar 命令打包上传至服务器,本地git可以通过 git config --global http.https://github.com.proxy http://加速器IP和端口 配置针对 GitHub 网站的加速访问。

3.2. 进入 ModSecurity 目录编译源代码

git submodule init
git submodule update
./build.sh
./configure
make -j 8
make install
cd ..

参照系统的实际处理能力,编译大约需要15分钟或更长时间。其中make -j 参数是指定使用几个核心执行编译,一般设置为CPU核心数。

注意:在编译过程中,下面这样的信息可以忽略。即使它们频繁出现,编译也会正常完成并生成一个.so库文件。

fatal: No names found, cannot describe anything.

4. 下载 NGINX 的 ModSecurity 连接器源代码并完成编译

将 NGINX 的 ModSecurity 连接器编译为NGINX的一个动态模块。

4.1. 克隆 GitHub 代码仓库

cd /usr/local/src
git clone https://github.com/SpiderLabs/ModSecurity-nginx.git
cd ModSecurity-nginx
git checkout v1.0.3

4.2. 确定将安装 ModSecurity 模块的主机上运行的 NGINX 版本

$ nginx -v
nginx version: nginx/1.23.3

4.3. 下载与已安装 NGINX 版本相对应的源代码用于编译动态模块(不影响通过 Yum 或 Apt 安装的 NGINX )

cd /usr/local/src
wget http://nginx.org/download/nginx-1.23.3.tar.gz
tar zxvf nginx-1.23.3.tar.gz

4.4. 编译动态模块并将其复制到模块的标准目录中。

cd nginx-1.23.3
./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
make modules
cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules
cd ..

5. 加载 NGINX ModSecurity 的动态模块

在 /etc/nginx/nginx.conf 配置文件中添加以下 load_module 配置。NGINX 将在启动时加载 ModSecurity 动态模块。

user root;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
 
# Load dynamic modules. See /usr/share/nginx/README.dynamic.
load_module modules/ngx_http_modsecurity_module.so;
 
include /usr/share/nginx/modules/*.conf;
include /etc/nginx/stream.conf;

6. 配置、启用并测试 ModSecurity

接下来是启用和测试 ModSecurity 。

6.1. 设置适当的 ModSecurity 配置文件。这里我们使用的是由 TrustWave Spiderlabs 提供的推荐的 ModSecurity 配置,TrustWave Spiderlabs 是 ModSecurity 的企业赞助商。

mkdir /etc/nginx/modsec
wget -P /etc/nginx/modsec/  https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended
mv /etc/nginx/modsec/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf

6.2. 为了保证 ModSecurity 能够找到 unicode.mapping 文件(在 GitHub repo 的顶层 ModSecurity 目录下),将其复制到 /etc/nginx/modsec。

cd /usr/local/src
cp ModSecurity/unicode.mapping /etc/nginx/modsec

6.3. 改变配置中的 SecRuleEngine 指令,从默认的“仅检测”模式改为主动丢弃恶意流量。

sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/nginx/modsec/modsecurity.conf

6.4. 配置一个或多个规则。在这篇博客中,首先我们创建一个简单的规则用于测试,实现的效果是如果URL的请求参数testparam中包含字符串test,则这个请求将会被丢弃,同时给客户端返回 403 错误。在 /etc/nginx/modsec/main.conf 中写入以下配置,可以实现这个测试效果。

# From https://github.com/SpiderLabs/ModSecurity/blob/master/
# modsecurity.conf-recommended
#
# Edit to set SecRuleEngine On
Include modsecurity.conf
    
# Basic test rule
SecRule ARGS:testparam "@contains test" "id:1234,deny,status:403"

后续会在进阶配置中我们将介绍如何使用免费的OWASP核心规则集

6.5. 在 NGINX 配置中添加 modsecurity 和 modsecurity_rules_file 指令,以启用 ModSecurity。

http {
       # ... 
       modsecurity on;
       modsecurity_rules_file /etc/nginx/modsec/main.conf;
}

6.6. 重启 NGINX。

systemctl restart nginx

6.7. 执行本地测试,返回 403 错误,日志文件 /var/log/modsec_audit.log 中会产生对应的日志记录。

$ curl localhost?testparam=test
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

7. ModSecurity 有用的默认配置



参数名

默认值

作用

SecRequestBodyLimit

13107200

限制上传文件时,单个请求最大 12.5 MB,用于减轻 DDoS 攻击

SecRequestBodyNoFilesLimit

131072

限制非上传文件时,单个请求最大 128 KB,用于减轻 DDoS 攻击

SecRequestBodyJsonDepthLimit

512

限制 JSON 请求最多嵌套 512 级



三、进阶配置

本章解释了如何 NGINX ModSecurity WAF 上启用和测试 Open Web Application Security Project Core Rule Set(OWASP CRS)规则集。OWASP CRS 包括可以检测许多类型的通用攻击的签名和模式。最新版本(CRS 3)包括重大改进,包括减少误报。OWASP CRS 能够防护的攻击类型:

  • SQL Injection (SQLi),SQL注入
  • Cross Site Scripting (XSS),跨站脚本攻击
  • Local File Inclusion (LFI),服务器本地文件暴露,泄漏敏感信息或导致XSS以及远程代码执行
  • Remote File Inclusion (RFI),攻击应用程序引用的远程文件源,从而攻击本应用程序
  • PHP Code Injection,PHP代码注入
  • Java Code Injection,Java代码注入
  • HTTPoxy
  • Shellshock
  • Unix/Windows Shell Injection,Shell注入
  • Session Fixation
  • Scripting/Scanner/Bot Detection,扫描检测
  • Metadata/Error Leakages

本章以前一章配置为基础,展示 CRS 如何保护前一章中已经配置了基本 WAF 规则的 Web 应用。在启用 CRS 之前,我们先运行一个扫描工具,生成攻击流量并报告它发现的漏洞。然后我们启用 CRS,观察它是如何阻止大多数恶意请求的,保护我们的应用程序免受常见攻击。

1. 运行 Nikto 扫描工具

我们首先向安装 NGINX ModSecurity WAF 的网络应用程序发送攻击流量。许多攻击者运行漏洞扫描器来识别目标网站或应用程序的安全漏洞。一旦他们了解到有哪些漏洞存在,他们就可以根据漏洞的情况发起有效的攻击。

我们使用 Nikto 扫描工具来生成恶意请求,包括探测是否存在已知的漏洞文件、XSS和其他类型的攻击。该工具还报告哪些请求通过了应用程序,揭示了应用程序的潜在漏洞。

运行以下命令以获得 Nikto 代码,并运行它。以下输出中显示存在324个漏洞。

$ git clone https://github.com/sullo/nikto
Cloning into 'nikto'...
$ cd nikto
$ perl program/nikto.pl -h localhost
- Nikto v2.1.6
...
+ 7531 requests: 0 error(s) and 324 item(s) reported on remote host
+ End Time:           2022-05-27 15:45:15 (GMT8) (273 seconds)

2. 解压缩 OWASP CRS

从 GitHub 下载最新的 OWASP CRS,并将规则提取到 /etc/nginx/modsec 位置。

cd /etc/nginx/modsec
wget https://github.com/coreruleset/coreruleset/archive/refs/tags/v3.3.2.tar.gz
tar xf coreruleset-3.3.2.tar.gz

3. 启用 OWASP CRS

参考以下命令行以 2 个 .example 文件为模版分别创建对应的配置文件。

cd /etc/nginx/modsec/coreruleset-3.3.2
cp -rp crs-setup.conf.example crs-setup.conf
cp -rp REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

4. 配置 OWASP CRS 规则集

在 NGINX ModSecurity WAF 配置文件 /etc/nginx/modsec/main.conf 添加 Include 指令,以读入 CRS 配置和规则,并注释掉文件中测试用 SecRule 指令。

# Edit to set SecRuleEngine On
Include modsecurity.conf

# include OWASP CRS
Include coreruleset-3.3.2/crs-setup.conf
Include coreruleset-3.3.2/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
Include coreruleset-3.3.2/rules/REQUEST-901-INITIALIZATION.conf
Include coreruleset-3.3.2/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
Include coreruleset-3.3.2/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
Include coreruleset-3.3.2/rules/REQUEST-912-DOS-PROTECTION.conf
Include coreruleset-3.3.2/rules/REQUEST-913-SCANNER-DETECTION.conf
Include coreruleset-3.3.2/rules/REQUEST-921-PROTOCOL-ATTACK.conf
Include coreruleset-3.3.2/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
Include coreruleset-3.3.2/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
Include coreruleset-3.3.2/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
Include coreruleset-3.3.2/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
Include coreruleset-3.3.2/rules/REQUEST-934-APPLICATION-ATTACK-NODEJS.conf
Include coreruleset-3.3.2/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
Include coreruleset-3.3.2/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
Include coreruleset-3.3.2/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
Include coreruleset-3.3.2/rules/REQUEST-944-APPLICATION-ATTACK-JAVA.conf

# Basic test rule
#SecRule ARGS:testparam "@contains test" "id:1234,deny,status:403"

微软 Azure 在自己的 WAF 应用中提供了使用 OWASP 规则的界面选项,各个规则的解释可以参考 Azure 云的帮助文档 Web Application Firewall CRS rule groups and rules,此文档会随着 CRS 更新而更新。本次选用规则借鉴了 Azure 云提供的可选规则。

5. 使新的配置生效

systemctl restart nginx

6. 使用 Nikto 扫描服务器,测试 CRS

在这一节中,我们将探讨 CRS 中的规则如何根据请求的特定特征来阻止 Nikto 的请求。我们的最终目标是要看到 CRS 阻止了 Nikto 的所有请求,所以 Nikto 无法发现任何漏洞,让攻击者无从下手。

禁用基于 User-Agent Header 的请求的阻断

CRS 通过检查 User-Agent 头来识别来自扫描器的请求,包括 Nikto。如下面的输出所示,CRS 预先配置为阻止 User-Agent 是 Nikto 的请求。

$ curl -H "User-Agent: Nikto" http://localhost/
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

(如果你想确切地看到 CRS 是如何根据 User-Agent 头特征来阻止请求的,你可以把在 NGINX 错误日志 /var/log/nginx/error.log 中发现的规则 ID 号码与 CRS 的扫描仪检测规则集 REQUEST-913-SCANNER-DETECTION.conf 中的规则联系起来。)

ModSecurity: Warning. Matched "Operator `Rx' with parameter `\xbc[^\xbe>]*[\xbe>]|<[^\xbe]*\xbe' against variable `ARGS:content' (Value: `\xe5\xb7\xa6\xe8\xbf\x9c\xe5\x90\x91\xe6\x82\xa8\xe6\x8f\x90\xe4\xba\xa4\xe4\xba\x86\xe3\x80\x8a\xe6 (165 characters omitted)' ) [file "/etc/nginx/modsec/coreruleset-3.3.2/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf"] [line "527"] [id "941310"] [rev ""] [msg "US-ASCII Malformed Encoding XSS Filter - Attack Detected"] [data "Matched Data: \xbc\xa0\xe9\x98\x85\xe5\xa4\x84\xe7\x90\x86\xe7\xad\xbe found within ARGS:content: \xe5\xb7\xa6\xe8\xbf\x9c\xe5\x90\x91\xe6\x82\xa8\xe6\x8f\x90\xe4\xba\xa4\xe4\xba\x86\xe3\x80\x8a\xe6\x96\x87\xe4\xbb\xb6\xe4\xbc\xa0\xe9\x98\x85\xe5\xa4\x84\xe7\x90\x86\xe7\xad\xbe\xe3\x80\x8b\xe7\x9d\xa3\xe5\x8a\x9e\xe9\x80\x9a\xe6\x8a\xa5\xe7\xac\xac5\xe6\x9c\x9f"] [severity "2"] [ver "OWASP_CRS/3.3.2"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-tomcat"] [tag "attack-xss"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/152/242"] [hostname "172.18.226.204"] [uri "/v/1.0/sendSms"] [unique_id "1653644038"] [ref "o31,14v108,67t:urlDecodeUni,t:lowercase,t:urlDecode,t:htmlEntityDecode,t:jsDecode"]

在当前的测试阶段,我们不希望阻止来自 Nikto 的所有请求,因为我们正在使用 Nikto 来检测我们应用程序中可能存在的漏洞。为了避免CRS 仅仅因为请求的 User-Agent 是 Nikto 而阻止请求,我们重新配置 Nikto,使其不在 User-Agent 中包括 Nikto 和相关值。接下来在program/nikto.conf中注释掉 USERAGENT 的当前设置,用所示的值代替它。

# USERAGENT=Mozilla/5.00 (Nikto/@VERSION) (Evasions:@EVASIONS) (Test:@TESTID)
USERAGENT=Mozilla/5.00

7. 消除易受攻击文件的假阳性报告

当我们针对网络应用程序重新运行 Nikto 时,我们看到只有 116 个 Nikto 的请求进入了应用服务器,而在没有启用 CRS 时,有 324 个。这表明,CRS 正在保护我们的应用程序免受 Nikto 的请求所暴露的大部分漏洞的影响。

$ perl program/nikto.pl -h localhost
...
+ 7531 requests: 0 error(s) and 116 item(s) reported on remote host

Nikto 的输出非常长,到目前为止,我们一直在截断它,只显示最后一行,其中报告了问题的数量。当我们更仔细地查看输出时,我们看到剩下的 116 个项目中有许多是指应用程序中的一个易受攻击的文件,如下:

$ perl program/nikto.pl -h localhost
...
+ /site.tar: Potentially interesting archive/cert file found.
...
+ 7531 requests: 0 error(s) and 116 item(s) reported on remote host

默认情况下应用程序配置为对每个请求都返回状态代码 200,但实际上这个请求并没有返回一个文件。Nikto 将这些 200 状态代码解释为它所请求的文件确实存在,这在我们的应用程序中是一个假阳性。

现在,我们要消除这些假阳性报告,以便我们能更好地看到可能存在的实际漏洞。通过在 program/nikto.conf 中添加 -sitefiles 来禁用这些请求,如下所示,在 @@DEFAULT 末尾增加 -sitefiles 配置:

# Default plug-in macros
# Remove plug-ins designed to be run standalone
@@EXTRAS=dictionary;siebel;embedded
@@DEFAULT=@@ALL;-@@EXTRAS;tests(report:500);-sitefiles

8. 阻止包含XSS内容的请求

当我们再次重新运行 Nikto 时,它只报告了 26 个问题。

$ perl program/nikto.pl -h localhost
- Nikto v2.1.6
...
+ 7435 requests: 0 error(s) and 26 item(s) reported on remote host

26 个问题中的大部分是因为 OWASP CRS 目前没有配置阻止包含 XSS 内容的请求 URL,例如:

<script>alert('Vulnerable')</script>

要阻止有 XSS 内容的请求,请编辑 CRS 的 XSS 应用攻击规则集(REQUEST-941-APPLICATION-ATTACK-XSS.conf)中的规则 941160和 941320,在这两个规则的变量列表的开头添加 REQUEST_URI,如下图所示:

SecRule REQUEST_URI|REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/ ...

使用开发分支的配置修复 REQUEST-941-APPLICATION-ATTACK-XSS.conf 中 941310 规则错误:

SecRule REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/|REQUEST_COOKIES_NAMES|ARGS_NAMES|ARGS|XML:/* "@rx \xbc[^\xbe>]*[\xbe>]|<[^\xbe]*\xbe" \
    "id:941310,\
    phase:2,\
    block,\
    capture,\
    t:none,t:lowercase,t:urlDecode,t:htmlEntityDecode,t:jsDecode,\
    msg:'US-ASCII Malformed Encoding XSS Filter - Attack Detected',\
    logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    tag:'application-multi',\
    tag:'language-multi',\
    tag:'platform-tomcat',\
    tag:'attack-xss',\
    tag:'paranoia-level/1',\
    tag:'OWASP_CRS',\
    tag:'capec/1000/152/242',\
    ver:'OWASP_CRS/3.4.0-dev',\
    severity:'CRITICAL',\
    chain"
    SecRule REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/|REQUEST_COOKIES_NAMES|ARGS_NAMES|ARGS|XML:/* "@rx (?:\xbc\s*/\s*[^\xbe>]*[\xbe>])|(?:<\s*/\s*[^\xbe]*\xbe)" \
        "t:none,t:lowercase,t:urlDecode,t:htmlEntityDecode,t:jsDecode,\
        ctl:auditLogParts=+E,\
        setvar:'tx.xss_score=+%{tx.critical_anomaly_score}',\
        setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'"

使新配置生效

systemctl reload nginx

当我们重新运行 Nikto 时,它只报告了 4 个问题,从报告的内容来看,属于假阳性问题,可以忽略。

$ perl program/nikto.pl -h localhost
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ /smg_Smxcfg30.exe?vcc=3560121183d3: This may be a Trend Micro Officescan 'backdoor'.
+ 7435 requests: 0 error(s) and 4 item(s) reported on remote host

9. 总结

我们使用 OWASP ModSecurity 核心规则集实现了保护 Web 应用免受普遍通用性攻击的目标,并学习到了如何调整 CRS 规则以阻止 Nikto 扫描工具产生的恶意请求。更多规则的应用请求,可以详细阅读每个规则的注释内容。