mirror of
https://github.com/ShutdownRepo/dcshadow
synced 2026-06-06 16:44:32 +00:00
327 lines
19 KiB
Python
327 lines
19 KiB
Python
import os
|
|
import socketserver
|
|
from binascii import unhexlify, hexlify
|
|
from struct import pack
|
|
|
|
from R2Log import logger
|
|
|
|
from impacket.dcerpc.v5.enum import Enum
|
|
from impacket.dcerpc.v5.rpcrt import DCERPCServer, MSRPCHeader, SEC_TRAILER, AUTH_TYPES, MSRPC_BINDACK, CtxItemResult, \
|
|
MSRPCBindAck, MSRPCBind, CtxItem, MSRPC_CONT_RESULT_ACCEPT, msrpc_message_type, MSRPC_BIND, MSRPC_ALTERCTX, \
|
|
MSRPC_REQUEST, MSRPC_STATUS_CODE_NCA_S_UNSUPPORTED_TYPE, RPC_C_AUTHN_GSS_KERBEROS, MSRPC_CONT_RESULT_USER_REJECT, \
|
|
MSRPC_ALTERCTX_R, MSRPCRequestHeader, MSRPCRespHeader, MSRPC_BINDNAK, MSRPC_FAULT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
|
from impacket.krb5 import gssapi, crypto, constants
|
|
from impacket.krb5.asn1 import AP_REQ, EncTicketPart, Authenticator, AP_REP, EncAPRepPart
|
|
from impacket.krb5.constants import KerberosMessageTypes
|
|
from impacket.krb5.crypto import Key, _enctype_table
|
|
from impacket.uuid import uuidtup_to_bin, bin_to_uuidtup
|
|
from pyasn1.codec.der import decoder, encoder
|
|
|
|
NDRSyntax = ('8a885d04-1ceb-11c9-9fe8-08002b104860', '2.0')
|
|
|
|
|
|
class RPCServer(socketserver.ThreadingMixIn, socketserver.TCPServer): # based on https://github.com/fortra/impacket/pull/1299
|
|
def __init__(self, server_address, handler_class, drs_port: int = None):
|
|
socketserver.TCPServer.allow_reuse_address = True
|
|
self.drs_port = drs_port
|
|
socketserver.TCPServer.__init__(self, server_address, handler_class)
|
|
|
|
# TODO setListenPort to avoid race condition with getListenPort and threads ?
|
|
|
|
def getListenPort(self):
|
|
return self.socket.getsockname()[1]
|
|
|
|
|
|
class RPCServerHandler(socketserver.BaseRequestHandler):
|
|
def __init__(self, request, client_address, server):
|
|
self.client = None
|
|
self.target = None
|
|
self.auth_user = None
|
|
self.transport = None
|
|
self.request_header = None
|
|
self.request_pdu_data = None
|
|
self.request_sec_trailer = None
|
|
self.request_auth_type = None
|
|
self.encryption_key_type = None
|
|
self.encryption_key = None
|
|
self.cipher = None
|
|
self.sequence_number = None
|
|
self.server = server # This is ugly, any other -more elegant- way?
|
|
socketserver.BaseRequestHandler.__init__(self, request, client_address, server)
|
|
|
|
def setup(self):
|
|
self.transport = DCERPCServer(self.request)
|
|
logger.debug(f"RPC: Received connection from {self.client_address[0]}")
|
|
|
|
def handle(self):
|
|
try:
|
|
while True:
|
|
data = self.transport.recv()
|
|
if data is None:
|
|
logger.debug('RPC: Connection closed by client')
|
|
return
|
|
response = self.handle_message_type(data)
|
|
if response:
|
|
logger.debug('RPC: Sending packet of type %s' % msrpc_message_type[response['type']])
|
|
self.transport.send(response)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except ConnectionResetError:
|
|
logger.error("Connection reset.")
|
|
except Exception as e:
|
|
logger.debug("Exception:", exc_info=True)
|
|
logger.error('Exception in RPC request handler: %s' % e)
|
|
|
|
def handle_message_type(self, data): # we need to re-do all of this by taking inspiration from rpcrt.py, which
|
|
self.request_header = MSRPCHeader(data)
|
|
if self.request_header['auth_len'] > 0:
|
|
self.request_sec_trailer = SEC_TRAILER(self.request_header['sec_trailer'])
|
|
for _auth_type in AUTH_TYPES:
|
|
if self.request_sec_trailer['auth_type'] == _auth_type.value:
|
|
self.request_auth_type = _auth_type
|
|
_auth_message = f"with {self.request_auth_type.name} authentication" if self.request_sec_trailer else "without authentication"
|
|
logger.debug(f"RPC: Received {msrpc_message_type[self.request_header['type']]} packet {_auth_message}")
|
|
if self.request_header['type'] == MSRPC_BIND:
|
|
return self.handle_rpc_bind(rpc_payload=data)
|
|
elif self.request_header['type'] == MSRPC_ALTERCTX:
|
|
return self.handle_rpc_alterctx(rpc_payload=data)
|
|
elif self.request_header['type'] == MSRPC_REQUEST:
|
|
return self.handle_rpc_request(rpc_payload=data)
|
|
else:
|
|
logger.error('RPC: Packet type received not supported: %a' % msrpc_message_type[self.request_header['type']])
|
|
return self.send_error(MSRPC_STATUS_CODE_NCA_S_UNSUPPORTED_TYPE)
|
|
|
|
def handle_rpc_bind(self, rpc_payload):
|
|
if self.request_sec_trailer:
|
|
if self.request_auth_type.value == RPC_C_AUTHN_GSS_KERBEROS:
|
|
# ap_req = decoder.decode(self.request_header['auth_data'], asn1Spec=AP_REQ())[0]
|
|
# if ap_req['authenticator']['etype'] == 23:
|
|
# response = MSRPCBindNak()
|
|
# # response['RejectedReason'] = b''
|
|
# # response['SupportedVersions'] = b''
|
|
# logger.debug('Answering to a BIND with Kerberos authentication. Rejecting because of RC4')
|
|
# return response
|
|
self.request_pdu_data = MSRPCBind(self.request_header['pduData'])
|
|
response = MSRPCBindAck()
|
|
replicated_attributes = ['ver_major', 'ver_minor', 'flags', 'representation', 'call_id']
|
|
for attr in replicated_attributes:
|
|
response[attr] = self.request_header[attr]
|
|
response['type'] = MSRPC_BINDACK
|
|
# resp['auth_len'] = 0 # todo can we, and must we set this?
|
|
response['max_tfrag'] = self.request_pdu_data['max_tfrag']
|
|
response['max_rfrag'] = self.request_pdu_data['max_rfrag']
|
|
response['assoc_group'] = 0x1337 # must NOT be 0
|
|
# CONTEXT (CTX)
|
|
response['ctx_num'] = 0
|
|
response['ctx_items'] = b''
|
|
data = self.request_pdu_data['ctx_items']
|
|
for i in range(self.request_pdu_data['ctx_num']):
|
|
itemResult = CtxItemResult()
|
|
item = CtxItem(data)
|
|
# removing item data to go to the next
|
|
data = data[len(item):]
|
|
# check if the item is 32bit NDR
|
|
if item['TransferSyntax'] == uuidtup_to_bin(NDRSyntax):
|
|
itemResult['TransferSyntax'] = uuidtup_to_bin(NDRSyntax)
|
|
# Now Check if the interface is what we listen
|
|
for listening_uuid in self.transport._listenUUIDS:
|
|
if item['AbstractSyntax'] == listening_uuid:
|
|
# Match, we accept the bind request
|
|
response['SecondaryAddr'] = self.transport._listenUUIDS[item['AbstractSyntax']]['SecondaryAddr'] # should be the port of the listening Endpoint
|
|
response['SecondaryAddrLen'] = len(response['SecondaryAddr']) + 1
|
|
self.transport._boundUUID = listening_uuid
|
|
itemResult['Reason'] = 0 # Abstract Syntax supported
|
|
itemResult['Result'] = MSRPC_CONT_RESULT_ACCEPT
|
|
else:
|
|
itemResult['Reason'] = 1 # Abstract Syntax not supported
|
|
itemResult['Result'] = MSRPC_CONT_RESULT_USER_REJECT
|
|
logger.error(
|
|
'Bind request for an unsupported interface %s' % bin_to_uuidtup(
|
|
item['AbstractSyntax']))
|
|
else:
|
|
itemResult['Reason'] = 2 # Transfer Syntax not supported
|
|
itemResult['Result'] = MSRPC_CONT_RESULT_USER_REJECT
|
|
response['ctx_items'] += itemResult.getData()
|
|
response['ctx_num'] += 1
|
|
response['Pad'] = 'A' * ((4 - ((response["SecondaryAddrLen"] + MSRPCBindAck._SIZE) % 4)) % 4)
|
|
response['sec_trailer'] = self.request_sec_trailer
|
|
try:
|
|
response['auth_data'] = self.gss_kerberos_ap_req() # todo need to have an if ap_req, then build ap_rep, to make sure we don't build ap_rep when not asked to. For example, alterctx_r relies on this and unsets auth-data and sec-trailer, we need to find a better way to do this.
|
|
except:
|
|
logger.debug("Couldn't decode auth data as AP-REQ, probably not an AP-REQ") # todo we NEED to be able to tell the difference and act accordingly, and have a wider kerberos packet handler
|
|
response['frag_len'] = len(response.getData())
|
|
logger.debug('Answering to a BIND with Kerberos authentication')
|
|
return response
|
|
else:
|
|
raise Exception(f'RPC: Auth type received not supported: {self.request_auth_type.name}')
|
|
else:
|
|
logger.debug('RPC: Answering to a BIND without authentication')
|
|
return self.transport.processRequest(rpc_payload)
|
|
|
|
def handle_rpc_alterctx(self, rpc_payload):
|
|
if self.request_sec_trailer:
|
|
if self.request_auth_type.value == RPC_C_AUTHN_GSS_KERBEROS:
|
|
self.request_pdu_data = MSRPCBind(self.request_header['pduData'])
|
|
response = self.handle_rpc_bind(rpc_payload=rpc_payload)
|
|
response['type'] = MSRPC_ALTERCTX_R
|
|
response['sec_trailer'] = b'' # unsetting authentication-related data, because it works right now, but maybe it's best we answer with something? Don't know
|
|
response['auth_data'] = b'' # unsetting authentication-related data, because it works right now, but maybe it's best we answer with something? Don't know
|
|
response['frag_len'] = len(response.getData())
|
|
logger.debug('Answering to a ALTERCTX with Kerberos authentication')
|
|
return response
|
|
else:
|
|
raise Exception(f'RPC: Auth type received not supported: {self.request_auth_type.name}')
|
|
|
|
def handle_rpc_request(self, rpc_payload):
|
|
if self.request_sec_trailer:
|
|
if self.request_auth_type.value == RPC_C_AUTHN_GSS_KERBEROS and self.request_sec_trailer['auth_level'] == RPC_C_AUTHN_LEVEL_PKT_PRIVACY:
|
|
_request_header = MSRPCRequestHeader(rpc_payload)
|
|
_encrypted_stub = _request_header['pduData']
|
|
self.request_sec_trailer = SEC_TRAILER(_request_header['sec_trailer'])
|
|
auth_data = _request_header['sec_trailer'] + _request_header['auth_data']
|
|
gss = gssapi.GSSAPI(cipher=self.cipher)
|
|
answer, cfounder = gss.GSS_Unwrap(
|
|
sessionKey=self.encryption_key,
|
|
data=_encrypted_stub,
|
|
sequenceNumber=self.sequence_number,
|
|
direction='accept', # not init
|
|
authData=auth_data,
|
|
keyUsage=gssapi.KG_USAGE_INITIATOR_SEAL,
|
|
dce_rpc_header=_request_header.getData()[:len(MSRPCRequestHeader())],
|
|
auth_data_header=auth_data[:8]
|
|
)
|
|
# if self.request_sec_trailer['auth_pad_len']:
|
|
# answer = answer[:-self.request_sec_trailer['auth_pad_len']]
|
|
_request_header['pduData'] = answer
|
|
# if len(_request_header['pduData']) & 3 != 0:
|
|
# _request_header['pduData'] = crypto._zeropad(_request_header['pduData'], 4)
|
|
_request_header['frag_len'] = len(_request_header.get_packet())
|
|
# _request_header['auth_len'] = 0
|
|
# _request_header['auth_dataLen'] = 0
|
|
# _request_header['dataLen'] = len(_request_header['pduData'])
|
|
# _request_header['sec_trailer'] = b''
|
|
# _request_header['auth_data'] = b''
|
|
# _request_header['frag_len'] = len(MSRPCRequestHeader())+len(_request_header['pduData'])+_request_header['_pad']
|
|
response = self.transport.processRequest(_request_header.getData())
|
|
|
|
# Impacket defines some of those when sending, but we want to define the right structure before since it will be part of the blob that is hashed for integrity
|
|
# Also, it would be cleaner to define those in rpcrt.processRequest() but it could potentially impact many other things... keeping it here atm
|
|
response['alloc_hint'] = len(response['pduData'])
|
|
# response['sec_trailer']['auth_pad_len'] = response['alloc_hint'] % 16
|
|
# response['pduData'] = response['pduData'].getData() + b'\x00' * response['sec_trailer']['auth_pad_len']
|
|
padded_pdudata = crypto._zeropad(response['pduData'].getData(),16)
|
|
response['sec_trailer'] = SEC_TRAILER(response['sec_trailer'])
|
|
response['sec_trailer']['auth_pad_len'] = len(padded_pdudata)-len(response['pduData'])
|
|
response['pduData'] = padded_pdudata
|
|
# response['sec_trailer']['auth_pad_len'] = (16 - (response['alloc_hint'] % 16)) % 16
|
|
# response['pduData'] = response['pduData'].getData() + b'\x00' * response['sec_trailer']['auth_pad_len']
|
|
response['auth_len'] = len(gss.get_filler(response['pduData'])) + len(cfounder) + len(gss.WRAP()) + self.cipher.macsize + len(gss.WRAP())
|
|
response['frag_len'] = len(response.get_packet())
|
|
sealedMessage, signature = gss.GSS_Wrap(
|
|
sessionKey=self.encryption_key,
|
|
data=response['pduData'],
|
|
direction='accept', # not init
|
|
encrypt=True, # Sealing
|
|
acceptorSubkey=False, # using subkey from ap-req
|
|
sequenceNumber=self.sequence_number,
|
|
keyUsage=gssapi.KG_USAGE_ACCEPTOR_SEAL,
|
|
dce_rpc_header=MSRPCRespHeader(response.getData()).getData()[:len(MSRPCRespHeader())], # FIXME This can be done better and cleaner imo
|
|
auth_data_header=response['sec_trailer'].getData()
|
|
)
|
|
response['pduData'] = sealedMessage
|
|
response['auth_data'] = signature
|
|
response['frag_len'] = len(response.get_packet())
|
|
self.sequence_number += 1
|
|
return response
|
|
else:
|
|
raise Exception(f'RPC: Auth type received not supported: {self.request_auth_type.name}')
|
|
else:
|
|
logger.debug('RPC: Answering to a REQUEST without authentication')
|
|
return self.transport.processRequest(rpc_payload)
|
|
pass
|
|
|
|
|
|
def gss_kerberos_ap_req(self):
|
|
class KeyUsageNumbers(Enum): # RFC 4120 - 7.5.1. Key Usage Numbers
|
|
# TODO add values and refer when using newCipher.enc/decrypt(key, KEYUSAGE, data)
|
|
pass
|
|
|
|
krb_blob = self.request_header['auth_data']
|
|
# TODO handle if not ap_rep, raise NotImplem
|
|
ap_req = decoder.decode(krb_blob, asn1Spec=AP_REQ())[0]
|
|
# TODO calculate the keys dynamically
|
|
# _rc4 = "11baff16dfa02faa432a18b47866fe22"
|
|
# _aes256 = "9192245209f61d54381cc45a76545d6e781445fad34f92e942a364c7a919f3f4"
|
|
# _aes128 = "5ea518c068a57ec97956af47269d4973"
|
|
# keys = {
|
|
# 23: unhexlify("11baff16dfa02faa432a18b47866fe22"),
|
|
# 18: unhexlify("9192245209f61d54381cc45a76545d6e781445fad34f92e942a364c7a919f3f4"),
|
|
# 17: unhexlify("5ea518c068a57ec97956af47269d4973"),
|
|
# }
|
|
_rc4 = os.environ["RC4"]
|
|
_aes256 = os.environ["AES256"]
|
|
_aes128 = os.environ["AES128"]
|
|
keys = {
|
|
23: unhexlify(_rc4),
|
|
18: unhexlify(_aes256),
|
|
17: unhexlify(_aes128),
|
|
}
|
|
ekeys = {}
|
|
for kt, key in keys.items():
|
|
ekeys[kt] = Key(kt, key)
|
|
etype = ap_req['authenticator']['etype']
|
|
logger.debug('Ticket is encrypted with %s (etype %d)' % (constants.EncryptionTypes(etype).name, etype))
|
|
key = ekeys[etype]
|
|
logger.debug('Using corresponding key: %s' % hexlify(key.contents).decode('utf-8'))
|
|
encTicket = ap_req['ticket']['enc-part']['cipher']
|
|
self.cipher = _enctype_table[int(etype)]
|
|
plainTextTicket, _ = self.cipher.decrypt(key, 2, encTicket)
|
|
logger.debug('Ticket successfully decrypted')
|
|
# TODO print the cname & crealm etc. of the domain controller account loggerger in, interesting info
|
|
encTicketPart = decoder.decode(plainTextTicket, asn1Spec=EncTicketPart())[0]
|
|
sessionKey = Key(encTicketPart['key']['keytype'], bytes(encTicketPart['key']['keyvalue']))
|
|
logger.debug("Using session key to decrypt authenticator: %s" % hexlify(sessionKey.contents).decode('utf-8'))
|
|
encApReqAuthenticator, _ = self.cipher.decrypt(sessionKey, 11, ap_req['authenticator']['cipher'])
|
|
logger.debug('Authenticator successfully decrypted')
|
|
ApRepAuthenticator = decoder.decode(encApReqAuthenticator, asn1Spec=Authenticator())[0]
|
|
self.encryption_key_type = ApRepAuthenticator['subkey']['keytype']
|
|
# self.encryption_key = Key(self.encryption_key_type,b"\xa9\x14\xcb\x0d\xa0\x3d\xcc\x32\x13\x49\xba\x8d\xe1\x07\xe4\xda" \
|
|
# b"\xf8\xf8\x1f\x10\xdc\xd7\xf7\x5a\x82\x06\x45\x5a\x1e\x6c\xbe\xde")
|
|
self.encryption_key = Key(self.encryption_key_type, ApRepAuthenticator['subkey']['keyvalue'].asOctets())
|
|
# self.encryption_key = Key(self.encryption_key_type, sessionKey.contents)
|
|
ap_rep = AP_REP()
|
|
ap_rep['pvno'] = 5
|
|
ap_rep['msg-type'] = KerberosMessageTypes.KRB_AP_REP.value
|
|
ap_rep['enc-part']['etype'] = ap_req['authenticator']['etype']
|
|
encAPRep = EncAPRepPart()
|
|
encAPRep['ctime'] = ApRepAuthenticator['ctime'].prettyPrint()
|
|
encAPRep['cusec'] = ApRepAuthenticator['cusec'].prettyPrint()
|
|
# sending randomly generated key
|
|
encAPRep['subkey']['keyvalue'] = self.encryption_key.contents
|
|
encAPRep['subkey']['keytype'] = self.encryption_key_type
|
|
encAPRep['seq-number'] = ApRepAuthenticator['seq-number'].prettyPrint()
|
|
self.sequence_number = int(ApRepAuthenticator['seq-number'])
|
|
# self.sequence_number = int.from_bytes(os.urandom(4))
|
|
# self.sequence_number = 1336131338
|
|
# encAPRep['seq-number'] = self.sequence_number
|
|
encEncApRep = encoder.encode(encAPRep)
|
|
encApRepCipher = self.cipher.encrypt(sessionKey, 12, encEncApRep, None)
|
|
ap_rep['enc-part']['cipher'] = encApRepCipher
|
|
return encoder.encode(ap_rep)
|
|
|
|
def send_error(self, status):
|
|
packet = MSRPCRespHeader(self.request_header.getData())
|
|
request_type = self.request_header['type']
|
|
if request_type == MSRPC_BIND:
|
|
packet['type'] = MSRPC_BINDNAK
|
|
else:
|
|
packet['type'] = MSRPC_FAULT
|
|
if status:
|
|
packet['pduData'] = pack('<L', status)
|
|
return packet
|
|
|
|
def finish(self):
|
|
# Thread/process is dying, we should tell the main SMB thread to remove all this thread data
|
|
logger.debug(f"RPC: Closing down connection {self.client_address[0]}")
|
|
return socketserver.BaseRequestHandler.finish(self)
|