WAF开发之CC攻击防护
2021-12-15 17:31:22 Author: www.freebuf.com(查看原文) 阅读量:17 收藏

CC攻击

CC攻击是一种洪水攻击(DDOS),主要攻击对象是WEB接口,通过大量请求WEB系统的某个或者某些接口,导致WEB服务崩溃不可用。

CC攻击防护

CC攻击既然是针对于WEB接口,那么就需要对源流量进行识别判断,确定源流量无害才可以让它访问WEB接口。

那么我们可以使用lua脚本写一个CC攻击防护功能,目前业内WAF对CC攻击的防护逻辑是人机识别、JS挑战

人机识别:当来源IP或者用户频率在单位时间内超过一定次数的时候,就会弹出图形验证码进行人机识别。其中图形验证码包括字符串验证和滑块验证。

JS挑战:让客户端执行一段JS脚本,比如让它计算一个结果,再将这个结果与后端结果进行校验。(类似于挖矿病毒,客户端在那几秒内计算结果会大量使用CPU,如果是攻击者,等于自己打自己)

接下来,分别对这两个模式进行LUA代码开发。

JS挑战

首先,先定义变量

local uri = ngx.var.uri  --获取WAF检测路径
    local request_uri = ngx.var.request_uri --获取业务URI
    local ip_addr = request.request['REMOTE_ADDR']()  #获取客户端IP
    local m1 = request.request['ARGS_GET']()['md5'] or ""  #获取MD5值

接着需要进入判断环节:

  • 判断来源IP是否存在黑名单IP,如果没有开始对来源IP进行计数。
  • 判断URI是否等于这个路径/waf/45aab42d-80a0-4711-9860-04dd435124ea和校验传进来的MD5值,如果没有问题,就直接清除black_cc_attack_ip变量里面黑名单IP,最后返回校验成功信息。
if ngx.shared.black_cc_attack_ip:get(attack_type .. ip_addr) == nil then
        ngx.shared.black_cc_attack_ip:set(attack_type .. ip_addr, 0)
    end

    if uri == "/waf/45aab42d-80a0-4711-9860-04dd435124ea" and m1 == ngx.shared.black_cc_attack_ip:get(ip_addr) then
        ngx.shared.black_cc_attack_ip:delete(attack_type .. ip_addr)
        ngx.shared.black_cc_attack_ip:delete(attck_statistics_type .. ip_addr)
        ngx.shared.black_cc_attack_ip:delete(ip_addr)

        ngx.say(cjson.encode({ msg = "校验成功" }))

在进入最后校验环境,需要先生成一个MD5存储到变量内进行后续校验。

  • 定义一个字符串数组
  • 循环随机生成长度为5的字符串
  • 对这个字符串进行MD5加密,并且存储到内存里面。
local t = {
            "7", "q", "t", "p", "0", "d", "o", "u", "v", "t", "b", "f", "q", "n", "2", "f", "c", "c", "c", "n", "a", "j", "$", "#", "@", "!", "~"
        }
        local s = ""
        for i = 1, 5, 1 do
            s = s .. t[math.random(#t)]
        end

        local md5_str = ngx.md5(s)
        ngx.shared.black_cc_attack_ip:set(ip_addr, md5_str)

JS挑战核心处理代码,这里面关键是HTML渲染!!!

  • 定义一个变量存储html内容
  • 加载MD5和jQuery JS脚本
  • 定义randomString函数,生成随机MD5
  • 来到while流程,客户端需要生成的一个MD5要与后端返回的MD5进行校验(写到这里发现这里可以加多一个明文值做两次校验,防止被人修改JS绕过)
  • 客户端生成正确的MD5的时间随着字符串的长度而变长,测试5个字符串加密挑战时间为1秒到5秒。
local default_attack_ip_html = [[

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>
    浏览器安全检测中。。。。
</h1>
<script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="text/javascript">

    function randomString(len) {
        var $chars = '7qtp0douvtbfqn2fcccnaj$#@!~';
        var maxPos = $chars.length;
        var pwd = '';
        for (i = 0; i < len; i++) {
            pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
        }
        return pwd;
    }

    // var date1 = new Date();  //开始时间
    var randstr_a = ']] .. md5_str .. [[';

    while (true) {
        var randstr_b = md5(randomString(5));

        if (randstr_a == randstr_b) {
            $.ajax({

                type: 'GET',
                url: '/waf/45aab42d-80a0-4711-9860-04dd435124ea?md5='+randstr_b,

                success: function (data) {
                    location.href = "]] .. request_uri .. [[";
                }
            });
            break;

        }

    }
    //
    // var date2 = new Date();
    // var date3 = date2.getTime() - date1.getTime();
    // console.log(date3);

</script>

</body>
</html>


]]
        if request.content_type ~= nil then
            ngx.header.content_type = request.content_type
        end

        ngx.print(default_attack_ip_html)

完整代码

local function standard_cc_check(attack_type, attck_statistics_type)
    local uri = ngx.var.uri
    local request_uri = ngx.var.request_uri

    local ip_addr = request.request['REMOTE_ADDR']()
    local m1 = request.request['ARGS_GET']()['md5'] or ""

    if ngx.shared.black_cc_attack_ip:get(attack_type .. ip_addr) == nil then
        ngx.shared.black_cc_attack_ip:set(attack_type .. ip_addr, 0)

    end

    if uri == "/waf/45aab42d-80a0-4711-9860-04dd435124ea" and m1 == ngx.shared.black_cc_attack_ip:get(ip_addr) then
        ngx.shared.black_cc_attack_ip:delete(attack_type .. ip_addr)
        ngx.shared.black_cc_attack_ip:delete(attck_statistics_type .. ip_addr)
        ngx.shared.black_cc_attack_ip:delete(ip_addr)

        ngx.say(cjson.encode({ msg = "校验成功" }))
    else

        local t = {
            "7", "q", "t", "p", "0", "d", "o", "u", "v", "t", "b", "f", "q", "n", "2", "f", "c", "c", "c", "n", "a", "j", "$", "#", "@", "!", "~"
        }
        local s = ""
        for i = 1, 5, 1 do
            s = s .. t[math.random(#t)]
        end

        local md5_str = ngx.md5(s)
        ngx.shared.black_cc_attack_ip:set(ip_addr, md5_str)

        local default_attack_ip_html = [[

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>
    浏览器安全检测中。。。。
</h1>
<script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="text/javascript">

    function randomString(len) {
        var $chars = '7qtp0douvtbfqn2fcccnaj$#@!~';
        var maxPos = $chars.length;
        var pwd = '';
        for (i = 0; i < len; i++) {
            pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
        }
        return pwd;
    }

    // var date1 = new Date();  //开始时间
    var randstr_a = ']] .. md5_str .. [[';

    while (true) {
        var randstr_b = md5(randomString(5));

        if (randstr_a == randstr_b) {
            $.ajax({

                type: 'GET',
                url: '/waf/45aab42d-80a0-4711-9860-04dd435124ea?md5='+randstr_b,

                success: function (data) {
                    location.href = "]] .. request_uri .. [[";
                }
            });
            break;

        }

    }
    //
    // var date2 = new Date();
    // var date3 = date2.getTime() - date1.getTime();
    // console.log(date3);

</script>

</body>
</html>


]]
        if request.content_type ~= nil then
            ngx.header.content_type = request.content_type
        end

        ngx.print(default_attack_ip_html)

    end


end

人机识别

首先先定义变量,跟上面一样

local uri = ngx.var.uri
    local request_uri = ngx.var.request_uri

    local ip_addr = request.request['REMOTE_ADDR']()

    if ngx.shared.black_cc_attack_ip:get(attack_type .. ip_addr) == nil then
        ngx.shared.black_cc_attack_ip:set(attack_type .. ip_addr, 0)
    end

校验这一步也跟上面原理差不多

if code == ngx.shared.cc_attack_code:get(ip_addr) then
            --清除黑名单状态
            ngx.shared.black_cc_attack_ip:delete(attack_type .. ip_addr)
            ngx.shared.black_cc_attack_ip:delete(attck_statistics_type .. ip_addr)
            ngx.shared.cc_attack_code:delete(ip_addr)
            ngx.say(cjson.encode({ msg = "校验成功", code = 200 }))
        else
            ngx.say(cjson.encode({ msg = "验证码有效期60秒,失效请刷新!!", code = 0 }))
        end

最后生成图形验证码核心还是HTML渲染!!!

local t = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' }
        local code = ""
        for i = 1, 4, 1 do
            code = code .. t[math.random(#t)]
        end
        ngx.shared.cc_attack_code:set(ip_addr, code, 60)  --图形验证码有效期是60秒
        local default_attack_ip_html = [[

<!DOCTYPE html>
<html>
<!-- head -->
<head>
  <meta charset="utf-8">
  <title>CC防护验证</title>
  <meta name="renderer" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

  <style>
    body{margin: 10px;}
    .demo-carousel{height: 200px; line-height: 200px; text-align: center;}
    .code {
        width: 400px;
        margin: 0 auto;
    }
    .input-val {
        width: 295px;
        background: #ffffff;
        height: 2.8rem;
        padding: 0 2%;
        border-radius: 5px;
        border: none;
        border: 1px solid rgba(0,0,0,.2);
        font-size: 1.0625rem;
    }
    #canvas {
        float: right;
        display: inline-block;
        border: 1px solid #ccc;
        border-radius: 5px;
        cursor: pointer;
    }
    .btn {
        width: 100px;
        height: 40px;
        background: #f1f1f1;
        border: 1px solid #ccc;
        border-radius: 5px;
        margin: 20px auto 0;
        display: block;
        font-size: 1.2rem;
        color: #e22e1c;
        cursor: pointer;
    }
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
  </style>
</head>


    <body cz-shortcut-listen="true" class="layui-layout-body">
        <div class="layui-layer-move">

        <div class="code">
            <input type="text" value="" placeholder="请输入验证码(不区分大小写)" class="input-val">
            <canvas id="canvas" width="100" height="43"></canvas>
            <button class="btn">提交</button>
        </div>

        </div>
    </body>

   <script type="text/javascript"  src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
 <script type="text/javascript"  src="https://www.layuicdn.com/layui/layui.js"></script>

<script>

    $(function(){

        var show_num = ']] .. code .. [[';
        draw(show_num);

        $("#canvas").on('click',function(){
            location.reload();
        })
        $(".btn").on('click',function(){
            var val = $(".input-val").val().toLowerCase();
            var num = show_num;
            if(val==''){
                alert('请输入验证码!');
            }else if(val == num){

                $.ajax({
                        type: 'POST',
                        url: "/waf/45aab42d-80a0-4711-9860-04dd435124ea",
                        async: true,
                        data: {
                            code: val
                        },
                        success: function (rs) {
                        var json_rs =eval("("+rs+")");

                            if (json_rs.code == 0) {
                            alert(json_rs.msg);

                            location.reload();

                            } else if (json_rs.code == 200) {
                            alert(json_rs.msg);
                             location.href = "]] .. request_uri .. [[";
                            }
                        }

                    });

            }else{
                alert('验证码错误!请重新输入!');
                location.reload();
            }
        })
    })

    function draw(show_num) {
        var canvas_width=$('#canvas').width();
        var canvas_height=$('#canvas').height();
        var canvas = document.getElementById("canvas");//获取到canvas的对象,演员
        var context = canvas.getContext("2d");//获取到canvas画图的环境,演员表演的舞台
        canvas.width = canvas_width;
        canvas.height = canvas_height;

        for (var i = 0; i <= 3; i++) {
            var deg = Math.random() * 30 * Math.PI / 180;//产生0~30之间的随机弧度

            var x = 10 + i * 20;//文字在canvas上的x坐标
            var y = 20 + Math.random() * 8;//文字在canvas上的y坐标
            context.font = "bold 23px 微软雅黑";
            var txt = show_num[i].toLowerCase()
            context.translate(x, y);
            context.rotate(deg);

            context.fillStyle = randomColor();
            context.fillText(txt, 0, 0);

            context.rotate(-deg);
            context.translate(-x, -y);
        }
        for (var i = 0; i <= 5; i++) { //验证码上显示线条
            context.strokeStyle = randomColor();
            context.beginPath();
            context.moveTo(Math.random() * canvas_width, Math.random() * canvas_height);
            context.lineTo(Math.random() * canvas_width, Math.random() * canvas_height);
            context.stroke();
        }
        for (var i = 0; i <= 30; i++) { //验证码上显示小点
            context.strokeStyle = randomColor();
            context.beginPath();
            var x = Math.random() * canvas_width;
            var y = Math.random() * canvas_height;
            context.moveTo(x, y);
            context.lineTo(x + 1, y + 1);
            context.stroke();
        }
    }

    function randomColor() {//得到随机的颜色值
        var r = Math.floor(Math.random() * 256);
        var g = Math.floor(Math.random() * 256);
        var b = Math.floor(Math.random() * 256);
        return "rgb(" + r + "," + g + "," + b + ")";
    }
</script>
</html>
]]
        ngx.header.content_type = "text/html;charset=utf-8"
        ngx.say(default_attack_ip_html)

    

完整代码

local function images_cc_check(attack_type, attck_statistics_type)
    local uri = ngx.var.uri
    local request_uri = ngx.var.request_uri

    local ip_addr = request.request['REMOTE_ADDR']()

    if ngx.shared.black_cc_attack_ip:get(attack_type .. ip_addr) == nil then
        ngx.shared.black_cc_attack_ip:set(attack_type .. ip_addr, 0)
    end

    if uri == "/waf/45aab42d-80a0-4711-9860-04dd435124ea" then
        ngx.req.read_body()
        local body = request.request['ARGS_POST']()
        local code = body['code']

        if code == ngx.shared.cc_attack_code:get(ip_addr) then
            --清除黑名单状态
            ngx.shared.black_cc_attack_ip:delete(attack_type .. ip_addr)
            ngx.shared.black_cc_attack_ip:delete(attck_statistics_type .. ip_addr)
            ngx.shared.cc_attack_code:delete(ip_addr)
            ngx.say(cjson.encode({ msg = "校验成功", code = 200 }))
        else
            ngx.say(cjson.encode({ msg = "验证码有效期60秒,失效请刷新!!", code = 0 }))
        end


    else
        local t = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' }
        local code = ""
        for i = 1, 4, 1 do
            code = code .. t[math.random(#t)]
        end
        ngx.shared.cc_attack_code:set(ip_addr, code, 60)  --图形验证码有效期是60秒
        local default_attack_ip_html = [[

<!DOCTYPE html>
<html>
<!-- head -->
<head>
  <meta charset="utf-8">
  <title>CC防护验证</title>
  <meta name="renderer" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

  <style>
    body{margin: 10px;}
    .demo-carousel{height: 200px; line-height: 200px; text-align: center;}
    .code {
        width: 400px;
        margin: 0 auto;
    }
    .input-val {
        width: 295px;
        background: #ffffff;
        height: 2.8rem;
        padding: 0 2%;
        border-radius: 5px;
        border: none;
        border: 1px solid rgba(0,0,0,.2);
        font-size: 1.0625rem;
    }
    #canvas {
        float: right;
        display: inline-block;
        border: 1px solid #ccc;
        border-radius: 5px;
        cursor: pointer;
    }
    .btn {
        width: 100px;
        height: 40px;
        background: #f1f1f1;
        border: 1px solid #ccc;
        border-radius: 5px;
        margin: 20px auto 0;
        display: block;
        font-size: 1.2rem;
        color: #e22e1c;
        cursor: pointer;
    }
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
  </style>
</head>


    <body cz-shortcut-listen="true" class="layui-layout-body">
        <div class="layui-layer-move">

        <div class="code">
            <input type="text" value="" placeholder="请输入验证码(不区分大小写)" class="input-val">
            <canvas id="canvas" width="100" height="43"></canvas>
            <button class="btn">提交</button>
        </div>

        </div>
    </body>

   <script type="text/javascript"  src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
 <script type="text/javascript"  src="https://www.layuicdn.com/layui/layui.js"></script>

<script>

    $(function(){

        var show_num = ']] .. code .. [[';
        draw(show_num);

        $("#canvas").on('click',function(){
            location.reload();
        })
        $(".btn").on('click',function(){
            var val = $(".input-val").val().toLowerCase();
            var num = show_num;
            if(val==''){
                alert('请输入验证码!');
            }else if(val == num){

                $.ajax({
                        type: 'POST',
                        url: "/waf/45aab42d-80a0-4711-9860-04dd435124ea",
                        async: true,
                        data: {
                            code: val
                        },
                        success: function (rs) {
                        var json_rs =eval("("+rs+")");

                            if (json_rs.code == 0) {
                            alert(json_rs.msg);

                            location.reload();

                            } else if (json_rs.code == 200) {
                            alert(json_rs.msg);
                             location.href = "]] .. request_uri .. [[";
                            }
                        }

                    });

            }else{
                alert('验证码错误!请重新输入!');
                location.reload();
            }
        })
    })

    function draw(show_num) {
        var canvas_width=$('#canvas').width();
        var canvas_height=$('#canvas').height();
        var canvas = document.getElementById("canvas");//获取到canvas的对象,演员
        var context = canvas.getContext("2d");//获取到canvas画图的环境,演员表演的舞台
        canvas.width = canvas_width;
        canvas.height = canvas_height;

        for (var i = 0; i <= 3; i++) {
            var deg = Math.random() * 30 * Math.PI / 180;//产生0~30之间的随机弧度

            var x = 10 + i * 20;//文字在canvas上的x坐标
            var y = 20 + Math.random() * 8;//文字在canvas上的y坐标
            context.font = "bold 23px 微软雅黑";
            var txt = show_num[i].toLowerCase()
            context.translate(x, y);
            context.rotate(deg);

            context.fillStyle = randomColor();
            context.fillText(txt, 0, 0);

            context.rotate(-deg);
            context.translate(-x, -y);
        }
        for (var i = 0; i <= 5; i++) { //验证码上显示线条
            context.strokeStyle = randomColor();
            context.beginPath();
            context.moveTo(Math.random() * canvas_width, Math.random() * canvas_height);
            context.lineTo(Math.random() * canvas_width, Math.random() * canvas_height);
            context.stroke();
        }
        for (var i = 0; i <= 30; i++) { //验证码上显示小点
            context.strokeStyle = randomColor();
            context.beginPath();
            var x = Math.random() * canvas_width;
            var y = Math.random() * canvas_height;
            context.moveTo(x, y);
            context.lineTo(x + 1, y + 1);
            context.stroke();
        }
    }

    function randomColor() {//得到随机的颜色值
        var r = Math.floor(Math.random() * 256);
        var g = Math.floor(Math.random() * 256);
        var b = Math.floor(Math.random() * 256);
        return "rgb(" + r + "," + g + "," + b + ")";
    }
</script>
</html>
]]
        ngx.header.content_type = "text/html;charset=utf-8"
        ngx.say(default_attack_ip_html)

    end

end

最终效果如下

1639502124_61b8d12c12c1d0050c185.png!small?1639502123468

总结

也没啥好总结的,有啥问题评论聊聊。


文章来源: https://www.freebuf.com/articles/web/311431.html
如有侵权请联系:admin#unsafe.sh