From 3a8dd261aebc56d6f56074a844932460ab6a311a Mon Sep 17 00:00:00 2001 From: David Maloney Date: Fri, 19 Oct 2012 15:08:58 -0500 Subject: [PATCH] WinRM mixin and basic discovery module --- lib/msf/core/exploit/winrm.rb | 480 ++++++++++++++++++ .../scanner/winrm/winrm_auth_methods.rb | 68 +++ 2 files changed, 548 insertions(+) create mode 100644 lib/msf/core/exploit/winrm.rb create mode 100644 modules/auxiliary/scanner/winrm/winrm_auth_methods.rb diff --git a/lib/msf/core/exploit/winrm.rb b/lib/msf/core/exploit/winrm.rb new file mode 100644 index 0000000000..e793f02cd1 --- /dev/null +++ b/lib/msf/core/exploit/winrm.rb @@ -0,0 +1,480 @@ +# -*- 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_advanced_options( + [ + OptString.new('UserAgent', [false, 'The User-Agent header to use for all requests', + Rex::Proto::Http::Client::DefaultUserAgent + ]), + ], self.class + ) + + register_evasion_options( + [ + OptEnum.new('HTTP::uri_encode_mode', [false, 'Enable URI encoding', 'hex-normal', ['none', 'hex-normal', 'hex-all', 'hex-random', 'u-normal', 'u-all', 'u-random']]), + OptBool.new('HTTP::uri_full_url', [false, 'Use the full URL for all HTTP requests', false]), + OptInt.new('HTTP::pad_method_uri_count', [false, 'How many whitespace characters to use between the method and uri', 1]), + OptInt.new('HTTP::pad_uri_version_count', [false, 'How many whitespace characters to use between the uri and version', 1]), + OptEnum.new('HTTP::pad_method_uri_type', [false, 'What type of whitespace to use between the method and uri', 'space', ['space', 'tab', 'apache']]), + OptEnum.new('HTTP::pad_uri_version_type', [false, 'What type of whitespace to use between the uri and version', 'space', ['space', 'tab', 'apache']]), + OptBool.new('HTTP::method_random_valid', [false, 'Use a random, but valid, HTTP method for request', false]), + OptBool.new('HTTP::method_random_invalid', [false, 'Use a random invalid, HTTP method for request', false]), + OptBool.new('HTTP::method_random_case', [false, 'Use random casing for the HTTP method', false]), + OptBool.new('HTTP::uri_dir_self_reference', [false, 'Insert self-referential directories into the uri', false]), + OptBool.new('HTTP::uri_dir_fake_relative', [false, 'Insert fake relative directories into the uri', false]), + OptBool.new('HTTP::uri_use_backslashes', [false, 'Use back slashes instead of forward slashes in the uri ', false]), + OptBool.new('HTTP::pad_fake_headers', [false, 'Insert random, fake headers into the HTTP request', false]), + OptInt.new('HTTP::pad_fake_headers_count', [false, 'How many fake headers to insert into the HTTP request', 0]), + OptBool.new('HTTP::pad_get_params', [false, 'Insert random, fake query string variables into the request', false]), + OptInt.new('HTTP::pad_get_params_count', [false, 'How many fake query string variables to insert into the request', 16]), + OptBool.new('HTTP::pad_post_params', [false, 'Insert random, fake post variables into the request', false]), + OptInt.new('HTTP::pad_post_params_count', [false, 'How many fake post variables to insert into the request', 16]), + OptBool.new('HTTP::uri_fake_end', [false, 'Add a fake end of URI (eg: /%20HTTP/1.0/../../)', false]), + OptBool.new('HTTP::uri_fake_params_start', [false, 'Add a fake start of params to the URI (eg: /%3fa=b/../)', false]), + OptBool.new('HTTP::header_folding', [false, 'Enable folding of HTTP headers', false]) + ], 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' => 'test' + } + 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'] + })) + return resp + end + + def parse_auth_methods(resp) + return [] unless 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}" + return resp.code + 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 + columns.uniq! + response_data = Rex::Ui::Text::Table.new( + 'Header' => "#{datastore['WQL']} (#{rhost})", + 'Indent' => 1, + 'Columns' => columns + ) + 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']] << Base64.decode64(node.text) + end + return streams + end + + 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 + + def generate_uuid + bytes = ::SecureRandom.random_bytes(16) + ::Rex::Proto::DCERPC::UUID.uuid_unpack(bytes) + 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 + + 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..c56af6c0ce --- /dev/null +++ b/modules/auxiliary/scanner/winrm/winrm_auth_methods.rb @@ -0,0 +1,68 @@ +## +# $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 Methos Detection', + 'Version' => '$Revision$', + 'Description' => %q{ + This module sends a request to a 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 + ) + + register_options( + [ + OptString.new('URI', [ true, "The URI of the WinRM service", "/wsman" ]) + ], self.class) + + deregister_options('USERNAME', 'PASSWORD') + + end + + + def run_host(ip) + resp = winrm_poke + 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