Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Winrm login workaround #13974

Merged
merged 6 commits into from
Aug 19, 2020
Merged

Conversation

dwelch-r7
Copy link
Contributor

This PR fixes an issue where the winrm login scanner was failing to authenticate with serves that did not accept 'Basic' authentication.

The issues arose from a recent PR which made an attempt to fix logging into winrm servers with 'Basic' authentication here #13442
The data this PR added to the request caused logging into winrm with anything other than 'Basic' authentication to fail

This PR addresses this issue in a couple ways

  • Only when the preferred auth method is set to 'Basic' will the extra data be sent in the request
  • The auth method is defaulted to 'Negotiate' whereas previously it was 'Basic'

@dwelch-r7 dwelch-r7 added the bug label Aug 10, 2020
@@ -246,7 +246,8 @@ def send_request(opts)
begin
cli.connect
req = cli.request_cgi(opts)
res = cli.send_recv(req)
# Authenticate by default
res = opts['authenticate'].nil? || opts['authenticate'] ? cli.send_recv(req) : cli._send_recv(req)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
res = opts['authenticate'].nil? || opts['authenticate'] ? cli.send_recv(req) : cli._send_recv(req)
res = if opts['authenticate'].nil? || opts['authenticate']
cli.send_recv(req)
else
cli._send_recv(req)
end

Comment on lines 54 to 56
opts['authenticate'] = false
allowed_auth_methods = parse_auth_methods(super(opts))
opts['authenticate'] = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This side effect was unexpected for me, maybe something like this is cleaner:

Suggested change
opts['authenticate'] = false
allowed_auth_methods = parse_auth_methods(super(opts))
opts['authenticate'] = true
allowed_auth_methods = parse_auth_methods(super(opts.merge({ 'authenticate' => true }))

But I don't have the full picture here to know if this is the best approach overall just yet

# send an HTTP request that WinRM would consider as valid (SOAP XML in the message matching the XML schema definition)
def send_request(opts)
opts['authenticate'] = false
allowed_auth_methods = parse_auth_methods(super(opts))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be better off in a check_request override to prevent doubling the volume of requests sent to the server.

@@ -9,6 +9,7 @@ module LoginScanner

# Windows Remote Management login scanner
class WinRM < HTTP
# include Msf::Exploit::Remote::WinRM
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed still? 👀

@@ -272,7 +272,6 @@ def send_auth(res, opts, t, persist)

# if several providers are available, the client may want one in particular
preferred_auth = opts['preferred_auth']

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this file be reverted? 👀

def parse_auth_methods(resp)
return [] unless resp and resp.code == 401
methods = []
methods << "Negotiate" if resp.headers['WWW-Authenticate'].include? "Negotiate"
Copy link
Contributor

@adfoster-r7 adfoster-r7 Aug 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwelch-r7 Under what circumstances will winrm return this header? I haven't found a target that sends this back yet. What target did you use? 👀

Edit: Found it https://docs.microsoft.com/en-us/windows/win32/winrm/authentication-for-remote-connections

# send an HTTP request that WinRM would consider as valid (SOAP XML in the message matching the XML schema definition)
def send_request(opts)
allowed_auth_methods = parse_auth_methods(super(opts.merge({ 'authenticate' => true })))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
allowed_auth_methods = parse_auth_methods(super(opts.merge({ 'authenticate' => true })))
allowed_auth_methods = parse_auth_methods(super(opts.merge({ 'authenticate' => false })))

Typo, will fix

@adfoster-r7
Copy link
Contributor

Tested successfully against metasploitable3, a windows 7 target, and the original box from #13839

Valid credentials:

use auxiliary/scanner/winrm/winrm_login
set RHOSTS 10.10.10.149
set USERNAME chase
set PASSWORD Q4)sJu\\Y8qz*A3?d
run

Output:

[+] 10.10.10.149:5985 - Login Successful: WORKSTATION\chase:Q4)sJu\Y8qz*A3?d
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

Invalid credentials working as expected:

[-] 10.10.10.149:5985 - LOGIN FAILED: WORKSTATION\chase:wrong (Incorrect: )
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

As a sanity test, I checked auxiliary/scanner/winrm/winrm_cmd - which appears to be broken:

msf6 auxiliary(scanner/winrm/winrm_cmd) > run

[-] Got unexpected response: 
 HTTP/1.1 500
Server: Microsoft-HTTPAPI/2.0
Date: Wed, 19 Aug 2020 10:43:14 GMT
Connection: close
Content-Length: 0


[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf6 auxiliary(scanner/winrm/winrm_cmd) >

@adfoster-r7 adfoster-r7 merged commit d488dab into rapid7:master Aug 19, 2020
@adfoster-r7
Copy link
Contributor

adfoster-r7 commented Aug 19, 2020

Release Notes

Improved the winrm_login module to correctly negotiate authentication, where previously it would always assume that basic auth is required.

@adfoster-r7 adfoster-r7 added the rn-enhancement release notes enhancement label Aug 19, 2020
@acammack-r7
Copy link
Contributor

This still sends two requests to the WinRM server per credential pair. Was there a reason that the check_setup method couldn't be used to check the authentication method once at the start of the run?

@dwelch-r7
Copy link
Contributor Author

@acammack-r7 I took a look at the check_setup method, it doesn't seem like the right place to make this kind of check

          # @note Override this to detect that the service is up, is the right
          #   version, etc.
          # @return [false] Indicates there were no errors
          # @return [String] a human-readable error message describing why
          #   this scanner can't run
          def check_setup
            false
          end

seems to be intended more for checking that the scanner can actually run rather than for setting options per server
I checked around to see if there was some other function that'd be more suited but I didn't see one and didn't want to block on making one just for this, but it'd definitely be good to have

@acammack-r7
Copy link
Contributor

It would be an error if the server wanted an authentication method we didn't support or we weren't configured to connect with, correct? We already check that there are authentication methods in

if !(response && response.code == 401 && response.headers['WWW-Authenticate'])
error_message = "No authentication required"

That would also be a reasonable place to set an instance variable and make sure that we use an expected authentication. For an example, see the Glassfish scanner which auto-detects SSL:

if !self.ssl && res.headers['Location'] =~ /^https:/
self.ssl = true

This code as-is creates a large performance regression for this scanner since it doubles the time it takes to run.

@adfoster-r7
Copy link
Contributor

@acammack-r7 We can circle back to this, for now the module was pretty broke - so a performance regression wasn't a priority in comparison to making it work 📈

if allowed_auth_methods.include? 'Negotiate'
opts['preferred_auth'] = 'Negotiate'
elsif allowed_auth_methods.include? 'Basic'
# Straight up hack since if Basic auth is used winrm complains about the content size being 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we set it to 1 and give them a null byte instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug library rn-enhancement release notes enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants