1
0
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:
Stefan Metzmacher 2016-01-22 21:52:26 +01:00
parent 67404bac52
commit deb2a0258e

View File

@ -20,10 +20,20 @@
import samba.getopt as options import samba.getopt as options
import ldb import ldb
import pwd import pwd
import os
import sys
import errno
import base64
import binascii
from getpass import getpass from getpass import getpass
from samba.auth import system_session from samba.auth import system_session
from samba.samdb import SamDB 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 ( from samba import (
credentials,
dsdb, dsdb,
gensec, gensec,
generate_random_password, generate_random_password,
@ -37,6 +47,113 @@ from samba.netcmd import (
Option, 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): class cmd_user_create(Command):
"""Create a new user. """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)) raise CommandError("%s: %s" % (command, msg))
self.outf.write("Changed password OK\n") 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): class cmd_user(SuperCommand):
"""User management.""" """User management."""
@ -687,3 +1094,4 @@ class cmd_user(SuperCommand):
subcommands["setexpiry"] = cmd_user_setexpiry() subcommands["setexpiry"] = cmd_user_setexpiry()
subcommands["password"] = cmd_user_password() subcommands["password"] = cmd_user_password()
subcommands["setpassword"] = cmd_user_setpassword() subcommands["setpassword"] = cmd_user_setpassword()
subcommands["getpassword"] = cmd_user_getpassword()