Skip to content

Commit aa84572

Browse files
authored
Merge pull request #1 from blacklanternsecurity/upstream-merge-backup
Upstream merge + apply custom NetExec-specific patches to LDAP
2 parents 0769c22 + e6fe6d9 commit aa84572

File tree

8 files changed

+126
-59
lines changed

8 files changed

+126
-59
lines changed

.github/workflows/build_and_test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ jobs:
4848
strategy:
4949
fail-fast: false
5050
matrix:
51-
python-version: ["3.8", "3.9", "3.10","3.11"]
51+
python-version: ["3.9", "3.10","3.11","3.12"]
5252
experimental: [false]
5353
os: [ubuntu-latest]
5454
include:
55-
- python-version: "3.12-dev"
55+
- python-version: "3.13-dev"
5656
experimental: true
5757
os: ubuntu-latest
5858
continue-on-error: ${{ matrix.experimental }}

examples/smbexec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from impacket.dcerpc.v5 import transport, scmr
5757
from impacket.krb5.keytab import Keytab
5858

59-
OUTPUT_FILENAME = '__output'
59+
OUTPUT_FILENAME = '__output_' + ''.join([random.choice(string.ascii_letters) for i in range(8)])
6060
SMBSERVER_DIR = '__tmp'
6161
DUMMY_SHARE = 'TMP'
6262
CODEC = sys.stdout.encoding

impacket/ldap/ldap.py

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#
2525

2626
import re
27+
import struct
2728
import socket
2829
from binascii import unhexlify
2930
import random
@@ -104,6 +105,7 @@ def __init__(self, url, baseDN='', dstIp=None, signing=True):
104105
raise LDAPSessionError(errorString="Unknown URL prefix: '%s'" % url)
105106

106107
self.__binded = False
108+
self.__channel_binding_value = None
107109

108110
### SASL Auth LDAP Signing arguments
109111
self.sequenceNumber = 0
@@ -141,26 +143,25 @@ def __init__(self, url, baseDN='', dstIp=None, signing=True):
141143
self._socket.connect(sa)
142144
self._socket.do_handshake()
143145

144-
def generateChannelBindingValue(self):
145-
# From: https://github.com/ly4k/ldap3/commit/87f5760e5a68c2f91eac8ba375f4ea3928e2b9e0#diff-c782b790cfa0a948362bf47d72df8ddd6daac12e5757afd9d371d89385b27ef6R1383
146-
from hashlib import md5
147-
# Ugly but effective, to get the digest of the X509 DER in bytes
148-
peer_cert_digest_str = self._socket.get_peer_certificate().digest('sha256').decode()
149-
peer_cert_digest_bytes = bytes.fromhex(peer_cert_digest_str.replace(':', ''))
150-
151-
channel_binding_struct = b''
152-
initiator_address = b'\x00'*8
153-
acceptor_address = b'\x00'*8
154-
155-
# https://datatracker.ietf.org/doc/html/rfc5929#section-4
156-
application_data_raw = b'tls-server-end-point:' + peer_cert_digest_bytes
157-
len_application_data = len(application_data_raw).to_bytes(4, byteorder='little', signed = False)
158-
application_data = len_application_data
159-
application_data += application_data_raw
160-
channel_binding_struct += initiator_address
161-
channel_binding_struct += acceptor_address
162-
channel_binding_struct += application_data
163-
return md5(channel_binding_struct).digest()
146+
# From: https://github.com/ly4k/ldap3/commit/87f5760e5a68c2f91eac8ba375f4ea3928e2b9e0#diff-c782b790cfa0a948362bf47d72df8ddd6daac12e5757afd9d371d89385b27ef6R1383
147+
from hashlib import md5
148+
# Ugly but effective, to get the digest of the X509 DER in bytes
149+
peer_cert_digest_str = self._socket.get_peer_certificate().digest('sha256').decode()
150+
peer_cert_digest_bytes = bytes.fromhex(peer_cert_digest_str.replace(':', ''))
151+
152+
channel_binding_struct = b''
153+
initiator_address = b'\x00'*8
154+
acceptor_address = b'\x00'*8
155+
156+
# https://datatracker.ietf.org/doc/html/rfc5929#section-4
157+
application_data_raw = b'tls-server-end-point:' + peer_cert_digest_bytes
158+
len_application_data = len(application_data_raw).to_bytes(4, byteorder='little', signed = False)
159+
application_data = len_application_data
160+
application_data += application_data_raw
161+
channel_binding_struct += initiator_address
162+
channel_binding_struct += acceptor_address
163+
channel_binding_struct += application_data
164+
self.__channel_binding_value = md5(channel_binding_struct).digest()
164165

165166
def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None,
166167
TGS=None, useCache=True):
@@ -268,8 +269,8 @@ def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey=
268269

269270
# If TLS is used, setup channel binding
270271

271-
if self._SSL:
272-
chkField['Bnd'] = self.generateChannelBindingValue()
272+
if self._SSL and self.__channel_binding_value is not None:
273+
chkField['Bnd'] = self.__channel_binding_value
273274
if self.__signing:
274275
chkField['Flags'] |= GSS_C_CONF_FLAG
275276
chkField['Flags'] |= GSS_C_INTEG_FLAG
@@ -372,8 +373,8 @@ def login(self, user='', password='', domain='', lmhash='', nthash='', authentic
372373

373374
# If TLS is used, setup channel binding
374375
channel_binding_value = b''
375-
if self._SSL:
376-
channel_binding_value = self.generateChannelBindingValue()
376+
if self._SSL and self.__channel_binding_value is not None:
377+
channel_binding_value = self.__channel_binding_value
377378

378379
# NTLM Auth
379380
type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash, channel_binding_value=channel_binding_value)
@@ -418,8 +419,8 @@ def login(self, user='', password='', domain='', lmhash='', nthash='', authentic
418419

419420
# channel binding
420421
channel_binding_value = b''
421-
if self._SSL:
422-
channel_binding_value = self.generateChannelBindingValue()
422+
if self._SSL and self.__channel_binding_value is not None:
423+
channel_binding_value = self.__channel_binding_value
423424

424425
# NTLM Auth
425426
type3, exportedSessionKey = getNTLMSSPType3(negotiate, type2, user, password, domain, lmhash, nthash, service='ldap', version=self.version, use_ntlmv2=True, channel_binding_value=channel_binding_value)
@@ -557,7 +558,7 @@ def send(self, request, controls=None):
557558
self.sequenceNumber += 1
558559
return self._socket.sendall(data)
559560

560-
def recv(self):
561+
def recv_raw(self):
561562
REQUEST_SIZE = 8192
562563
data = b''
563564
done = False
@@ -567,16 +568,31 @@ def recv(self):
567568
done = True
568569
data += recvData
569570

571+
if self.__binded and self.__signing: # we need to decrypt every TCP frames, all at once
572+
message_length = struct.unpack('!I', data[:4])[0]
573+
574+
while message_length != len(data) - 4:
575+
done = False
576+
while not done:
577+
recvData = self._socket.recv(REQUEST_SIZE)
578+
if len(recvData) < REQUEST_SIZE:
579+
done = True
580+
data += recvData
581+
582+
data = self.decrypt(data)
583+
584+
return data
585+
586+
def recv(self):
570587
response = []
571-
if self.__binded and self.__signing:
572-
data = self.decrypt(data)
588+
data = self.recv_raw()
573589
while len(data) > 0:
574590
try:
575591
# need to decrypt before
576592
message, remaining = decoder.decode(data, asn1Spec=LDAPMessage())
577593
except SubstrateUnderrunError:
578594
# We need more data
579-
remaining = data + self._socket.recv(REQUEST_SIZE)
595+
remaining = data + self.recv_raw()
580596
else:
581597
if message['messageID'] == 0: # unsolicited notification
582598
name = message['protocolOp']['extendedResp']['responseName'] or message['responseName']

impacket/ldap/ldapasn1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class AttributeValue(univ.OctetString):
161161

162162

163163
class AssertionValue(univ.OctetString):
164-
pass
164+
encoding = 'utf-8'
165165

166166

167167
class MatchingRuleID(LDAPString):

impacket/smb.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,8 @@ def __init__(self, commandOrData = None, data = None, **kargs):
814814
class AsciiOrUnicodeStructure(Structure):
815815
UnicodeStructure = ()
816816
AsciiStructure = ()
817+
ENCODING = 'utf-8'
818+
817819
def __init__(self, flags = 0, **kargs):
818820
if flags & SMB.FLAGS2_UNICODE:
819821
self.structure = self.UnicodeStructure

impacket/smbserver.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4887,9 +4887,10 @@ class SimpleSMBServer:
48874887
:param string configFile: a file with all the servers' configuration. If no file specified, this class will create the basic parameters needed to run. You will need to add your shares manually tho. See addShare() method
48884888
"""
48894889

4890-
def __init__(self, listenAddress='0.0.0.0', listenPort=445, configFile=''):
4890+
def __init__(self, listenAddress='0.0.0.0', listenPort=445, configFile='', smbserverclass=SMBSERVER):
48914891
if configFile != '':
4892-
self.__server = SMBSERVER((listenAddress, listenPort))
4892+
#self.__server = SMBSERVER((listenAddress, listenPort))
4893+
self.__server = smbserverclass((listenAddress, listenPort))
48934894
self.__server.processConfigFile(configFile)
48944895
self.__smbConfig = None
48954896
else:
@@ -4914,20 +4915,23 @@ def __init__(self, listenAddress='0.0.0.0', listenPort=445, configFile=''):
49144915
self.__smbConfig.set('IPC$', 'read only', 'yes')
49154916
self.__smbConfig.set('IPC$', 'share type', '3')
49164917
self.__smbConfig.set('IPC$', 'path', '')
4917-
self.__server = SMBSERVER((listenAddress, listenPort), config_parser=self.__smbConfig)
4918+
self.__server = smbserverclass((listenAddress, listenPort), config_parser=self.__smbConfig)
49184919
self.__server.processConfigFile()
49194920

49204921
# Now we have to register the MS-SRVS server. This specially important for
49214922
# Windows 7+ and Mavericks clients since they WON'T (specially OSX)
49224923
# ask for shares using MS-RAP.
49234924

49244925
self.__srvsServer = SRVSServer()
4925-
self.__srvsServer.daemon = True
4926+
self.__srvsServer.daemon=True
49264927
self.__wkstServer = WKSTServer()
4927-
self.__wkstServer.daemon = True
4928+
self.__wkstServer.daemon=True
49284929
self.__server.registerNamedPipe('srvsvc', ('127.0.0.1', self.__srvsServer.getListenPort()))
49294930
self.__server.registerNamedPipe('wkssvc', ('127.0.0.1', self.__wkstServer.getListenPort()))
49304931

4932+
def getServer(self):
4933+
return self.__server
4934+
49314935
def start(self):
49324936
self.__srvsServer.start()
49334937
self.__wkstServer.start()

impacket/structure.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from __future__ import division
1313
from __future__ import print_function
1414
from struct import pack, unpack, calcsize
15+
16+
import six
1517
from six import b, PY3
1618
from binascii import hexlify
1719

@@ -80,13 +82,19 @@ class is the class to use when unpacking ':' fields.
8082
commonHdr = ()
8183
structure = ()
8284
debug = 0
85+
# Encoding defaults to latin-1 which already was the de facto encoding for structures and works for most use cases.
86+
# Now it can be configured to another encoding if needed.
87+
ENCODING = 'latin-1' # https://github.com/fortra/impacket/pull/1958
8388

8489
def __init__(self, data = None, alignment = 0):
8590
if not hasattr(self, 'alignment'):
8691
self.alignment = alignment
8792

8893
self.fields = {}
8994
self.rawData = data
95+
96+
self.b = lambda x: six.ensure_binary(x, encoding=self.ENCODING)
97+
9098
if data is not None:
9199
self.fromString(data)
92100
else:
@@ -197,7 +205,7 @@ def pack(self, format, data, field = None):
197205

198206
# quote specifier
199207
if format[:1] == "'" or format[:1] == '"':
200-
return b(format[1:])
208+
return self.b(format[1:])
201209

202210
# code specifier
203211
two = format.split('=')
@@ -245,26 +253,26 @@ def pack(self, format, data, field = None):
245253
# "printf" string specifier
246254
if format[:1] == '%':
247255
# format string like specifier
248-
return b(format % data)
256+
return self.b(format % data)
249257

250258
# asciiz specifier
251259
if format[:1] == 'z':
252260
if isinstance(data,bytes):
253-
return data + b('\0')
254-
return bytes(b(data)+b('\0'))
261+
return data + self.b('\0')
262+
return bytes(self.b(data)+self.b('\0'))
255263

256264
# unicode specifier
257265
if format[:1] == 'u':
258-
return bytes(data+b('\0\0') + (len(data) & 1 and b('\0') or b''))
266+
return bytes(data+self.b('\0\0') + (len(data) & 1 and self.b('\0') or b''))
259267

260268
# DCE-RPC/NDR string specifier
261269
if format[:1] == 'w':
262270
if len(data) == 0:
263-
data = b('\0\0')
271+
data = self.b('\0\0')
264272
elif len(data) % 2:
265-
data = b(data) + b('\0')
273+
data = self.b(data) + self.b('\0')
266274
l = pack('<L', len(data)//2)
267-
return b''.join([l, l, b('\0\0\0\0'), data])
275+
return b''.join([l, l, self.b('\0\0\0\0'), data])
268276

269277
if data is None:
270278
raise Exception("Trying to pack None")
@@ -279,7 +287,7 @@ def pack(self, format, data, field = None):
279287
elif isinstance(data, int):
280288
return bytes(data)
281289
elif isinstance(data, bytes) is not True:
282-
return bytes(b(data))
290+
return bytes(self.b(data))
283291
else:
284292
return data
285293

@@ -288,7 +296,7 @@ def pack(self, format, data, field = None):
288296
if isinstance(data, bytes) or isinstance(data, bytearray):
289297
return pack(format, data)
290298
else:
291-
return pack(format, b(data))
299+
return pack(format, self.b(data))
292300

293301
# struct like specifier
294302
return pack(format, data)
@@ -315,7 +323,7 @@ def unpack(self, format, data, dataClassOrCode = b, field = None):
315323
# quote specifier
316324
if format[:1] == "'" or format[:1] == '"':
317325
answer = format[1:]
318-
if b(answer) != data:
326+
if self.b(answer) != data:
319327
raise Exception("Unpacked data doesn't match constant value '%r' should be '%r'" % (data, answer))
320328
return answer
321329

@@ -361,7 +369,7 @@ def unpack(self, format, data, dataClassOrCode = b, field = None):
361369

362370
# asciiz specifier
363371
if format == 'z':
364-
if data[-1:] != b('\x00'):
372+
if data[-1:] != self.b('\x00'):
365373
raise Exception("%s 'z' field is not NUL terminated: %r" % (field, data))
366374
if PY3:
367375
return data[:-1].decode('latin-1')
@@ -370,7 +378,7 @@ def unpack(self, format, data, dataClassOrCode = b, field = None):
370378

371379
# unicode specifier
372380
if format == 'u':
373-
if data[-2:] != b('\x00\x00'):
381+
if data[-2:] != self.b('\x00\x00'):
374382
raise Exception("%s 'u' field is not NUL-NUL terminated: %r" % (field, data))
375383
return data[:-2] # remove trailing NUL
376384

@@ -524,11 +532,11 @@ def calcUnpackSize(self, format, data, field = None):
524532

525533
# asciiz specifier
526534
if format[:1] == 'z':
527-
return data.index(b('\x00'))+1
535+
return data.index(self.b('\x00'))+1
528536

529537
# asciiz specifier
530538
if format[:1] == 'u':
531-
l = data.index(b('\x00\x00'))
539+
l = data.index(self.b('\x00\x00'))
532540
return l + (l & 1 and 3 or 2)
533541

534542
# DCE-RPC/NDR string specifier
@@ -584,7 +592,7 @@ def zeroValue(self, format):
584592
if format in ['z',':','u']:
585593
return b''
586594
if format == 'w':
587-
return b('\x00\x00')
595+
return self.b('\x00\x00')
588596

589597
return 0
590598

0 commit comments

Comments
 (0)