-
Notifications
You must be signed in to change notification settings - Fork 13.8k
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
Add error handing to rdp_web_login.py to handle malformed NetNTLM messages #15727
Conversation
This looks good but my one concern is the fact that this is going to silently handle the exception without informing the user that an error occurred. This may lead to confusion if the user expected something to work and then all of a sudden now it doesn't work but they are provided no information as to why their attempt might have failed. |
Changing PR title again as this PR presently doesn't add in any additional error messages, and mainly serves to just provide additional error handling. |
Looking at this closer I'm wondering if there shouldn't just be more validation done on the actual values. The error you posted suggests that an index went out of range and looking at the code it seems to assume that quite a few values won't be empty or that it will be formatted correctly. Rather than adding input validation I think it may be better to address the root issue here. I'll see if I can cook something up. |
Correct me if I'm wrong but now the user should be confronted with an error message if an error occurs during the NTML hash handling.
should be presented, resulting in stopping the script. Currently i got no testing environment, which would provide me additional NTML hashes, besides the one that crashes. If you got more NTML hashes could you perhaps give me functioning hashes, in order to track down the problem and maybe add a fix to the crash? |
Yes i am pretty sure it doesn't find a \n in my hash and therefore crashes at the last split, as mentioned in #15717. If you can provide me with more hashes i can try to track down the problem and add a fix. |
As a couldn't fix the problem with my given resources i at least tried to provide an error message in order to inform the user about the (until now) silent crash. |
That isn't a great message imho. Keep in mind your within a loop so you wouldn't stop, you would just go on to the next execution of that loop. There are multiple URLs that you are processing. How do you know which one caused the error? Unfortunately I don't have additional hashes that would trigger this error but I don't think this is needed in this case; its a simple enough piece of code that it can likely be fixed by reading the source code and fixing some edge cases that seem to be apparent. |
Yeah I'll see if I can add some code to fix this up. My guess is that the code should be checking certain elements aren't NULL or will at the very least contain the right format of data before proceeding and right now it just assumes responses are formatted correctly. What should be happening is that we have a series of steps for validating the data and then print out more specific error messages if one of those steps fails. |
5c47ee0
to
26372b6
Compare
@NeffIsBack I actually got the original NTLM hash parsing idea from the auxiliary owa_login.rb module. It might have a bit more protection than mine given the NTLM parsing it uses, but if you have an environment that produces this crash, it could be helpful to check if that module crashes on the enum_domain or not also 🙂 |
try: | ||
ntlm_header_array = request.headers['WWW-Authenticate'].split('NTLM ') | ||
if len(ntlm_header_array) < 2: | ||
module.log("NTLM authenticate header was not in the expected format for %s" % url) | ||
continue | ||
|
||
domain_hash = ntlm_header_array[1].split(',')[0] | ||
domain = base64.b64decode(bytes(domain_hash, 'utf-8')).replace(b'\x00',b'').split(b'\n') | ||
if len(domain) < 2: | ||
module.log("Domain is not in the right format, skipping processing %s" % url) | ||
continue | ||
|
||
domain = domain[1] | ||
start = domain.find(b'\x0f') | ||
end = domain.find(b'\x02') | ||
if (start == -1) or (end == -1): | ||
module.log("Couldn't find byte 0xf or 0x2 in the request, skipping processing %s", url) | ||
continue | ||
|
||
domain = domain[start + 1:end].decode('utf-8') | ||
module.log('Found Domain: {}'.format(domain), level='good') | ||
return domain | ||
except: | ||
module.log("An unhandled exception occured when processing %s. Please file a bug report." %url) | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not tested this, but what about using a method that will parse the NTLM type 1 message. Something along the lines of:
import struct
def parse_ntlm_t1(message):
if not message.startswith(b'NTLMSSP\x00\x01\x00\x00\x00'):
return None
struct_fmt = '<8xIIHHIHHI'
size = struct.calcsize(struct_fmt)
if len(message) < size:
return None
fields = struct.unpack(struct_fmt, message[:size])
properties = {'type': fields[0], 'flags': fields[1]}
length, _, offset = fields[2:5]
if len(message) < offset + length:
return None
properties['domain'] = message[offset:offset+length].decode('utf-8')
length, _, offset = fields[5:8]
if len(message) < offset + length:
return None
properties['workstation'] = message[offset:offset+length].decode('utf-8')
return properties
Then
try: | |
ntlm_header_array = request.headers['WWW-Authenticate'].split('NTLM ') | |
if len(ntlm_header_array) < 2: | |
module.log("NTLM authenticate header was not in the expected format for %s" % url) | |
continue | |
domain_hash = ntlm_header_array[1].split(',')[0] | |
domain = base64.b64decode(bytes(domain_hash, 'utf-8')).replace(b'\x00',b'').split(b'\n') | |
if len(domain) < 2: | |
module.log("Domain is not in the right format, skipping processing %s" % url) | |
continue | |
domain = domain[1] | |
start = domain.find(b'\x0f') | |
end = domain.find(b'\x02') | |
if (start == -1) or (end == -1): | |
module.log("Couldn't find byte 0xf or 0x2 in the request, skipping processing %s", url) | |
continue | |
domain = domain[start + 1:end].decode('utf-8') | |
module.log('Found Domain: {}'.format(domain), level='good') | |
return domain | |
except: | |
module.log("An unhandled exception occured when processing %s. Please file a bug report." %url) | |
continue | |
type1_msg = request.headers['WWW-Authenticate'].split('NTLM ')[1].split(',')[0] | |
type1_msg = parse_ntlm_t1(base64.b64decode(type1_msg)) | |
if type1_msg is None: | |
module.log("NTLM authenticate header was not in the expected format for %s" % url) | |
continue | |
return type1_msg['domain'] |
I will have a look at it and test out the module when i get back to work, but can't promise anything. If we could get any working NTLM hashes i could further track down the problem. :) |
You can generate a Net-NTLM type 1 message with Metasploit, that's what I did. Start Generate one without a domain or workstation:
Generate one with a domain and workstation:
|
As mentioned in rapid7#15720 with some NTML responses the code crashes, unable to enumerate the domain. To prevent this behavior a try and except block was added, to inform the user about the crash.
Release NotesThis PR adds more robust NTLM message parsing with better error handling and messaging when pulling out the NTLM hashes. |
As mentioned in #15720 with some NTLM responses the
modules/auxiliary/scanner/http/rdp_web_login.py
module crashes, unable to enumerate the domain. To prevent this behavior a try and except block was added, to inform the user about the crash.Debug output from above mentioned crash: