浅谈短信业务中的格式化漏洞

  • A+

关于短信业务

  短信服务(Short Message Service)是指通过调用短信发送API,将指定短信内容发送给指定手机用户。短信的内容多用于企业向用户传递验证码、系统通知、会员服务等信息。大型网站的业务实现中都提供有手机短信业务功能,可以比较准确和安全地保证业务的安全性,验证用户的正确性。
  常见业务有:
* 注册
* 忘记密码
* 交易验证
* 通知(异地登录、会议通知)
* ......

  一般的短信业务功能都是通过与短信平台的相关接口进行交互实现的,通过定制对应的短信内容进行发送。例如下面是通过聚合数据的接口实现的短信验证码验证业务:
```java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.net.URL;
import java.net.URLConnection;
//用于调用短信接口的类
public class SMSCode {
//传递手机号码以及随机生成的验证码
public static boolean sendCode(String phoneNumber,String AppName,String code) throws Exception {
String code_Str = URLEncoder.encode("#code#="+code+"&#app#="+AppName,"utf-8");
//封装短信接口地址
URL url =new URL("http://v.juhe.cn/sms/send.php?mobile="+phoneNumber+"&tpl_id=userid&key=相关key&tpl_value="+code_Str);
//打开链接,得到链接对象
URLConnection connection = url.openConnection();
//发起请求
connection.connect();

    //获得服务器响应的数据
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
    StringBuffer buffer = new StringBuffer();
    String line = null;
    while((line=bufferedReader.readLine())!=null) {
        buffer.append(line);
    }
    System.out.println(buffer.toString());
    bufferedReader.close();
    if(buffer.toString().indexOf(""error_code":0")>=0) {
        return true;
    }
    return false;
}

}
```
  那么在service层直接封装对应的内容,然后调用sendCode方法即可完成短信的发送了。

格式化漏洞

  短信业务实现过程中若存在设计缺陷,那么会带来一系列的安全问题。最常见的就是短信炸弹了。通过短信炸弹漏洞可发送大量的短信,大量的短信会增加企业短信费用。同时广度短信炸弹还可能造成类似用户投诉等不良影响。

  一般来说针对短信炸弹,会通过限制发送频率进行防护。例如如下的例子,忘记密码操作,首先获取身份验证所需的短信验证码:

java
@RequestMapping(value="/recoverPwdSend",method=RequestMethod.POST)
public Response<BaseRsp> recoverPwdSend(String phone){
ReturnModel<BaseRsp> busiData =null;
busiData =UserService.getVerifyCode(phone);
return ResponseInfo(busiData);
}

  跟进service层的getVerifyCode方法,查看短信业务的具体实现,首先会通过checkSmsFrequency()函数校验发送频率,若频率范围合理的话进一步获取短信模版内容,然后进行封装,最后调用短信接口完成短信的发送:

```java
//......
SmsConfig config =getSmsConfig();
if(config==null){
return new ReturnModel<>(ErrorCode,notFound);
}
//检查发送频率
boolean frequency=checkSmsFrequency(phone);
if(frequency){
return new ReturnModel<>(rsp,ErrorCode.frequency);
}else{
//获取模版内容
......

SmsBO smsBO =new SmsBO();
try{
    smsBO.setTmeplate(URLDecoder.decode(templateText,"UTF-8"));
}catch(Exceptione){
    return new ReturnModel<>(ErrorCode.fail);
}
smsBO.setUser(new String[]{userDo.getUerName()});
smsBO.setCode(code);
......
smsService.sendQueue(config,smsBO);
//redis记录当前发送的手机号

redisService.countSmsFrequency(phone);
......
}
```

  查看checkSmsFrequency()函数的具体实现,通过redis维护表的方式,限制了一个手机号一天短信发送的次数:

java
protected Boolean checkSmsFrequency(String key){
int count =redisService.querySmsFrequency(key);
if(count>5){
return true;
}else{
return false;
}
}

  整个流程梳理下来貌似没有太大的问题,输入手机号进行短信获取,同时在redis缓存中对请求次数进行记录,当次数达到阈值后禁止该手机号进行短信获取,防止短信炸弹的风险:

image.png
  其实这里仅仅审查了我们系统本身的业务代码,对于实际调用的第三方平台的短信发送接口,并未知道具体的代码实现。这里有个比较容易忽略的点是,第三方平台接口为了保证接口处理不规范的输入带来的异常,常常会进行格式化处理。例如最常见的trim()方法删除字符串的头尾空白符,校验手机号的格式剔除非数字内容等。那么结合一个调用顺序就会造成所谓的格式化漏洞
  以上述流程为例,具体流程细化成:

image.png
  可以看到,格式化处理在维护redis表之前,且格式化处理是在第三方平台接口进行处理的。那么上述场景就会存在短信炸弹的缺陷了,正常来说第6次请求短信的过程如下:

image.png
  例如这里一般一个手机号一天只有6次机会,正常来说此时18888888888手机号已经达到本日的发送限制了,无法进行短信发送,那么利用第三方平台的格式化处理,在手机号前面或者后面加上空格时又可以正常发送了,并且可以正常接收到短信内容。具体流程如下:

image.png
  当18888888888%20达到发送限制时,继续追加空格并刷新redis频次表中维护的数据,达到短信炸弹的效果,简易的表维护内容如下:

{"18888888888","6"}
{"18888888888 ","6"}
{"18888888888 ","6"}
{"18888888888 ","6"}
......

  因为每一次追加空格,都是在频次表中维护的都是新的记录,从而绕过了频次限制导致了短信炸弹问题。还有的平台会校验手机号的格式并剔除非数字内容,那么同理,可以尝试请求18888888888m这类字符混淆的手机号,绕过频次发送的同时还能正常进行短信接收。
  此外,在实际业务中,经常会存在需要批量短信发送的场景(例如发送会议通知,营销推广等),相应的第三方短信平台会提供对应的接口使用方式。例如某平台会通过逗号,分割发送的目标,完成发送的业务。并且单个手机号以及批量发送的接口往往是同一个接口,仅仅是提交内容的区别:
18888888888(发送一个手机号)
18888888888,18888888887(发送两个手机号)
......

  同样的,因为对第三方平台的接口具体实现不熟悉,同时开发短信业务时直接沿用对应的sms工具类,上述代码案例同样会出现频次限制被绕过问题:

image.png
  因为第三方短信平台批量发送的特性,可以通过提交18888888888,18888888887的方式来进行请求,结果是两个手机号均能接收到短信,且redis中频次表维护时永远不会对18888888888为key的记录进行处理,达到了绕过频次进行短信炸弹的效果。

  以下是其他的一些格式化绕过的补充:

  • 手机号中添加空格%20
  • 手机号中添加字符串符号等无关内容:188xxxx8888{random payload}
  • 手机号头部追加+86/86类的国际区号
  • 使用逗号,/分号;尝试批量发送
  • ......

  所有用户输入都不可信。大量针对Web应用程序的不同攻击都与提交错误输入有关,攻击者专门设计这类输入,以引发应用程序设计者无法预料的行为。针对短信业务中的格式化漏洞,输入确认(input validation)就显得很有必要了。例如在服务器端校验手机号的格式是否符合业务需求,针对批量发送的调用加入额外的安全检查等。

相关推荐: Spring 反序列化 RCE 漏洞分析

说在前面 原标题叫 《Spring framework 反序列化 RCE 漏洞分析》 奈何只能输入 26 个字符以下,故叫 《Spring 反序列化 RCE 漏洞分析》 相关分析漏洞是一个几年前的漏洞,并非是最近刚出的。之所以分析这个漏洞是因为笔者在梳理 fa…