diff --git a/lib/msf/core/exploit/mixins.rb b/lib/msf/core/exploit/mixins.rb index 4bda434584..76cfac86e4 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -88,3 +88,6 @@ require 'msf/core/exploit/java' # WBEM require 'msf/core/exploit/wbemexec' +#WinRM +require 'msf/core/exploit/winrm' + diff --git a/lib/msf/core/exploit/winrm.rb b/lib/msf/core/exploit/winrm.rb new file mode 100644 index 0000000000..40db61aa8b --- /dev/null +++ b/lib/msf/core/exploit/winrm.rb @@ -0,0 +1,471 @@ +# -*- coding: binary -*- +require 'uri' +require 'digest' +require 'rex/proto/ntlm/crypt' +require 'rex/proto/ntlm/constants' +require 'rex/proto/ntlm/utils' +require 'rex/proto/ntlm/exceptions' + +module Msf +module Exploit::Remote::WinRM + + include Exploit::Remote::NTLM::Client + include Exploit::Remote::HttpClient + + # + # Constants + # + NTLM_CRYPT ||= Rex::Proto::NTLM::Crypt + NTLM_CONST ||= Rex::Proto::NTLM::Constants + NTLM_UTILS ||= Rex::Proto::NTLM::Utils + NTLM_XCEPT ||= Rex::Proto::NTLM::Exceptions + + def initialize(info = {}) + super + register_options( + [ + Opt::RHOST, + Opt::RPORT(5985), + OptString.new('VHOST', [ false, "HTTP server virtual host" ]), + OptBool.new('SSL', [ false, 'Negotiate SSL for outgoing connections', false]), + OptEnum.new('SSLVersion', [ false, 'Specify the version of SSL that should be used', 'SSL3', ['SSL2', 'SSL3', 'TLS1']]), + OptString.new('DOMAIN', [ true, 'The domain to use for Windows authentification', 'WORKSTATION']), + OptString.new('URI', [ true, "The URI of the WinRM service", "/wsman" ]), + OptString.new('USERNAME', [ false, 'A specific username to authenticate as' ]), + OptString.new('PASSWORD', [ false, 'A specific password to authenticate with' ]) + ], self.class + ) + + register_autofilter_ports([ 80,443,5985,5986 ]) + register_autofilter_services(%W{ winrm }) + end + + def winrm_poke(timeout = 20) + opts = { + 'uri' => datastore['URI'], + 'data' => Rex::Text.rand_text_alpha(8) + } + + c = connect(opts) + to = opts[:timeout] || timeout + ctype = "application/soap+xml;charset=UTF-8" + + resp, c = send_request_cgi(opts.merge({ + 'uri' => opts['uri'], + 'method' => 'POST', + 'ctype' => ctype, + 'data' => opts['data'] + }), to) + + return resp + end + + def parse_auth_methods(resp) + return [] unless resp and resp.code == 401 + methods = [] + methods << "Negotiate" if resp.headers['WWW-Authenticate'].include? "Negotiate" + methods << "Kerberos" if resp.headers['WWW-Authenticate'].include? "Kerberos" + methods << "Basic" if resp.headers['WWW-Authenticate'].include? "Basic" + return methods + end + + def winrm_run_cmd(cmd, timeout=20) + resp,c = send_request_ntlm(winrm_open_shell_msg,timeout) + + if resp.code == 401 + print_error "Login failure! Recheck supplied credentials." + return resp .code + end + + unless resp.code == 200 + print_error "Got unexpected response: \n #{resp.to_s}" + retval == resp.code || 0 + return retval + end + + shell_id = winrm_get_shell_id(resp) + resp,c = send_request_ntlm(winrm_cmd_msg(cmd, shell_id),timeout) + cmd_id = winrm_get_cmd_id(resp) + resp,c = send_request_ntlm(winrm_cmd_recv_msg(shell_id,cmd_id),timeout) + streams = winrm_get_cmd_streams(resp) + resp,c = send_request_ntlm(winrm_terminate_cmd_msg(shell_id,cmd_id),timeout) + resp,c = send_request_ntlm(winrm_delete_shell_msg(shell_id)) + + return streams + end + + def winrm_wql_msg(wql) + action = winrm_uri_action("wql") + contents = winrm_header(action) + winrm_wql_body(wql) + msg = winrm_envelope(contents) + + return msg + end + + def winrm_open_shell_msg + action = winrm_uri_action("create_shell") + options = winrm_option_set([['WINRS_NOPROFILE', 'FALSE'], ['WINRS_CODEPAGE', '437']]) + header_data = action + options + contents = winrm_header(header_data) + winrm_open_shell_body + msg = winrm_envelope(contents) + + return msg + end + + def winrm_cmd_msg(cmd,shell_id) + action = winrm_uri_action("send_cmd") + options = winrm_option_set([['WINRS_CONSOLEMODE_STDIN', 'TRUE'], ['WINRS_SKIP_CMD_SHELL', 'FALSE']]) + selectors = winrm_selector_set([['ShellId', shell_id]]) + header_data = action + options + selectors + contents = winrm_header(header_data) + winrm_cmd_body(cmd) + msg = winrm_envelope(contents) + + return msg + end + + def winrm_cmd_recv_msg(shell_id,cmd_id) + action = winrm_uri_action("recv_cmd") + selectors = winrm_selector_set([['ShellId', shell_id]]) + header_data = action + selectors + contents = winrm_header(header_data) + winrm_cmd_recv_body(cmd_id) + msg = winrm_envelope(contents) + + return msg + end + + def winrm_terminate_cmd_msg(shell_id,cmd_id) + action = winrm_uri_action("signal_shell") + selectors = winrm_selector_set([['ShellId', shell_id]]) + header_data = action + selectors + contents = winrm_header(header_data) + winrm_terminate_cmd_body(cmd_id) + msg = winrm_envelope(contents) + + return msg + end + + def winrm_delete_shell_msg(shell_id) + action = winrm_uri_action("delete_shell") + selectors = winrm_selector_set([['ShellId', shell_id]]) + header_data = action + selectors + contents = winrm_header(header_data) + winrm_empty_body + msg = winrm_envelope(contents) + + return msg + end + + def parse_wql_response(response) + xml = response.body + columns = [] + rows =[] + rxml = REXML::Document.new(xml).root + items = rxml.elements["///w:Items"] + + items.elements.to_a("///w:XmlFragment").each do |node| + row_data = [] + + node.elements.to_a.each do |sub_node| + columns << sub_node.name + row_data << sub_node.text + end + + rows << row_data + end + + response_data = Rex::Ui::Text::Table.new( + 'Header' => "#{datastore['WQL']} (#{rhost})", + 'Indent' => 1, + 'Columns' => columns.uniq! + ) + + rows.each do |row| + response_data << row + end + + return response_data + end + + def winrm_get_shell_id(response) + xml = response.body + shell_id = REXML::Document.new(xml).elements["//w:Selector"].text + end + + def winrm_get_cmd_id(response) + xml = response.body + cmd_id = REXML::Document.new(xml).elements["//rsp:CommandId"].text + end + + def winrm_get_cmd_streams(response) + streams = { + 'stdout' => '', + 'stderr' => '', + } + + xml = response.body + rxml = REXML::Document.new(xml).root + + rxml.elements.to_a("//rsp:Stream").each do |node| + next if node.text.nil? + streams[node.attributes['Name']] << Rex::Text.base64_decode(node.text) + end + + return streams + end + + def generate_uuid + ::Rex::Proto::DCERPC::UUID.uuid_unpack(Rex::Text.rand_text(16)) + end + + def send_request_ntlm(data, timeout = 20) + opts = { + 'uri' => datastore['URI'], + 'data' => data, + 'username' => datastore['USERNAME'], + 'password' => datastore['PASSWORD'] + } + + ntlm_options = + { + :signing => false, + :usentlm2_session => datastore['NTLM::UseNTLM2_session'], + :use_ntlmv2 => datastore['NTLM::UseNTLMv2'], + :send_lm => datastore['NTLM::SendLM'], + :send_ntlm => datastore['NTLM::SendNTLM'] + } + + ntlmssp_flags = NTLM_UTILS.make_ntlm_flags(ntlm_options) + workstation_name = Rex::Text.rand_text_alpha(rand(8)+1) + domain_name = datastore['DOMAIN'] + ntlm_message_1 = "NEGOTIATE " + Rex::Text::encode_base64(NTLM_UTILS::make_ntlmssp_blob_init( domain_name, + workstation_name, + ntlmssp_flags)) + + to = opts[:timeout] || timeout + + begin + c = connect(opts) + ctype = "application/soap+xml;charset=UTF-8" + # First request to get the challenge + r = c.request_cgi(opts.merge({ + 'uri' => opts['uri'], + 'method' => 'POST', + 'ctype' => ctype, + 'headers' => { 'Authorization' => ntlm_message_1}, + 'data' => opts['data'] + })) + + resp = c.send_recv(r, to) + + unless resp.kind_of? Rex::Proto::Http::Response + return [nil,nil] + end + + return [nil,nil] if resp.code == 404 + return [nil,nil] unless resp.code == 401 && resp.headers['WWW-Authenticate'] + # Get the challenge and craft the response + ntlm_challenge = resp.headers['WWW-Authenticate'].match(/NEGOTIATE ([A-Z0-9\x2b\x2f=]+)/i)[1] + return [nil,nil] unless ntlm_challenge + + #old and simplier method but not compatible with windows 7/2008r2 + #ntlm_message_2 = Rex::Proto::NTLM::Message.decode64(ntlm_challenge) + #ntlm_message_3 = ntlm_message_2.response( {:user => opts['username'],:password => opts['password']}, {:ntlmv2 => true}) + ntlm_message_2 = Rex::Text::decode_base64(ntlm_challenge) + blob_data = NTLM_UTILS.parse_ntlm_type_2_blob(ntlm_message_2) + challenge_key = blob_data[:challenge_key] + server_ntlmssp_flags = blob_data[:server_ntlmssp_flags] #else should raise an error + #netbios name + default_name = blob_data[:default_name] || '' + #netbios domain + default_domain = blob_data[:default_domain] || '' + #dns name + dns_host_name = blob_data[:dns_host_name] || '' + #dns domain + dns_domain_name = blob_data[:dns_domain_name] || '' + #Client time + chall_MsvAvTimestamp = blob_data[:chall_MsvAvTimestamp] || '' + spnopt = {:use_spn => datastore['NTLM::SendSPN'], :name => self.rhost} + resp_lm, + resp_ntlm, + client_challenge, + ntlm_cli_challenge = NTLM_UTILS.create_lm_ntlm_responses(opts['username'], opts['password'], challenge_key, + domain_name, default_name, default_domain, + dns_host_name, dns_domain_name, chall_MsvAvTimestamp, + spnopt, ntlm_options) + ntlm_message_3 = NTLM_UTILS.make_ntlmssp_blob_auth(domain_name, workstation_name, opts['username'], + resp_lm, resp_ntlm, '', ntlmssp_flags) + ntlm_message_3 = Rex::Text::encode_base64(ntlm_message_3) + + # Send the response + r = c.request_cgi(opts.merge({ + 'uri' => opts['uri'], + 'method' => 'POST', + 'ctype' => ctype, + 'headers' => { 'Authorization' => "NEGOTIATE #{ntlm_message_3}"}, + 'data' => opts['data'] + })) + + resp = c.send_recv(r, to, true) + + unless resp.kind_of? Rex::Proto::Http::Response + return [nil,nil] + end + + return [nil,nil] if resp.code == 404 + return [resp,c] + rescue ::Errno::EPIPE, ::Timeout::Error + end + end + + def accepts_ntlm_auth + parse_auth_methods(winrm_poke).include? "Negotiate" + end + + def target_url + proto = "http" + if rport == 5986 or datastore['SSL'] + proto = "https" + end + + if datastore['VHOST'] + return "#{proto}://#{datastore ['VHOST']}:#{rport}#{@uri.to_s}" + else + return "#{proto}://#{rhost}:#{rport}#{@uri.to_s}" + end + end + + + + private + + def winrm_option_set(options) + xml = "" + + options.each do |option_pair| + xml << winrm_option(*option_pair) + end + + xml << "" + return xml + end + + def winrm_option(name,value) + %Q{#{value}} + end + + def winrm_selector_set(selectors) + xml = "" + + selectors.each do |selector_pair| + xml << winrm_selector(*selector_pair) + end + + xml << "" + return xml + end + + def winrm_selector(name,value) + %Q{#{value}} + end + + def winrm_wql_body(wql) + %Q{ + + + + 32000 + #{wql} + + + } + end + + def winrm_open_shell_body + %q{ + + stdin + stdout stderr + + } + end + + def winrm_cmd_body(cmd) + %Q{ + + "#{cmd}" + + } + end + + def winrm_cmd_recv_body(cmd_id) + %Q{ + + stdout stderr + + } + end + + def winrm_terminate_cmd_body(cmd_id) + %Q{ + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate + + } + end + + def winrm_empty_body + %q{} + end + + def winrm_envelope(data) + %Q{ + + #{data} + } + end + + def winrm_header(data) + %Q{ + + #{target_url} + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:#{generate_uuid} + + + PT60S + #{data} + + } + end + + def winrm_uri_action(type) + case type + when "wql" + return %q{http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/* + http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate} + when "create_shell" + return %q{http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.xmlsoap.org/ws/2004/09/transfer/Create} + when "send_cmd" + return %q{http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command} + when "recv_cmd" + return %q{http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive} + when "signal_shell" + return %q{http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal} + when "delete_shell" + return %q{http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete} + end + end + +end +end diff --git a/modules/auxiliary/scanner/winrm/winrm_auth_methods.rb b/modules/auxiliary/scanner/winrm/winrm_auth_methods.rb new file mode 100644 index 0000000000..910fe06ff2 --- /dev/null +++ b/modules/auxiliary/scanner/winrm/winrm_auth_methods.rb @@ -0,0 +1,64 @@ +## +# $Id$ +## + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + + +require 'msf/core' +require 'rex/proto/ntlm/message' + + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::WinRM + include Msf::Auxiliary::Report + + + include Msf::Auxiliary::Scanner + + def initialize + super( + 'Name' => 'WinRM Authentication Method Detection', + 'Version' => '$Revision$', + 'Description' => %q{ + This module sends a request to an HTTP/HTTPS service to see if it is a WinRM service. + If it is a WinRM service, it also gathers the Authentication Methods supported. + }, + 'Author' => [ 'thelightcosine' ], + 'License' => MSF_LICENSE + ) + + deregister_options('USERNAME', 'PASSWORD') + + end + + + def run_host(ip) + resp = winrm_poke + return nil if resp.nil? + if resp.code == 401 and resp.headers['Server'].include? "Microsoft-HTTPAPI" + methods = parse_auth_methods(resp) + desc = resp.headers['Server'] + " Authentication Methods: " + methods.to_s + report_service( + :host => ip, + :port => rport, + :proto => 'tcp', + :name => 'winrm', + :info => desc + ) + print_good "Negotiate protocol supported" if methods.include? "Negotiate" + print_good "Kerberos protocol supported" if methods.include? "Kerberos" + print_good "Basic protocol supported" if methods.include? "Basic" + else + print_error "#{ip}:#{rport} Does not appear to be a WinRM server" + end + end + + +end