对付短信发送攻击

简介

自从某公司使用短信验证码验证用户真实性以来,短信便逐渐成了公司业务的标配。现在几乎每家公司的服务都包含了短信发送这一功能。而用户请求短信时,一般还未注册,所以这个接口是匿名接口(不需要登录)。

于是,坏蛋们就开始捣蛋了。他们通过对软件抓包,得到用户的请求消息,然后模拟用户对服务器疯狂的发送这一请求,消耗公司的短信资源,骚扰无辜手机用户,甚至造成短信通道堵塞,无法发出正常消息。搞不好还被那些被骚扰者的投诉,封掉公司的短信通道,或者被各手机厂商识别为垃圾短信从而失去营销效果,甚至影响正常业务。

今天,我就遇到这么个捣蛋鬼。他的手里有大约上千台肉鸡(科普一下:“肉鸡”是指那些可以被黑客操控的无辜者的电脑,这些电脑用户并不知情。之所以可以被操控,可能是因为安装了有木马程序的软件,或者系统存在漏洞被攻破),于是模拟消息从全国铺天盖地而来,服务器日志疯狂刷屏。于是,我们立刻着手处理这件事情。

解决策略

我们想到了两种策略:

  1. 使用iptables封IP
  2. 使用nginx lua + redis

两种方案各有好处,如果访问量不大的话,任选其一即可,如果访问量极高,就需要酌情考虑了。

我们采取的策略是第二种:

  1. 使用嵌入到nginx中的lua程序对用户请求(仅限短信)+IP进行限定,使其:
    • 在1分钟内的请求只允许发送1次短信
    • 如果一分钟内超过1次,但在3次以内,则不发送短信,并给与警告
    • 如果一分钟内超过3次,则禁止这个IP的任何访问
  2. 使用redis保存这些被封IP以及带时限的接口请求,以免nginx无法定义全局变量。这样,nginx就可以实现:
    • 对IP是否允许放行的检查
    • 令1分钟前的IP访问记录自动过期

刚开始我们也使用了第一种,将这些IP直接加入到iptables中去,从内核层面封掉这些IP,只是我们觉得解封起来比较麻烦,而且无法与我们的软件集成,尤其是很难实现逻辑集成(区别接口,允许1分钟1次,3次以上才封IP),遂使用nginx+redis的方案,毕竟lua程序可以写成我想要的逻辑。

实现

假如我们发送短信的接口是 /sendSms
在nginx.conf文件的 server 段内新建一个location块,使其匹配正则表达式 ~* sendSms ,然后开始写代码吧:

        location ~* sendSms { 
                default_type 'application/json; charset=UTF-8';
                lua_need_request_body on;
                access_by_lua ' 
                    -- 为了lua代码可以语法高亮,这里的内容放在后一个代码块,请自行复制到此处即可
                ';
                proxy_pass http://app_backend;
                proxy_set_header X-Forwarded-For $remote_addr;
                proxy_pass_header Origin;
        }

下面的lua代码请复制到上文单引号内:

-- ngx.exit(ngx.OK)
local cjson = require "cjson"
local ip = ngx.var.remote_addr
if ngx.var.http_user_agent ~= nil or ngx.var.http_user_agent == "" then
        local agent = string.lower(ngx.var.http_user_agent)
        -- ngx.say("user agent:", ngx.var.http_user_agent)
        -- ngx.exit(200)
        local sIdx = string.find(agent, "httpclient") or string.find(agent, "java")
        -- ngx.say("sIdx:", sIdx)
        if (sIdx ~= nil) then
                ngx.status = ngx.HTTP_FORBIDDEN
                local msg = "你把硬盘拿过来,我直接把数据库给你拷贝一份吧,这样太慢了,我都急死了"
                ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
                ngx.exit(ngx.status)
                return
        end 
end
local redis = require "resty.redis"
local red = redis.new()

red:set_timeout(1000)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
        ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
        ngx.say("failed to connect: ", err)
        return
end 

local forbidden, err1 = red:sismember("fbdIP", ip) 
-- ngx.say("forbidden:", forbidden)
if forbidden == 1 then
        ngx.status = ngx.HTTP_FORBIDDEN
        local msg = "你把硬盘拿过来,我直接把数据库给你拷贝一份吧,这样太慢了,我都急死了"
        ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
        ngx.exit(ngx.status)
        return
end 

local key = "ip::" .. ip
-- ngx.say("key:", key)
local ttl, err1 = red:ttl(key)
if ttl == -1 then
        red:del(key)
end

local res, err = red:get(key)
red:incr(key)
if (not res) or (res == ngx.null) then
        --[[
        local msg = "failed to get cache"
        ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
        --]]
        red:expire(key, 55) -- 55秒内不允许同一IP超过30次访问
        ngx.exit(ngx.OK)
elseif tonumber(res) < 1 then
        ngx.exit(ngx.OK)
elseif tonumber(res) >= 1 then
        ngx.status = 200 
        -- ngx.say("redis result is string 1")
        -- local msg = "我们认为你有恶意请求的嫌疑,请不要使用及其程序进行访问"
        local msg = "慢点,无影手得多累啊"
        ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
       if tonumber(res) > 10 then
                red:sadd("fbdIP", ip)
        end
        ngx.exit(ngx.status)
        return
end

解释

  • 前面对key的检查不止使用了get() ,还使用了 ttl(),是因为redis的过期回收策略使用的是一种近似LRU算法,导致一定概率的不删除,所以使用ttl进行检查。

前提

在大家读到这篇文章的时候,我要顺便说一下这个方案适用的前提。如果没有这些前提,那这个方案对你就不可用,当然,解决思路也许可以有些帮助,如果你善于动手的话,很快也能弄好自己的解决方法。我们的系统满足以下几个条件:

  1. 当然是使用nginx做前端代理的web架构了
  2. 使用支持lua的nginx。直接从nginx官网下载按照的nginx是不支持的,需要额外下载lua代码,并编译到nginx中去。或者直接使用openresty

建议

由于之前redis有个可以拿到root权限的漏洞,所以:

  1. 务必要对redis设置访问密码
  2. 最好将redis服务的绑定IP限定在内网IP上

redis 的key千万不能被污染,否则正常用户的IP会被误伤封禁。

工具

在解决这个问题的时候,我们也是用废了很多脑细胞的,为了节省你的脑细胞,我就免费让你看看。

怎么得知坏蛋在攻击我的服务器呢?

我的办法是:
通过将日志内所有对/sendSms接口的调用IP进行统计,找到那些调用次数比较多的,比如大于10次的。用这个命令就好了:

grep  "POST /sendSms" logs/ikuaiyue.log | awk '{print $6}' | sed s/IP:// | sort | uniq -c | awk '{print $1 "\t" $2}' | sort -n

然后就会看到这样的结果:

1       115.205.13.179
2       117.136.40.20
2       117.136.94.44
2       117.59.39.22
122     223.104.10.28

第一列是此IP的调用次数,第二列你懂。
好了,现在知道改怎么办了吧?

顺便说一下,我们的日志是这个样子滴:

[2016-05-30 01:25:20.451] [INFO] normal - IP:117.174.26.32 POST /sendSms
[2016-05-30 01:26:17.918] [INFO] normal - IP:117.174.26.32 POST /sendSms
...

稍微解释下上面的命令:

grep  "POST /sendSms" logs/ikuaiyue.log 
 | awk '{print $6}'       #按空格分割后的第6列(即IP:117.174.26.32)
 | sed s/IP://            #删除字符"IP:"
 | sort                   #排序
 | uniq -c                #去重,并记下重复数,相当与做了个统计操作
                          #此时重复数字为第一列,IP被放在了第二列.
                          #但此时格式上有个问题:首列数字是右对齐的
 | awk '{print $1 "\t" $2}'  #为了解除其右对齐,重新打印以便,并以tab分隔
 | sort -n                #以首列为依据排序。-n表示当做数字来排列,默认是当做字符串的

当你按我的方法设置好了nginx后,怎么知道有没有起作用呢?

把刚刚那个命令改改,只输出最后3000行(具体数字看你的业务繁忙程度了)用做统计:

 tail -n3000 logs/ikuaiyue.log | grep  "POST /sendSms" | awk '{print $6}' | sed s/IP:// | sort | uniq -c | awk '{print $1 "\t" $2}' | sort -n
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值