pytest: compare Samba vs Windows SDDL security descriptors
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Wed, 9 Aug 2023 02:15:27 +0000 (14:15 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Thu, 24 Aug 2023 02:53:31 +0000 (02:53 +0000)
Can Samba understand Windows security descriptors? Does it parse SDDL
the same way?

Here we test on over 7000 SDDL/descriptor pairs and find the answer
is pleasing. In later commits we will add more tests using different
classes of ACE.

The test cases are derived from fuzz seeds, exported to Windows via
the script in the last commit, with the Windows descriptor bytes found
using libcli/security/tests/windows/windows-sddl-test.py.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
libcli/security/tests/data/registry-object-rights.json [new file with mode: 0644]
libcli/security/tests/data/short-ordinary-acls.json.gz [new file with mode: 0644]
python/samba/tests/security_descriptors.py [new file with mode: 0644]
selftest/knownfail.d/security-descriptors [new file with mode: 0644]
source4/selftest/tests.py

diff --git a/libcli/security/tests/data/registry-object-rights.json b/libcli/security/tests/data/registry-object-rights.json
new file mode 100644 (file)
index 0000000..97a64ea
--- /dev/null
@@ -0,0 +1 @@
+{"D:(A;;CCLCRPRC;;;WD)(A;;KA;;;BA)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 52, 0, 2, 0, 0, 0, 0, 0, 20, 0, 21, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 76, 0, 3, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)(A;;KA;;;S-1-5-21-1069531106-184984463-4116541046-512)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 112, 0, 4, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0, 0, 0, 36, 0, 63, 0, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 226, 191, 191, 63, 143, 163, 6, 11, 118, 110, 93, 245, 0, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)(A;;KA;;;S-1-5-21-1378461354-3939386343-493233828-512)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 112, 0, 4, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0, 0, 0, 36, 0, 63, 0, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 170, 166, 41, 82, 231, 67, 206, 234, 164, 38, 102, 29, 0, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)(A;;KA;;;S-1-5-21-3587273675-3237974979-2131186439-512)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 112, 0, 4, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0, 0, 0, 36, 0, 63, 0, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 203, 115, 209, 213, 195, 147, 255, 192, 7, 83, 7, 127, 0, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)(A;;KA;;;S-1-5-21-3984653172-1380167674-707033525-512)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 112, 0, 4, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0, 0, 0, 36, 0, 63, 0, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 116, 251, 128, 237, 250, 175, 67, 82, 181, 121, 36, 42, 0, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)(A;;KA;;;S-1-5-21-4154349010-984067676-209295477-512)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 112, 0, 4, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0, 0, 0, 36, 0, 63, 0, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 210, 85, 158, 247, 92, 174, 167, 58, 117, 152, 121, 12, 0, 2, 0, 0], "D:(A;;CCRPWPRC;;;WD)(A;;KA;;;BA)(A;;KA;;;AO)(A;;KA;;;S-1-5-21-536441700-3718478525-2547843259-512)": [1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 112, 0, 4, 0, 0, 0, 0, 0, 20, 0, 49, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 36, 2, 0, 0, 0, 0, 36, 0, 63, 0, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 100, 115, 249, 31, 189, 122, 163, 221, 187, 0, 221, 151, 0, 2, 0, 0], "O:BAG:SYD:(A;;KR;;;WD)(A;;KA;;;BA)(A;;KA;;;SY)": [1, 0, 4, 128, 92, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 72, 0, 3, 0, 0, 0, 0, 0, 20, 0, 25, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 24, 0, 63, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 20, 0, 63, 0, 15, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0], "O:S-1-5-21-3984653172-1380167674-707033525-1000G:S-1-22-2-50133D:(A;;0x1f019f;;;S-1-5-21-3984653172-1380167674-707033525-1000)(A;;0x1f019f;;;S-1-22-2-50133)(A;;0x1f019f;;;WD)(A;;KA;;;SY)": [1, 0, 4, 128, 128, 0, 0, 0, 156, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 108, 0, 4, 0, 0, 0, 0, 0, 36, 0, 159, 1, 31, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 116, 251, 128, 237, 250, 175, 67, 82, 181, 121, 36, 42, 232, 3, 0, 0, 0, 0, 24, 0, 159, 1, 31, 0, 1, 2, 0, 0, 0, 0, 0, 22, 2, 0, 0, 0, 213, 195, 0, 0, 0, 0, 20, 0, 159, 1, 31, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 20, 0, 63, 0, 15, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 116, 251, 128, 237, 250, 175, 67, 82, 181, 121, 36, 42, 232, 3, 0, 0, 1, 2, 0, 0, 0, 0, 0, 22, 2, 0, 0, 0, 213, 195, 0, 0], "O:S-1-5-21-536441700-3718478525-2547843259-1000G:S-1-22-2-50133D:(A;;0x1f019f;;;S-1-5-21-536441700-3718478525-2547843259-1000)(A;;0x1f019f;;;S-1-22-2-50133)(A;;0x1f019f;;;WD)(A;;KA;;;SY)": [1, 0, 4, 128, 128, 0, 0, 0, 156, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 2, 0, 108, 0, 4, 0, 0, 0, 0, 0, 36, 0, 159, 1, 31, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 100, 115, 249, 31, 189, 122, 163, 221, 187, 0, 221, 151, 232, 3, 0, 0, 0, 0, 24, 0, 159, 1, 31, 0, 1, 2, 0, 0, 0, 0, 0, 22, 2, 0, 0, 0, 213, 195, 0, 0, 0, 0, 20, 0, 159, 1, 31, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 20, 0, 63, 0, 15, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 100, 115, 249, 31, 189, 122, 163, 221, 187, 0, 221, 151, 232, 3, 0, 0, 1, 2, 0, 0, 0, 0, 0, 22, 2, 0, 0, 0, 213, 195, 0, 0]}
\ No newline at end of file
diff --git a/libcli/security/tests/data/short-ordinary-acls.json.gz b/libcli/security/tests/data/short-ordinary-acls.json.gz
new file mode 100644 (file)
index 0000000..8554b7c
Binary files /dev/null and b/libcli/security/tests/data/short-ordinary-acls.json.gz differ
diff --git a/python/samba/tests/security_descriptors.py b/python/samba/tests/security_descriptors.py
new file mode 100644 (file)
index 0000000..e6feafe
--- /dev/null
@@ -0,0 +1,147 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Volker Lendecke <vl@samba.org> 2021
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+"""These tests compare Windows security descriptors with Samba
+descriptors derived from the same SDDL.
+
+They use json and json.gz files in libcli/security/tests/data.
+"""
+
+from samba.dcerpc import security
+from samba.ndr import ndr_pack, ndr_unpack, ndr_print
+from samba.tests import TestCase, DynamicTestCase
+from samba.colour import colourdiff
+from hashlib import md5
+import gzip
+
+import os
+import json
+from pathlib import Path
+
+TEST_DIR = Path(__name__).parent.parent.parent / 'libcli/security/tests/data'
+
+
+class SDDLvsDescriptorBase(TestCase):
+    """These tests have no explicit cases and no inline data. The actual
+    data is kept in JSON files in libcli/security/tests/data, so that
+    it easy to share those files with Windows. To control what tests
+    are run, set the `json_file` attribute in subclasses, and/or add a
+    filter_test_cases class method.
+    """
+    maxDiff = 10000
+    json_file = TEST_DIR / 'conditional_aces.txt.json'
+    munge_to_v4 = True
+    domain_sid = security.dom_sid("S-1-5-21-2457507606-2709100691-398136650")
+
+    @classmethod
+    def filter_test_cases(cls, data):
+        """Filter out some cases before running the tests.
+        Like this, for example:
+            return {k:v for k, v in data.items() if len(k) < 200 and
+                    '(D;;;;;MP)(D;;;;;MP)(D;;;;;MP)' in k}
+        """
+        return data
+
+    @classmethod
+    def setUpDynamicTestCases(cls):
+        try:
+            with gzip.open(cls.json_file, 'rt') as f:
+                data = json.load(f)
+        except Exception:
+            with open(cls.json_file) as f:
+                data = json.load(f)
+
+        data = cls.filter_test_cases(data)
+
+        for sddl, sdl in data.items():
+            name = sddl
+            if len(name) > 130:
+                tag = md5(sddl.encode()).hexdigest()[:10]
+                name = f"{name[:100]}+{len(name) - 100}-more-characters-{tag}"
+            cls.generate_dynamic_test('test_sddl_vs_sd', name, sddl, sdl)
+
+
+    def _test_sddl_vs_sd_with_args(self, sddl, sdl):
+        sdb_win = bytes(sdl)
+        try:
+            sd_sam = security.descriptor.from_sddl(sddl, self.domain_sid)
+        except (TypeError, ValueError) as e:
+            self.fail(f"failed to parse {sddl} into SD: {e}")
+
+        try:
+            sdb_sam = ndr_pack(sd_sam)
+        except RuntimeError as e:
+            self.fail(f"failed to pack samba SD from {sddl} into bytes: {e}\n"
+                      f"{ndr_print(sd_sam)}")
+
+        try:
+            sd_win = ndr_unpack(security.descriptor, sdb_win)
+        except RuntimeError as e:
+            self.fail(f"could not unpack windows descriptor for {sddl}: {e}\n"
+                      f"windows {len(sdb_win)} bytes: {sdb_win.hex(' ')}\n"
+                      f"samba   {len(sdb_sam)} bytes: {sdb_sam.hex(' ')}\n"
+                      f"colour diff {colourdiff(sdb_win, sdb_sam)}\n"
+                      f"SAMBA SD is\n{ndr_print(sd_sam)}")
+
+        if self.munge_to_v4:
+            # Force the ACL revisions to match Samba. Windows seems to
+            # use the lowest possible revision, while Samba uses
+            # ACL_REVISION_DS when generating from SDDL. The _DS
+            # version allows more ACE types, but is otherwise the same.
+            #
+            # MS-DTYP 2.4.5 ACL:
+            #
+            # ACL_REVISION 0x02
+            #
+            # When set to 0x02, only AceTypes 0x00, 0x01,
+            # 0x02, 0x03, 0x11, 0x12, and 0x13 can be present in the ACL.
+            # An AceType of 0x11 is used for SACLs but not for DACLs. For
+            # more information about ACE types, see section 2.4.4.1.
+            #
+            # ACL_REVISION_DS 0x04
+            #
+            # When set to 0x04, AceTypes 0x05, 0x06, 0x07, 0x08, and 0x11
+            # are allowed. ACLs of revision 0x04 are applicable only to
+            # directory service objects. An AceType of 0x11 is used for
+            # SACLs but not for DACLs.
+            #
+            # 5, 6, 7, 8 are object ACES.
+            if sd_win.dacl:
+                sd_win.dacl.revision = 4
+            if sd_win.sacl:
+                sd_win.sacl.revision = 4
+
+        if (sd_win != sd_sam):
+            self.fail(f"Descriptors differ for {sddl}\n"
+                      f"windows {len(sdb_win)} bytes: {sdb_win.hex(' ')}\n"
+                      f"samba   {len(sdb_sam)} bytes: {sdb_sam.hex(' ')}\n"
+                      f"colour diff {colourdiff(sdb_win, sdb_sam)}")
+
+
+@DynamicTestCase
+class SDDLvsDescriptorShortOrdinaryAcls(SDDLvsDescriptorBase):
+    """These are not conditional ACEs or resource attribute aces, the SDDL
+    is less than 1000 characters long, and success is expected.
+    """
+    json_file = TEST_DIR / 'short-ordinary-acls.json.gz'
+
+
+@DynamicTestCase
+class SDDLvsDescriptorRegistryObjectRights(SDDLvsDescriptorBase):
+    """We'll fail these because we don't recognise 'KA' and related object
+    rights strings that are used for registry objects."""
+    json_file = TEST_DIR / 'registry-object-rights.json'
diff --git a/selftest/knownfail.d/security-descriptors b/selftest/knownfail.d/security-descriptors
new file mode 100644 (file)
index 0000000..e9bf642
--- /dev/null
@@ -0,0 +1 @@
+^samba.tests.security_descriptors.+SDDLvsDescriptorRegistryObjectRights.+
index ab43dada142e3e439e6bb6475d86fc18a72e05b2..e41f4677553dd66bdd4b32e3f33af73b1b09674b 100755 (executable)
@@ -2126,3 +2126,4 @@ planoldpythontestsuite("proclimitdc",
 planoldpythontestsuite("none", "samba.tests.usage")
 planpythontestsuite("fileserver", "samba.tests.dcerpc.mdssvc")
 planoldpythontestsuite("none", "samba.tests.compression")
+planpythontestsuite("none", "samba.tests.security_descriptors")