From b1d51f6cf9251aee60842f765d65a56e7ebfcb4b Mon Sep 17 00:00:00 2001 From: alexisbalbachan Date: Fri, 27 Mar 2026 05:21:01 -0300 Subject: [PATCH 1/4] Fix issue #1924 large-page tag count parsing --- impacket/ese.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/impacket/ese.py b/impacket/ese.py index 18e0827d9c..d5c6dc4ea6 100644 --- a/impacket/ese.py +++ b/impacket/ese.py @@ -58,6 +58,11 @@ FLAGS_NEW_FORMAT = 0x2000 FLAGS_NEW_CHECKSUM = 0x2000 +# On 16 KiB and 32 KiB pages, the raw 16-bit tag state appears to store the total +# tag count in the lower 12 bits. The upper 4 bits seem to represent a reserved tag count. +FIRST_AVAILABLE_PAGE_TAG_MASK = 0x0fff +FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT = 12 + # Tag Flags TAG_UNKNOWN = 0x1 TAG_DEFUNCT = 0x2 @@ -436,8 +441,17 @@ def __init__(self, db, data=None): self.__DBHeader = db self.data = data self.record = None + self.tagCount = 0 + self.tagReserved = 1 if data is not None: self.record = ESENT_PAGE_HEADER(self.__DBHeader['Version'], self.__DBHeader['FileFormatRevision'], self.__DBHeader['PageSize'], data) + self.tagCount = self.record['FirstAvailablePageTag'] + if self.__DBHeader['FileFormatRevision'] >= 0x11 and self.__DBHeader['PageSize'] > 8192: + # TODO: The upper 4 bits may encode how many leading tags are reserved on large pages. + # Logical node counts should be derived from the effective reserved-tag count + # instead of assuming only tag 0 is reserved, the logical node count should be tagCount - tagReserved. + self.tagReserved = (self.record['FirstAvailablePageTag'] >> FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT) or 1 + self.tagCount = self.record['FirstAvailablePageTag'] & FIRST_AVAILABLE_PAGE_TAG_MASK def printFlags(self): flags = self.record['PageFlags'] @@ -465,14 +479,14 @@ def printFlags(self): def dump(self): baseOffset = len(self.record) self.record.dump() - tags = self.data[-4*self.record['FirstAvailablePageTag']:] + tags = self.data[-4*self.tagCount:] print("FLAGS: ") self.printFlags() print() - for i in range(self.record['FirstAvailablePageTag']): + for i in range(self.tagCount): tag = tags[-4:] if self.__DBHeader['Version'] == 0x620 and self.__DBHeader['FileFormatRevision'] > 11 and self.__DBHeader['PageSize'] > 8192: valueSize = unpack(' 0: # Leaf page @@ -678,7 +692,7 @@ def parseCatalog(self, pageNum): page = self.getPage(pageNum) self.parsePage(page) - for i in range(1, page.record['FirstAvailablePageTag']): + for i in range(1, page.tagCount): flags, data = page.getTag(i) if page.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page @@ -721,10 +735,10 @@ def openTable(self, tableName): done = False while done is False: page = self.getPage(pageNum) - if page.record['FirstAvailablePageTag'] <= 1: + if page.tagCount <= 1: # There are no records done = True - for i in range(1, page.record['FirstAvailablePageTag']): + for i in range(1, page.tagCount): flags, data = page.getTag(i) if page.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page, move on to the next page @@ -747,7 +761,7 @@ def openTable(self, tableName): def __getNextTag(self, cursor): page = cursor['CurrentPageData'] - if cursor['CurrentTag'] >= page.record['FirstAvailablePageTag']: + if cursor['CurrentTag'] >= page.tagCount: # No more data in this page, chau return None From 42fe79c7fd3ec57c8b30cf58756001dcd6969bbf Mon Sep 17 00:00:00 2001 From: alexisbalbachan Date: Fri, 27 Mar 2026 14:59:16 -0300 Subject: [PATCH 2/4] Applied code review changes, added unit test --- impacket/ese.py | 8 ++--- tests/misc/test_ese.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/misc/test_ese.py diff --git a/impacket/ese.py b/impacket/ese.py index d5c6dc4ea6..e6e9c7bced 100644 --- a/impacket/ese.py +++ b/impacket/ese.py @@ -446,10 +446,10 @@ def __init__(self, db, data=None): if data is not None: self.record = ESENT_PAGE_HEADER(self.__DBHeader['Version'], self.__DBHeader['FileFormatRevision'], self.__DBHeader['PageSize'], data) self.tagCount = self.record['FirstAvailablePageTag'] - if self.__DBHeader['FileFormatRevision'] >= 0x11 and self.__DBHeader['PageSize'] > 8192: - # TODO: The upper 4 bits may encode how many leading tags are reserved on large pages. - # Logical node counts should be derived from the effective reserved-tag count - # instead of assuming only tag 0 is reserved, the logical node count should be tagCount - tagReserved. + if self.__DBHeader['Version'] == 0x620 and self.__DBHeader['FileFormatRevision'] >= 0x11 and self.__DBHeader['PageSize'] > 8192: + # TODO: If samples with effective tagReserved > 1 appear, logical node + # iteration should be derived from the reserved-tag count instead of + # assuming only tag 0 is reserved. self.tagReserved = (self.record['FirstAvailablePageTag'] >> FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT) or 1 self.tagCount = self.record['FirstAvailablePageTag'] & FIRST_AVAILABLE_PAGE_TAG_MASK diff --git a/tests/misc/test_ese.py b/tests/misc/test_ese.py new file mode 100644 index 0000000000..dcb2b86bba --- /dev/null +++ b/tests/misc/test_ese.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# Copyright Fortra, LLC and its affiliated companies +# +# All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +import struct +import unittest + +from impacket.ese import ( + ESENT_PAGE, + ESENT_PAGE_HEADER, + FIRST_AVAILABLE_PAGE_TAG_MASK, + FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT, +) + + +class TestESENTLargePageTags(unittest.TestCase): + PAGE_SIZE = 32768 + VERSION = 0x620 + REVISION = 0x122 + + def _build_page(self, raw_tag_state): + tag_count = raw_tag_state & FIRST_AVAILABLE_PAGE_TAG_MASK + header = ESENT_PAGE_HEADER(self.VERSION, self.REVISION, self.PAGE_SIZE) + header['FirstAvailableDataOffset'] = tag_count * 4 + header['FirstAvailablePageTag'] = raw_tag_state + header['PageFlags'] = 0x2001 + + header_bytes = header.getData() + page = bytearray(self.PAGE_SIZE) + page[:len(header_bytes)] = header_bytes + + base_offset = len(header_bytes) + payloads = [bytes([i, i, i, i]) for i in range(tag_count)] + tags = [] + offset = 0 + for payload in payloads: + page[base_offset + offset:base_offset + offset + len(payload)] = payload + tags.append(struct.pack('> FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT) or 1) + self.assertEqual(page.tagCount, 0x100c & FIRST_AVAILABLE_PAGE_TAG_MASK) + + def test_iterates_large_page_tags_with_high_bits_set(self): + page, payloads = self._build_page(0x100c) + + for tag_num in range(1, page.tagCount): + self.assertEqual(page.getTag(tag_num), (0, payloads[tag_num])) + + with self.assertRaisesRegex(Exception, r'unknown tag'): + page.getTag(page.tagCount) + + +if __name__ == '__main__': + unittest.main(verbosity=1) From 3cf4a828d380d80c0b14f67780e9b1f601657037 Mon Sep 17 00:00:00 2001 From: Kali Date: Thu, 23 Apr 2026 11:42:51 -0300 Subject: [PATCH 3/4] improved code added testcase --- impacket/ese.py | 22 ++++++++++++---------- tests/misc/test_ese.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/impacket/ese.py b/impacket/ese.py index e6e9c7bced..e4e3ab0200 100644 --- a/impacket/ese.py +++ b/impacket/ese.py @@ -443,15 +443,17 @@ def __init__(self, db, data=None): self.record = None self.tagCount = 0 self.tagReserved = 1 + self.firstDataTag = 1 if data is not None: self.record = ESENT_PAGE_HEADER(self.__DBHeader['Version'], self.__DBHeader['FileFormatRevision'], self.__DBHeader['PageSize'], data) self.tagCount = self.record['FirstAvailablePageTag'] if self.__DBHeader['Version'] == 0x620 and self.__DBHeader['FileFormatRevision'] >= 0x11 and self.__DBHeader['PageSize'] > 8192: - # TODO: If samples with effective tagReserved > 1 appear, logical node - # iteration should be derived from the reserved-tag count instead of - # assuming only tag 0 is reserved. self.tagReserved = (self.record['FirstAvailablePageTag'] >> FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT) or 1 self.tagCount = self.record['FirstAvailablePageTag'] & FIRST_AVAILABLE_PAGE_TAG_MASK + self.firstDataTag = min(self.tagReserved, self.tagCount) + + def iterDataTagNums(self): + return range(self.firstDataTag, self.tagCount) def printFlags(self): flags = self.record['PageFlags'] @@ -524,7 +526,7 @@ def dump(self): leafHeader.dump() # Print the leaf/branch tags - for tagNum in range(1,self.tagCount): + for tagNum in self.iterDataTagNums(): flags, data = self.getTag(tagNum) if self.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page @@ -672,7 +674,7 @@ def __addLongValue(self, entry): def parsePage(self, page): # Print the leaf/branch tags - for tagNum in range(1,page.tagCount): + for tagNum in page.iterDataTagNums(): flags, data = page.getTag(tagNum) if page.record['PageFlags'] & FLAGS_LEAF > 0: # Leaf page @@ -692,7 +694,7 @@ def parseCatalog(self, pageNum): page = self.getPage(pageNum) self.parsePage(page) - for i in range(1, page.tagCount): + for i in page.iterDataTagNums(): flags, data = page.getTag(i) if page.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page @@ -735,10 +737,10 @@ def openTable(self, tableName): done = False while done is False: page = self.getPage(pageNum) - if page.tagCount <= 1: + if page.tagCount <= page.firstDataTag: # There are no records done = True - for i in range(1, page.tagCount): + for i in page.iterDataTagNums(): flags, data = page.getTag(i) if page.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page, move on to the next page @@ -753,7 +755,7 @@ def openTable(self, tableName): cursor['TableData'] = self.__tables[tableName] cursor['FatherDataPageNumber'] = catalogEntry['FatherDataPageNumber'] cursor['CurrentPageData'] = page - cursor['CurrentTag'] = 0 + cursor['CurrentTag'] = page.firstDataTag - 1 return cursor else: return None @@ -795,7 +797,7 @@ def getNextRow(self, cursor, filter_tables = None): return None else: cursor['CurrentPageData'] = self.getPage(page.record['NextPageNumber']) - cursor['CurrentTag'] = 0 + cursor['CurrentTag'] = cursor['CurrentPageData'].firstDataTag - 1 return self.getNextRow(cursor, filter_tables = filter_tables) else: return self.__tagToRecord(cursor, tag['EntryData'], filter_tables = filter_tables) diff --git a/tests/misc/test_ese.py b/tests/misc/test_ese.py index dcb2b86bba..6688c9bc04 100644 --- a/tests/misc/test_ese.py +++ b/tests/misc/test_ese.py @@ -69,6 +69,16 @@ def test_iterates_large_page_tags_with_high_bits_set(self): with self.assertRaisesRegex(Exception, r'unknown tag'): page.getTag(page.tagCount) + def test_iter_data_tags_skips_all_reserved_large_page_tags(self): + page, payloads = self._build_page(0x200c) + + self.assertEqual(page.tagReserved, 2) + self.assertEqual(page.firstDataTag, 2) + self.assertEqual(list(page.iterDataTagNums()), list(range(2, page.tagCount))) + + iterated_payloads = [page.getTag(tag_num)[1] for tag_num in page.iterDataTagNums()] + self.assertEqual(iterated_payloads, payloads[page.firstDataTag:]) + if __name__ == '__main__': unittest.main(verbosity=1) From de1e923002c91f90c9ca0df36336933097acfcb8 Mon Sep 17 00:00:00 2001 From: alexisbalbachan Date: Tue, 28 Apr 2026 11:12:52 -0300 Subject: [PATCH 4/4] Fix USER_PROPERTIES parsing per MS-SAMR spec --- examples/raiseChild.py | 14 +++- impacket/dcerpc/v5/samr.py | 33 +++++++- impacket/examples/secretsdump.py | 15 +++- tests/SMB_RPC/test_secretsdump.py | 120 ++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 10 deletions(-) diff --git a/examples/raiseChild.py b/examples/raiseChild.py index 13c2fd7f53..62f0fdc2ea 100755 --- a/examples/raiseChild.py +++ b/examples/raiseChild.py @@ -737,14 +737,20 @@ def __decryptSupplementalInfo(self, record, prefixTable=None): if plainText: try: - userProperties = samr.USER_PROPERTIES(plainText) + _, propertyCount, propertiesData = samr.unpack_user_properties(plainText) except: # On some old w2k3 there might be user properties that don't # match [MS-SAMR] structure, discarding them return - propertiesData = userProperties['UserProperties'] - for propertyCount in range(userProperties['PropertyCount']): - userProperty = samr.USER_PROPERTY(propertiesData) + for _ in range(propertyCount): + try: + userProperty = samr.USER_PROPERTY(propertiesData) + except Exception: + logging.debug( + 'Malformed supplemental credential property, discarding the remaining data', + exc_info=True, + ) + return propertiesData = propertiesData[len(userProperty):] if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': propertyValueBuffer = unhexlify(userProperty['PropertyValue']) diff --git a/impacket/dcerpc/v5/samr.py b/impacket/dcerpc/v5/samr.py index 5e0bb28c1f..0ae928ae3d 100644 --- a/impacket/dcerpc/v5/samr.py +++ b/impacket/dcerpc/v5/samr.py @@ -1399,10 +1399,39 @@ class USER_PROPERTIES(Structure): ('Reserved3','