Files
fortra-impacket/tests/misc/test_structure.py
Herbenderbler 0be6514bdf BUG-2099: Fix Unhandled ValueError during authentication (#2151)
* Fix #2099: handle truncated SMB responses (SessionError + debug log)

- structure: asciiz without NUL raises clear ValueError with field name
- smb: catch ValueError at session/negotiate parse sites; log at debug, raise SessionError
- smbconnection: docstring notes invalid/truncated server response
- ci: fix flake8 F824 (goldenPac, ldapattack)
- tests: regression for asciiz NUL and session setup parsing; align with TESTING.md
- docs: ChangeLog

* Fix #2099 struct.error mapping.
2026-05-06 01:53:30 -03:00

278 lines
9.1 KiB
Python

#!/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.
#
from __future__ import print_function
import six
import unittest
from binascii import hexlify
from impacket.structure import Structure
def hexl(b):
hexstr = str(hexlify(b).decode('ascii'))
return ' '.join([hexstr[i:i + 8] for i in range(0, len(hexstr), 8)])
class _StructureTest(object):
# Subclass:
# - must define theClass
# - may override alignment
alignment = 0
def create(self, data=None):
if data is not None:
return self.theClass(data, alignment=self.alignment)
else:
return self.theClass(alignment=self.alignment)
def test_structure(self):
# print()
# print("-"*70)
# testName = self.__class__.__name__
# print("starting test: %s....." % testName)
# Create blank structure and fill its fields
a = self.create()
self.populate(a)
# a.dump("packing.....")
# Get its binary representation
a_str = a.getData()
self.check_data(a_str)
# print("packed: %r" % a_str)
# print("unpacking.....")
b = self.create(a_str)
# b.dump("unpacked.....")
# print("repacking.....")
b_str = b.getData()
self.assertEqual(b_str, a_str,
"ERROR: original packed and repacked don't match")
def check_data(self, a_str):
if hasattr(self, 'hexData'):
# Regression check
self.assertEqual(hexl(a_str), self.hexData)
else:
# Show result, to aid adding regression check
print(self.__class__.__name__, hexl(a_str))
class Test_simple(_StructureTest, unittest.TestCase):
class theClass(Structure):
commonHdr = ()
structure = (
('int1', '!L'),
('len1', '!L-z1'),
('arr1', 'B*<L'),
('z1', 'z'),
('u1', 'u'),
('', '"COCA'),
('len2', '!H-:1'),
('', '"COCA'),
(':1', ':'),
('int3', '>L'),
('code1', '>L=len(arr1)*2+0x1000'),
)
def populate(self, a):
a['default'] = 'hola'
a['int1'] = 0x3131
a['int3'] = 0x45444342
a['z1'] = 'hola'
a['u1'] = 'hola'.encode('utf_16_le')
a[':1'] = ':1234:'
a['arr1'] = (0x12341234, 0x88990077, 0x41414141)
# a['len1'] = 0x42424242
hexData = '00003131 00000005 03341234 12770099 88414141 41686f6c 61006800 6f006c00 61000000 434f4341 0006434f 43413a31 3233343a 45444342 00001006'
class Test_fixedLength(Test_simple):
def test_structure(self):
a = self.create()
self.populate(a)
# Set a bogus length...
a['len1'] = 0x42424242
a_str = a.getData()
if hasattr(self, 'hexData'):
# Regression check
self.assertEqual(hexl(a_str), self.hexData)
else:
print(hexl(a_str))
# ... so that unpacking will now fail
with six.assertRaisesRegex(self, Exception, r'not NUL terminated'):
self.create(a_str)
hexData = '00003131 42424242 03341234 12770099 88414141 41686f6c 61006800 6f006c00 61000000 434f4341 0006434f 43413a31 3233343a 45444342 00001006'
class Test_simple_aligned4(Test_simple):
alignment = 4
hexData = '00003131 00000005 03341234 12770099 88414141 41000000 686f6c61 00000000 68006f00 6c006100 00000000 434f4341 00060000 434f4341 3a313233 343a0000 45444342 00001006'
class Test_unicode_with_embedded_null(_StructureTest, unittest.TestCase):
class theClass(Structure):
structure = (
('utf16', 'u'),
)
def populate(self, a):
# Use a character whose low byte is 0x00 (U+0400) following an ASCII char.
# Older calcUnpackSize logic stopped at the odd-offset 0x00 0x00 pair between chars.
a['utf16'] = 'A\u0400'.encode('utf_16_le')
hexData = '41000004 0000'
class Test_nested(_StructureTest, unittest.TestCase):
class theClass(Structure):
class _Inner(Structure):
structure = (('data', 'z'),)
structure = (
('nest1', ':', _Inner),
('nest2', ':', _Inner),
('int', '<L'),
)
def populate(self, a):
a['nest1'] = Test_nested.theClass._Inner()
a['nest2'] = Test_nested.theClass._Inner()
a['nest1']['data'] = 'hola manola'
a['nest2']['data'] = 'chau loco'
a['int'] = 0x12345678
hexData = '686f6c61 206d616e 6f6c6100 63686175 206c6f63 6f007856 3412'
class Test_Optional(_StructureTest, unittest.TestCase):
class theClass(Structure):
structure = (
('pName', '<L&Name'),
('pList', '<L&List'),
('Name', 'w'),
('List', '<H*<L'),
)
def populate(self, a):
a['Name'] = 'Optional test'
a['List'] = (1, 2, 3, 4)
def check_data(self, a_str):
# Pointer values change between runs, so ignore them
filtered = ''.join(['-' if h == '-' else a
for a, h in zip(hexl(a_str), self.hexData)])
self.assertEqual(filtered, self.hexData)
hexData = '-------- -------- 07000000 07000000 00000000 4f707469 6f6e616c 20746573 74000400 01000000 02000000 03000000 04000000'
class Test_Optional_sparse(Test_Optional):
def populate(self, a):
Test_Optional.populate(self, a)
del a['Name']
hexData = '00000000 -------- 04000100 00000200 00000300 00000400 0000'
class Test_AsciiZArray(_StructureTest, unittest.TestCase):
class theClass(Structure):
structure = (
('head', '<L'),
('array', 'B*z'),
('tail', '<L'),
)
def populate(self, a):
a['head'] = 0x1234
a['tail'] = 0xabcd
a['array'] = ('hola', 'manola', 'te traje')
hexData = '34120000 03686f6c 61006d61 6e6f6c61 00746520 7472616a 6500cdab 0000'
class Test_UnpackCode(_StructureTest, unittest.TestCase):
class theClass(Structure):
structure = (
('leni', '<L=len(uno)*2'),
('cuchi', '_-uno', 'leni//2'),
('uno', ':'),
('dos', ':'),
)
def populate(self, a):
a['uno'] = 'soy un loco!'
a['dos'] = 'que haces fiera'
hexData = '18000000 736f7920 756e206c 6f636f21 71756520 68616365 73206669 657261'
class Test_asciiz_no_nul_raises_clear_error(unittest.TestCase):
"""Regression test for #2099: asciiz field with no NUL raises descriptive ValueError.
Local test (no remote target). Runs with pytest -m 'not remote'.
"""
def test_asciiz_missing_nul_raises_value_error_with_field_name(self):
from impacket.structure import Structure
class WithAsciiz(Structure):
structure = (('name', 'z'),)
with six.assertRaisesRegex(self, ValueError, r"NUL terminator"):
WithAsciiz(b'NoNULhere')
with self.assertRaises(ValueError) as ctx:
WithAsciiz(b'Truncated')
self.assertIn('name', str(ctx.exception),
"Exception message should include the field name")
def test_session_like_structure_truncated_asciiz_raises_clear_error(self):
"""Regression for #2099: structure with length-prefixed blob then asciiz (like SMB NativeOS) raises clear ValueError when no NUL."""
from impacket.structure import Structure
# Mimics SMB session response: length-prefixed blob then asciiz (NativeOS). Set Len=0 so blob consumes 0 bytes.
class SessionLike(Structure):
structure = (
('Len', '_-Blob', 'self["Len"]'),
('Blob', ':'),
('NativeOS', 'z=""'),
)
inst = SessionLike()
inst['Len'] = 0
with self.assertRaises(ValueError) as ctx:
inst.fromString(b'TruncatedOrMalformed')
msg = str(ctx.exception)
self.assertIn('NUL', msg, "Message should mention missing NUL terminator")
self.assertIn('NativeOS', msg, "Message should identify the field (NativeOS)")
class Test_AAA(_StructureTest, unittest.TestCase):
class theClass(Structure):
commonHdr = ()
structure = (
('iv', '!L=((init_vector & 0xFFFFFF) << 8) | ((pad & 0x3f) << 2) | (keyid & 3)'),
('init_vector', '_', '(iv >> 8)'),
('pad', '_', '((iv >>2) & 0x3F)'),
('keyid', '_', '( iv & 0x03 )'),
('dataLen', '_-data', 'len(inputDataLeft)-4'),
('data', ':'),
('icv', '>L'),
)
def populate(self, a):
a['init_vector'] = 0x01020304
# a['pad']=int('01010101',2)
a['pad'] = int('010101', 2)
a['keyid'] = 0x07
a['data'] = "\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9"
a['icv'] = 0x05060708
# a['iv'] = 0x01020304
hexData = '02030457 a0a1a2a3 a4a5a6a7 a8a90506 0708'
if __name__ == "__main__":
unittest.main(verbosity=1)