一次SQL注入导致的"越权"

原文来自 SecIN社区 —作者:tkswifty

相关背景

在实际的业务开发中,SQL交互往往是业务系统中不可或缺的一项。在Java中提供了类似Mybatis、Hibernate、SpringData JPA等来满足相关的数据库交互需要。但是由于种种原因,开发人员在处理应用程序和数据库交互时,使用字符串拼接的方式构造SQL语句,导致了SQL注入问题。那么有时候面对大量的接口存在SQL注入,迭代困难的时候,过滤器/拦截器便是很多开发人员的首选,通过过滤相关的SQL关键字,避免SQL注入得到进一步利用。

针对上述场景,很多时候需要加检查过滤器设计是否严谨,检查是否有漏网之鱼,导致SQL注入漏洞被攻击者进行利用。前段时间审计某项目时发现一处SQL注入导致的"越权",以下是相关的过程。

挖掘过程

系统基于SpringMVC进行开发,业务主要是与简历编辑相关。相关的问题接口主要在修改个人简历处。一般来说,这种修改个人信息的业务,除了修改内容以外,主要传递两个关键信息:

  • 当前用户的身份凭证userId
  • 当前用户的业务编号(这里是简历),resumeId

在进行接口业务请求时,将业务相关的关键参数userid聪当前用户的身份凭证(一般是session)获取,绑定个人用户身份,然后从前端获取需要修改的resumeId,最后在保存信息进行SQL交互时,从会话在获取的userId再与resumeId进行二次绑定,保证userId对应的用户仅能修改自己的简历。类似的SQL语句如下:

UPDATE user_resume SET content='test',user_name='test'{省略相关内容} where userId = $userId and resumeId=$resumeId;

如下是相关的代码:

首先是Controller,在Controller层对用户的输入进行相关的封装(这里是简历的相关信息),通过自动绑定的处理方式,直接将用户的输入绑定到resume对象里,然后通过当前登录会话获取当前用户的userId,通过调用service的update方法进行简历内容的更新:

@ResponseBody
	@RequestMapping(value = "/updateResume", method = RequestMethod.POST)
	public PagedResult<ResponseRes> updateResume(Resume resume) {
		String userId = (String)session.getAttribute("userid");
		PagedResult<ResponseRes> pageResult = this.resumeService    
				.update(resume,userid);    
		return pageResult;
	}

查看service层实现,调用的是resumeService的update方法:

public PagedResult<ResponseRes> updateResume(Resume resume,String userid) {
		String rusumeId = resume.getId();
  	if(resumeId!=null){
				boolean updateStatus = resumeDao.update(resume, userid,resumeId);		
				if(updateStatus){
				//更新成功,封装返回结果
						......
				}else{
				//更新失败
						......
    }else{
		return updateResule;
	}

讲封装好的简历内容以及userId跟ResumeId传入resumeDao的update方法进行处理,这里使用的是mybatis框架进行处理,查看mapper的具体实现:

UPDATE user_resume SET
		......
		<if test='resume.address!=null'>
			,address=${resume.address},
			......
		where userId=${userId} and resumeId=${resumeId}; 
	</select>

所以整个简历更新的流程如上描述,因为在进行SQL交互时,通过update对简历表进行维护,通过userId和resumeId限定要更新的行。因为userId跟用户会话进行绑定,所以用户仅能更新自己对应的简历,防止了越权问题。

但是这里由于在Mybatis中使用了$进行注解,存在SQL注入风险。那么进一步检查是否存在相关的防护措施。这里发现相关的过滤器措施,应该是系统注入太多了,所以开发统一通过过滤器对用户输入进行处理,检查过滤器的具体实现。

同样的过滤器还是检查如下几点:

  • 过滤器的顺序
  • 获取数据的方式是否覆盖全面
  • 过滤的规则内容

这里由两个过滤器组成,分别负责xss以及SQL注入的输入检测。因为都是直接检测到恶意输入就直接返回通用报错页面,所以这里不存在顺序问题导致的filter绕过。

其次是获取数据的方式,这里通过MultipartHttpServletRequest multiReq = multipartResolver.resolveMultipart(request);对当前文件上传进行转换,转换成普通的request对象后再进行相关的输入检查,防止multipart提交导致的bypass:

private CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
 	public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
 			throws IOException, ServletException {
 		public static String[] MULTI_FILE_WHITE_LIST = new String[]{"/manager/uplode"};
 		HttpServletRequest request = (HttpServletRequest) req;
 		HttpServletResponse response = (HttpServletResponse) res;
 		String pathInfo = request.getPathInfo() == null ? "" : request.getPathInfo();
 		String url = request.getServletPath() + pathInfo;
 		String contentType = request.getContentType();// 获取请求的content-type
 		//如果是文件上传接口不需要进行转换,需要在接口处直接进行输入检查
 		// 文件上传请求 *特殊请求