mirror of
https://github.com/samba-team/samba.git
synced 2025-02-02 09:47:23 +03:00
samba-tool: add 'user getpassword' command
This provides an easy way to get the passwords of a user including the cleartext passwords (if stored) and derived hashes. This is done by providing virtual attributes like: virtualClearTextUTF16, virtualClearTextUTF8, virtualCryptSHA256, virtualCryptSHA512, virtualSSHA This is much easier than using ldbsearch and manually parsing the supplementalCredentials attribute. Signed-off-by: Stefan Metzmacher <metze@samba.org> Reviewed-by: Alexander Bokovoy <ab@samba.org>
This commit is contained in:
parent
67404bac52
commit
deb2a0258e
@ -20,10 +20,20 @@
|
||||
import samba.getopt as options
|
||||
import ldb
|
||||
import pwd
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
import base64
|
||||
import binascii
|
||||
from getpass import getpass
|
||||
from samba.auth import system_session
|
||||
from samba.samdb import SamDB
|
||||
from samba.dcerpc import misc
|
||||
from samba.dcerpc import security
|
||||
from samba.dcerpc import drsblobs
|
||||
from samba.ndr import ndr_unpack, ndr_pack, ndr_print
|
||||
from samba import (
|
||||
credentials,
|
||||
dsdb,
|
||||
gensec,
|
||||
generate_random_password,
|
||||
@ -37,6 +47,113 @@ from samba.netcmd import (
|
||||
Option,
|
||||
)
|
||||
|
||||
disabled_virtual_attributes = {
|
||||
}
|
||||
|
||||
virtual_attributes = {
|
||||
"virtualClearTextUTF8": {
|
||||
"flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
|
||||
},
|
||||
"virtualClearTextUTF16": {
|
||||
"flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
|
||||
},
|
||||
}
|
||||
|
||||
get_random_bytes_fn = None
|
||||
if get_random_bytes_fn is None:
|
||||
try:
|
||||
import Crypto.Random
|
||||
get_random_bytes_fn = Crypto.Random.get_random_bytes
|
||||
except ImportError as e:
|
||||
pass
|
||||
if get_random_bytes_fn is None:
|
||||
try:
|
||||
import M2Crypto.Rand
|
||||
get_random_bytes_fn = M2Crypto.Rand.rand_bytes
|
||||
except ImportError as e:
|
||||
pass
|
||||
|
||||
def check_random():
|
||||
if get_random_bytes_fn is not None:
|
||||
return None
|
||||
return "Crypto.Random or M2Crypto.Rand required"
|
||||
|
||||
def get_random_bytes(num):
|
||||
random_reason = check_random()
|
||||
if random_reason is not None:
|
||||
raise ImportError(random_reason)
|
||||
return get_random_bytes_fn(num)
|
||||
|
||||
def get_crypt_value(alg, utf8pw):
|
||||
algs = {
|
||||
"5": {"length": 43},
|
||||
"6": {"length": 86},
|
||||
}
|
||||
assert alg in algs
|
||||
salt = get_random_bytes(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)
|
||||
crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
|
||||
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:
|
||||
random_reason = check_random()
|
||||
if random_reason is not None:
|
||||
raise ImportError(random_reason)
|
||||
import hashlib
|
||||
h = hashlib.sha1()
|
||||
h = None
|
||||
virtual_attributes["virtualSSHA"] = {
|
||||
}
|
||||
except ImportError as e:
|
||||
reason = "hashlib.sha1()"
|
||||
if random_reason:
|
||||
reason += " and " + random_reason
|
||||
reason += " required"
|
||||
disabled_virtual_attributes["virtualSSHA"] = {
|
||||
"reason" : reason,
|
||||
}
|
||||
|
||||
for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
|
||||
try:
|
||||
random_reason = check_random()
|
||||
if random_reason is not None:
|
||||
raise ImportError(random_reason)
|
||||
import crypt
|
||||
v = get_crypt_value(alg, "")
|
||||
v = None
|
||||
virtual_attributes[attr] = {
|
||||
}
|
||||
except ImportError as e:
|
||||
reason = "crypt"
|
||||
if random_reason:
|
||||
reason += " and " + random_reason
|
||||
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,
|
||||
}
|
||||
|
||||
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 cmd_user_create(Command):
|
||||
"""Create a new user.
|
||||
@ -673,6 +790,296 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
|
||||
raise CommandError("%s: %s" % (command, msg))
|
||||
self.outf.write("Changed password OK\n")
|
||||
|
||||
class GetPasswordCommand(Command):
|
||||
|
||||
def __init__(self):
|
||||
super(GetPasswordCommand, self).__init__()
|
||||
self.lp = None
|
||||
|
||||
def connect_system_samdb(self, url, allow_local=False, verbose=False):
|
||||
|
||||
# using anonymous here, results in no authentication
|
||||
# which means we can get system privileges via
|
||||
# the privileged ldapi socket
|
||||
creds = credentials.Credentials()
|
||||
creds.set_anonymous()
|
||||
|
||||
if url is None and allow_local:
|
||||
pass
|
||||
elif url.lower().startswith("ldapi://"):
|
||||
pass
|
||||
elif url.lower().startswith("ldap://"):
|
||||
raise CommandError("--url ldap:// is not supported for this command")
|
||||
elif url.lower().startswith("ldaps://"):
|
||||
raise CommandError("--url ldaps:// is not supported for this command")
|
||||
elif not allow_local:
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
# 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)
|
||||
|
||||
return samdb
|
||||
|
||||
def get_account_attributes(self, samdb, username,
|
||||
basedn, filter, scope, attrs):
|
||||
|
||||
require_supplementalCredentials = False
|
||||
search_attrs = attrs[:]
|
||||
lower_attrs = [x.lower() for x in search_attrs]
|
||||
for a in virtual_attributes.keys():
|
||||
if a.lower() in lower_attrs:
|
||||
require_supplementalCredentials = True
|
||||
add_supplementalCredentials = False
|
||||
if require_supplementalCredentials:
|
||||
a = "supplementalCredentials"
|
||||
if a.lower() not in lower_attrs:
|
||||
search_attrs += [a]
|
||||
add_supplementalCredentials = True
|
||||
add_sAMAcountName = False
|
||||
a = "sAMAccountName"
|
||||
if a.lower() not in lower_attrs:
|
||||
search_attrs += [a]
|
||||
add_sAMAcountName = True
|
||||
|
||||
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]
|
||||
|
||||
sc = None
|
||||
if "supplementalCredentials" in obj:
|
||||
sc_blob = obj["supplementalCredentials"][0]
|
||||
sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
|
||||
if add_supplementalCredentials:
|
||||
del obj["supplementalCredentials"]
|
||||
account_name = obj["sAMAccountName"][0]
|
||||
if add_sAMAcountName:
|
||||
del obj["sAMAccountName"]
|
||||
|
||||
def get_package(name):
|
||||
if sc is None:
|
||||
return None
|
||||
for p in sc.sub.packages:
|
||||
if name != p.name:
|
||||
continue
|
||||
|
||||
return binascii.a2b_hex(p.data)
|
||||
return None
|
||||
|
||||
def get_utf8(a, b, username):
|
||||
try:
|
||||
u = unicode(b, 'utf-16-le')
|
||||
except UnicodeDecodeError as e:
|
||||
self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
|
||||
username, a))
|
||||
return None
|
||||
u8 = u.encode('utf-8')
|
||||
return u8
|
||||
|
||||
# We use sort here in order to have a predictable processing order
|
||||
for a in sorted(virtual_attributes.keys()):
|
||||
if not a.lower() in lower_attrs:
|
||||
continue
|
||||
|
||||
if a == "virtualClearTextUTF8":
|
||||
b = get_package("Primary:CLEARTEXT")
|
||||
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_package("Primary:CLEARTEXT")
|
||||
if v is None:
|
||||
continue
|
||||
elif a == "virtualSSHA":
|
||||
b = get_package("Primary:CLEARTEXT")
|
||||
if b is None:
|
||||
continue
|
||||
u8 = get_utf8(a, b, username or account_name)
|
||||
if u8 is None:
|
||||
continue
|
||||
salt = get_random_bytes(4)
|
||||
h = hashlib.sha1()
|
||||
h.update(u8)
|
||||
h.update(salt)
|
||||
bv = h.digest() + salt
|
||||
v = "{SSHA}" + base64.b64encode(bv)
|
||||
elif a == "virtualCryptSHA256":
|
||||
b = get_package("Primary:CLEARTEXT")
|
||||
if b is None:
|
||||
continue
|
||||
u8 = get_utf8(a, b, username or account_name)
|
||||
if u8 is None:
|
||||
continue
|
||||
sv = get_crypt_value("5", u8)
|
||||
v = "{CRYPT}" + sv
|
||||
elif a == "virtualCryptSHA512":
|
||||
b = get_package("Primary:CLEARTEXT")
|
||||
if b is None:
|
||||
continue
|
||||
u8 = get_utf8(a, b, username or account_name)
|
||||
if u8 is None:
|
||||
continue
|
||||
sv = get_crypt_value("6", u8)
|
||||
v = "{CRYPT}" + sv
|
||||
else:
|
||||
continue
|
||||
obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
|
||||
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
|
||||
|
||||
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 only supports ldapi:// or [tdb://] and can be
|
||||
used to adjust the local path. By default tdb:// is used by default.
|
||||
|
||||
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' 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.
|
||||
|
||||
virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
|
||||
checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
|
||||
with a $6$... salt, see crypt(3) on modern systems.
|
||||
|
||||
Example1:
|
||||
samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
|
||||
|
||||
Example2:
|
||||
samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
super(cmd_user_getpassword, self).__init__()
|
||||
|
||||
synopsis = "%prog (<username>|--filter <filter>) [options]"
|
||||
|
||||
takes_optiongroups = {
|
||||
"sambaopts": options.SambaOptions,
|
||||
"versionopts": options.VersionOptions,
|
||||
}
|
||||
|
||||
takes_options = [
|
||||
Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
|
||||
metavar="URL", dest="H"),
|
||||
Option("--filter", help="LDAP Filter to set password on", type=str),
|
||||
Option("--attributes", type=str,
|
||||
help=virtual_attributes_help,
|
||||
metavar="ATTRIBUTELIST", dest="attributes"),
|
||||
]
|
||||
|
||||
takes_args = ["username?"]
|
||||
|
||||
def run(self, username=None, H=None, filter=None,
|
||||
attributes=None,
|
||||
sambaopts=None, versionopts=None):
|
||||
self.lp = sambaopts.get_loadparm()
|
||||
|
||||
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)
|
||||
|
||||
samdb = self.connect_system_samdb(url=H, allow_local=True)
|
||||
|
||||
obj = self.get_account_attributes(samdb, username,
|
||||
basedn=None,
|
||||
filter=filter,
|
||||
scope=ldb.SCOPE_SUBTREE,
|
||||
attrs=password_attrs)
|
||||
|
||||
ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
|
||||
self.outf.write("%s" % ldif)
|
||||
self.outf.write("Got password OK\n")
|
||||
|
||||
class cmd_user(SuperCommand):
|
||||
"""User management."""
|
||||
@ -687,3 +1094,4 @@ class cmd_user(SuperCommand):
|
||||
subcommands["setexpiry"] = cmd_user_setexpiry()
|
||||
subcommands["password"] = cmd_user_password()
|
||||
subcommands["setpassword"] = cmd_user_setpassword()
|
||||
subcommands["getpassword"] = cmd_user_getpassword()
|
||||
|
Loading…
x
Reference in New Issue
Block a user