freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

HVV前沿 | 积木报表组件(Jeecg-Boot)权限绕过漏洞分析
2024-08-27 19:02:34

漏洞概述

JeecgBoot是适用于企业 Web 应用程序的国产Java低代码平台。

近期,网宿安全演武实验室监测到JeecgBoot JimuReport 1.7.8版本及之前版本存在安全漏洞(网宿评分:高危):该漏洞源于权限设置不安全,攻击者在请求中添加特定参数,即可绕过授权机制,从而查看敏感信息并通过 Aviator 表达式注入在1.6.0版本后实现远程代码执行。

目前该漏洞POC状态已在互联网公开,建议用户尽快做好自查及防护。

受影响版本

JimuReport <= 1.7.8

漏洞分析

首先看权限绕过问题的成因,以历史漏洞接口/jeecg-boot/jmreport/queryFieldBySql为例。

1724756295_66cdb147f19cc07328013.png!small

显然这里已经做了权限限制,而/jeecg-boot/jmreport/**具体的拦截器实现在

org.jeecg.modules.jmreport.config.firewall.interceptor.JimuReportTokenInterceptor#preHandle()。

这是一个返回值为boolean类型的public方法,主要对用户请求进行了一些预处理和判断,那么只要在校验token前使其返回true,即可实现权限绕过。

当我们请求/jeecg-boot/jmreport/queryFieldBySql时,该方法先进行了xss检测。

String var4 = d.i(request.getRequestURI().substring(request.getContextPath().length()));
log.debug("JimuReportInterceptor check requestPath = " + var4);
int var5 = 500;
if (n.a(var4)) {
    log.error("请注意,请求地址有xss攻击风险!" + var4);
    this.backError(response, "请求地址有xss攻击风险!", var5);
    return false;
}
public class n {
    private static final String[] a = new String[]{"<", "%3C", ">", "%3E", "\\(", "%28", "\\)", "%29", "'", "eval\\((.*)\\)"};
    private static Pattern[] b = new Pattern[]{Pattern.compile("<script>(.*?)</script>", 2), Pattern.compile("src[\r\n]*=[\r\n]*\\'(.*?)\\'", 42), Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", 42), Pattern.compile("</script>", 2), Pattern.compile("<script(.*?)>", 42), Pattern.compile("eval\\((.*?)\\)", 42), Pattern.compile("e\u00adxpression\\((.*?)\\)", 42), Pattern.compile("javascript:", 2), Pattern.compile("vbscript:", 2), Pattern.compile("onload(.*?)=", 42)};

    ...

    private static boolean a(String var0, Pattern var1) {
        Matcher var2 = var1.matcher(var0);
        return var2.find();
    }

    ...
}

并且对/jmreport/shareView/和JimuNoLoginRequired注解的路由直接放行。

if (var4.contains("/jmreport/shareView/")) {
    return true;
}
JimuNoLoginRequired var9 = (JimuNoLoginRequired)var8.getAnnotation(JimuNoLoginRequired.class);
if (j.d(var9)) {
    return true;
}

接着通过jwt验证用户传入token的合法性。

try {
    var10 = this.verifyToken(request);
} catch (Exception var14) {
}
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) {
    if (StringUtils.isBlank(token)) {
        throw new JeecgBoot401Exception("token不能为空!");
    }

    // 解密获得username,用于和数据库进行对比
    String username = JwtUtil.getUsername(token);
    if (username == null) {
        throw new JeecgBoot401Exception("token非法无效!");
    }

    // 查询用户信息
    LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
    //LoginUser user = commonApi.getUserByName(username);
    if (user == null) {
        throw new JeecgBoot401Exception("用户不存在!");
    }
    // 判断用户状态
    if (user.getStatus() != 1) {
        throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
    }
    // 校验token是否超时失效 & 或者账号密码是否错误
    if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
        throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
    }
    return true;
}

显然攻击者未持有合法token,继续往下走发现如果访问的路由在白名单内即放行。

if (!var10) {
    if (this.jimuReportShareService.isSharingEffective(var4, request)) {
        return true;
    } else {
        ...
    }
}
GET_QUERY_INFO("/jmreport/getQueryInfo"),
SHARE_VERIFICATION("/jmreport/share/verification"),
ADD_VIEW_COUNT("/jmreport/addViewCount/"),
SHOW_DATA("/jmreport/show"),
EXPORT_PDF_STREAM("/jmreport/exportPdfStream"),
EXPORT_ALL_EXCEL_STREAM("/jmreport/exportAllExcelStream"),
CHECK_PARAM("/jmreport/checkParam/"),
QUERYMAP_BY_CODE("/jmreport/map/queryMapByCode"),
QUREST_SQL("/jmreport/qurestSql"),
QUREST_API("/jmreport/qurestApi"),
GET_CHAR_DATA("/jmreport/getCharData");

但/jeecg-boot/jmreport/queryFieldBySql未在其中,与此同时我们发现接下来的else分支存在突破点,只要用户在请求中携带previousPage参数,且使isShareingToken方法返回true,即可成功绕过拦截器。

else {
    String var16 = request.getParameter("previousPage");
    if (j.d(var16)) {
        if (this.jimuReportShareService.isShareingToken(var4, request)) {
            return true;
        }
    ...
}

步入isShareingToken方法。

public boolean isShareingToken(String requestPath, HttpServletRequest request) {
    String var3 = request.getHeader("JmReport-Share-Token");
    String var4 = "";
    if (j.c(var3)) {
        var3 = request.getParameter("shareToken");
    }

    String var5 = request.getParameter("jmLink");
    if (j.d(var5)) {
        try {
            byte[] var6 = Base64Utils.decodeFromString(var5);
            String var7 = new String(var6);
            String[] var8 = var7.split("\\|\\|");
            if (ArrayUtils.isNotEmpty(var8) && var8.length == 2) {
                var3 = var8[0];
                var4 = var8[1];
            }
        } catch (IllegalArgumentException var9) {
            a.error("解密失败:" + var9.getMessage());
            a.error(var9.getMessage(), var9);
            return false;
        }
    }

    if (j.c(var3)) {
        return false;
    } else {
        JimuReportShare var10 = this.jimuReportShareDao.getShareByShareToken(var3);
        if (var10 != null) {
            var10 = this.compareToDate(var10);
            if (!"0".equals(var10.getStatus())) {
                return false;
            }
        }

        if (requestPath.startsWith("/jmreport/view")) {
            if (!j.d(var4)) {
                return false;
            }

            Long var11 = this.jimuReportLinkDao.selectLinkCountByLinkId(var4);
            if (null != var11 && var11 > 0L) {
                return true;
            }
        }

        return true;
    }
}

不难发现其先提取解码后的jmLink参数值赋给var3、var4,并交由getShareByShareToken判断share_token有效性,但显然攻击者传入的值,在数据库中是查不到对应的 JimuReportShare 对象的,即var10为null。

@Sql("select status,share_token,last_update_time,term_of_validity from jimu_report_share where share_token = :shareToken")
JimuReportShare getShareByShareToken(@Param("shareToken") String var1);

而这里之后又辨别了路由是否以/jmreport/view开头,如果不是则返回true,显然我们请求的路由不包括在内。

所以实际环境验证成功。

1724756315_66cdb15b34ab85e763271.png!small

接着根据漏洞情报(https://github.com/jeecgboot/JimuReport/issues/2848),可定位至/jeecg-boot/jmreport/save和/jeecg-boot/jmreport/show两个接口。

对应函数分别为:

org.jeecg.modules.jmreport.desreport.service.a.e#saveReport()
org.jeecg.modules.jmreport.desreport.service.a.e#show()

前者的作用是将用户的报表数据保存至数据库,后者则是通过前面传入的id提取数据。

@GetMapping({"/show"})
public Result<?> a(@RequestParam(name = "id") String var1, HttpServletRequest var2) {
    a.info("--------进入show方法-----------, id = " + var1);
    String var3 = var2.getParameter("params");

    try {
        return this.jmReportDesignService.show(var1, var3, (List)null);
    } catch (Exception var5) {
        a.error(var5.getMessage(), var5);
        return Result.error(var5.getMessage());
    }
}

至于漏洞字段,则位于org.jeecg.modules.jmreport.desreport.express.a#c(),这里限制了传入格式,即只解析”text”字段中,通过=()包含的值。

String var9 = var8.getString("text");
if (var9 != null && !var9.isEmpty()) {
    var9 = var9.trim();
    Pattern var10 = Pattern.compile("^([^=]*)(=[A-Z_.a-z0-9]*\\(.*\\))(([*/+\\-][0-9]+)*)(.*)");
    Matcher var11 = var10.matcher(var9);
    String var12;
    String var15;
    if (var11.find()) {
        var12 = var11.group(2);
        String var13 = var12.substring(var12.startsWith("=") ? 1 : 0, var12.indexOf("("));
        if (ExpressUtil.f.contains(var13.toLowerCase())) {
            String var14 = var11.group(1);
            var15 = var11.group(3);
            String var16 = var11.group(5);
            var8.put("jeTempText", var14 + "$expVal$" + var16);
            var9 = var12.trim() + var15.trim();
            var8.put("text", var9);
        }
    }
...

最终编译攻击者传入的payload。

public Expression compile(String expression, boolean cached) {
    return this.compile(expression, expression, cached);
}

漏洞复现

首先通过/jeecg-boot/jmreport/save,构造恶意报表数据。

1724756333_66cdb16d0d6b67299a9ad.png!small

再借助/jeecg-boot/jmreport/show根据id提取数据,即可触发该漏洞。

1724756344_66cdb178398457ff25a5b.png!small

1724756353_66cdb1816353f6dc38f10.png!small

修复方案

不推荐直接将脚本的执行开放到任何不可信的环境,如果确实需要,可以参考https://www.yuque.com/boyan-avfmj/aviatorscript/ou23gy#elOSu,设置必要的选项。

# 漏洞 # web安全 # 攻防演练 # 漏洞分析 # 企业安全
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录