记一次"发短信"造成的session覆盖越权

  • A+

相关背景

  大大小小的网站都是由各式各样的业务功能组合而成的。例如登录、忘记密码、查看个人信息等。一般情况下,相关的业务凭证存在设计缺陷的话,会存在类似平行/垂直越权等安全问题。常见的业务凭证及越权场景有:

  • 用户身份id(userid,例如通过userid越权查看用户信息)
  • 对象id(fileid,例如通过fileid越权查看其他用户的备案资料(身份证、手机号、住址等))
  • 基于文件名(例如可以遍历时间戳组成的文件名获取用户订单)
  • Cookie中的字段(例如增加user=admin,即可垂直越权访问管理员模块)
  • Session中的内容(例如session覆盖)
  • ......

  一般在测试时,可以通过寻找相关的凭证,通过遍历、猜解等方式,进行业务逻辑缺陷的发掘。
  前段时间审计某项目时发现一处结合忘记密码发送短信功能进行session覆盖越权的业务缺陷,下面是具体的发现过程。

挖掘过程

  系统基于SpringMVC进行开发,在实际代码中,以查看用户个人身份信息为例,一般都会与session会话进行绑定,在登录成功后把对应的用户凭证保存在会话session中,当访问业务接口获取查看用户个人信息时,因为相关的业务凭证不是以userid这种可控的参数形式传递的,从一定程度上避免了越权的缺陷。

  下面是登录过程的具体实现:

java
@RequestMapping(value = "/loginAuth")
public String login(String loginName,String password,String captcha,HttpSession session){
String VerifyCode = session.getAttribute("captcha");
if(StringUtils.isBlank(loginName)){
return ResponseData.fail("用户名为空");
}
if(StringUtils.isBlank(password)){
return ResponseData.fail("password");
}
if(StringUtils.isBlank(VerifyCode)||StringUtils.isBlank(captcha)||!VerifyCode.equals("captcha")){
return ResponseData.fail("验证码错误");
}
//防止验证码复用问题
session.removeAttribute("captcha");
//登录逻辑
User user=userService.loginAuth(loginName,password);
if(user!=null){
session.setAttribute("loginName",user.getName());
......
}else{
return ResponseData.fail("账号密码错误");
}
}

  大致流程如下,当一系列的合法性校验完成后,会将当前登录用户的用户名作为业务凭证存入到对应的session容器中:

image.png

  通过相应的业务凭证就可以完成很多的安全措施了,例如通过拦截器防止接口的未授权访问:

java
String loginName=(String)session.getAttribute("loginName");
if(StringUtils.isBlank(loginName)){
throw new Exception("Not logged in Exception");
}

  同样的以查询用户卡号信息接口为例,因为整个过程都是通过session容器中的业务凭证loginName来交互的,绑定了当前会话,无法越权查看他人的卡号信息:

java
@RequestMapping(value = "/getUserCard")
public Object getUserCard(HttpSession session){
List<String> cardIds = userService.getUserCard(session.getAttribute("loginName"));
return CardService.getCardInfo(cardIds);
}

  上述过程梳理下来乍一看好像没法发现越权类的缺陷,若想越权查询其他用户卡号信息,思路也很简单,必须控制session会话中的loginName参数。

  比较幸运的是,在忘记密码功能处,找到了对应的突破口。忘记密码功能一般通过结合短信验证码等方式进行密码的重置,同样的为了防止相关的验证码多用户可用,会跟当前会话进行绑定。保证当前请求的短信验证码仅仅可以供请求的手机号业务使用。

  当前业务系统忘记密码模块获取短信的部分代码如下,可以看到第一步是传入需要重置密码的用户名,然后将该用户名loginName同样的存入到session容器中,这里应该是为了后面校验验证码使用的,但是不重要,已经得到想要的可控点loginName了:

java
@RequestMapping(value={"/getMessage"})
@ResponseBody
public Object getSecPwdQuestion(HttpServletRequest request, HttpServletResponse response,HttpSession session) throws Exception
{
String loginName = request.getParameter(“loginName”);
if (StringUtils.isNotBlank(loginName)) {
session.setAttribute("loginName",loginName);
User user = this.UserService.getUserByLoginName(loginName);
if (user != null) {
//获取短信
......
}else{
return ResponseData.fail("用户不存在");
}
}

  可以看到这里其实是可以通过发送短信的方式来控制session会话中的loginName参数的。联系前面登录过程以及获取用户卡号的过程,这里利用思路就出来了,首先通过忘记密码模块的/getMessage接口进行请求,通过变化对应的需要忘记密码的手机号设置对应的loginName,然后再访问/getUserCard接口,即可越权获取对应用户的卡号信息了,具体流程如下:

image.png

拓展延伸

  整个利用过程有点类似session覆盖,但是覆盖的方法是通过跨业务模块进行操作的。关键点还是业务凭证在两个业务功能点都是用到了,并且在忘记密码模块用户可控。
  因为HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息),每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次 的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过Cookie或者Session 去实现。

  那么很多时候都会在session中存入对应的业务凭证进行相关的业务维护,那么在实际黑盒/白盒测试中,可以通过分析猜测对应的代码实现,梳理每个模块共通的业务凭证(例如这里的loginName),可能会有意外的惊喜。

相关推荐: 【工具分享】AssetScan内网脆弱面分析工具

前言 分享个写的工具,关于内网漏洞检测的,我给它起了个听起来很牛皮的名字,AssetScan内网脆弱面分析工具(AS),糊弄糊弄人,哈哈。也不知道这个工具怎么样?希望它华而有实! 仅个人之见,感觉这两年的红队比较火,不管是在招聘,项目,都在考察攻击人员的内网测…