前言
本文参考:jeecg-boot 3.8.3 debug远程命令执行漏洞分析
漏洞描述及详情见: https://github.com/jeecgboot/JeecgBoot/issues/9144
需任意用户权限
本文主要是静态分析,动态调试可见参考文章
漏洞分析
下面是漏洞利用请求包,也是接下来进行分析的前提
1 | POST /jeecgboot/airag/flow/debug HTTP/1.1 |
漏洞路由地址/jeecgboot/airag/flow/debug
看到调用了debugFlow方法,我们跟进去

前面的代码片段都是在进行赋值等操作,重点关注最后两行调用的a方法,我们先跟进第一个a方法,看看它的作用是什么
该方法的主要作用是解析 design JSON、注册脚本节点、注册 Chain、构建 JeecgFlowContext
对于我们来说,重点关注解析design JSON部分和注册Chain部分
首先是第一部分
1 | String design = flow.getDesign(); //用户可控的 JSON 字符串 |
CodeNode.a(flowNodeObj) 是对每个流程节点的类型检查与脚本注册方法。它只对 type="code" 的节点生效,其他类型(start、end、llm 等)直接跳过。
1 | public static void a(FlowNode flowNodeObj) { |
其中的build()方法主要做了如下的操作,在这里面重点除了编译了恶意脚本外就是绑定了ScriptCommonComponent组件(后续分析中会用到)
1 | .build() |
编译过程中将编译好的 Groovy 脚本存进了compiledScriptMap,而compiledScriptMap是一个Map<String, CompiledScript>,比如如下所示
1 | compiledScriptMap = { |
接下来关注注册Chain部分的代码
1 | //将编排规则注册到 FlowBus |
FlowBus.reloadChain() 将用户传入的 chain EL 表达式编译为 ThenCondition 并注册。后续 FlowExecutor.doExecute() 通过 FlowBus.getChain(chainId) 取出这个编排规则来执行。
至此第一个a方法便结束了,往下走到第二个a方法

开头是执行一个a()方法
该方法的目的是为了确保内置节点已注册到 FlowBus,在后面的代码中会用到
然后根据 responseMode 选择同步或异步方式执行 LiteFlow 链路
从请求包中的"responseMode": "streaming"可以知道是走进else从句中
1 | } else { |
显而易见重点就在于倒二行的this.flowExecutor.execute2Future方法,我们跟进去
库库跟了之后会走到FlowExecutor.doExcute方法处
首先进行了一个FlowBus的初始化检查之后,会进行Slot的分配操作
1 | Integer slotIndex; |
Slot 是什么:LiteFlow 的数据总线 DataBus 维护一个 Slot 池(类似线程池的概念)。每次流程执行都需要占用一个 Slot 来存储本次执行的上下文数据、执行步骤、结果等。
在本漏洞场景:contextBeanClazzArray = null,contextBeanArray = [JeecgFlowContext],所以走 offerSlotByBean 分支——直接用用户传入的 JeecgFlowContext 实例初始化 Slot。这意味着攻击者通过 POST body 间接控制的上下文对象被放入了执行环境。
往下走,FlowExecutor.doExcute方法中重点关注后面chain的获取和执行操作
1 | chain = FlowBus.getChain(chainId); |
编排规则是 THEN(start, code_252727837582266368, end),所以chain.execute方法会按顺序依次执行节点
我们跟进去

首先会进行一个condition列表的获取,conditionList 就是经过 EL 编译后的执行条件列表,这个列表即是
1 | conditionList = [ ThenCondition ] |
该方法继续往下走,会遍历condition并依次执行

对每个 Condition:
- 设置当前 Chain ID(用于嵌套链路追踪)
- 调用
condition.execute(slotIndex)
而我们的列表中只有一个ThenCondition,跟进condition.execute(slotIndex);
可以看到该类是一个抽象类,所以真正运行该方法的是它的继承类
Condition.execute() 中调用的 this.executeCondition(slotIndex) 是抽象方法,**this 永远指向实际对象**,不是声明类型
而在这里很明显就是ThenCondition类,走进去
List<PreCondition> preConditionList = this.getPreConditionList();该行代码所要收集的List列表对应 LiteFlow EL 中的 PRE(...) 语法。在我们的 PoC 中没有 PRE 条件,preConditionList 为空
因此在while循环中会直接进入到if从句中
var17 = this.getExecutableList().iterator();返回 EL 编译后的执行元素列表,对 PoC 中的 chain
executableList=[Node("start"), Node("code_xxx"), Node("end")]在while循环中会串行遍历执行,每个 Executable 在这里就是一个Node 对象,我们重点关注的是第二个Node
我们继续跟进去
继续跟进去
在这里第二个Node绑定的instance根据上面的LiteFlowNodeBuilder.build()可知绑定的组件是ScriptCommonComponent
继续跟进instance.execute();
由图我们可知该类中并没有execute方法,所以会向上查找其父类NodeComponent,利用它的execute方法
跟进this.self.process();,就走回到了ScriptCommonComponent类里面的process方法
this.buildWrap(this) 的作用是将当前脚本组件的运行时上下文打包成一个 ScriptExecuteWrap 对象,供后续脚本执行器使用。其实本质上就是将分散在各处的运行信息给集中起来
继续跟进this.scriptExecutor.execute(wrap);
抽象类,由于poc里面的是groovy脚本,所以是走进JSR223ScriptExecutor的executeScript方法
1 | public Object executeScript(ScriptExecuteWrap wrap) throws Exception { |
if从句检测脚本是否已经成加载并存到了compiledScriptMap中,而从我们一开始的分析中便知道了在LiteFlowNodeBuilder.build()方法中便已经存放进去了
跟进return compiledScript.eval(bindings);
动用groovy引擎来执行恶意脚本,达到rce的成果
漏洞复现
AI大模型–AI流程设计–添加流程

添加成功后进去添加脚本执行
