SQL注入
01
MyBatis框架中的注入漏洞
Mybatis框架支持的CURD功能可以直接搜索XML文件中的${和${}拼接的SQL语句,如果SQL的参数可控,就可能造成注入风险。
另外,有的SQL语句使用的是注解开发,把SQL语句可以直接写在了代理接口方法上方,审计的时候可以将两种情况都注意一下,或许有不同的发现。
安全的方式应该是使用#号接收参数,但很多时候如数据库表的字段名,又只能使用$符接收参数。白名单的方案是可以将数据库表的字段名维护为一个数组,再检查参数是否在数组内,经过拼接的payload就不能通过检查, 例如:对于排序,只用了到ASC、DESC中的一个,把这两做成白名单即可,SQL注入的payload不在这个名单中,在反序列化时就会被拦截。
02
JPA中的SQL注入
JPA受SpringBoot官方支持直接提供启动依赖,和MyBatis一样,JPA框架方便开发人员完成与数据库的各种交互操作。
对于JPA中的SQL注入,一般出现时使用的是+号直接拼接。
安全的方式应该用冒号或者问号进行参数绑定:
03
无法利用的SQL注入
在文章开头提到,如果SQL参数可控可能造成注入风险,即使直接拼接到SQL语句的参数可控,也并不意味着这个SQL注入漏洞可以被利用。
比如前端将JSON传送到后端后,会有一个反序列化的操作,将JSON串反序列化为目标类,期间会有一个数据类型转换过程。
查看如下两个字段在实体类的定义,发现是int类型,但是SQL注入的payload是字符串类型(形如”OR IF(SLEEP(5), 0, 1)--+"的字符串),数据类型不匹配,在反序列化这一步的数据类型转换就通过不了,因此这个注入漏洞并不能利用。
Druid未授权访问
这也是安全测试中经常出现的一个漏洞,Druid是阿里云计算平台DataWorks团队研发的著名高性能数据库连接池工具,在项目中一般有两种配置方式。
01
创建类引入Druid
通过Druid依赖引入Druid,需要创建一个DruidConfig配置类加载Druid的配置信息,增加对Druid的监控。
在config包的配置类DruidConfig中除了配置Druid的访问路由,还要增加用户名密码的登录校验机制,甚至进一步配置IP黑/白名单,如果仅仅配置了Druid的访问路径会造成未授权访问漏洞。
02
起步依赖引入Druid
使用Druid起步依赖,只需要在配置文件(application.properties/application.yml)中配置相关信息即可开启Druid监控,不再需要创建Druid的配置类,这也是目前更为流行的配置方式。
但如果仅仅配置了Druid的访问路径会导致Druid未授权访问风险。
配置Web访问路由、账户密码,启动项目后访问http://ip:port/druid/login.html,此时需要正确的账号密码才有权限访问Druid的Web控制台。
权限绕过
01
检查免鉴权目录
当用户访问某个功能后,后台服务先检查请求会话是否合法以及过期,否则就重定向到登录页面,如登录、注册页面以及各种静态文件资源并不需要经过会话有效性检查,可以直接访问,在代码审计时候可以检查都有哪些路由资源是可以直接访问的。
在这个案例中,项目使用的Shiro设置了static路由下的资源可以直接访问
filterChainDefinitionMap.put("/static/**", ANON)
查看对外暴露的服务,由于static路由下的资源可以不登录直接调用,如下面的这个上传文件的服务。
然后存储文件,但是未校验文件后缀和文件内容就直接保存文件了。
文件上传后存放在static-resource目录,同样,static-resource路由在Shiro中也配置为可直接访问。
结合这两个免鉴权的目录访问,以及未校验上传文件合法性的缺陷,于是造成了前台未授权文件上传漏洞。
02
检查配置不当
在Spring Security中,antMatchers是一个用于定义安全配置的方法,它用于指定哪些URL模式或路径应该被特定的安全规则所保护。在下面的配置中,任何访问以/admin/开头的URL都需要用户具有ADMIN角色,访问/user/需要具有USER角色,而其他路由则对所有人都开放:
.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/**").permitAll()
如果访问/admin,提示403错误。
增加一个/,访问/admin/,此时匹配的是antMatchers("/**").permitAll(),则可以绕过权限校验。
在Spring Security中,regexMatchers是ant风格之外基于正则表达式的URL模式匹配的方法。使用regexMatchers,可以定义复杂的URL模式,这些模式可能不容易或不方便用Ant风格的路径匹配来表示。例如:
.authorizeRequests()
.regexMatchers("^/administrator /.*$").hasRole("ADMIN")
.regexMatchers("/admin") .hasRole('ADMIN')")
.regexMatchers("^/user/.*/profile$").hasRole("USER")
.regexMatchers("^/public/.*$").permitAll()
.anyRequest().permitAll()
在这个配置中:
^/ administrator /.*$ 匹配以 / administrator / 开头的任何URL。
^/user/.*/profile$ 匹配任何以 /user/ 开头,以 /profile 结尾的URL。
^/public/.*$ 匹配以 /public/ 开头的任何URL。
此时直接访问http://127.0.0.1:8012/admin将被拒绝。
可以增加其他字符改变匹配方式去实现绕过。
还有一种发生在路径穿越后的权限绕过,漏洞代码示例如下:
`public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException, IOException {` `HttpServletRequest request = (HttpServletRequest) req;` `String uri = request.getRequestURI();// /system/login` `StringBuffer requestURL = request.getRequestURL(); // http://localhost:8080/system/login` `System.out.println("requestURL: " + requestURL);` `System.out.println("getRequestURI: " + uri);` ` if(uri.startsWith("/system/login")) {` ` System.out.println("this is login page...");` `resp.getWriter().write("this is login page...\n");` `chain.doFilter(req, resp);` `} else if(uri.endsWith(".do") || uri.endsWith(".action")) {` ` User user = (User) request.getSession().getAttribute("user");` `if (user == null) {` `resp.getWriter().write("unauthorized access\n");`` System.out.println("unauthorized access");` `return;` `}` `}` `}`
首先访问:http://localhost:8080/system/login, 此时正常匹配到登录路由。
访问:http://localhost:8080/login/admin.do ,由于没有登录就直接访问路由,session为空,提示未授权。
此时获取的URI为/system/login/../../login/admin.do,可以通过第一步检查,之后就调用chain.doFilter(req, resp)放行请求,绕过了session有效性校验。
权限绕过问题的规避思路:
在定义安全配置时,应尽量考虑全面,并测试配置方式是否可靠,同时也要检查使用的第三方组件自身是否存在有关漏洞,如CVE-2022-22978、CVE-2022-32532、CVE-2023-20860等。
越权漏洞
垂直越权漏洞:也称为权限提升,是一种由于Web应用程序没有正确实施权限控制而导致的安全漏洞。这种漏洞允许低权限用户访问或操作本应只有高权限用户才能访问的资源或功能。
IDOR,即不安全的直接对象引用(Insecure Direct Object References),是一种常见的水平越权漏洞,发生在应用程序未能适当地验证用户对特定资源的访问权限时。这种漏洞允许攻击者通过修改请求中的参数(如用户ID、文件ID等)来访问或操作其他用户的资源或数据。
01
未做身份校验
如下对于高权限用户才能操作的增删改查服务,如果系统中没有使用第三方的Shiro、Spring Security做权限划分,也没有使用框架自带的如AOP做权限切面管理,将会暴露诸多垂直越权的风险,此时,只需要修改纯数字id就可以删除其他用户
`@RequestMapping(value = "delete")``public String delete(HttpServletRequest request, @RequestParam Long id)` `throws Exception {` `try {` `userManager.delete(id);` `request.setAttribute("msg", "删除用户成功");` `} catch (ServiceException e) {` `// logger.error(e.getMessage(), e);` `request.setAttribute("msg", "删除用户失败");` `}` `return list(request);`
配置 Shiro 的权限注解:
@RequiresAuthentication // 表示当前用户必须已经认证(登录)
@RequiresPermissions("user:delete") // 表示当前用户必须拥有 "user:delete" 权限
@RequiresRoles("admin") // 表示当前用户必须拥有 "admin" 角色
在业务逻辑中使用这些注解。当你调用 deleteUser 或 deleteAllUsers 方法时,Shiro 会检查当前用户是否拥有相应的权限或角色,如果没有,则会抛出异常,防止越权操作。
`public class UserService {` ` @RequiresPermissions("user:delete")` `public void deleteUser(String username) {` `// 删除用户的逻辑` `}` ` @RequiresRoles("admin")` `public void deleteAllUsers() {` `// 删除所有用户的逻辑` `}``}`
02
鉴权不严格
大部分场景下,使用了第三方鉴权框架或者自定义的鉴权配置能有效规避越权类漏洞,但有时候安全设计跟业务需求存在冲突,又或者是鉴权流程并不严谨,存在绕过的可能。这类越权漏洞需要了解完整的鉴权流程再结合业务逻辑分析某一个环节可能存在的疏漏。
如下是一个更新应用信息的服务,只有管理员可以编辑系统的应用信息,接口配置了AOP注解,通过AOP来鉴权。
这里使用的是注解方式,即在需要使用切面管理的方法上配置AOP注解(这里就是@DePermissions注解),AOP注解方法的定义需要声明@Retention、@Target,表示这是一个AOP注解。
然后在切面类上方使用@Aspect注解声明这个是切面类,用于切面编程,为系统中添加了@DePermission注解的业务方法增加额外功能但又不会修改原始方法的代码逻辑。
在切面类中就可以定义切面方法,最常用的就是环绕通知,用于在目标方法(添加了AOP注解的方法)执行前、执行过程中、执行后为其添加增强方法。@Around声明这个方法是环绕通知,他的value属性的值就是上方声明的AOP注解的全限定名。
来看看这个AOP方法做了什么,首先调用AuthUtils.getUser().getIsAdmin()判断当前用户是不是管理员,进入看看是怎么判断的。
从USER_INFO这个常量中用GET方法获取CurrentUserDto对象。
CurrentUserDto继承SysUserEntity,是用户实体类,包含了用户的完整身份信息。
查看这个USER_INFO常量,ThreadLocal,他是一个线程局部变量的值,当用户发送一个请求过来,这个请求就是一个线程,线程局部变量中保存了CurrentUserDto对象,就可以从请求中拿到CurrentUserDto对象,再通过CurrentUserDto对象的getIsAdmin方法返回一个Boolean值判断是不是管理员。
如果是管理员,说明权限是够的,就会调用point.proceed()放行去执行原始目标方法。
如果不是管理员,就要继续鉴权,这里笔者把代码含义写在注释中。
判断AOP注解的属性logical的值是不是AND,指一个逻辑条件,只有当两个AOP注解都满足条件才能访问原始目标方法。
我们回到原始目标方法的AOP注解,他的AOP注解的属性logical是AND,也就是这里 @DePermissions 中的两个 @DePermission 都要满足条件才能访问update方法。
回去看AOP的切面代码,会调用access方法依次处理这两个 @DePermission。
最后会根据access方法的返回结果来判断有没有权限调用原始目标方法(update方法)。
access方法的返回类型是Boolean,他接收了三个参数:被AOP注解的方法的参数列表、AOP注解、0 。access方法中,先获取了AOP注解的type、value、requireLevel这几个属性的值。
然后调用AuthUtils.permissionByType(type),这个方法中,返回了当前用户可以操作的type对应的数据。
再用stream流去过滤返回的数据中,level大于等于@DePermission上配置的所要求的level属性值的数据。
回到原始目标方法update上配置的注解,可以看到@DePermissions中的第一个@DePermission注解没有声明level属性值。
如果没有声明,level的默认值是1。
普通用户的level值是满足这个level要求的,但是这里要同时校验两个DePermission,另一个DePermission配置的level属性值是5,普通用户的level小于这个level,也就是说,普通用户是没有权限调用这个update方法的。
再看access方法的代码,如果传入的Object为空,则直接返回true。
于是使用level为1的普通用户去调用更新的接口,只传入id指定应用,不传入pid,pid对应的注解就会为空,即可绕过权限校验去更新应用。