Apache ShenYu Admin JWT认证绕过漏洞(CVE-2021-37580)

Apache ShenYu Admin JWT认证绕过漏洞(CVE-2021-37580)

0x01 漏洞简介

Apache ShenYu(原名 Soul)是一个异步的、跨语言的、多协议的高性能响应式API 网关,并可应用于所有微服务场景。
2021 年 11 月 16日,Apache发布安全公告,公开了Apache ShenYu中的一个身份验证绕过漏洞(CVE-2021-37580),该漏洞的CVSS评分为9.8。由于ShenyuAdminBootstrap中JWT的错误使用,导致攻击者可以绕过身份验证,直接进入目标系统后台。

0x02 影响版本

2.3.0 <= Apache ShenYu <= 2.4.0

0x03 环境搭建

docker下载运行环境:docker run -d -P vulfocus/apache_shenyu-ce_2021_37580

0x04 漏洞分析

下载2.4.0源码:https://github.com/apache/shenyu/archive/refs/tags/v2.4.0.zip

使用开发工具启动 org.apache.shenyu.admin.ShenyuAdminBootstrap,访问 http://localhost:9095 , 默认用户名和密码分别为: admin123456

漏洞为JWT认证问题,当HTTP请求中包含X-Access-Token时,调用StatelessAuthFilter进行token校验,认证的框架是Apache Shiro。

private static final String HEAD_TOKEN = "X-Access-Token";

@Override
protected boolean onAccessDenied(final ServletRequest servletRequest, final ServletResponse servletResponse)
        throws Exception {
    HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    if (StringUtils.equals(HttpMethod.OPTIONS.name(), httpServletRequest.getMethod())) {
        return true;
    }

    String tokenValue = httpServletRequest.getHeader(HEAD_TOKEN);
    if (StringUtils.isBlank(tokenValue)) {
        log.error("token is null.");
        unionFailResponse(servletResponse);
        return false;
    }

    StatelessToken token = new StatelessToken(tokenValue);

    Subject subject = getSubject(servletRequest, servletResponse);
	//import org.apache.shiro.subject.Subject;
    try {
        subject.login(token);
    } catch (Exception e) {
        log.error("token is warning. token : {}.", tokenValue, e);
        unionFailResponse(servletResponse);
        return false;
    }

    return true;
}

那么直接找到shiro的验证逻辑进行断点即可跟进整个认证登录流程。在shenyu-admin/src/main/java/org/apache/shenyu/admin/shiro/config/ShiroRealm.java中 可以看到两个doGetAuthorizationInfo方法,其中doGetAuthorizationInfo(final PrincipalCollection principalCollection)是用来鉴权使用的,我们先跳过,主要是下面的doGetAuthenticationInfo(final AuthenticationToken authenticationToken),这个方法是用来进行认证的。

 @Override
    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
        UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
        Set<String> permissions = permissionService.getAuthPermByUserName(userInfo.getUserName());
        if (CollectionUtils.isEmpty(permissions)) {
            return null;
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);

        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
        String token = (String) authenticationToken.getCredentials();
        if (StringUtils.isEmpty(token)) {
            return null;
        }

        UserInfo userInfo = getUserInfoByToken(token);

        return new SimpleAuthenticationInfo(userInfo, token, this.getName());
    }

在这里插入图片描述

我们可以看到整个认证的代码很简单,首先获取到token,然后判断token是否为空,不为空的话接着调用getUserInfoByToken方法获取用户信息实体,然后返回一个SimpleAuthenticationInfo信息,整个逻辑结束,那么用户能不能登录,核心点就是在getUserInfoByToken方法中。

/**
     * check token valid.
     *
     * @param token user token
     * @return userInfo {@link UserInfo}
     */
    private UserInfo getUserInfoByToken(final String token) {

        String userName = JwtUtils.getIssuer(token);
        if (StringUtils.isEmpty(userName)) {
            throw new AuthenticationException("userName is null");
        }

        DashboardUserVO dashboardUserVO = dashboardUserService.findByUserName(userName);
        if (dashboardUserVO == null) {
            throw new AuthenticationException(String.format("userName(%s) can not be found.", userName));
        }

        return UserInfo.builder()
                .userName(userName)
                .userId(dashboardUserVO.getId())
                .build();
    }

上面的代码逻辑也很简单,就是根据token获取用户名,然后用户名不为空的话就从数据库中查找该用户名相对应的用户实体信息,找到后就返回一个UserInfo对象。

接着我们看JwtUtils.getIssuer这个方法,可以看到就是简单的decode了下token,然后从里面获取userName相关的信息并且返回。看到这里其实已经发现问题了,在目前看到的逻辑中,没有对token是否有效进行校验。

 /**
     * according to token to get issuer.
     *
     * @param token token
     * @return Issuer {@link String}
     */
    public static String getIssuer(final String token) {
        DecodedJWT jwt = JWT.decode(token);
        return Optional.ofNullable(jwt).map(item -> item.getClaim("userName").asString()).orElse("");
    }

0x05 漏洞复现

访问环境9095所映射出的端口,就是网站的登录入口

在这里插入图片描述

首先使用默认初始账号密码 admin/123456 登录进入后台

在System Manager->User 新建一个用户,角色为 default
在这里插入图片描述

使用新建的账号进行登录

此时登录界面会提示用户角色为配置权限错误,无法进入后台

在这里插入图片描述

抓包发现该账号的登录包已经生成了 jwt token

在这里插入图片描述

接下来用上述不具备任何权限的账号的 jwt token 去访问管理员才能访问的接口

http://192.168.237.129:49153/dashboardUser?currentPage=1&pageSize=12

发现访问成功,获取到了所有的账号信息,漏洞复现成功

接下来用上述不具备任何权限的账号的 jwt token 去访问管理员才能访问的接口

http://192.168.237.129:49153/dashboardUser?currentPage=1&pageSize=12

发现访问成功,获取到了所有的账号信息,漏洞复现成功

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值