系统运维

保护网站免受攻击:OpenResty与开源WAF的结合实践

浅时光博客 · 7月18日 · 2024年 · 5.9k 次已读

一、实现场景


OpenResty是一个基于nginx的Web应用服务器,它支持Lua脚本语言和各种第三方模块,可以实现高性能、可扩展、易开发的Web应用程序。与此同时,OpenResty也可以通过结合WAF(Web应用防火墙)来保护网站安全。

  1. 通过OpenResty提供高性能的Web服务,并使用WAF对进出流量进行实时监控和分析,保护Web应用程序的安全。
  2. 使用OpenResty的Lua脚本语言和第三方模块,对WAF进行自定义配置和扩展,以适应不同的业务需求和安全策略。

二、WAF介绍


WAF(Web Application Firewall),也叫Web应用程序防火墙,是一种专门用于保护Web应用程序免受恶意攻击的安全解决方案。WAF可以在Web应用程序和客户端之间进行拦截和过滤,以检测并防止常见的Web攻击类型(如SQL注入、跨站脚本等)。

WAF可以通过多种方式进行检测和拦截,包括基于规则、行为、机器学习等技术。当WAF检测到异常流量或恶意攻击时,可以根据预设的规则或策略,执行相应的动作,如拦截、禁止访问、重定向等。

文章来源(Source):浅时光博客

WAF通常被部署在Web应用程序的前端,拦截和分析所有进出流量,从而提供更全面的安全保护。它可以与其他安全组件(如IDS、IPS等)结合使用,形成完整的Web安全防御体系。

WAF可以通过以下方式保护Web应用程序:

  1. 阻止基于漏洞的攻击,如SQL注入、跨站点脚本(XSS)、跨站点请求伪造(CSRF)等。
  2. 防范恶意爬虫、扫描器、爆破工具等自动化攻击工具。
  3. 保护Web应用程序的敏感信息,如身份证号、银行卡号等。
  4. 阻止DDoS攻击。

WAF可以部署在云上或本地,可以作为硬件设备或软件实现,也可以作为一种云服务提供。

三、WAF部署


开源WAF功能

  1. 支持IP白名单和黑名单功能,直接将黑名单的IP访问拒绝。
  2. 支持URL白名单,将不需要过滤的URL进行定义。
  3. 支持User-Agent的过滤,匹配自定义规则中的条目,然后进行处理(返回403)。
  4. 支持CC攻击防护,单个URL指定时间的访问次数,超过设定值,直接返回403。
  5. 支持Cookie过滤,匹配自定义规则中的条目,然后进行处理(返回403)。
  6. 支持URL过滤,匹配自定义规则中的条目,如果用户请求的URL包含这些,返回403。
  7. 支持URL参数过滤,原理同上。
  8. 支持日志记录,将所有拒绝的操作,记录到日志中去。
  9. 日志记录为JSON格式,便于日志分析,例如使用ELK进行攻击日志收集、存储、搜索和展示。

开源WAF部署

1、测试Lua

  • 因为我们是基于Lua脚本实现防CC的,那么就需要先测试OpenResty是否可以正常返回Lua
[root@dqzboy ~]# vim /usr/local/openresty/nginx/conf/conf.d/default.conf
    #在server配置中增加如下规则    
    location /hello {
        default_type text/html;
        content_by_lua_block {
            ngx.say("<p>hello, world</p>")
        }
    }
  • 检查语法是否正确,没问题则访问测试
[root@dqzboy ~]# openresty -t
[root@dqzboy ~]# openresty -s reload
  • 返回正常,那么我们就可以把上面添加的location规则配置删除了

2、安装部署

2.1:下载WAF


[root@dqzboy ~]# git clone https://github.com/unixhot/waf.git
[root@dqzboy ~]# cp -a ./waf/waf /usr/local/openresty/nginx/conf/

2.2:WAF结构


2.2.1:WAF内目录和文件说明


  • access.luainit.lualib.lub  waf功能实现lua代码
  • config.lua   配置文件
  • rule-config  防御规则文件存储目录

rule-config目录内文件说明:

  • args.rule          异常get请求参数策略文件
  • blackip.rule       IP黑名单策略文件
  • cookie.rule        Cookie策略文件
  • post.rule          异常post请求参数策略文件
  • url.rule           异常url策略文件
  • useragent.rule     异常useragent策略文件
  • whiteip.rule       IP白名单策略文件
  • whiteurl.rule      URL白名单策略文件

2.2.2:获取客户端真实IP方法


原代码获取客户端真实IP,如果经过多个代理节点传过来的X_Forwarded_For的IP值不止一个的时候会有问题

此功能函数定义的脚本位置:

/usr/local/openresty/nginx/conf/waf/lib.lua
  • 部分客户端访问经过多个代理节点之后,X_Forwarded_For获得的IP地址可能不止一个,我们只取第一个ip地址即为客户端真实IP地址
function get_client_ip()
    loacl CLIENT_IP = ngx.req.get_headers()["X_real_ip"]
    if CLIENT_IP == nil then
        if ngx.var.http_x_forwarded_for ~= nil then
        CLIENT_IP = string.match(ngx.var.http_x_forwarded_for, "%d+.%d+.%d+.%d+", 1);
        end
    end
    if CLIENT_IP == nil then
        CLIENT_IP  = ngx.var.remote_addr or '127.0.0.1'
    end
    if CLIENT_IP == nil then
        CLIENT_IP  = "unknown"
    end
    return CLIENT_IP
end

2.2.3:WAF主要配置文件说明


  • 配置文件config.lua 参数详细解释
--WAF config file,enable = "on",disable = "off"

--waf 开启与关闭
config_waf_enable = "on"
--log 日志目录;注意openresty运行用户需要有对应目录的权限,不然无法输出日志
config_log_dir = "/usr/local/openresty/nginx/logs/"
--WAF规则文件存储路径
config_rule_dir = "/usr/local/openresty/nginx/conf/waf/rule-config"
--是否开启 URL 白名单
config_white_url_check = "on"
--是否开启ip 白名单
config_white_ip_check = "on"
--是否开启ip 黑名单
config_black_ip_check = "on"
--启用/禁用URL过滤
config_url_check = "on"
--启用/禁用URL参数筛选
config_url_args_check = "on"
--启用/禁用用户代理筛选
config_user_agent_check = "on"
--是否拦截 cookie 攻击
config_cookie_check = "on"
--是否开启拦截 cc 攻击
config_cc_check = "on"
--设置cc攻击频率,单位为秒, 这里表示1分钟同一个IP只能请求同一个url地址20次,超过20次返回403;默认示例中为单个IP地址在60秒内访问同一个页面次数超过10次则认为是cc攻击,则自动禁止此IP地址访问此页面60秒,60秒后解封(封禁过程中此IP地址依然可以访问其它页面,如果同一个页面访问次数超过10次依然会被禁止)
config_cc_rate = "20/60" 
--是否拦截 post 攻击
config_post_check = "on"
--对于违反规则的请求则跳转到一个自定义html页面还是指定页面,值为 html 和 redirect
config_waf_output = "html"
--指定违反请求后跳转的指定html页面
config_waf_redirect_url = "https://www.dqzboy.com"
config_output_html=[[
--指定违反规则后跳转的自定义html页面,页面输出警告内容,可在中括号内自定义;注意修改默认配置文件里的内容
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>浅时光云WAF防火墙</title>
    <link rel="icon" href="https://www.dqzboy.com/images/logo.png" type="image/x-icon"/>
    <style type="text/css">
        /* 背景图片 */
        body {
            background: url(https://www.dqzboy.com/images/waf.jpg);
            background-size: cover;
        }
        /* 外边框 */
        .container {
            margin: 100px auto;
            width: 500px;
            height: 700px;
            border-radius: 10px;
            border: 3px solid #FFF;
            background-color: rgba(255,255,255,0.8);
            box-shadow: 0 0 20px #000;
            padding: 20px;
            text-align: center;
            font-family: Arial, sans-serif;
        }
        /* 标题 */
        .title {
            font-size: 30px;
            font-weight: bold;
            margin-bottom: 20px;
            color: #333;
            text-shadow: 2px 2px #FFF;
        }
        /* 提示信息 */
        .info {
            margin-bottom: 30px;
            font-size: 18px;
            font-weight: bold;
            color: #333;
            text-shadow: 1px 1px #FFF;
        }
        /* 按钮 */
        .button {
            display: inline-block;
            margin-top: 30px;
            padding: 10px 30px;
            border-radius: 5px;
            border: none;
            background-color: #F44336;
            color: #FFF;
            font-size: 20px;
            font-weight: bold;
            cursor: pointer;
            text-shadow: 1px 1px #000;
        }
        /* 按钮悬停效果 */
        .button:hover {
            background-color: #D32F2F;
        }
        /* 客户端信息 */
        .client-info {
            margin-top: 50px;
            font-size: 20px;
            font-weight: bold;
            color: #333;
            text-shadow: 1px 1px #FFF;
        }
        /* 客户端IP */
        .client-ip {
            margin-top: 20px;
            font-size: 16px;
            color: #333;
            text-shadow: 1px 1px #FFF;
        }
        /* 客户端地区 */
        .client-region {
            margin-top: 10px;
            font-size: 16px;
            color: #333;
            text-shadow: 1px 1px #FFF;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="title">浅时光博客WAF防火墙</div>
        <div class="info">
            <p>抱歉,您的访问被拦截。</p>
            <p>我们检测到您的请求可能存在安全威胁。</p>
        </div>
        <button class="button" onclick="location.href='https://www.dqzboy.com'">返回主页</button>
        <div class="ip-container">
            <h2>请求信息</h2>
            <p>请求IP地址: <span id="ip-address">正在获取IP地址...</span></p>
            <p>请求地理位置: <span id="ip-location">正在获取地理位置...</span></p>
        </div>
        <script>
            var xhr = new XMLHttpRequest();
            xhr.open("GET", "https://api.ipify.org?format=json", true);
            xhr.onload = function () {
                if (xhr.status === 200) {
                    var data = JSON.parse(xhr.responseText);
                    var ip = data.ip;
                    document.getElementById("ip-address").innerHTML = "IP: " + ip;

                    // Get client location using IP
                    var xhr2 = new XMLHttpRequest();
                    xhr2.open("GET", "https://ipapi.co/" + ip + "/json/", true);
                    xhr2.onload = function () {
                        if (xhr2.status === 200) {
                            var locationData = JSON.parse(xhr2.responseText);
                            var region = locationData.region;
                            var city = locationData.city;
                            var country = locationData.country_name;
                            document.getElementById("ip-location").innerHTML = city + ", " + region + ", " + country;
                        } else {
                            console.error(xhr2.statusText);
                        }
                    };
                    xhr2.onerror = function () {
                        console.error(xhr2.statusText);
                    };
                    xhr2.send();
                } else {
                    console.error(xhr.statusText);
                }
            };
            xhr.onerror = function () {
                console.error(xhr.statusText);
            };
            xhr.send();
        </script>
        <br>
        <br>
        <br>
        <br>
        <br>
        <br>
        <div>
            <p> 浅时光博客 | 精彩程序人生</p>
        </div>
    </div>
</body>
</html>
]]

2.3:调用Lua


  • nginx.confhttp 段添加引入lua脚本文章来源(Source):https://dqzboy.com配置,同时WAF日志默认存放在/tmp/日期_waf.log
[root@dqzboy ~]# vim /usr/local/openresty/nginx/conf/nginx.conf
    lua_shared_dict limit 50m;
    lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua";
    init_by_lua_file "/usr/local/openresty/nginx/conf/waf/init.lua";
    access_by_lua_file "/usr/local/openresty/nginx/conf/waf/access.lua";
  • 配置软链接,指向lua库文件
[root@dqzboy ~]# ln -s /usr/local/openresty/lualib/resty/ /usr/local/openresty/nginx/conf/waf/resty
  • 检查nginx配置,没有问题则重载nginx服务
[root@dqzboy ~]# /usr/local/openresty/nginx/sbin/nginx -t
[root@dqzboy ~]# /usr/local/openresty/nginx/sbin/nginx -s reload

2.4:测试验证


  • 部署完毕可以尝试url规则文件里面定义的拦截URl路径进行测试。
  • 例如:https://www.xxxx.com/.inc

四、报错总结


错误提示:

[warn] 4042011#4042011: *961242 [lua] _G write guard:12: __newindex(): writing a global Lua variable ('CLIENT_IP') which may lead to race conditions between concurrent requests, so prefer the use of 'local' variables stack traceback:

解决方法:

  • 修改lib.lua 代码, CLIENT_IP 加个local 修饰
function get_client_ip()
    -- 添加local 解决CLIENT_IP变量报错问题
   localCLIENT_IP = ngx.req.get_headers()["X_real_ip"]
    if CLIENT_IP == nil then
        if ngx.var.http_x_forwarded_for ~= nil then
        CLIENT_IP = string.match(ngx.var.http_x_forwarded_for, "%d+.%d+.%d+.%d+", 1);
        end
    end
    if CLIENT_IP == nil then
        CLIENT_IP  = ngx.var.remote_addr or '127.0.0.1'
    end
    if CLIENT_IP == nil then
        CLIENT_IP  = "unknown"
    end
    return CLIENT_IP
end

本文作者:浅时光博客
原文链接:https://www.dqzboy.com/13362.html
版权声明:知识共享署名-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)协议进行许可,转载时请以>超链接形式标明文章原始出处和作者信息
免责声明:本站内容仅供个人学习与研究,严禁用于商业或非法目的。请在下载后24小时内删除相应内容。继续浏览或下载即表明您接受上述条件,任何后果由用户自行承担。

0 条回应

必须 注册 为本站用户, 登录 后才可以发表评论!