CC攻击是一种洪水攻击(DDOS),主要攻击对象是WEB接口,通过大量请求WEB系统的某个或者某些接口,导致WEB服务崩溃不可用。
CC攻击既然是针对于WEB接口,那么就需要对源流量进行识别判断,确定源流量无害才可以让它访问WEB接口。
那么我们可以使用lua脚本写一个CC攻击防护功能,目前业内WAF对CC攻击的防护逻辑是人机识别、JS挑战。
人机识别:当来源IP或者用户频率在单位时间内超过一定次数的时候,就会弹出图形验证码进行人机识别。其中图形验证码包括字符串验证和滑块验证。
JS挑战:让客户端执行一段JS脚本,比如让它计算一个结果,再将这个结果与后端结果进行校验。(类似于挖矿病毒,客户端在那几秒内计算结果会大量使用CPU,如果是攻击者,等于自己打自己)
接下来,分别对这两个模式进行LUA代码开发。
首先,先定义变量
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值
接着需要进入判断环节:
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存储到变量内进行后续校验。
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渲染!!!
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
最终效果如下
也没啥好总结的,有啥问题评论聊聊。