- 引言
- 天降飞锅
- 攻防阶段1:封IP
- 攻防阶段2:关入口
- 攻防阶段3:调整验证方式
- 攻防阶段4:升级验证方式
引言最近处理了一起针对开源社区的攻击行为,对方疑似动用黑产势力,连续几天发动境内外的肉鸡IP发起大量注册用户请求,当然了,并不是真正的要注册用户,而是利用注册用户的动作,把短信验证码额度刷爆,最疯狂的那天刷了一万多条验证短信,真是丧心病狂。
面对这些坏蛋,我们当然不能坐以待毙,经过几次攻防交手,暂时是我们取得小胜。
本文分享这次的攻防经过,希望对其他使用Discuz系统的朋友能有所帮助。
天降飞锅某天,突然收到短信运营商告警,在一天内被刷掉一万多条短信验证码,这显然有问题,如果这些都是真实注册请求的话,那我肯定超级开心,可惜并不是。
攻防阶段1:封IP经过对访问日志的分析,发现短时间内有大量的境外IP请求用户注册接口,很快就把每天的短信验证码额度耗尽,导致正常的用户注册和登录请求无法使用。
对于这种情况,第一时间想到的是封禁这些IP,把它们加到路由黑洞(/sbin/ip route add blackhole $IP)中,这样做比用IPTABLE加防火墙规则效率更高,对服务器的性能损耗更小,而且不会给攻击者回包,反过来影响其效率。
不过,这些专业的黑产势力,显然是有充足的肉鸡资源,直接封IP的做法效果有限,还是无法阻止它们的攻击。
攻防阶段2:关入口黑产势力实在太猖狂,除了封IP外,暂时还没找到更好的办法,只能先避其锋芒,我惹不起还是躲得起的。因此决定暂时先关闭注册入口,以及短信验证码方式登录,只保留密码登录功能。
在Discuz管理后台关闭注册入口,如下图所示:

调整完后,黑产的请求量大概下降了一半,不过这招相当于是杀敌一千,自损八百,用户的有些功能受限了,不是长久之计。
攻防阶段3:调整验证方式在敌人的攻势减弱后,就有更多时间思考和尝试其他各种御敌之策了。
相对最优的解决办法是修改注册和登录方式,只允许通过微信扫码以及gitee/github等SSO单点登录方式,不过这需要额外功能开发,也就是要另外付费,先作为备选方案吧,你懂得的。
在管理后台反复查看后,就试着修改验证方式,把原来的的“英文图片验证码”修改为“位图验证码”,肉眼看起来识别难度高一丢丢,不过事实证明,对于黑产来说,这不是事,应该是有方法可以直接破解的,因为它们的请求量并没明显下降。
在Discuz管理后台修改验证码设置:

修改前后验证码图片对比:
攻防阶段4:升级验证方式再次研究Discuz系统管理后台,发现它的验证方式,除了验证码,还支持提问时互动验证,默认支持100以内的加减法问题交互验证。
在管理后台启用该功能:

启用后效果如下图所示:

出人意料的是,启用该功能后,防护效果非常好,几乎所有的恶意请求都失效了,虽然还能发起注册接口请求,但已经无法正确识别互动问题验证码,也就没办法再把验证短信额度给刷爆了。完美!
值得表扬的是,该功能还支持自定义互动问题,这就给了我们极大发挥空间,我干脆把部分GreatSQL GCP认证考试题作为互动问题加进来。这样一来,不但可以防范黑势力刷接口,还可以让正常的社区用户顺便当做GCP考试练习,一举多得。
美中不足的时,这个功能只支持针对 新用户注册、发帖、修改密码这三个动作生效,不支持 用户登录(尤其是短信登录)、忘记密码这两个动作,还不能全面防住,还需要进一步想办法。

经过一番艰难的摸索测试,最终发现只需要对Discuz源码中的模板文件做非常小的修改即可实现。
1、修改模板文件 common/seccheck.htm,删除原文件中第5、8行的条件判断
1 {eval
2 $sechash = !isset($sechash) ? 'S'.($_G['inajax'] ? 'A'.random(3) : '').$_G['sid'] : $sechash.random(3);
3 $sectpl = str_replace("'", "\'", $sectpl);
4 }
5 <!--{if $secqaacheck}-->
6 <span id="secqaa_q$sechash"></span>
7 <script type="text/javascript" reload="1">updatesecqaa('q$sechash', '$sectpl', '{$_G[basescript]}::{CURMODULE}');</script>
8 <!--{/if}-->
9 <!--{if $seccodecheck}-->
10 <span id="seccode_c$sechash"></span>
11 <script type="text/javascript" reload="1">updateseccode('c$sechash', '$sectpl', '{$_G[basescript]}::{CURMODULE}');</script>
12 <!--{/if}-->
也就是,将上述原文件修改为
1 {eval
2 $sechash = !isset($sechash) ? 'S'.($_G['inajax'] ? 'A'.random(3) : '').$_G['sid'] : $sechash.random(3);
3 $sectpl = str_replace("'", "\'", $sectpl);
4 }
6 <span id="secqaa_q$sechash"></span>
7 <script type="text/javascript" reload="1">updatesecqaa('q$sechash', '$sectpl', '{$_G[basescript]}::{CURMODULE}');</script>
9 <!--{if $seccodecheck}-->
10 <span id="seccode_c$sechash"></span>
11 <script type="text/javascript" reload="1">updateseccode('c$sechash', '$sectpl', '{$_G[basescript]}::{CURMODULE}');</script>
12 <!--{/if}-->
上述改动的作用是使得 用户登录功能也能同时启用两种验证方式。

2、修改模板文件 member/login.htm,在第140行附近插入下面的代码(这里直接展示git diff的结果)
--- a/member/login.htm
+++ b/member/login.htm
@@ -137,6 +137,11 @@
<div class="layui-form-item">
<input type="text" name="phone" lay-verify="required|phone" autocomplete="off" placeholder="手机号" lay-reqtext="请填写手机号" class="layui-input phone">
</div>
+ <!--{if $secqaacheck || $seccodecheck}-->
+ <!--{block sectpl}--><div class="layui-form-item secode"><i class="layui-hide"><sec>:</i><sec><i class="img_box"><sec></i></div><!--{/block}-->
+ <!--{subtemplate common/seccheck}-->
+ <!--{/if}-->
+ <div class="layui-form-item">^M
<div class="layui-form-item">
上述改动的作用是使得 忘记密码功能也能同时启用两种验证方式。

至此,用户注册、用户登录、忘记密码 等多处需要用到短信验证码的入口,均已同时启用两种验证方式。
问题暂时得以解决,接下来要继续关注黑势力还有什么新的小动作了。
以上,全文完。
如果对你有帮助的话,还请帮忙动动可爱的小手点赞、转发。