From 128710c2f3c1ee3dd73eba8d755ea7caeb9f3196 Mon Sep 17 00:00:00 2001 From: Rob van der Linde Date: Thu, 7 Dec 2023 15:53:01 +1300 Subject: [PATCH] python: tests: blackbox test for GMSA Signed-off-by: Rob van der Linde Reviewed-by: Andrew Bartlett Reviewed-by: Douglas Bagnall --- .../tests/samba_tool/user_getpassword_gmsa.py | 171 ++++++++++++++++++ selftest/knownfail.d/user_getpassword_gmsa | 1 + source4/selftest/tests.py | 3 + 3 files changed, 175 insertions(+) create mode 100644 python/samba/tests/samba_tool/user_getpassword_gmsa.py create mode 100644 selftest/knownfail.d/user_getpassword_gmsa diff --git a/python/samba/tests/samba_tool/user_getpassword_gmsa.py b/python/samba/tests/samba_tool/user_getpassword_gmsa.py new file mode 100644 index 00000000000..98444563362 --- /dev/null +++ b/python/samba/tests/samba_tool/user_getpassword_gmsa.py @@ -0,0 +1,171 @@ +# Unix SMB/CIFS implementation. +# +# Blackbox tests for reading Group Managed Service Account passwords +# +# Copyright (C) Catalyst.Net Ltd. 2023 +# +# Written by Rob van der Linde +# +# 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 . +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from ldb import SCOPE_BASE + +from samba.credentials import Credentials, MUST_USE_KERBEROS +from samba.dcerpc import security, samr +from samba.dsdb import UF_WORKSTATION_TRUST_ACCOUNT +from samba.netcmd.domain.models import User +from samba.ndr import ndr_pack, ndr_unpack +from samba.tests import connect_samdb, connect_samdb_env, delete_force + +from samba.tests import BlackboxTestCase + +DC_SERVER = os.environ["SERVER"] +SERVER = os.environ["SERVER"] +SERVER_USERNAME = os.environ["USERNAME"] +SERVER_PASSWORD = os.environ["PASSWORD"] + +HOST = f"ldap://{SERVER}" +CREDS = f"-U{SERVER_USERNAME}%{SERVER_PASSWORD}" + + +class GMSAPasswordTest(BlackboxTestCase): + """Blackbox tests for GMSA getpassword and connecting as that user.""" + + @classmethod + def setUpClass(cls): + cls.lp = cls.get_loadparm() + cls.env_creds = cls.get_env_credentials(lp=cls.lp, + env_username="USERNAME", + env_password="PASSWORD", + env_domain="DOMAIN", + env_realm="REALM") + cls.samdb = connect_samdb(HOST, lp=cls.lp, credentials=cls.env_creds) + super().setUpClass() + + @classmethod + def setUpTestData(cls): + cls.username = "GMSA_Test_User$" + cls.base_dn = f"CN=Managed Service Accounts,{cls.samdb.domain_dn()}" + cls.user_dn = f"CN={cls.username},{cls.base_dn}" + + msg = cls.samdb.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0] + connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0])) + + domain_sid = security.dom_sid(cls.samdb.get_domain_sid()) + allow_sddl = f"O:SYD:(A;;RP;;;{connecting_user_sid})" + allow_sd = ndr_pack(security.descriptor.from_sddl(allow_sddl, domain_sid)) + + details = { + "dn": str(cls.user_dn), + "objectClass": "msDS-GroupManagedServiceAccount", + "msDS-ManagedPasswordInterval": "1", + "msDS-GroupMSAMembership": allow_sd, + "sAMAccountName": cls.username, + "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT), + } + + cls.samdb.add(details) + cls.addClassCleanup(delete_force, cls.samdb, cls.user_dn) + + cls.user = User.get(cls.samdb, username=cls.username) + + def getpassword(self, attrs): + cmd = f"user getpassword --attributes={attrs} {self.username}" + + ldif = self.check_output(cmd).decode() + res = self.samdb.parse_ldif(ldif) + _, user_message = next(res) + + # check each attr is returned + for attr in attrs.split(","): + self.assertIn(attr, user_message) + + return user_message + + def test_getpassword(self): + self.getpassword("virtualClearTextUTF16,unicodePwd") + self.getpassword("virtualClearTextUTF16") + self.getpassword("unicodePwd") + + def test_utf16_password(self): + user_msg = self.getpassword("virtualClearTextUTF16") + password = user_msg["virtualClearTextUTF16"][0] + + creds = self.insta_creds(template=self.env_creds) + creds.set_username(self.username) + creds.set_utf16_password(password) + db = connect_samdb(HOST, credentials=creds, lp=self.lp) + + msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0] + connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0])) + + self.assertEqual(self.user.object_sid, connecting_user_sid) + + def test_utf8_password(self): + user_msg = self.getpassword("virtualClearTextUTF8") + password = str(user_msg["virtualClearTextUTF8"][0]) + + creds = self.insta_creds(template=self.env_creds) + # Because the password has been converted to utf-8 via UTF16_MUNGED + # the nthash is no longer valid. We need to use AES kerberos ciphers + # for this to work. + creds.set_kerberos_state(MUST_USE_KERBEROS) + creds.set_username(self.username) + creds.set_password(password) + db = connect_samdb(HOST, credentials=creds, lp=self.lp) + + msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0] + connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0])) + + self.assertEqual(self.user.object_sid, connecting_user_sid) + + def test_unicode_pwd(self): + user_msg = self.getpassword("unicodePwd") + + creds = self.insta_creds(template=self.env_creds) + creds.set_username(self.username) + nt_pass = samr.Password() + nt_pass.hash = list(user_msg["unicodePwd"][0]) + creds.set_nt_hash(nt_pass) + db = connect_samdb(HOST, credentials=creds, lp=self.lp) + + msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0] + connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0])) + + self.assertEqual(self.user.object_sid, connecting_user_sid) + + @classmethod + def _make_cmdline(cls, line): + """Override to pass line as samba-tool subcommand instead. + + Automatically fills in HOST and CREDS as well. + """ + if isinstance(line, list): + cmd = ["samba-tool"] + line + ["-H", SERVER, CREDS] + else: + cmd = f"samba-tool {line} -H {HOST} {CREDS}" + + return super()._make_cmdline(cmd) + + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/selftest/knownfail.d/user_getpassword_gmsa b/selftest/knownfail.d/user_getpassword_gmsa new file mode 100644 index 00000000000..4a4fec93d6f --- /dev/null +++ b/selftest/knownfail.d/user_getpassword_gmsa @@ -0,0 +1 @@ +^samba.tests.samba_tool.user_getpassword_gmsa.samba.tests.samba_tool.user_getpassword_gmsa.GMSAPasswordTest diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index a518bcf79e2..934aec5e1fb 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -1124,6 +1124,9 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize") for env in all_fl_envs: planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo") +# test getpassword for group managed service accounts +planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.user_getpassword_gmsa") + # test samba-tool user, group, contact and computer edit command for env in all_fl_envs: env += ":local" -- 2.34.1