RmiTaste源码浅析及实战运用

这两天社区里有一位师傅分享了他对JiraCVE-2020-36239的分析,内容写的很详细。于是我跟着他的教程进行了复现,在对RMI进行探测时,师傅说使用了RmiTast...

这两天社区里有一位师傅分享了他对JiraCVE-2020-36239的分析,内容写的很详细。于是我跟着他的教程进行了复现,在对RMI进行探测时,师傅说使用了RmiTaste进行检测,没有收到结果。同时我在另一篇分析CVE-2020-36239的文章中看到作者说他用RmiTaste检测时遇到了些问题,简单的patch后,RmiTaste变得可以使用,并且利用RmiTaste完成了后续的攻击。因此我对这个工具的使用产生了兴趣,因此搭建了一个Jira的环境,并进行了复现,中间产生了一些意料之外的问题,比较有意思,因此写下此文记录一下学到的知识,以及遇到的和解决的问题。

源码分析

RmiTaste可以帮助广大安全研究专家通过调用ysoserial实用工具所提供的远程方法来检测、枚举、交互和攻击RMI服务。除此之外,它还允许我们使用特定的参数来调用远程方法。

运行RmiTaste,可以看到它存在四种指令

  1. 连接。检测到目标服务是否联通
  2. 枚举。枚举目标主机中的服务
  3. 攻击。使用ysoserial中的序列化链攻击目标主机
  4. 调用。调用远程目标服务上的特殊方法。

它的四种指令,在代码里对应的是Commands四个类。它们都继承自BasicCommand

这里顺带说一下RmiTaste的目录结构。

  • helpers目录主要是一些IO读写类、命令行参数解析类

  • rmitaste目录是主要的代码,下面包含三个目录以及一个入口文件
  1. - commands
  2. 四种命令的实现类
  3. - rmi
  4. 和远程主机交互时的连接、处理类
  5. - utils
  6. 日志相关的工具类

在命令行输入命令,以及其需要的参数,就会调用相应命令类的call函数

1.ConnectionCommand执行的命令是

  1. Enumerate.connect(this.target, this.port);

这里的Enumerate是这个工具处理远程操作的一个总的处理类,它的connect方法,本质上调用的是

  1. Registry reg = LocateRegistry.getRegistry(host, port);

2.EnumerateCommand是最主要的一个类,它和后面的两个命令类都会调用如下的命令

  1. Enumerate enumerate = new Enumerate(this.target, this.port);
  2. enumerate.enumerate();
  3. RmiRegistry rmiRegistry = enumerate.getRegistry();

其中最主要的就是enumerate.enumerate();,这个函数负责连接目标、获取对象名称和相应的类以及绑定。

  1. public void enumerate() throws Throwable{
  2. // Connect
  3. this.registry = Enumerate.connect(this.registry);
  4. // Get object names and corresponding classes
  5. this.registry.loadObjects();
  6. // Get references to objects
  7. this.registry.loadObjectRef();
  8. }

第一步connect会生成一个和远程建立了连接的Registry对象,重点是第二步的loadObjects(),它首先会调用registry的list方法,获取目标主机的RMI服务名列表。接着遍历服务名调用lookup方法,这里的lookup方法,不再是RegistryImpl_Stub原生的lookup方法,最主要的区别是,原生的lookup方法,在向目标主机发送了我们期望的服务名后,会直接调用readObject方法,序列化远程主机传过来的对象。但是这样做的前提是我们的classpath中存在目标主机传回来的对象的class文件,否则就会报错。

举个例子

  1. public static void main(String[] args) throws Exception {
  2. String host = "127.0.0.1";
  3. int port =40001;
  4. Registry registry = LocateRegistry.getRegistry(host, port);
  5. for (String name :registry.list()){
  6. System.out.println(name);
  7. }
  8. Remote r= registry.lookup(registry.list()[0]);
  9. }

使用如上代码去请求jira的远程服务,可以看到在lookup时出错了

根本原因在于,原生反序列化中会调用Class.forName去获取类,并生成对象。RmiTaste的处理方法是首先通过反射获取输入流里的byte流,然后使用RmiObjectParser去解析并生成对象。RmiObjectParser的解析流程和ObjectInputStream中的解析流程保持一致,但是避免使用class.forName等需要目标类在classpath中的操作,而是将class名作为一个RmiObjectClass对象的属性进行保存,并把所有获取到的类保存到RmiObject对象的classes属性中

  1. public RmiObject(String nameL){
  2. name = nameL;
  3. classes = new HashMap<String, RmiObjectClass>();
  4. isJMX = false;
  5. isDynamicStub = true;
  6. }
  7. public RmiObjectClass(String nameL, boolean isInterface){
  8. name = nameL;
  9. methods = new HashMap<String, RmiObjectMethod>();
  10. reference = null;
  11. simpleClassLoader = new SimpleClassLoader();
  12. this.isInterface = isInterface;
  13. this.isRemote = false;
  14. }

RmiTaste用这样的方式代替了原生的readObject。

接着是第三步loadObjectRef();这里主要是遍历获取到的RmiObject列表,执行真正的RegistryImpl_Stub.lookup方法,以获取远程对象。

至此,和远程服务的交互结束,接下来就是收集到信息的展示。在处理完远程流之后,就会调用RmiObject的toString方法对服务名和类、方法等进行输出,如果在前面第三步中成功获取到了远程对象,那么此处就会打印Method信息,否则,只会打印服务名和绑定的类名

如果想要读到完整信息,还是需要将目标类也加入classpath中。

3.AttackCommand是攻击模块,在攻击前同样使用如下代码进行信息收集

  1. Enumerate enumerate = new Enumerate(this.target, this.port);
  2. enumerate.enumerate();
  3. RmiRegistry rmiRegistry = enumerate.getRegistry();

该模块存在许多参数,允许调用目标单个方法、或者从文件中读多个方法、或者批量执行目标绑定类的方法,以及执行ysoserial中所有的payload、或者指定payload,最终调用反射进行远程调用,同时把恶意代码作为参数传入远程调用中。

  1. Attack attack = new Attack(rmiRegistry, payloadGenerator);
  2. attack.attackRegistry(methodsList);
  3. RemoteRef.invoke()

4.CallCommand相当于一个工具命令,封装了一些远程调用的方法,通过传入参数进行远程调用。

  1. Attack attack = new Attack(rmiRegistry);
  2. Object result = attack.invokeMethod(elements.get(2), elements.get(3), elements.get(4), params);

实战运用

在对Jira的40001端口探测的使用中出现了两个Bug,都是出现在RmiObjectParser的解析过程中,由于这里模拟的是原生的对java字节码的处理,原作者可能考虑的情况不够完全,所以出现了一些问题。在Debug的同时,也学习了很多java字节码相关的知识,因此记录一下。

  1. Class Desc有一个classAnnotations的属性,在解析目标Class的Desc的时候,程序总是走到TC_STRING的处理流程,然后在获取UTF-String的时候出错,其获取UTF-String的代码如下
  1. private String getUtfShort(){
  2. int len = this.getShort();
  3. byte[] bytes = this.getBytes(len);
  4. return new String(bytes);
  5. }
  6. private Short getShort(){
  7. Short r = (short) (((this.getByte() << 8) &amp; 0xFF00 ) | (this.getByte() &amp;0xFF));
  8. return r;
  9. }

每次在解析classAnnotations时,len总会为负数,这个len会用于生成一个new byte[num]数组,由于长度不能为负,程序就会报错。

为了解决这个bug,我进行了抓包,

这样的数据看不直观,将返回的数据,从aced开始,复制一部分序列化数据,使用SerializationDumper进行分析,SerializationDumper是一个分析序列化数据流的工具。

可以看到,SerializationDumper分析出来的classAnnotation中,第一个byte为0x74,对应TC_STRING,然后两个byte标识String的长度,这里的长度为0x99cf,转换为int为39375。但是在RmiTaste的代码中,这里获取String长度使用的是short,这本身也没有错,因为两byte的数字就应该对应short,但是short类型,最大为32767,因此更大的数就会变为负数,导致了bug的产生。这里修复的方法就是新写一个getBigShort函数,直接将两byte的数据转换为int类型

  1. private Integer getBigShort(){
  2. Integer r = (Integer) (((this.getByte() << 8) &amp; 0xFF00 ) | (this.getByte() &amp;0xFF));
  3. return r;
  4. }
  1. 第二个遇到的问题和国外的那篇jira分析文章的作者遇到的问题相同
  1. private void parseClassAnnotation(){
  2. // Skip annotation element
  3. Byte b = 0x00;
  4. while (true){
  5. b = this.getByte();
  6. if(b == TC_ENDBLOCKDATA){
  7. break;
  8. }
  9. else if(b == TC_REFERENCE){
  10. this.getInteger();
  11. }
  12. else if(b == TC_STRING){
  13. this.getUtfShort();
  14. }
  15. }
  16. }

parseClassAnnotation函数中,如果程序读完了Annotation那么它还会继续循环,因为它使用的是while(true)循环,但是如果此时数据已经读完了,getByte就会返回-1,当然正常情况下b应该会返回TC_ENDBLOCKDATA从而结束循环,但是如第一个bug所示,当Annotation特别大时,更容易会出现问题。

  1. private Byte getByte(){
  2. if(this.index < this.streamSize){
  3. Byte b = this.stream.get(this.index);
  4. this.index++;
  5. return b;
  6. }
  7. this.logger.severe("No more bytes to read. Limit has been reached!");
  8. return -1;
  9. }

如果你足够清醒,你会意识到classAnnotations表示和类相关的Annotation的描述信息,它在这个工具中后续的流程中其实起不了什么作用,因此如果只是为了让工具正常运行,其实不需要对这个Annotation的读取太过关心,只需要处理好异常就可以了。

因此这里debug的方法也很简单,在parseClassAnnotation的循环中,如果getByte返回了-1,那么直接退出循环即可,而不是继续在循环里读数据。

总结

对这个工具进行分析和使用的起因是社区师傅的一篇Jira漏洞的分析文章。接着就面临了贴近实战的Rmi探测到攻击的实践,在实践的同时遇到了一些现象和问题,通过阅读源码和动态调试去解释现象,解决问题,最后就有了这篇源码浅析以及bug记录的文章。文章中可能存在一些不严谨的地方,希望大家一起讨论一起学习一起进步。

参考

奇安信攻防社区-CVE-2020-36239 - Jira 多款产品RCE漏洞分析 (butian.net)

Developing an exploit for the Jira Data Center Ehcache RCE (CVE-2020-36239) | dozer.nz

  • 发表于 2021-09-23 18:03:55
  • 阅读 ( 6823 )
  • 分类:漏洞分析

0 条评论

无糖
无糖

8 篇文章

站长统计