summaryrefslogtreecommitdiffstats
path: root/python/samba/netcmd/user/readpasswords
diff options
context:
space:
mode:
Diffstat (limited to 'python/samba/netcmd/user/readpasswords')
-rw-r--r--python/samba/netcmd/user/readpasswords/__init__.py25
-rw-r--r--python/samba/netcmd/user/readpasswords/common.py907
-rw-r--r--python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py146
-rw-r--r--python/samba/netcmd/user/readpasswords/getpassword.py210
-rw-r--r--python/samba/netcmd/user/readpasswords/show.py144
-rw-r--r--python/samba/netcmd/user/readpasswords/syncpasswords.py878
6 files changed, 2310 insertions, 0 deletions
diff --git a/python/samba/netcmd/user/readpasswords/__init__.py b/python/samba/netcmd/user/readpasswords/__init__.py
new file mode 100644
index 0000000..75ba313
--- /dev/null
+++ b/python/samba/netcmd/user/readpasswords/__init__.py
@@ -0,0 +1,25 @@
+# user management
+#
+# user readpasswords commands
+#
+# Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
+#
+# 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/>.
+#
+
+from .getpassword import cmd_user_getpassword
+from .show import cmd_user_show
+from .syncpasswords import cmd_user_syncpasswords
+from .get_kerberos_ticket import cmd_user_get_kerberos_ticket
diff --git a/python/samba/netcmd/user/readpasswords/common.py b/python/samba/netcmd/user/readpasswords/common.py
new file mode 100644
index 0000000..6d44881
--- /dev/null
+++ b/python/samba/netcmd/user/readpasswords/common.py
@@ -0,0 +1,907 @@
+# user management
+#
+# common code
+#
+# Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
+#
+# 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/>.
+#
+
+import base64
+import builtins
+import binascii
+import errno
+import io
+import os
+
+import ldb
+from samba import credentials, nttime2float
+from samba.auth import system_session
+from samba.common import get_bytes, get_string
+from samba.dcerpc import drsblobs, security, gmsa
+from samba.ndr import ndr_unpack
+from samba.netcmd import Command, CommandError
+from samba.samdb import SamDB
+
+
+# python[3]-gpgme is abandoned since ubuntu 1804 and debian 9
+# have to use python[3]-gpg instead
+# The API is different, need to adapt.
+
+def _gpgme_decrypt(encrypted_bytes):
+ """
+ Use python[3]-gpgme to decrypt GPG.
+ """
+ ctx = gpgme.Context()
+ ctx.armor = True # use ASCII-armored
+ out = io.BytesIO()
+ ctx.decrypt(io.BytesIO(encrypted_bytes), out)
+ return out.getvalue()
+
+
+def _gpg_decrypt(encrypted_bytes):
+ """
+ Use python[3]-gpg to decrypt GPG.
+ """
+ ciphertext = gpg.Data(string=encrypted_bytes)
+ ctx = gpg.Context(armor=True)
+ # plaintext, result, verify_result
+ plaintext, _, _ = ctx.decrypt(ciphertext)
+ return plaintext
+
+
+gpg_decrypt = None
+
+if not gpg_decrypt:
+ try:
+ import gpgme
+ gpg_decrypt = _gpgme_decrypt
+ except ImportError:
+ pass
+
+if not gpg_decrypt:
+ try:
+ import gpg
+ gpg_decrypt = _gpg_decrypt
+ except ImportError:
+ pass
+
+if gpg_decrypt:
+ decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as "
+ "cleartext source")
+else:
+ decrypt_samba_gpg_help = ("Decrypt the SambaGPG password not supported, "
+ "python[3]-gpgme or python[3]-gpg required")
+
+
+disabled_virtual_attributes = {
+}
+
+virtual_attributes = {
+ "virtualClearTextUTF8": {
+ "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+ },
+ "virtualClearTextUTF16": {
+ "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+ },
+ "virtualSambaGPG": {
+ "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+ },
+ "unicodePwd": {
+ "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+ },
+}
+
+
+def get_crypt_value(alg, utf8pw, rounds=0):
+ algs = {
+ "5": {"length": 43},
+ "6": {"length": 86},
+ }
+ assert alg in algs
+ salt = os.urandom(16)
+ # The salt needs to be in [A-Za-z0-9./]
+ # base64 is close enough and as we had 16
+ # random bytes but only need 16 characters
+ # we can ignore the possible == at the end
+ # of the base64 string
+ # we just need to replace '+' by '.'
+ b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
+ crypt_salt = ""
+ if rounds != 0:
+ crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
+ else:
+ crypt_salt = "$%s$%s$" % (alg, b64salt)
+
+ crypt_value = crypt.crypt(utf8pw, crypt_salt)
+ if crypt_value is None:
+ raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
+ expected_len = len(crypt_salt) + algs[alg]["length"]
+ if len(crypt_value) != expected_len:
+ raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
+ crypt_salt, len(crypt_value), expected_len))
+ return crypt_value
+
+
+try:
+ import hashlib
+ hashlib.sha1()
+ virtual_attributes["virtualSSHA"] = {
+ }
+except ImportError as e:
+ reason = "hashlib.sha1()"
+ reason += " required"
+ disabled_virtual_attributes["virtualSSHA"] = {
+ "reason": reason,
+ }
+
+for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
+ try:
+ import crypt
+ get_crypt_value(alg, "")
+ virtual_attributes[attr] = {
+ }
+ except ImportError as e:
+ reason = "crypt"
+ reason += " required"
+ disabled_virtual_attributes[attr] = {
+ "reason": reason,
+ }
+ except NotImplementedError as e:
+ reason = "modern '$%s$' salt in crypt(3) required" % (alg)
+ disabled_virtual_attributes[attr] = {
+ "reason": reason,
+ }
+
+# Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
+for x in range(1, 30):
+ virtual_attributes["virtualWDigest%02d" % x] = {}
+
+# Add Kerberos virtual attributes
+virtual_attributes["virtualKerberosSalt"] = {}
+
+virtual_attributes_help = "The attributes to display (comma separated). "
+virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
+if len(disabled_virtual_attributes) != 0:
+ virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
+
+
+class GetPasswordCommand(Command):
+
+ def __init__(self):
+ super().__init__()
+ self.lp = None
+
+ def inject_virtual_attributes(self, samdb):
+ # We use sort here in order to have a predictable processing order
+ # this might not be strictly needed, but also doesn't hurt here
+ for a in sorted(virtual_attributes.keys()):
+ flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
+ samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
+
+ def connect_for_passwords(self, url,
+ creds=None,
+ require_ldapi=True,
+ verbose=False):
+
+ # using anonymous here, results in no authentication
+ # which means we can get system privileges via
+ # the privileged ldapi socket
+ anon_creds = credentials.Credentials()
+ anon_creds.set_anonymous()
+
+ if url is None and not require_ldapi:
+ pass
+ elif url.lower().startswith("ldapi://"):
+ creds = anon_creds
+ pass
+ elif require_ldapi:
+ raise CommandError("--url requires an ldapi:// url for this command")
+
+ if verbose:
+ self.outf.write("Connecting to '%s'\n" % url)
+
+ samdb = SamDB(url=url, session_info=system_session(),
+ credentials=creds, lp=self.lp)
+
+ if require_ldapi or url is None:
+ try:
+ #
+ # Make sure we're connected as SYSTEM
+ #
+ res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ assert len(res) == 1
+ sids = res[0].get("tokenGroups")
+ assert len(sids) == 1
+ sid = ndr_unpack(security.dom_sid, sids[0])
+ assert str(sid) == security.SID_NT_SYSTEM
+ except Exception as msg:
+ raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
+ (security.SID_NT_SYSTEM))
+
+ self.inject_virtual_attributes(samdb)
+
+ return samdb
+
+ def get_account_attributes(self, samdb, username, basedn, filter, scope,
+ attrs, decrypt, support_pw_attrs=True):
+
+ def get_option(opts, name):
+ if not opts:
+ return None
+ for o in opts:
+ if o.lower().startswith("%s=" % name.lower()):
+ (key, _, val) = o.partition('=')
+ return val
+ return None
+
+ def get_virtual_attr_definition(attr):
+ for van in sorted(virtual_attributes.keys()):
+ if van.lower() != attr.lower():
+ continue
+ return virtual_attributes[van]
+ return None
+
+ formats = [
+ "GeneralizedTime",
+ "UnixTime",
+ "TimeSpec",
+ ]
+
+ def get_virtual_format_definition(opts):
+ formatname = get_option(opts, "format")
+ if formatname is None:
+ return None
+ for fm in formats:
+ if fm.lower() != formatname.lower():
+ continue
+ return fm
+ return None
+
+ def parse_raw_attr(raw_attr, is_hidden=False):
+ (attr, _, fullopts) = raw_attr.partition(';')
+ if fullopts:
+ opts = fullopts.split(';')
+ else:
+ opts = []
+ a = {}
+ a["raw_attr"] = raw_attr
+ a["attr"] = attr
+ a["opts"] = opts
+ a["vattr"] = get_virtual_attr_definition(attr)
+ a["vformat"] = get_virtual_format_definition(opts)
+ a["is_hidden"] = is_hidden
+ return a
+
+ raw_attrs = attrs[:]
+ has_wildcard_attr = "*" in raw_attrs
+ has_virtual_attrs = False
+ requested_attrs = []
+ implicit_attrs = []
+
+ for raw_attr in raw_attrs:
+ a = parse_raw_attr(raw_attr)
+ requested_attrs.append(a)
+
+ search_attrs = []
+ has_virtual_attrs = False
+ for a in requested_attrs:
+ if a["vattr"] is not None:
+ has_virtual_attrs = True
+ continue
+ if a["vformat"] is not None:
+ # also add it as implicit attr,
+ # where we just do
+ # search_attrs.append(a["attr"])
+ # later on
+ implicit_attrs.append(a)
+ continue
+ if a["raw_attr"] in search_attrs:
+ continue
+ search_attrs.append(a["raw_attr"])
+
+ if not has_wildcard_attr:
+ required_attrs = [
+ "sAMAccountName",
+ "userPrincipalName"
+ ]
+ for required_attr in required_attrs:
+ a = parse_raw_attr(required_attr)
+ implicit_attrs.append(a)
+
+ if has_virtual_attrs:
+ if support_pw_attrs:
+ required_attrs = [
+ "supplementalCredentials",
+ "unicodePwd",
+ "msDS-ManagedPassword",
+ ]
+ for required_attr in required_attrs:
+ a = parse_raw_attr(required_attr, is_hidden=True)
+ implicit_attrs.append(a)
+
+ for a in implicit_attrs:
+ if a["attr"] in search_attrs:
+ continue
+ search_attrs.append(a["attr"])
+
+ if scope == ldb.SCOPE_BASE:
+ search_controls = ["show_deleted:1", "show_recycled:1"]
+ else:
+ search_controls = []
+ try:
+ res = samdb.search(base=basedn, expression=filter,
+ scope=scope, attrs=search_attrs,
+ controls=search_controls)
+ if len(res) == 0:
+ raise Exception('Unable to find user "%s"' % (username or filter))
+ if len(res) > 1:
+ raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
+ except Exception as msg:
+ # FIXME: catch more specific exception
+ raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
+ obj = res[0]
+
+ calculated = {}
+
+ sc = None
+ unicodePwd = None
+ if "supplementalCredentials" in obj:
+ sc_blob = obj["supplementalCredentials"][0]
+ sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
+ if "unicodePwd" in obj:
+ unicodePwd = obj["unicodePwd"][0]
+ if "msDS-ManagedPassword" in obj:
+ # unpack a GMSA managed password as if we could read the
+ # hidden password attributes.
+ managed_password = obj["msDS-ManagedPassword"][0]
+ unpacked_managed_password = ndr_unpack(gmsa.MANAGEDPASSWORD_BLOB,
+ managed_password)
+ calculated["Primary:CLEARTEXT"] = \
+ unpacked_managed_password.passwords.current
+ calculated["OLDCLEARTEXT"] = \
+ unpacked_managed_password.passwords.previous
+
+ account_name = str(obj["sAMAccountName"][0])
+ if "userPrincipalName" in obj:
+ account_upn = str(obj["userPrincipalName"][0])
+ else:
+ realm = samdb.domain_dns_name()
+ account_upn = "%s@%s" % (account_name, realm.lower())
+
+ def get_package(name, min_idx=0):
+ if name in calculated:
+ return calculated[name]
+ if sc is None:
+ return None
+ if min_idx < 0:
+ min_idx = len(sc.sub.packages) + min_idx
+ idx = 0
+ for p in sc.sub.packages:
+ idx += 1
+ if idx <= min_idx:
+ continue
+ if name != p.name:
+ continue
+
+ return binascii.a2b_hex(p.data)
+ return None
+
+ def get_cleartext(attr_opts):
+ param = get_option(attr_opts, "previous")
+ if param:
+ if param != "1":
+ raise CommandError(
+ f"Invalid attribute parameter ;previous={param}, "
+ "only supported value is previous=1")
+ return calculated.get("OLDCLEARTEXT")
+ else:
+ return get_package("Primary:CLEARTEXT")
+
+ def get_kerberos_ctr():
+ primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
+ if primary_krb5 is None:
+ primary_krb5 = get_package("Primary:Kerberos")
+ if primary_krb5 is None:
+ return (0, None)
+ krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
+ primary_krb5)
+ return (krb5_blob.version, krb5_blob.ctr)
+
+ aes256_key = None
+ kerberos_salt = None
+
+ (krb5_v, krb5_ctr) = get_kerberos_ctr()
+ if krb5_v in [3, 4]:
+ kerberos_salt = krb5_ctr.salt.string
+
+ if krb5_ctr.keys:
+ def is_aes256(k):
+ return k.keytype == 18
+ aes256_key = next(builtins.filter(is_aes256, krb5_ctr.keys),
+ None)
+
+ if decrypt:
+ #
+ # Samba adds 'Primary:SambaGPG' at the end.
+ # When Windows sets the password it keeps
+ # 'Primary:SambaGPG' and rotates it to
+ # the beginning. So we can only use the value,
+ # if it is the last one.
+ #
+ # In order to get more protection we verify
+ # the nthash of the decrypted utf16 password
+ # against the stored nthash in unicodePwd if
+ # available, otherwise against the first 16
+ # bytes of the AES256 key.
+ #
+ sgv = get_package("Primary:SambaGPG", min_idx=-1)
+ if sgv is not None:
+ try:
+ cv = gpg_decrypt(sgv)
+ #
+ # We only use the password if it matches
+ # the current nthash stored in the unicodePwd
+ # attribute, or the current AES256 key.
+ #
+ tmp = credentials.Credentials()
+ tmp.set_anonymous()
+ tmp.set_utf16_password(cv)
+
+ decrypted = None
+ current_hash = None
+
+ if unicodePwd is not None:
+ decrypted = tmp.get_nt_hash()
+ current_hash = unicodePwd
+ elif aes256_key is not None and kerberos_salt is not None:
+ decrypted = tmp.get_aes256_key(kerberos_salt)
+ current_hash = aes256_key.value
+
+ if current_hash is not None and current_hash == decrypted:
+ calculated["Primary:CLEARTEXT"] = cv
+
+ except Exception as e:
+ self.outf.write(
+ "WARNING: '%s': SambaGPG can't be decrypted "
+ "into CLEARTEXT: %s\n" % (
+ username or account_name, e))
+
+ def get_utf8(a, b, username):
+ creds_for_charcnv = credentials.Credentials()
+ creds_for_charcnv.set_anonymous()
+ creds_for_charcnv.set_utf16_password(get_bytes(b))
+
+ # This can't fail due to character conversion issues as it
+ # includes a built-in fallback (UTF16_MUNGED) matching
+ # exactly what we need.
+ return creds_for_charcnv.get_password().encode()
+
+ # Extract the WDigest hash for the value specified by i.
+ # Builds an htdigest compatible value
+ DIGEST = "Digest"
+
+ def get_wDigest(i, primary_wdigest, account_name, account_upn,
+ domain, dns_domain):
+ if i == 1:
+ user = account_name
+ realm = domain
+ elif i == 2:
+ user = account_name.lower()
+ realm = domain.lower()
+ elif i == 3:
+ user = account_name.upper()
+ realm = domain.upper()
+ elif i == 4:
+ user = account_name
+ realm = domain.upper()
+ elif i == 5:
+ user = account_name
+ realm = domain.lower()
+ elif i == 6:
+ user = account_name.upper()
+ realm = domain.lower()
+ elif i == 7:
+ user = account_name.lower()
+ realm = domain.upper()
+ elif i == 8:
+ user = account_name
+ realm = dns_domain.lower()
+ elif i == 9:
+ user = account_name.lower()
+ realm = dns_domain.lower()
+ elif i == 10:
+ user = account_name.upper()
+ realm = dns_domain.upper()
+ elif i == 11:
+ user = account_name
+ realm = dns_domain.upper()
+ elif i == 12:
+ user = account_name
+ realm = dns_domain.lower()
+ elif i == 13:
+ user = account_name.upper()
+ realm = dns_domain.lower()
+ elif i == 14:
+ user = account_name.lower()
+ realm = dns_domain.upper()
+ elif i == 15:
+ user = account_upn
+ realm = ""
+ elif i == 16:
+ user = account_upn.lower()
+ realm = ""
+ elif i == 17:
+ user = account_upn.upper()
+ realm = ""
+ elif i == 18:
+ user = "%s\\%s" % (domain, account_name)
+ realm = ""
+ elif i == 19:
+ user = "%s\\%s" % (domain.lower(), account_name.lower())
+ realm = ""
+ elif i == 20:
+ user = "%s\\%s" % (domain.upper(), account_name.upper())
+ realm = ""
+ elif i == 21:
+ user = account_name
+ realm = DIGEST
+ elif i == 22:
+ user = account_name.lower()
+ realm = DIGEST
+ elif i == 23:
+ user = account_name.upper()
+ realm = DIGEST
+ elif i == 24:
+ user = account_upn
+ realm = DIGEST
+ elif i == 25:
+ user = account_upn.lower()
+ realm = DIGEST
+ elif i == 26:
+ user = account_upn.upper()
+ realm = DIGEST
+ elif i == 27:
+ user = "%s\\%s" % (domain, account_name)
+ realm = DIGEST
+ elif i == 28:
+ # Differs from spec, see tests
+ user = "%s\\%s" % (domain.lower(), account_name.lower())
+ realm = DIGEST
+ elif i == 29:
+ # Differs from spec, see tests
+ user = "%s\\%s" % (domain.upper(), account_name.upper())
+ realm = DIGEST
+ else:
+ user = ""
+
+ digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
+ primary_wdigest)
+ try:
+ digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
+ return "%s:%s:%s" % (user, realm, get_string(digest))
+ except IndexError:
+ return None
+
+ # get the value for a virtualCrypt attribute.
+ # look for an exact match on algorithm and rounds in supplemental creds
+ # if not found calculate using Primary:CLEARTEXT
+ # if no Primary:CLEARTEXT return the first supplementalCredential
+ # that matches the algorithm.
+ def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
+ sv = None
+ fb = None
+ b = get_package("Primary:userPassword")
+ if b is not None:
+ (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
+ if sv is None:
+ # No exact match on algorithm and number of rounds
+ # try and calculate one from the Primary:CLEARTEXT
+ b = get_cleartext(attr_opts)
+ if b is not None:
+ u8 = get_utf8(a, b, username or account_name)
+ if u8 is not None:
+ # in py2 using get_bytes should ensure u8 is unmodified
+ # in py3 it will be decoded
+ sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
+ if sv is None:
+ # Unable to calculate a hash with the specified
+ # number of rounds, fall back to the first hash using
+ # the specified algorithm
+ sv = fb
+ if sv is None:
+ return None
+ return "{CRYPT}" + sv
+
+ def get_userPassword_hash(blob, algorithm, rounds):
+ up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
+ SCHEME = "{CRYPT}"
+
+ # Check that the NT hash or AES256 key have not been changed
+ # without updating the user password hashes. This indicates that
+ # password has been changed without updating the supplemental
+ # credentials.
+ if unicodePwd is not None:
+ current_hash = unicodePwd
+ elif aes256_key is not None:
+ current_hash = aes256_key.value[:16]
+ else:
+ return None, None
+
+ if current_hash != bytearray(up.current_nt_hash.hash):
+ return None, None
+
+ scheme_prefix = "$%d$" % algorithm
+ prefix = scheme_prefix
+ if rounds > 0:
+ prefix = "$%d$rounds=%d" % (algorithm, rounds)
+ scheme_match = None
+
+ for h in up.hashes:
+ # in PY2 this should just do nothing and in PY3 if bytes
+ # it will decode them
+ h_value = get_string(h.value)
+ if (scheme_match is None and
+ h.scheme == SCHEME and
+ h_value.startswith(scheme_prefix)):
+ scheme_match = h_value
+ if h.scheme == SCHEME and h_value.startswith(prefix):
+ return (h_value, scheme_match)
+
+ # No match on the number of rounds, return the value of the
+ # first matching scheme
+ return (None, scheme_match)
+
+ # Extract the rounds value from the options of a virtualCrypt attribute
+ # i.e. options = "rounds=20;other=ignored;" will return 20
+ # if the rounds option is not found or the value is not a number, 0 is returned
+ # which indicates that the default number of rounds should be used.
+ def get_rounds(opts):
+ val = get_option(opts, "rounds")
+ if val is None:
+ return 0
+ try:
+ return int(val)
+ except ValueError:
+ return 0
+
+ def get_unicode_pwd_hash(pwd):
+ # We can't read unicodePwd directly, but we can regenerate
+ # it from msDS-ManagedPassword
+ tmp = credentials.Credentials()
+ tmp.set_anonymous()
+ tmp.set_utf16_password(pwd)
+ return tmp.get_nt_hash()
+
+ # We use sort here in order to have a predictable processing order
+ for a in sorted(virtual_attributes.keys()):
+ vattr = None
+ for ra in requested_attrs:
+ if ra["vattr"] is None:
+ continue
+ if ra["attr"].lower() != a.lower():
+ continue
+ vattr = ra
+ break
+ if vattr is None:
+ continue
+ attr_opts = vattr["opts"]
+
+ if a == "virtualClearTextUTF8":
+ b = get_cleartext(attr_opts)
+ if b is None:
+ continue
+ u8 = get_utf8(a, b, username or account_name)
+ if u8 is None:
+ continue
+ v = u8
+ elif a == "virtualClearTextUTF16":
+ v = get_cleartext(attr_opts)
+ if v is None:
+ continue
+ elif a == "virtualSSHA":
+ b = get_cleartext(attr_opts)
+ if b is None:
+ continue
+ u8 = get_utf8(a, b, username or account_name)
+ if u8 is None:
+ continue
+ salt = os.urandom(4)
+ h = hashlib.sha1()
+ h.update(u8)
+ h.update(salt)
+ bv = h.digest() + salt
+ v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
+ elif a == "virtualCryptSHA256":
+ rounds = get_rounds(attr_opts)
+ x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
+ if x is None:
+ continue
+ v = x
+ elif a == "virtualCryptSHA512":
+ rounds = get_rounds(attr_opts)
+ x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
+ if x is None:
+ continue
+ v = x
+ elif a == "virtualSambaGPG":
+ # Samba adds 'Primary:SambaGPG' at the end.
+ # When Windows sets the password it keeps
+ # 'Primary:SambaGPG' and rotates it to
+ # the beginning. So we can only use the value,
+ # if it is the last one.
+ v = get_package("Primary:SambaGPG", min_idx=-1)
+ if v is None:
+ continue
+ elif a == "virtualKerberosSalt":
+ v = kerberos_salt
+ if v is None:
+ continue
+ elif a == "unicodePwd" and unicodePwd is None:
+ if "Primary:CLEARTEXT" in calculated and not get_option(attr_opts, "previous"):
+ v = get_unicode_pwd_hash(calculated["Primary:CLEARTEXT"])
+ elif "OLDCLEARTEXT" in calculated and get_option(attr_opts, "previous"):
+ v = get_unicode_pwd_hash(calculated["OLDCLEARTEXT"])
+ else:
+ continue
+ elif a.startswith("virtualWDigest"):
+ primary_wdigest = get_package("Primary:WDigest")
+ if primary_wdigest is None:
+ continue
+ x = a[len("virtualWDigest"):]
+ try:
+ i = int(x)
+ except ValueError:
+ continue
+ domain = samdb.domain_netbios_name()
+ dns_domain = samdb.domain_dns_name()
+ v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
+ if v is None:
+ continue
+ else:
+ continue
+ obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, vattr["raw_attr"])
+
+ def get_src_attrname(srcattrg):
+ srcattrl = srcattrg.lower()
+ srcattr = None
+ for k in obj.keys():
+ if srcattrl != k.lower():
+ continue
+ srcattr = k
+ break
+ return srcattr
+
+ def get_src_time_float(srcattr):
+ if srcattr not in obj:
+ return None
+ vstr = str(obj[srcattr][0])
+ if vstr.endswith(".0Z"):
+ vut = ldb.string_to_time(vstr)
+ vfl = float(vut)
+ return vfl
+
+ try:
+ vnt = int(vstr)
+ except ValueError as e:
+ return None
+ # 0 or 9223372036854775807 mean no value too
+ if vnt == 0:
+ return None
+ if vnt >= 0x7FFFFFFFFFFFFFFF:
+ return None
+ vfl = nttime2float(vnt)
+ return vfl
+
+ def get_generalizedtime(srcattr):
+ vfl = get_src_time_float(srcattr)
+ if vfl is None:
+ return None
+ vut = int(vfl)
+ try:
+ v = "%s" % ldb.timestring(vut)
+ except OSError as e:
+ if e.errno == errno.EOVERFLOW:
+ return None
+ raise
+ return v
+
+ def get_unixepoch(srcattr):
+ vfl = get_src_time_float(srcattr)
+ if vfl is None:
+ return None
+ vut = int(vfl)
+ v = "%d" % vut
+ return v
+
+ def get_timespec(srcattr):
+ vfl = get_src_time_float(srcattr)
+ if vfl is None:
+ return None
+ v = "%.9f" % vfl
+ return v
+
+ generated_formats = {}
+ for fm in formats:
+ for ra in requested_attrs:
+ if ra["vformat"] is None:
+ continue
+ if ra["vformat"] != fm:
+ continue
+ srcattr = get_src_attrname(ra["attr"])
+ if srcattr is None:
+ continue
+ an = "%s;format=%s" % (srcattr, fm)
+ if an in generated_formats:
+ continue
+ generated_formats[an] = fm
+
+ v = None
+ if fm == "GeneralizedTime":
+ v = get_generalizedtime(srcattr)
+ elif fm == "UnixTime":
+ v = get_unixepoch(srcattr)
+ elif fm == "TimeSpec":
+ v = get_timespec(srcattr)
+ if v is None:
+ continue
+ obj[an] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, an)
+
+ # Now filter out implicit attributes
+ for delname in obj.keys():
+ keep = False
+ for ra in requested_attrs:
+ if delname.lower() != ra["raw_attr"].lower():
+ continue
+ keep = True
+ break
+ if keep:
+ continue
+
+ dattr = None
+ for ia in implicit_attrs:
+ if delname.lower() != ia["attr"].lower():
+ continue
+ dattr = ia
+ break
+ if dattr is None:
+ continue
+
+ if has_wildcard_attr and not dattr["is_hidden"]:
+ continue
+ del obj[delname]
+ return obj
+
+ def parse_attributes(self, attributes):
+
+ if attributes is None:
+ raise CommandError("Please specify --attributes")
+ attrs = attributes.split(',')
+ password_attrs = []
+ for pa in attrs:
+ pa = pa.lstrip().rstrip()
+ for da in disabled_virtual_attributes.keys():
+ if pa.lower() == da.lower():
+ r = disabled_virtual_attributes[da]["reason"]
+ raise CommandError("Virtual attribute '%s' not supported: %s" % (
+ da, r))
+ for va in virtual_attributes.keys():
+ if pa.lower() == va.lower():
+ # Take the real name
+ pa = va
+ break
+ password_attrs += [pa]
+
+ return password_attrs
diff --git a/python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py b/python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py
new file mode 100644
index 0000000..3a8296b
--- /dev/null
+++ b/python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py
@@ -0,0 +1,146 @@
+# user management
+#
+# user get-kerberos-ticket command - obtain a TGT for a database user
+#
+# Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
+# Copyright Andrew Bartlett 2023 <abartlet@samba.org>
+#
+# 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/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.netcmd import CommandError, Option
+from samba.credentials import Credentials
+from .common import (
+ GetPasswordCommand,
+ gpg_decrypt,
+ decrypt_samba_gpg_help,
+)
+from samba.dcerpc import samr
+
+class cmd_user_get_kerberos_ticket(GetPasswordCommand):
+ """Get a Kerberos Ticket Granting Ticket as a user
+
+This command gets a Kerberos TGT using the password for a user/computer account.
+
+The username specified on the command is the sAMAccountName.
+The username may also be specified using the --filter option.
+
+The command must be run from the root user id or another authorized
+user id. The '-H' or '--URL' option supports ldap:// for remote Group
+Managed Service accounts, and ldapi:// or tdb:// can be used to
+adjust the local path. tdb:// is used by default for a bare path.
+
+The --output-krb5-ccache option should point to a location for the
+credentials cache. The default is a FILE: type cache if no prefix is
+specified.
+
+The '--decrypt-samba-gpg' option triggers decryption of the
+Primary:SambaGPG buffer to get the password.
+
+Check with '--help' if this feature is available
+in your environment or not (the python-gpgme package is required). Please
+note that you might need to set the GNUPGHOME environment variable. If the
+decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
+environment variable has been set correctly and the passphrase is already
+known by the gpg-agent.
+
+Example1:
+samba-tool user get-kerberos-ticket TestUser1 --output-krb5-ccache=/srv/service/krb5_ccache
+
+Example2:
+samba-tool user get-kerberos-ticket --filter='(samAccountName=TestUser3)' --output-krb5-ccache=FILE:/srv/service/krb5_ccache
+
+ """
+ synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--filter", help="LDAP Filter to get Kerberos ticket for (must match single account)", type=str),
+ Option("--output-krb5-ccache", type=str,
+ help="Location of Kerberos credentials cache to write ticket into",
+ metavar="CCACHE", dest="output_krb5_ccache"),
+ Option("--decrypt-samba-gpg",
+ help=decrypt_samba_gpg_help,
+ action="store_true", default=False, dest="decrypt_samba_gpg"),
+ ]
+
+ takes_args = ["username?"]
+
+ def run(self, username=None, H=None, filter=None,
+ attributes=None, decrypt_samba_gpg=None,
+ sambaopts=None, versionopts=None, hostopts=None,
+ credopts=None, output_krb5_ccache=None):
+ self.lp = sambaopts.get_loadparm()
+
+ if decrypt_samba_gpg and not gpg_decrypt:
+ raise CommandError(decrypt_samba_gpg_help)
+
+ if filter is None and username is None:
+ raise CommandError("Either the username or '--filter' must be specified!")
+
+ if filter is None:
+ filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+ password_attrs = ["virtualClearTextUTF16", "samAccountName", "unicodePwd"]
+
+ creds = credopts.get_credentials(self.lp)
+ samdb = self.connect_for_passwords(url=hostopts.H, require_ldapi=False, creds=creds)
+
+ obj = self.get_account_attributes(samdb, username,
+ basedn=None,
+ filter=filter,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=password_attrs,
+ decrypt=decrypt_samba_gpg)
+
+ lp_ctx = sambaopts.get_loadparm()
+
+ creds = Credentials()
+ creds.set_username(str(obj["samAccountName"][0]))
+ creds.set_realm(samdb.domain_dns_name())
+
+ utf16_pw = None
+ nt_pass = None
+ try:
+ utf16_pw = obj["virtualClearTextUTF16"][0]
+ creds.set_utf16_password(utf16_pw)
+ except KeyError:
+ pass
+
+ if utf16_pw is None:
+ try:
+ nt_pass = samr.Password()
+ nt_pass.hash = list(obj["unicodePwd"][0])
+ creds.set_nt_hash(nt_pass)
+ except KeyError:
+ pass
+
+ if nt_pass is None and utf16_pw is None:
+ if samdb.url.startswith("ldap://") or samdb.url.startswith("ldaps://"):
+ raise CommandError("No password was available for this user. "
+ "Only Group Managed Service accounts allow access to passwords over LDAP, "
+ "you may need to access the sam.ldb directly on the Samba AD DC and export the file.")
+ else:
+ raise CommandError("No password was available for this user")
+ creds.guess(lp_ctx)
+ creds.get_named_ccache(lp_ctx, output_krb5_ccache)
diff --git a/python/samba/netcmd/user/readpasswords/getpassword.py b/python/samba/netcmd/user/readpasswords/getpassword.py
new file mode 100644
index 0000000..f962412
--- /dev/null
+++ b/python/samba/netcmd/user/readpasswords/getpassword.py
@@ -0,0 +1,210 @@
+# user management
+#
+# user getpassword command
+#
+# Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
+#
+# 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/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.netcmd import CommandError, Option
+
+from .common import (
+ GetPasswordCommand,
+ gpg_decrypt,
+ decrypt_samba_gpg_help,
+ virtual_attributes_help
+)
+
+
+class cmd_user_getpassword(GetPasswordCommand):
+ """Get the password fields of a user/computer account.
+
+This command gets the logon password for a user/computer account.
+
+The username specified on the command is the sAMAccountName.
+The username may also be specified using the --filter option.
+
+The command must be run from the root user id or another authorized user id.
+The '-H' or '--URL' option normally only supports ldapi:// or [tdb://] and
+can be used to adjust the local path. By default, tdb:// is used.
+if the target account is a group managed service account, then in this
+case the -H can point to a remote AD DC LDAP server.
+
+The '--attributes' parameter takes a comma separated list of attributes,
+which will be printed or given to the script specified by '--script'. If a
+specified attribute is not available on an object it's silently omitted.
+All attributes defined in the schema (e.g. the unicodePwd attribute holds
+the NTHASH) and the following virtual attributes are possible (see --help
+for which virtual attributes are supported in your environment):
+
+ virtualClearTextUTF16: The raw cleartext as stored in the
+ 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
+ with '--decrypt-samba-gpg') buffer inside the
+ supplementalCredentials attribute. This typically
+ contains valid UTF-16-LE, but may contain random
+ bytes, e.g. for computer and gMSA accounts.
+ When the account is a group managed service account,
+ and the user is permitted to access
+ msDS-ManagedPassword then the current and previous
+ password can be read over LDAP. Add ;previous=1
+ to read the previous password.
+
+ virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
+ (invalid UTF-16-LE is mapped in the same way as
+ Windows).
+
+ virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
+ checksum, useful for OpenLDAP's '{SSHA}' algorithm.
+
+ virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
+ checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+ with a $5$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA256;rounds=10000
+ will calculate a SHA256 hash with 10,000 rounds.
+ Non-numeric values for rounds are silently ignored.
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG'
+ with '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds.
+ 3) Return the first CryptSHA256 value in
+ 'Primary:userPassword'.
+
+
+ virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
+ checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+ with a $6$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA512;rounds=10000
+ will calculate a SHA512 hash with 10,000 rounds.
+ Non-numeric values for rounds are silently ignored.
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG'
+ with '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds.
+ 3) Return the first CryptSHA512 value in
+ 'Primary:userPassword'.
+
+ virtualWDigestNN: The individual hash values stored in
+ 'Primary:WDigest' where NN is the hash number in
+ the range 01 to 29.
+ NOTE: As at 22-05-2017 the documentation:
+ 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+ https://msdn.microsoft.com/en-us/library/cc245680.aspx
+ is incorrect.
+
+ virtualKerberosSalt: This results the salt string that is used to compute
+ Kerberos keys from a UTF-8 cleartext password.
+
+ virtualSambaGPG: The raw cleartext as stored in the
+ 'Primary:SambaGPG' buffer inside the
+ supplementalCredentials attribute.
+ See the 'password hash gpg key ids' option in
+ smb.conf.
+
+The '--decrypt-samba-gpg' option triggers decryption of the
+Primary:SambaGPG buffer. Check with '--help' if this feature is available
+in your environment or not (the python-gpgme package is required). Please
+note that you might need to set the GNUPGHOME environment variable. If the
+decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
+environment variable has been set correctly and the passphrase is already
+known by the gpg-agent.
+
+Attributes with time values can take an additional format specifier, which
+converts the time value into the requested format. The format can be specified
+by adding ";format=formatSpecifier" to the requested attribute name, whereby
+"formatSpecifier" must be a valid specifier. The syntax looks like:
+
+ --attributes=attributeName;format=formatSpecifier
+
+The following format specifiers are available:
+ - GeneralizedTime (e.g. 20210224113259.0Z)
+ - UnixTime (e.g. 1614166392)
+ - TimeSpec (e.g. 161416639.267546892)
+
+Attributes with an original NTTIME value of 0 and 9223372036854775807 are
+treated as non-existing value.
+
+Example1:
+samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
+
+Example2:
+samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
+
+"""
+
+ synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--filter", help="LDAP Filter to get password for (must match single account)", type=str),
+ Option("--attributes", type=str,
+ help=virtual_attributes_help,
+ metavar="ATTRIBUTELIST", dest="attributes"),
+ Option("--decrypt-samba-gpg",
+ help=decrypt_samba_gpg_help,
+ action="store_true", default=False, dest="decrypt_samba_gpg"),
+ ]
+
+ takes_args = ["username?"]
+
+ def run(self, username=None, H=None, filter=None,
+ attributes=None, decrypt_samba_gpg=None,
+ sambaopts=None, versionopts=None, hostopts=None,
+ credopts=None):
+ self.lp = sambaopts.get_loadparm()
+
+ if decrypt_samba_gpg and not gpg_decrypt:
+ raise CommandError(decrypt_samba_gpg_help)
+
+ if filter is None and username is None:
+ raise CommandError("Either the username or '--filter' must be specified!")
+
+ if filter is None:
+ filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+ if attributes is None:
+ raise CommandError("Please specify --attributes")
+
+ password_attrs = self.parse_attributes(attributes)
+
+ creds = credopts.get_credentials(self.lp)
+ samdb = self.connect_for_passwords(url=hostopts.H, require_ldapi=False, creds=creds)
+
+ obj = self.get_account_attributes(samdb, username,
+ basedn=None,
+ filter=filter,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=password_attrs,
+ decrypt=decrypt_samba_gpg)
+
+ ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
+ self.outf.write("%s" % ldif)
+ self.errf.write("Got password OK\n")
diff --git a/python/samba/netcmd/user/readpasswords/show.py b/python/samba/netcmd/user/readpasswords/show.py
new file mode 100644
index 0000000..1cdec89
--- /dev/null
+++ b/python/samba/netcmd/user/readpasswords/show.py
@@ -0,0 +1,144 @@
+# user management
+#
+# user show command
+#
+# Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba import dsdb, ldb
+from samba.auth import system_session
+from samba.netcmd import Option, common
+from samba.samdb import SamDB
+
+from .common import GetPasswordCommand
+
+
+class cmd_user_show(GetPasswordCommand):
+ """Display a user AD object.
+
+This command displays a user account and it's attributes in the Active
+Directory domain.
+The username specified on the command is the sAMAccountName.
+
+The command may be run from the root userid or another authorized userid.
+
+The -H or --URL= option can be used to execute the command against a remote
+server.
+
+The '--attributes' parameter takes a comma separated list of the requested
+attributes. Without '--attributes' or with '--attributes=*' all usually
+available attributes are selected.
+Hidden attributes in addition to all usually available attributes can be
+selected with e.g. '--attributes=*,msDS-UserPasswordExpiryTimeComputed'.
+If a specified attribute is not available on a user object it's silently
+omitted.
+
+Attributes with time values can take an additional format specifier, which
+converts the time value into the requested format. The format can be specified
+by adding ";format=formatSpecifier" to the requested attribute name, whereby
+"formatSpecifier" must be a valid specifier. The syntax looks like:
+
+ --attributes=attributeName;format=formatSpecifier
+
+The following format specifiers are available:
+ - GeneralizedTime (e.g. 20210224113259.0Z)
+ - UnixTime (e.g. 1614166392)
+ - TimeSpec (e.g. 161416639.267546892)
+
+Attributes with an original NTTIME value of 0 and 9223372036854775807 are
+treated as non-existing value.
+
+Example1:
+samba-tool user show User1 -H ldap://samba.samdom.example.com \\
+ -U administrator --password=passw1rd
+
+Example1 shows how to display a users attributes in the domain against a remote
+LDAP server.
+
+The -H parameter is used to specify the remote target server.
+
+Example2:
+samba-tool user show User2
+
+Example2 shows how to display a users attributes in the domain against a local
+LDAP server.
+
+Example3:
+samba-tool user show User2 --attributes=objectSid,memberOf
+
+Example3 shows how to display a users objectSid and memberOf attributes.
+
+Example4:
+samba-tool user show User2 \\
+ --attributes='pwdLastSet;format=GeneralizedTime,pwdLastSet;format=UnixTime'
+
+The result of Example 4 provides the pwdLastSet attribute values in the
+specified format:
+ dn: CN=User2,CN=Users,DC=samdom,DC=example,DC=com
+ pwdLastSet;format=GeneralizedTime: 20210120105207.0Z
+ pwdLastSet;format=UnixTime: 1611139927
+"""
+ synopsis = "%prog <username> [options]"
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ type=str, metavar="URL", dest="H"),
+ Option("--attributes",
+ help=("Comma separated list of attributes, "
+ "which will be printed. "
+ "Possible supported virtual attributes: "
+ "virtualGeneralizedTime, virtualUnixTime, virtualTimeSpec."),
+ type=str, dest="user_attrs"),
+ ]
+
+ takes_args = ["username"]
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ def run(self, username, credopts=None, sambaopts=None, versionopts=None,
+ H=None, user_attrs=None):
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ self.inject_virtual_attributes(samdb)
+
+ if user_attrs:
+ attrs = self.parse_attributes(user_attrs)
+ else:
+ attrs = ["*"]
+
+ filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+ (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
+
+ domaindn = samdb.domain_dn()
+
+ obj = self.get_account_attributes(samdb, username,
+ basedn=domaindn,
+ filter=filter,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ decrypt=False,
+ support_pw_attrs=False)
+ user_ldif = common.get_ldif_for_editor(samdb, obj)
+ self.outf.write(user_ldif)
diff --git a/python/samba/netcmd/user/readpasswords/syncpasswords.py b/python/samba/netcmd/user/readpasswords/syncpasswords.py
new file mode 100644
index 0000000..a909123
--- /dev/null
+++ b/python/samba/netcmd/user/readpasswords/syncpasswords.py
@@ -0,0 +1,878 @@
+# user management
+#
+# user syncpasswords command
+#
+# Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
+#
+# 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/>.
+#
+
+import base64
+import errno
+import fcntl
+import os
+import signal
+import time
+from subprocess import Popen, PIPE, STDOUT
+
+import ldb
+import samba.getopt as options
+from samba import Ldb, dsdb
+from samba.dcerpc import misc, security
+from samba.ndr import ndr_unpack
+from samba.common import get_bytes
+from samba.netcmd import CommandError, Option
+
+from .common import (
+ GetPasswordCommand,
+ gpg_decrypt,
+ decrypt_samba_gpg_help,
+ virtual_attributes_help
+)
+
+
+class cmd_user_syncpasswords(GetPasswordCommand):
+ """Sync the password of user accounts.
+
+This syncs logon passwords for user accounts.
+
+Note that this command should run on a single domain controller only
+(typically the PDC-emulator). However the "password hash gpg key ids"
+option should to be configured on all domain controllers.
+
+The command must be run from the root user id or another authorized user id.
+The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
+local path. By default, ldapi:// is used with the default path to the
+privileged ldapi socket.
+
+This command has three modes: "Cache Initialization", "Sync Loop Run" and
+"Sync Loop Terminate".
+
+
+Cache Initialization
+====================
+
+The first time, this command needs to be called with
+'--cache-ldb-initialize' in order to initialize its cache.
+
+The cache initialization requires '--attributes' and allows the following
+optional options: '--decrypt-samba-gpg', '--script', '--filter' or
+'-H/--URL'.
+
+The '--attributes' parameter takes a comma separated list of attributes,
+which will be printed or given to the script specified by '--script'. If a
+specified attribute is not available on an object it will be silently omitted.
+All attributes defined in the schema (e.g. the unicodePwd attribute holds
+the NTHASH) and the following virtual attributes are possible (see '--help'
+for supported virtual attributes in your environment):
+
+ virtualClearTextUTF16: The raw cleartext as stored in the
+ 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
+ with '--decrypt-samba-gpg') buffer inside of the
+ supplementalCredentials attribute. This typically
+ contains valid UTF-16-LE, but may contain random
+ bytes, e.g. for computer accounts.
+
+ virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
+ (only from valid UTF-16-LE).
+
+ virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
+ checksum, useful for OpenLDAP's '{SSHA}' algorithm.
+
+ virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
+ checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+ with a $5$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA256;rounds=10000
+ will calculate a SHA256 hash with 10,000 rounds.
+ Non numeric values for rounds are silently ignored.
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
+ '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds
+ 3) Return the first CryptSHA256 value in
+ 'Primary:userPassword'.
+
+ virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
+ checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+ with a $6$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA512;rounds=10000
+ will calculate a SHA512 hash with 10,000 rounds.
+ Non numeric values for rounds are silently ignored.
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
+ '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds.
+ 3) Return the first CryptSHA512 value in
+ 'Primary:userPassword'.
+
+ virtualWDigestNN: The individual hash values stored in
+ 'Primary:WDigest' where NN is the hash number in
+ the range 01 to 29.
+ NOTE: As at 22-05-2017 the documentation:
+ 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+ https://msdn.microsoft.com/en-us/library/cc245680.aspx
+ is incorrect.
+
+ virtualKerberosSalt: This results the salt string that is used to compute
+ Kerberos keys from a UTF-8 cleartext password.
+
+ virtualSambaGPG: The raw cleartext as stored in the
+ 'Primary:SambaGPG' buffer inside of the
+ supplementalCredentials attribute.
+ See the 'password hash gpg key ids' option in
+ smb.conf.
+
+The '--decrypt-samba-gpg' option triggers decryption of the
+Primary:SambaGPG buffer. Check with '--help' if this feature is available
+in your environment or not (the python-gpgme package is required). Please
+note that you might need to set the GNUPGHOME environment variable. If the
+decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
+environment variable has been set correctly and the passphrase is already
+known by the gpg-agent.
+
+The '--script' option specifies a custom script that is called whenever any
+of the dirsyncAttributes (see below) was changed. The script is called
+without any arguments. It gets the LDIF for exactly one object on STDIN.
+If the script processed the object successfully it has to respond with a
+single line starting with 'DONE-EXIT: ' followed by an optional message.
+
+Note that the script might be called without any password change, e.g. if
+the account was disabled (a userAccountControl change) or the
+sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
+are always returned as unique identifier of the account. It might be useful
+to also ask for non-password attributes like: objectSid, sAMAccountName,
+userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
+Depending on the object, some attributes may not be present/available,
+but you always get the current state (and not a diff).
+
+If no '--script' option is specified, the LDIF will be printed on STDOUT or
+into the logfile.
+
+The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
+(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
+ (!(sAMAccountName=krbtgt*)))
+This means only normal (non-krbtgt) user
+accounts are monitored. The '--filter' can modify that, e.g. if it's
+required to also sync computer accounts.
+
+
+Sync Loop Run
+=============
+
+This (default) mode runs in an endless loop waiting for password related
+changes in the active directory database. It makes use of the
+LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
+get changes in a reliable fashion. Objects are monitored for changes of the
+following dirsyncAttributes:
+
+ unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
+ userPrincipalName and userAccountControl.
+
+It recovers from LDAP disconnects and updates the cache in conservative way
+(in single steps after each successfully processed change). An error from
+the script (specified by '--script') will result in fatal error and this
+command will exit. But the cache state should be still valid and can be
+resumed in the next "Sync Loop Run".
+
+The '--logfile' option specifies an optional (required if '--daemon' is
+specified) logfile that takes all output of the command. The logfile is
+automatically reopened if fstat returns st_nlink == 0.
+
+The optional '--daemon' option will put the command into the background.
+
+You can stop the command without the '--daemon' option, also by hitting
+strg+c.
+
+If you specify the '--no-wait' option the command skips the
+LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
+all LDAP_SERVER_DIRSYNC_OID changes are consumed.
+
+Sync Loop Terminate
+===================
+
+In order to terminate an already running command (likely as daemon) the
+'--terminate' option can be used. This also requires the '--logfile' option
+to be specified.
+
+
+Example1:
+samba-tool user syncpasswords --cache-ldb-initialize \\
+ --attributes=virtualClearTextUTF8
+samba-tool user syncpasswords
+
+Example2:
+samba-tool user syncpasswords --cache-ldb-initialize \\
+ --attributes=objectGUID,objectSID,sAMAccountName,\\
+ userPrincipalName,userAccountControl,pwdLastSet,\\
+ msDS-KeyVersionNumber,virtualCryptSHA512 \\
+ --script=/path/to/my-custom-syncpasswords-script.py
+samba-tool user syncpasswords --daemon \\
+ --logfile=/var/log/samba/user-syncpasswords.log
+samba-tool user syncpasswords --terminate \\
+ --logfile=/var/log/samba/user-syncpasswords.log
+
+"""
+
+ synopsis = "%prog [--cache-ldb-initialize] [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ takes_options = [
+ Option("--cache-ldb-initialize",
+ help="Initialize the cache for the first time",
+ dest="cache_ldb_initialize", action="store_true"),
+ Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
+ metavar="CACHE-LDB-PATH", dest="cache_ldb"),
+ Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
+ metavar="URL", dest="H"),
+ Option("--filter", help="optional LDAP filter to set password on", type=str,
+ metavar="LDAP-SEARCH-FILTER", dest="filter"),
+ Option("--attributes", type=str,
+ help=virtual_attributes_help,
+ metavar="ATTRIBUTELIST", dest="attributes"),
+ Option("--decrypt-samba-gpg",
+ help=decrypt_samba_gpg_help,
+ action="store_true", default=False, dest="decrypt_samba_gpg"),
+ Option("--script", help="Script that is called for each password change", type=str,
+ metavar="/path/to/syncpasswords.script", dest="script"),
+ Option("--no-wait", help="Don't block waiting for changes",
+ action="store_true", default=False, dest="nowait"),
+ Option("--logfile", type=str,
+ help="The logfile to use (required in --daemon mode).",
+ metavar="/path/to/syncpasswords.log", dest="logfile"),
+ Option("--daemon", help="daemonize after initial setup",
+ action="store_true", default=False, dest="daemon"),
+ Option("--terminate",
+ help="Send a SIGTERM to an already running (daemon) process",
+ action="store_true", default=False, dest="terminate"),
+ ]
+
+ def run(self, cache_ldb_initialize=False, cache_ldb=None,
+ H=None, filter=None,
+ attributes=None, decrypt_samba_gpg=None,
+ script=None, nowait=None, logfile=None, daemon=None, terminate=None,
+ sambaopts=None, versionopts=None):
+
+ self.lp = sambaopts.get_loadparm()
+ self.logfile = None
+ self.samdb_url = None
+ self.samdb = None
+ self.cache = None
+
+ if not cache_ldb_initialize:
+ if attributes is not None:
+ raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
+ if decrypt_samba_gpg:
+ raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
+ if script is not None:
+ raise CommandError("--script is only allowed together with --cache-ldb-initialize")
+ if filter is not None:
+ raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
+ if H is not None:
+ raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
+ else:
+ if nowait is not False:
+ raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
+ if logfile is not None:
+ raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
+ if daemon is not False:
+ raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
+ if terminate is not False:
+ raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
+
+ if nowait is True:
+ if daemon is True:
+ raise CommandError("--daemon is not allowed together with --no-wait")
+ if terminate is not False:
+ raise CommandError("--terminate is not allowed together with --no-wait")
+
+ if terminate is True and daemon is True:
+ raise CommandError("--terminate is not allowed together with --daemon")
+
+ if daemon is True and logfile is None:
+ raise CommandError("--daemon is only allowed together with --logfile")
+
+ if terminate is True and logfile is None:
+ raise CommandError("--terminate is only allowed together with --logfile")
+
+ if script is not None:
+ if not os.path.exists(script):
+ raise CommandError("script[%s] does not exist!" % script)
+
+ sync_command = "%s" % os.path.abspath(script)
+ else:
+ sync_command = None
+
+ dirsync_filter = filter
+ if dirsync_filter is None:
+ dirsync_filter = "(&" + \
+ "(objectClass=user)" + \
+ "(userAccountControl:%s:=%u)" % (
+ ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
+ "(!(sAMAccountName=krbtgt*))" + \
+ ")"
+
+ dirsync_secret_attrs = [
+ "unicodePwd",
+ "dBCSPwd",
+ "supplementalCredentials",
+ ]
+
+ dirsync_attrs = dirsync_secret_attrs + [
+ "pwdLastSet",
+ "sAMAccountName",
+ "userPrincipalName",
+ "userAccountControl",
+ "isDeleted",
+ "isRecycled",
+ ]
+
+ password_attrs = None
+
+ if cache_ldb_initialize:
+ if H is None:
+ H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
+
+ if decrypt_samba_gpg and not gpg_decrypt:
+ raise CommandError(decrypt_samba_gpg_help)
+
+ password_attrs = self.parse_attributes(attributes)
+ lower_attrs = [x.lower() for x in password_attrs]
+ # We always return these in order to track deletions
+ for a in ["objectGUID", "isDeleted", "isRecycled"]:
+ if a.lower() not in lower_attrs:
+ password_attrs += [a]
+
+ if cache_ldb is not None:
+ if cache_ldb.lower().startswith("ldapi://"):
+ raise CommandError("--cache_ldb ldapi:// is not supported")
+ elif cache_ldb.lower().startswith("ldap://"):
+ raise CommandError("--cache_ldb ldap:// is not supported")
+ elif cache_ldb.lower().startswith("ldaps://"):
+ raise CommandError("--cache_ldb ldaps:// is not supported")
+ elif cache_ldb.lower().startswith("tdb://"):
+ pass
+ else:
+ if not os.path.exists(cache_ldb):
+ cache_ldb = self.lp.private_path(cache_ldb)
+ else:
+ cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
+
+ self.lockfile = "%s.pid" % cache_ldb
+
+ def log_msg(msg):
+ if self.logfile is not None:
+ info = os.fstat(0)
+ if info.st_nlink == 0:
+ logfile = self.logfile
+ self.logfile = None
+ log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
+ logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
+ os.dup2(logfd, 0)
+ os.dup2(logfd, 1)
+ os.dup2(logfd, 2)
+ os.close(logfd)
+ log_msg("Reopened logfile[%s]\n" % (logfile))
+ self.logfile = logfile
+ msg = "%s: pid[%d]: %s" % (
+ time.ctime(),
+ os.getpid(),
+ msg)
+ self.outf.write(msg)
+ return
+
+ def load_cache():
+ cache_attrs = [
+ "samdbUrl",
+ "dirsyncFilter",
+ "dirsyncAttribute",
+ "dirsyncControl",
+ "passwordAttribute",
+ "decryptSambaGPG",
+ "syncCommand",
+ "currentPid",
+ ]
+
+ self.cache = Ldb(cache_ldb)
+ self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
+ res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
+ attrs=cache_attrs)
+ if len(res) == 1:
+ try:
+ self.samdb_url = str(res[0]["samdbUrl"][0])
+ except KeyError as e:
+ self.samdb_url = None
+ else:
+ self.samdb_url = None
+ if self.samdb_url is None and not cache_ldb_initialize:
+ raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
+ cache_ldb))
+ if self.samdb_url is not None and cache_ldb_initialize:
+ raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
+ cache_ldb))
+ if self.samdb_url is None:
+ self.samdb_url = H
+ self.dirsync_filter = dirsync_filter
+ self.dirsync_attrs = dirsync_attrs
+ self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
+ self.password_attrs = password_attrs
+ self.decrypt_samba_gpg = decrypt_samba_gpg
+ self.sync_command = sync_command
+ add_ldif = "dn: %s\n" % self.cache_dn +\
+ "objectClass: userSyncPasswords\n" +\
+ "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\
+ "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\
+ "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\
+ "dirsyncControl: %s\n" % self.dirsync_controls[0] +\
+ "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs)
+ if self.decrypt_samba_gpg:
+ add_ldif += "decryptSambaGPG: TRUE\n"
+ else:
+ add_ldif += "decryptSambaGPG: FALSE\n"
+ if self.sync_command is not None:
+ add_ldif += "syncCommand: %s\n" % self.sync_command
+ add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
+ self.cache.add_ldif(add_ldif)
+ self.current_pid = None
+ self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
+ msgs = self.cache.parse_ldif(add_ldif)
+ changetype, msg = next(msgs)
+ ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
+ self.outf.write("%s" % ldif)
+ else:
+ self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
+ self.dirsync_attrs = []
+ for a in res[0]["dirsyncAttribute"]:
+ self.dirsync_attrs.append(str(a))
+ self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
+ self.password_attrs = []
+ for a in res[0]["passwordAttribute"]:
+ self.password_attrs.append(str(a))
+ decrypt_string = str(res[0]["decryptSambaGPG"][0])
+ assert(decrypt_string in ["TRUE", "FALSE"])
+ if decrypt_string == "TRUE":
+ self.decrypt_samba_gpg = True
+ else:
+ self.decrypt_samba_gpg = False
+ if "syncCommand" in res[0]:
+ self.sync_command = str(res[0]["syncCommand"][0])
+ else:
+ self.sync_command = None
+ if "currentPid" in res[0]:
+ self.current_pid = int(res[0]["currentPid"][0])
+ else:
+ self.current_pid = None
+ log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
+
+ return
+
+ def run_sync_command(dn, ldif):
+ log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
+ sync_command_p = Popen(self.sync_command,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=STDOUT)
+
+ res = sync_command_p.poll()
+ assert res is None
+
+ input = "%s" % (ldif)
+ reply = sync_command_p.communicate(
+ input.encode('utf-8'))[0].decode('utf-8')
+ log_msg("%s\n" % (reply))
+ res = sync_command_p.poll()
+ if res is None:
+ sync_command_p.terminate()
+ res = sync_command_p.wait()
+
+ if reply.startswith("DONE-EXIT: "):
+ return
+
+ log_msg("RESULT: %s\n" % (res))
+ raise Exception("ERROR: %s - %s\n" % (res, reply))
+
+ def handle_object(idx, dirsync_obj):
+ binary_guid = dirsync_obj.dn.get_extended_component("GUID")
+ guid = ndr_unpack(misc.GUID, binary_guid)
+ binary_sid = dirsync_obj.dn.get_extended_component("SID")
+ sid = ndr_unpack(security.dom_sid, binary_sid)
+ domain_sid, rid = sid.split()
+ if rid == security.DOMAIN_RID_KRBTGT:
+ log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
+ return
+ for a in list(dirsync_obj.keys()):
+ for h in dirsync_secret_attrs:
+ if a.lower() == h.lower():
+ del dirsync_obj[a]
+ dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
+ dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
+ log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
+ obj = self.get_account_attributes(self.samdb,
+ username="%s" % sid,
+ basedn="<GUID=%s>" % guid,
+ filter="(objectClass=user)",
+ scope=ldb.SCOPE_BASE,
+ attrs=self.password_attrs,
+ decrypt=self.decrypt_samba_gpg)
+ ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
+ log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
+ if self.sync_command is None:
+ self.outf.write("%s" % (ldif))
+ return
+ self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
+ run_sync_command(obj.dn, ldif)
+
+ def check_current_pid_conflict(terminate):
+ flags = os.O_RDWR
+ if not terminate:
+ flags |= os.O_CREAT
+
+ try:
+ self.lockfd = os.open(self.lockfile, flags, 0o600)
+ except IOError as e4:
+ (err, msg) = e4.args
+ if err == errno.ENOENT:
+ if terminate:
+ return False
+ log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
+ (self.lockfile, msg, err))
+ raise
+
+ got_exclusive = False
+ try:
+ fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ got_exclusive = True
+ except IOError as e5:
+ (err, msg) = e5.args
+ if err != errno.EACCES and err != errno.EAGAIN:
+ log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
+ (self.lockfile, msg, err))
+ raise
+
+ if not got_exclusive:
+ buf = os.read(self.lockfd, 64)
+ self.current_pid = None
+ try:
+ self.current_pid = int(buf)
+ except ValueError as e:
+ pass
+ if self.current_pid is not None:
+ return True
+
+ if got_exclusive and terminate:
+ try:
+ os.ftruncate(self.lockfd, 0)
+ except IOError as e2:
+ (err, msg) = e2.args
+ log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
+ (self.lockfile, msg, err))
+ raise
+ os.close(self.lockfd)
+ self.lockfd = -1
+ return False
+
+ try:
+ fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
+ except IOError as e6:
+ (err, msg) = e6.args
+ log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
+ (self.lockfile, msg, err))
+
+ # We leave the function with the shared lock.
+ return False
+
+ def update_pid(pid):
+ if self.lockfd != -1:
+ got_exclusive = False
+ # Try 5 times to get the exclusive lock.
+ for i in range(0, 5):
+ try:
+ fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ got_exclusive = True
+ except IOError as e:
+ (err, msg) = e.args
+ if err != errno.EACCES and err != errno.EAGAIN:
+ log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
+ (pid, self.lockfile, msg, err))
+ raise
+ if got_exclusive:
+ break
+ time.sleep(1)
+ if not got_exclusive:
+ log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
+ (pid, self.lockfile))
+ raise CommandError("update_pid(%r): failed to get "
+ "exclusive lock[%s] after 5 seconds" %
+ (pid, self.lockfile))
+
+ if pid is not None:
+ buf = "%d\n" % pid
+ else:
+ buf = None
+ try:
+ os.ftruncate(self.lockfd, 0)
+ if buf is not None:
+ os.write(self.lockfd, get_bytes(buf))
+ except IOError as e3:
+ (err, msg) = e3.args
+ log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
+ (self.lockfile, msg, err))
+ raise
+ self.current_pid = pid
+ if self.current_pid is not None:
+ log_msg("currentPid: %d\n" % self.current_pid)
+
+ modify_ldif = "dn: %s\n" % (self.cache_dn) +\
+ "changetype: modify\n" +\
+ "replace: currentPid\n"
+ if self.current_pid is not None:
+ modify_ldif += "currentPid: %d\n" % (self.current_pid)
+ modify_ldif += "replace: currentTime\n" +\
+ "currentTime: %s\n" % ldb.timestring(int(time.time()))
+ self.cache.modify_ldif(modify_ldif)
+ return
+
+ def update_cache(res_controls):
+ assert len(res_controls) > 0
+ assert res_controls[0].oid == "1.2.840.113556.1.4.841"
+ res_controls[0].critical = True
+ self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
+ # This cookie can be extremely long
+ # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
+
+ modify_ldif = "dn: %s\n" % (self.cache_dn) +\
+ "changetype: modify\n" +\
+ "replace: dirsyncControl\n" +\
+ "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\
+ "replace: currentTime\n" +\
+ "currentTime: %s\n" % ldb.timestring(int(time.time()))
+ self.cache.modify_ldif(modify_ldif)
+ return
+
+ def check_object(dirsync_obj, res_controls):
+ assert len(res_controls) > 0
+ assert res_controls[0].oid == "1.2.840.113556.1.4.841"
+
+ binary_sid = dirsync_obj.dn.get_extended_component("SID")
+ sid = ndr_unpack(security.dom_sid, binary_sid)
+ dn = "KEY=%s" % sid
+ lastCookie = str(res_controls[0])
+
+ res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
+ expression="(lastCookie=%s)" % (
+ ldb.binary_encode(lastCookie)),
+ attrs=[])
+ if len(res) == 1:
+ return True
+ return False
+
+ def update_object(dirsync_obj, res_controls):
+ assert len(res_controls) > 0
+ assert res_controls[0].oid == "1.2.840.113556.1.4.841"
+
+ binary_sid = dirsync_obj.dn.get_extended_component("SID")
+ sid = ndr_unpack(security.dom_sid, binary_sid)
+ dn = "KEY=%s" % sid
+ lastCookie = str(res_controls[0])
+
+ self.cache.transaction_start()
+ try:
+ res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
+ expression="(objectClass=*)",
+ attrs=["lastCookie"])
+ if len(res) == 0:
+ add_ldif = "dn: %s\n" % (dn) +\
+ "objectClass: userCookie\n" +\
+ "lastCookie: %s\n" % (lastCookie) +\
+ "currentTime: %s\n" % ldb.timestring(int(time.time()))
+ self.cache.add_ldif(add_ldif)
+ else:
+ modify_ldif = "dn: %s\n" % (dn) +\
+ "changetype: modify\n" +\
+ "replace: lastCookie\n" +\
+ "lastCookie: %s\n" % (lastCookie) +\
+ "replace: currentTime\n" +\
+ "currentTime: %s\n" % ldb.timestring(int(time.time()))
+ self.cache.modify_ldif(modify_ldif)
+ self.cache.transaction_commit()
+ except Exception as e:
+ self.cache.transaction_cancel()
+
+ return
+
+ def dirsync_loop():
+ while True:
+ res = self.samdb.search(expression=str(self.dirsync_filter),
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=self.dirsync_attrs,
+ controls=self.dirsync_controls)
+ log_msg("dirsync_loop(): results %d\n" % len(res))
+ ri = 0
+ for r in res:
+ done = check_object(r, res.controls)
+ if not done:
+ handle_object(ri, r)
+ update_object(r, res.controls)
+ ri += 1
+ update_cache(res.controls)
+ if len(res) == 0:
+ break
+
+ def sync_loop(wait):
+ notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
+ notify_controls = ["notification:1", "show_recycled:1"]
+ notify_handle = self.samdb.search_iterator(expression="objectClass=*",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=notify_attrs,
+ controls=notify_controls,
+ timeout=-1)
+
+ if wait is True:
+ log_msg("Resuming monitoring\n")
+ else:
+ log_msg("Getting changes\n")
+ self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
+ self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
+ self.outf.write("syncCommand: %s\n" % self.sync_command)
+ dirsync_loop()
+
+ if wait is not True:
+ return
+
+ for msg in notify_handle:
+ if not isinstance(msg, ldb.Message):
+ self.outf.write("referral: %s\n" % msg)
+ continue
+ created = msg.get("uSNCreated")[0]
+ changed = msg.get("uSNChanged")[0]
+ log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
+ (msg.dn, created, changed))
+
+ dirsync_loop()
+
+ res = notify_handle.result()
+
+ def daemonize():
+ self.samdb = None
+ self.cache = None
+ orig_pid = os.getpid()
+ pid = os.fork()
+ if pid == 0:
+ os.setsid()
+ pid = os.fork()
+ if pid == 0: # Actual daemon
+ pid = os.getpid()
+ log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
+ load_cache()
+ return
+ os._exit(0)
+
+ if cache_ldb_initialize:
+ self.samdb_url = H
+ self.samdb = self.connect_for_passwords(url=self.samdb_url,
+ verbose=True)
+ load_cache()
+ return
+
+ if logfile is not None:
+ import resource # Resource usage information.
+ maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+ if maxfd == resource.RLIM_INFINITY:
+ maxfd = 1024 # Rough guess at maximum number of open file descriptors.
+ logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
+ self.outf.write("Using logfile[%s]\n" % logfile)
+ for fd in range(0, maxfd):
+ if fd == logfd:
+ continue
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+ os.dup2(logfd, 0)
+ os.dup2(logfd, 1)
+ os.dup2(logfd, 2)
+ os.close(logfd)
+ log_msg("Attached to logfile[%s]\n" % (logfile))
+ self.logfile = logfile
+
+ load_cache()
+ conflict = check_current_pid_conflict(terminate)
+ if terminate:
+ if self.current_pid is None:
+ log_msg("No process running.\n")
+ return
+ if not conflict:
+ log_msg("Process %d is not running anymore.\n" % (
+ self.current_pid))
+ update_pid(None)
+ return
+ log_msg("Sending SIGTERM to process %d.\n" % (
+ self.current_pid))
+ os.kill(self.current_pid, signal.SIGTERM)
+ return
+ if conflict:
+ raise CommandError("Exiting pid %d, command is already running as pid %d" % (
+ os.getpid(), self.current_pid))
+
+ if daemon is True:
+ daemonize()
+ update_pid(os.getpid())
+
+ wait = True
+ while wait is True:
+ retry_sleep_min = 1
+ retry_sleep_max = 600
+ if nowait is True:
+ wait = False
+ retry_sleep = 0
+ else:
+ retry_sleep = retry_sleep_min
+
+ while self.samdb is None:
+ if retry_sleep != 0:
+ log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
+ time.sleep(retry_sleep)
+ retry_sleep = retry_sleep * 2
+ if retry_sleep >= retry_sleep_max:
+ retry_sleep = retry_sleep_max
+ log_msg("Connecting to '%s'\n" % self.samdb_url)
+ try:
+ self.samdb = self.connect_for_passwords(url=self.samdb_url)
+ except Exception as msg:
+ self.samdb = None
+ log_msg("Connect to samdb Exception => (%s)\n" % msg)
+ if wait is not True:
+ raise
+
+ try:
+ sync_loop(wait)
+ except ldb.LdbError as e7:
+ (enum, estr) = e7.args
+ self.samdb = None
+ log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
+
+ update_pid(None)
+ return