2022-05-24 19:59:16 +12:00
#!/usr/bin/env python3
# Unix SMB/CIFS implementation.
# Copyright (C) Stefan Metzmacher 2020
# Copyright (C) Catalyst.Net Ltd
#
# 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 sys
2022-11-29 14:14:32 +01:00
import os
sys . path . insert ( 0 , ' bin/python ' )
os . environ [ ' PYTHONUNBUFFERED ' ] = ' 1 '
2022-05-24 19:59:16 +12:00
from functools import partial
from samba import generate_random_password , unix2nttime
from samba . dcerpc import krb5pac , security
from samba . sd_utils import SDUtils
from samba . tests . krb5 . kdc_base_test import KDCBaseTest
from samba . tests . krb5 . rfc4120_constants import (
KDC_ERR_TGT_REVOKED ,
KDC_ERR_TKT_EXPIRED ,
KPASSWD_ACCESSDENIED ,
2022-06-10 19:17:11 +12:00
KPASSWD_AUTHERROR ,
2022-05-24 19:59:16 +12:00
KPASSWD_HARDERROR ,
KPASSWD_INITIAL_FLAG_NEEDED ,
KPASSWD_MALFORMED ,
KPASSWD_SOFTERROR ,
KPASSWD_SUCCESS ,
NT_PRINCIPAL ,
NT_SRV_INST ,
)
global_asn1_print = False
global_hexdump = False
# Note: these tests do not pass on Windows, which returns different error codes
# to the ones we have chosen, and does not always return additional error data.
class KpasswdTests ( KDCBaseTest ) :
def setUp ( self ) :
super ( ) . setUp ( )
self . do_asn1_print = global_asn1_print
self . do_hexdump = global_hexdump
samdb = self . get_samdb ( )
# Get the old 'dSHeuristics' if it was set
dsheuristics = samdb . get_dsheuristics ( )
# Reset the 'dSHeuristics' as they were before
self . addCleanup ( samdb . set_dsheuristics , dsheuristics )
# Set the 'dSHeuristics' to activate the correct 'userPassword'
# behaviour
samdb . set_dsheuristics ( ' 000000001 ' )
# Get the old 'minPwdAge'
minPwdAge = samdb . get_minPwdAge ( )
# Reset the 'minPwdAge' as it was before
self . addCleanup ( samdb . set_minPwdAge , minPwdAge )
# Set it temporarily to '0'
samdb . set_minPwdAge ( ' 0 ' )
def _get_creds ( self , expired = False ) :
opts = {
' expired_password ' : expired
}
# Create the account.
creds = self . get_cached_creds ( account_type = self . AccountType . USER ,
opts = opts ,
use_cache = False )
return creds
def issued_by_rodc ( self , ticket ) :
krbtgt_creds = self . get_mock_rodc_krbtgt_creds ( )
krbtgt_key = self . TicketDecryptionKey_from_creds ( krbtgt_creds )
checksum_keys = {
krb5pac . PAC_TYPE_KDC_CHECKSUM : krbtgt_key ,
}
return self . modified_ticket (
ticket ,
new_ticket_key = krbtgt_key ,
checksum_keys = checksum_keys )
def get_kpasswd_sname ( self ) :
return self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = [ ' kadmin ' , ' changepw ' ] )
def get_ticket_lifetime ( self , ticket ) :
enc_part = ticket . ticket_private
authtime = enc_part [ ' authtime ' ]
starttime = enc_part . get ( ' starttime ' , authtime )
endtime = enc_part [ ' endtime ' ]
starttime = self . get_EpochFromKerberosTime ( starttime )
endtime = self . get_EpochFromKerberosTime ( endtime )
return endtime - starttime
def add_requester_sid ( self , pac , sid ) :
pac_buffers = pac . buffers
buffer_types = [ pac_buffer . type for pac_buffer in pac_buffers ]
self . assertNotIn ( krb5pac . PAC_TYPE_REQUESTER_SID , buffer_types )
requester_sid = krb5pac . PAC_REQUESTER_SID ( )
requester_sid . sid = security . dom_sid ( sid )
requester_sid_buffer = krb5pac . PAC_BUFFER ( )
requester_sid_buffer . type = krb5pac . PAC_TYPE_REQUESTER_SID
requester_sid_buffer . info = requester_sid
pac_buffers . append ( requester_sid_buffer )
pac . buffers = pac_buffers
pac . num_buffers + = 1
return pac
# Test setting a password with kpasswd.
def test_kpasswd_set ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Test the newly set password.
creds . update_password ( new_password )
self . get_tgt ( creds , fresh = True )
# Test changing a password with kpasswd.
def test_kpasswd_change ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test the newly set password.
creds . update_password ( new_password )
self . get_tgt ( creds , fresh = True )
# Test kpasswd without setting the canonicalize option.
def test_kpasswd_no_canonicalize ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
sname = self . get_kpasswd_sname ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
creds . update_password ( new_password )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
kdc_options = ' 0 ' )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test kpasswd with the canonicalize option reset and a non-canonical
# (by conversion to title case) realm.
def test_kpasswd_no_canonicalize_realm_case ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
sname = self . get_kpasswd_sname ( )
realm = creds . get_realm ( ) . capitalize ( ) # We use a title-cased realm.
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
realm = realm ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
creds . update_password ( new_password )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
realm = realm ,
kdc_options = ' 0 ' )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test kpasswd with the canonicalize option set.
def test_kpasswd_canonicalize ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd. We set the canonicalize flag here.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' canonicalize ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
creds . update_password ( new_password )
# Get an initial ticket to kpasswd. We set the canonicalize flag here.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' canonicalize ' )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test kpasswd with the canonicalize option set and a non-canonical (by
# conversion to title case) realm.
def test_kpasswd_canonicalize_realm_case ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
sname = self . get_kpasswd_sname ( )
realm = creds . get_realm ( ) . capitalize ( ) # We use a title-cased realm.
# Get an initial ticket to kpasswd. We set the canonicalize flag here.
ticket = self . get_tgt ( creds , sname = sname ,
realm = realm ,
kdc_options = ' canonicalize ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
creds . update_password ( new_password )
# Get an initial ticket to kpasswd. We set the canonicalize flag here.
ticket = self . get_tgt ( creds , sname = sname ,
realm = realm ,
kdc_options = ' canonicalize ' )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test kpasswd rejects a password that does not meet complexity
# requirements.
def test_kpasswd_too_weak ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SOFTERROR
expected_msg = b ' Password does not meet complexity requirements '
# Set the password.
new_password = ' password '
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test kpasswd rejects an empty new password.
def test_kpasswd_empty ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SOFTERROR , KPASSWD_HARDERROR
expected_msg = ( b ' Password too short, password must be at least 7 '
b ' characters long. ' ,
b ' String conversion failed! ' )
# Set the password.
new_password = ' '
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
expected_code = KPASSWD_HARDERROR
expected_msg = b ' String conversion failed! '
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test kpasswd rejects a request that does not include a random sequence
# number.
def test_kpasswd_no_seq_number ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_HARDERROR
expected_msg = b ' gensec_unwrap failed - NT_STATUS_ACCESS_DENIED \n '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET ,
send_seq_number = False )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE ,
send_seq_number = False )
# Test kpasswd rejects a ticket issued by an RODC.
def test_kpasswd_from_rodc ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
# Have the ticket be issued by the RODC.
ticket = self . issued_by_rodc ( ticket )
expected_code = KPASSWD_HARDERROR
expected_msg = b ' gensec_update failed - NT_STATUS_LOGON_FAILURE \n '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test setting a password, specifying the principal of the target user.
def test_kpasswd_set_target_princ_only ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
username = creds . get_username ( )
cname = self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = username . split ( ' / ' ) )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_MALFORMED
expected_msg = ( b ' Realm and principal must be both present, or '
b ' neither present ' ,
b ' Failed to decode packet ' )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET ,
target_princ = cname )
# Test that kpasswd rejects a password set specifying only the realm of the
# target user.
def test_kpasswd_set_target_realm_only ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_MALFORMED , KPASSWD_ACCESSDENIED
expected_msg = ( b ' Realm and principal must be both present, or '
b ' neither present ' ,
b ' Failed to decode packet ' ,
b ' No such user when changing password ' )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET ,
target_realm = creds . get_realm ( ) )
# Show that a user cannot set a password, specifying both principal and
# realm of the target user, without having control access.
def test_kpasswd_set_target_princ_and_realm_no_access ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
username = creds . get_username ( )
cname = self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = username . split ( ' / ' ) )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_ACCESSDENIED
expected_msg = b ' Not permitted to change password '
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET ,
target_princ = cname ,
target_realm = creds . get_realm ( ) )
# Test setting a password, specifying both principal and realm of the
# target user, whem the user has control access on their account.
def test_kpasswd_set_target_princ_and_realm_access ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
username = creds . get_username ( )
tgt = self . get_tgt ( creds )
cname = self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = username . split ( ' / ' ) )
samdb = self . get_samdb ( )
sd_utils = SDUtils ( samdb )
user_dn = creds . get_dn ( )
user_sid = self . get_objectSid ( samdb , user_dn )
# Give the user control access on their account.
ace = f ' (A;;CR;;; { user_sid } ) '
sd_utils . dacl_add_ace ( user_dn , ace )
# Get a non-initial ticket to kpasswd. Since we have the right to
# change the account's password, we don't need an initial ticket.
krbtgt_creds = self . get_krbtgt_creds ( )
ticket = self . get_service_ticket ( tgt ,
krbtgt_creds ,
service = ' kadmin ' ,
target_name = ' changepw ' ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET ,
target_princ = cname ,
target_realm = creds . get_realm ( ) )
# Test setting a password when the existing password has expired.
def test_kpasswd_set_expired_password ( self ) :
# Create an account for testing, with an expired password.
creds = self . _get_creds ( expired = True )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Test changing a password when the existing password has expired.
def test_kpasswd_change_expired_password ( self ) :
# Create an account for testing, with an expired password.
creds = self . _get_creds ( expired = True )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Check the lifetime of a kpasswd ticket is not more than two minutes.
def test_kpasswd_ticket_lifetime ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
# Check the lifetime of the ticket is equal to two minutes.
lifetime = self . get_ticket_lifetime ( ticket )
self . assertEqual ( 2 * 60 , lifetime )
# Ensure we cannot perform a TGS-REQ with a kpasswd ticket.
def test_kpasswd_ticket_tgs ( self ) :
creds = self . get_client_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
# Change the sname of the ticket to match that of a TGT.
realm = creds . get_realm ( )
krbtgt_sname = self . PrincipalName_create ( name_type = NT_SRV_INST ,
names = [ ' krbtgt ' , realm ] )
ticket . set_sname ( krbtgt_sname )
# Try to use that ticket to get a service ticket.
service_creds = self . get_service_creds ( )
# This fails due to missing REQUESTER_SID buffer.
self . _make_tgs_request ( creds , service_creds , ticket ,
expect_error = ( KDC_ERR_TGT_REVOKED ,
KDC_ERR_TKT_EXPIRED ) )
def modify_requester_sid_time ( self , ticket , sid , lifetime ) :
# Get the krbtgt key.
krbtgt_creds = self . get_krbtgt_creds ( )
krbtgt_key = self . TicketDecryptionKey_from_creds ( krbtgt_creds )
checksum_keys = {
krb5pac . PAC_TYPE_KDC_CHECKSUM : krbtgt_key ,
}
# Set authtime and starttime to an hour in the past, to show that they
# do not affect ticket rejection.
start_time = self . get_KerberosTime ( offset = - 60 * 60 )
# Set the endtime of the ticket relative to our current time, so that
# the ticket has 'lifetime' seconds remaining to live.
end_time = self . get_KerberosTime ( offset = lifetime )
# Modify the times in the ticket.
def modify_ticket_times ( enc_part ) :
enc_part [ ' authtime ' ] = start_time
if ' starttime ' in enc_part :
enc_part [ ' starttime ' ] = start_time
enc_part [ ' endtime ' ] = end_time
return enc_part
# We have to set the times in both the ticket and the PAC, otherwise
# Heimdal will complain.
def modify_pac_time ( pac ) :
pac_buffers = pac . buffers
for pac_buffer in pac_buffers :
if pac_buffer . type == krb5pac . PAC_TYPE_LOGON_NAME :
logon_time = self . get_EpochFromKerberosTime ( start_time )
pac_buffer . info . logon_time = unix2nttime ( logon_time )
break
else :
self . fail ( ' failed to find LOGON_NAME PAC buffer ' )
pac . buffers = pac_buffers
return pac
# Add a requester SID to show that the KDC will then accept this
# kpasswd ticket as if it were a TGT.
def modify_pac_fn ( pac ) :
pac = self . add_requester_sid ( pac , sid = sid )
pac = modify_pac_time ( pac )
return pac
# Do the actual modification.
return self . modified_ticket ( ticket ,
new_ticket_key = krbtgt_key ,
modify_fn = modify_ticket_times ,
modify_pac_fn = modify_pac_fn ,
checksum_keys = checksum_keys )
# Ensure we cannot perform a TGS-REQ with a kpasswd ticket containing a
# requester SID and having a remaining lifetime of two minutes.
def test_kpasswd_ticket_requester_sid_tgs ( self ) :
creds = self . get_client_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
# Change the sname of the ticket to match that of a TGT.
realm = creds . get_realm ( )
krbtgt_sname = self . PrincipalName_create ( name_type = NT_SRV_INST ,
names = [ ' krbtgt ' , realm ] )
ticket . set_sname ( krbtgt_sname )
# Get the user's SID.
samdb = self . get_samdb ( )
user_dn = creds . get_dn ( )
user_sid = self . get_objectSid ( samdb , user_dn )
# Modify the ticket to add a requester SID and give it two minutes to
# live.
ticket = self . modify_requester_sid_time ( ticket ,
sid = user_sid ,
lifetime = 2 * 60 )
# Try to use that ticket to get a service ticket.
service_creds = self . get_service_creds ( )
# This fails due to the lifetime being too short.
self . _make_tgs_request ( creds , service_creds , ticket ,
expect_error = KDC_ERR_TKT_EXPIRED )
# Show we can perform a TGS-REQ with a kpasswd ticket containing a
# requester SID if the remaining lifetime exceeds two minutes.
def test_kpasswd_ticket_requester_sid_lifetime_tgs ( self ) :
creds = self . get_client_creds ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = self . get_kpasswd_sname ( ) ,
kdc_options = ' 0 ' )
# Change the sname of the ticket to match that of a TGT.
realm = creds . get_realm ( )
krbtgt_sname = self . PrincipalName_create ( name_type = NT_SRV_INST ,
names = [ ' krbtgt ' , realm ] )
ticket . set_sname ( krbtgt_sname )
# Get the user's SID.
samdb = self . get_samdb ( )
user_dn = creds . get_dn ( )
user_sid = self . get_objectSid ( samdb , user_dn )
# Modify the ticket to add a requester SID and give it two minutes and
# ten seconds to live.
ticket = self . modify_requester_sid_time ( ticket ,
sid = user_sid ,
lifetime = 2 * 60 + 10 )
# Try to use that ticket to get a service ticket.
service_creds = self . get_service_creds ( )
# This succeeds.
self . _make_tgs_request ( creds , service_creds , ticket ,
expect_error = False )
2022-06-10 19:17:11 +12:00
# Show that we cannot provide a TGT to kpasswd to change the password.
def test_kpasswd_tgt ( self ) :
# Create an account for testing, and get a TGT.
creds = self . _get_creds ( )
tgt = self . get_tgt ( creds )
# Change the sname of the ticket to match that of kadmin/changepw.
tgt . set_sname ( self . get_kpasswd_sname ( ) )
expected_code = KPASSWD_AUTHERROR
expected_msg = b ' A TGT may not be used as a ticket to kpasswd '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( tgt ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( tgt ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
2022-05-24 19:59:16 +12:00
# Test that kpasswd rejects requests with a service ticket.
def test_kpasswd_non_initial ( self ) :
# Create an account for testing, and get a TGT.
creds = self . _get_creds ( )
tgt = self . get_tgt ( creds )
# Get a non-initial ticket to kpasswd.
krbtgt_creds = self . get_krbtgt_creds ( )
ticket = self . get_service_ticket ( tgt ,
krbtgt_creds ,
service = ' kadmin ' ,
target_name = ' changepw ' ,
kdc_options = ' 0 ' )
expected_code = KPASSWD_INITIAL_FLAG_NEEDED
expected_msg = b ' Expected an initial ticket '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Show that kpasswd accepts requests with a service ticket modified to set
# the 'initial' flag.
def test_kpasswd_initial ( self ) :
# Create an account for testing, and get a TGT.
creds = self . _get_creds ( )
krbtgt_creds = self . get_krbtgt_creds ( )
# Get a service ticket, and modify it to set the 'initial' flag.
def get_ticket ( ) :
tgt = self . get_tgt ( creds , fresh = True )
# Get a non-initial ticket to kpasswd.
ticket = self . get_service_ticket ( tgt ,
krbtgt_creds ,
service = ' kadmin ' ,
target_name = ' changepw ' ,
kdc_options = ' 0 ' ,
fresh = True )
set_initial_flag = partial ( self . modify_ticket_flag , flag = ' initial ' ,
value = True )
checksum_keys = self . get_krbtgt_checksum_key ( )
return self . modified_ticket ( ticket ,
modify_fn = set_initial_flag ,
checksum_keys = checksum_keys )
expected_code = KPASSWD_SUCCESS
expected_msg = b ' Password changed '
ticket = get_ticket ( )
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
creds . update_password ( new_password )
ticket = get_ticket ( )
# Change the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test that kpasswd rejects requests where the ticket is encrypted with a
# key other than the krbtgt's.
def test_kpasswd_wrong_key ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
sname = self . get_kpasswd_sname ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
kdc_options = ' 0 ' )
# Get a key belonging to the Administrator account.
admin_creds = self . get_admin_creds ( )
admin_key = self . TicketDecryptionKey_from_creds ( admin_creds )
self . assertIsNotNone ( admin_key . kvno ,
' a kvno is required to tell the DB '
' which key to look up. ' )
checksum_keys = {
krb5pac . PAC_TYPE_KDC_CHECKSUM : admin_key ,
}
# Re-encrypt the ticket using the Administrator's key.
ticket = self . modified_ticket ( ticket ,
new_ticket_key = admin_key ,
checksum_keys = checksum_keys )
# Set the sname of the ticket to that of the Administrator account.
admin_sname = self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = [ ' Administrator ' ] )
ticket . set_sname ( admin_sname )
expected_code = KPASSWD_HARDERROR
expected_msg = b ' gensec_update failed - NT_STATUS_LOGON_FAILURE \n '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
def test_kpasswd_wrong_key_service ( self ) :
# Create an account for testing.
creds = self . get_cached_creds ( account_type = self . AccountType . COMPUTER ,
use_cache = False )
sname = self . get_kpasswd_sname ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
kdc_options = ' 0 ' )
# Get a key belonging to our account.
our_key = self . TicketDecryptionKey_from_creds ( creds )
self . assertIsNotNone ( our_key . kvno ,
' a kvno is required to tell the DB '
' which key to look up. ' )
checksum_keys = {
krb5pac . PAC_TYPE_KDC_CHECKSUM : our_key ,
}
# Re-encrypt the ticket using our key.
ticket = self . modified_ticket ( ticket ,
new_ticket_key = our_key ,
checksum_keys = checksum_keys )
# Set the sname of the ticket to that of our account.
username = creds . get_username ( )
sname = self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = username . split ( ' / ' ) )
ticket . set_sname ( sname )
expected_code = KPASSWD_HARDERROR
expected_msg = b ' gensec_update failed - NT_STATUS_LOGON_FAILURE \n '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
# Test that kpasswd rejects requests where the ticket is encrypted with a
# key belonging to a server account other than the krbtgt.
def test_kpasswd_wrong_key_server ( self ) :
# Create an account for testing.
creds = self . _get_creds ( )
sname = self . get_kpasswd_sname ( )
# Get an initial ticket to kpasswd.
ticket = self . get_tgt ( creds , sname = sname ,
kdc_options = ' 0 ' )
# Get a key belonging to the DC's account.
dc_creds = self . get_dc_creds ( )
dc_key = self . TicketDecryptionKey_from_creds ( dc_creds )
self . assertIsNotNone ( dc_key . kvno ,
' a kvno is required to tell the DB '
' which key to look up. ' )
checksum_keys = {
krb5pac . PAC_TYPE_KDC_CHECKSUM : dc_key ,
}
# Re-encrypt the ticket using the DC's key.
ticket = self . modified_ticket ( ticket ,
new_ticket_key = dc_key ,
checksum_keys = checksum_keys )
# Set the sname of the ticket to that of the DC's account.
dc_username = dc_creds . get_username ( )
dc_sname = self . PrincipalName_create ( name_type = NT_PRINCIPAL ,
names = dc_username . split ( ' / ' ) )
ticket . set_sname ( dc_sname )
expected_code = KPASSWD_HARDERROR
expected_msg = b ' gensec_update failed - NT_STATUS_LOGON_FAILURE \n '
# Set the password.
new_password = generate_random_password ( 32 , 32 )
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . SET )
# Change the password.
self . kpasswd_exchange ( ticket ,
new_password ,
expected_code ,
expected_msg ,
mode = self . KpasswdMode . CHANGE )
if __name__ == ' __main__ ' :
global_asn1_print = False
global_hexdump = False
import unittest
unittest . main ( )