一次短信验证码“撞库”,发生的惨案!!!

卡二条的技术圈

共 3567字,需浏览 8分钟

 ·

2021-12-22 20:00

讲故事

故事要从一天中午开始说起,同事小张正在午休,睡的正酣,突然被产品经理给叫醒。运营反馈,大量用户打客服电话,说到没有注册平台却收到成功注册平台账号的短信内容。

小张心里,瞬间有一万只草泥马在奔跑。心想怎么会出现这种情况呢?马上打开短信发送平台,发现一分钟内有几万条注册短信发送。小张心里瞬间慌了,怎么会出现这种情况呢?小张边找到发送短信和注册的代码,发现也没有啥问题呀,便找到我协助帮助查看。虽然我心里也不爽,但无奈还是帮着查看,没几分钟变发现其中存在严重的bug。下面就来讲讲这次事故发生的原因。

注册验证逻辑

要说到这次事故,我们先来看看通过短信验证码注册的逻辑。下面两张图,就很好的说明了代码的逻辑。a. 用户点击页面发送短信按钮,想服务端发起发送验证码的请求。

b. 服务端接收到之后,先会验证是否存在验证码,存在就提示用户60s内不能进行重新获取。如果没有,则调用短信服务,发送成功之后,就把验证码放在缓存中(Redis)并设置一个过期时间(例如60s)。

c. 如果短信发送失败,则告知客户端,发送失败,进行重新发起。

function sendCode(string $mobile)
{
  $redisClient = new Redis();
  // 1. 先验证缓存中是否存在
  if (!$redisClient->exists($mobile)) {
    $code = "xxxx";// 随机生成一个验证码
    $smsService = new SmsService();
    // 调用短信服务,进行验证码发送
    if ($smsService->sendSmsCode($mobile)) {
      // 将验证码添加到缓存中
      if ($redisClient->set($mobile, $code, 60)) {
        echo "设置成功!";
      }
    }
    echo "短信发送失败!";
  }
  echo "已发送验证码,一分钟内不能进行重复发送!";
}

// 调用短信验证码方法
sendCode((string)156xxxx2305);

上面只是一个伪代码,在实际中要考虑添加缓存和发送短信验证码的一致性,不能出现给用户成功发送了验证码,但是缓存每天就成功。这样就会出现,验证时误判验证码错误。

a. 当用户接收到短信验证码之后,点击页面注册按钮。前端会把验证码和手机号一并发送到服务端。

b. 服务端根据手机号去查询缓存(Redis)中是否存在验证码。

c. 如果存在验证码,则进行对比。看缓存中的验证码和提交的验证码是否一致,如果一致就进行注册。不一致就返回客户端,验证码错误。

d. 短信验证码一致,用户账号自动注册的同时把对应的短信验证码进行删除。

上面的两张图就是同事小张的一个代码逻辑,大家看到这里,可以先想想这种逻辑是不是正确的?为什么会出现文章开头说的情况?你平常在写短信验证码的服务时,是不是这么写的?

function verifyCode(string $mobile, string $code)
{
  $redisClient = new Redis();
  // 1. 直接查询缓存中是否存在验证
  $cacheCode = $redisClient->get($mobile);
  if (empty($code)) {
      if ($code == $code) {
        // 验证成功之后删除缓存中的验证码信息
        $redisClient->del($mobile);
        echo "验证成功!";
      }
      echo "验证码错误,请重新输入!";
  }
  echo "短信验证码不存在!";
}

// 调用短信验证码方法
verifyCode((string)156xxxx2305, (string)$code);

问题发现

看到这里,你可能发现其中的问题,并能总结出这次事故的原因。或许你还没有发现,下面我就来为大家揭秘一下具体的原因。这个问题修复之后,个人用这样的方式实现了一次,也发现会绕开短信验证。

接下来,就分析一下发送和验证存在的逻辑。a. 从正常的逻辑来看,发送短信验证码的逻辑,这样写是没有问题的。先验证缓存,在发送短信并添加缓存。

b. 但是缺乏验证。这里只验证了手机号,却没有一个全面的验证,例如IP限流、IP发送次数、IP黑名单、某个时段短信发送服务是否存在异常(例如突然大量增加)等等。在后来项目复盘也发现这个问题,同一个IP在一个时段,大量请求发送验证码的请求。推测是攻击者,购买的的手机号,进行发送,否者也不会出现部分用户投诉的情况。

c. 接下来,就是验证环境。验证按照上面的逻辑,其实很容易发生问题的。因为发送验证码是攻击者在操作,当验证环节,攻击者可以使用轮询的方式进行验证码撞库。根据验证码的位数,依次去一个一个的调用验证接口,如果对就注册成功,如果不对在发起下一次请求。

问题解决

通过上面的分析,后来针对系统做了紧急的迭代。发现还是存在大量的攻击者,但是没有发现一个成功的攻击操作。这里总结一下,在这次解决中的一些环节。

a. 短信发送环节。在网管层做了验证、限流。IP异常记录、IP黑名单。存在异常的情况下,依赖拉黑。

b. 短信服务监控,如果出现某一时段,大量发送短信服务进行异常报警并做限流控制。这里根据系统的实际业务处理,像系统营销活动,可能存在真实的峰值期。

c. 日志监控。日志发送监控、客户端请求监控等等。一旦发现异常,就进行报警,针对请求日志做分析并处理。我们在实际过程中,发现了大量的异常IP或者异常的手机号段。针对这批号段做了异常禁用、IP异常拦截。

d. 验证环节,做错误次数验证、异常IP封禁、异常号码封禁。一旦发现某一个号码出现三次验证错误,就进行封禁一分钟才能重新获取验证码。如果发现某一个手机号请求次数多,也同样当做异常号码处理。

总结

在日常开发中,我们尽量做好系统的日志监控、异常拦截等情况。可能业务量小,一直不会出现问题。但是业务量一上来,就是各种问题,因此就需要在前期就有一个完善的策略来应对。

本文也是针对问题,做了一个大致的策略介绍。后续通过代码进行深度剖析其中的实现逻辑。欢迎关注,后续继续更新。


浏览 119
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报