-
Notifications
You must be signed in to change notification settings - Fork 9
/
macOSLAPS
executable file
·228 lines (218 loc) · 10.5 KB
/
macOSLAPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/python
'''LAPS for macOS devices'''
# pylint: disable=C0103, E0611, W0703
# ############################################################
# This python script will set a randomly generated password for your
# local adminsitrator account on macOS if the expiration date has passed
# in your Active Directory. Mimics behavior of LAPS
# (Local Administrator Password Solution) for Windows
#############################################################
# Active Directory Attributes Modified:
# dsAttrTypeNative:ms-Mcs-AdmPwd - Where Password is stored
# dsAttrTypeNative:ms-Mcs-AdmPwdself.expirationTime - Expiration Time
# #############################################################
# Joshua D. Miller - [email protected] - The Pennsylvania State University
# Script was Last Updated June 2, 2017
# #############################################################
from Foundation import CFPreferencesCopyAppValue
from datetime import datetime, timedelta
from logging import (basicConfig as log_config,
error as log_error, info as log_info)
from OpenDirectory import (ODSession, ODNode,
kODRecordTypeComputers, kODRecordTypeUsers)
from os import path
from random import choice
from shutil import rmtree
from string import ascii_letters, punctuation, digits
from SystemConfiguration import (SCDynamicStoreCreate,
SCDynamicStoreCopyValue)
from time import mktime
from unicodedata import normalize
class macOSLAPS(object):
'''main class of application'''
# Current time
now = datetime.now()
# Preference Variables
bundleid = 'edu.psu.macoslaps'
defaultpreferences = {
'LocalAdminAccount': 'admin',
'PasswordLength': 12,
'DaysTillExpiration': 60,
'RemoveKeyChain': True,
'RemovePassChars': '\''
}
# Define Active Directory Attributes
adpath = ''
computerpath = ''
expirationtime = ''
lapsattributes = dict()
computer_record = None
# Setup Logging
log_format = '%(asctime)s|%(levelname)s:%(message)s'
log_config(filename='/Library/Logs/macOSLAPS.log',
level=10, format=log_format)
def get_config_settings(self, preference_key):
'''Function to retrieve configuration settings from
/Library/Preferences or /Library/Managed Preferences'''
preference_file = self.bundleid
preference_value = CFPreferencesCopyAppValue(preference_key,
preference_file)
if preference_value is None:
preference_value = self.defaultpreferences.get(preference_key)
if isinstance(preference_value, unicode):
preference_value = normalize(
'NFKD', preference_value).encode('ascii', 'ignore')
return preference_value
def connect_to_ad(self):
'''Function to connect and pull information from Active Directory
some code borrowed from AD PassMon - Thanks @macmuleblog'''
# Active Directory Connection and Extraction of Data
try:
# Create Net Config
net_config = SCDynamicStoreCreate(None, "net", None, None)
# Get Active Directory Info
ad_info = dict(
SCDynamicStoreCopyValue(
net_config, 'com.apple.opendirectoryd.ActiveDirectory'))
# Create Active Directory Path
self.adpath = '{0:}/{1:}'.format(ad_info['NodeName'],
ad_info['DomainNameDns'])
# Computer Path
self.computerpath = 'Computers/{0:}'.format(
ad_info['TrustAccount'])
# Use Open Directory To Connect to Active Directory
node, error = ODNode.nodeWithSession_name_error_(
ODSession.defaultSession(), self.adpath, None)
# Grab the Computer Record
self.computer_record, error = node.\
recordWithRecordType_name_attributes_error_(
kODRecordTypeComputers, ad_info
['TrustAccount'], None, None)
# Convert to Readable Values
values, error = self.computer_record.\
recordDetailsForAttributes_error_(None, None)
# LAPS Attributes
self.lapsattributes[0] = 'dsAttrTypeNative:ms-Mcs-AdmPwd'
self.lapsattributes[1] = '{0:}'.format(
'dsAttrTypeNative:ms-Mcs-AdmPwdExpirationTime')
# Get Expiration Time of Password
try:
self.expirationtime = values[self.lapsattributes[1]]
except Exception:
log_info('There has never been a random password generated'
' for this device. Setting a default expiration'
' date of 01/01/2001 in Active Directory to'
' force a password change...')
self.expirationtime = '126227988000000000'
except Exception as error:
log_error(error)
exit(1)
@staticmethod
def make_random_password(length):
'''Generate a Random Password
Thanks Mike Lynn - @frogor'''
# Characters used for random password
characters = ascii_letters + punctuation + digits
remove_pass_characters = macOSLAPS().get_config_settings(
'RemovePassChars')
# Remove Characters if specified
if remove_pass_characters:
characters = characters.translate(None, remove_pass_characters)
password = []
for i in range(length):
password.insert(i, choice(characters))
return ''.join(password)
def windows_epoch_time_converter(self, time_type, expires):
'''Convert from Epoch to Windows or from Windows
to Epoch - Thanks Rusty Myers for determining Windows vs.
Epoch Time @rustymyers'''
if time_type == 'epoch':
# Convert Windows Time to Epoch Time
format_expiration_time = int(
self.expirationtime[0]) / 10000000 - 11644473600
format_expiration_time = datetime.fromtimestamp(
format_expiration_time)
return format_expiration_time
elif time_type == 'windows':
# Convert the time back from Time Stamp to Epoch to Windows
# and add 30 days onto the time
new_expiration_time = (self.now + timedelta(days=expires))
formatted_new_expiration_time = new_expiration_time
new_expiration_time = new_expiration_time.timetuple()
new_expiration_time = mktime(new_expiration_time)
new_expiration_time = ((new_expiration_time + 11644473600) *
10000000)
return (new_expiration_time, formatted_new_expiration_time)
def password_check(self):
'''Perform a password check and change the local
admin password and write it to Active Directory if
needed - Thanks to Tom Burgin and Ben Toms
@tomjburgin, @macmuleblog'''
local_admin = LAPS.get_config_settings('LocalAdminAccount')
exp_days = LAPS.get_config_settings('DaysTillExpiration')
pass_length = LAPS.get_config_settings('PasswordLength')
keychain_remove = LAPS.get_config_settings('RemoveKeyChain')
password = LAPS.make_random_password(pass_length)
formatted_expiration_time = LAPS.windows_epoch_time_converter(
'epoch', exp_days)
# Determine if the password expired and then change it
if formatted_expiration_time < self.now:
# Log that the password change is being started
log_info('Password change required.'
' Performing password change...')
try:
# Set new random password in Active Directory
self.computer_record.setValue_forAttribute_error_(
password, self.lapsattributes[0], None)
# Change the local admin password
log_info('Setting random password for local'
' admin account %s...', local_admin)
# Connect to Local Node
local_node, error = ODNode.nodeWithSession_name_error_(
ODSession.defaultSession(), '/Local/Default', None)
# Pull Local Administrator Record
local_admin_change, error = local_node.\
recordWithRecordType_name_attributes_error_(
kODRecordTypeUsers, local_admin, None, None)
# Change the password for the account
local_admin_change.changePassword_toPassword_error_(
None, password, None)
# Convert Time to Windows Time to prepare
# for new expiration time to be written to AD
new_expires = dict()
new_expires[0], new_expires[1] = LAPS.\
windows_epoch_time_converter('windows', exp_days)
# Set the Expiration Time in AD
self.computer_record.setValue_forAttribute_error_(
str(int(new_expires[0])), self.lapsattributes[1], None)
log_info('Password change has been completed. '
'New expiration date is %s',
new_expires[1])
if keychain_remove is True:
local_admin_path = '/Users/{0:}/Library/Keychains'.\
format(local_admin)
if path.exists(local_admin_path):
rmtree(local_admin_path)
log_info('Removed keychains for local '
'administrator account {0:}.'
.format(local_admin))
else:
log_info('The keychain directory for '
'{0:} does not exist. Keychain '
'removal not required...'.format(local_admin))
else:
log_info('Keychain has NOT been modified. Keep '
'in mind that this may cause keychain '
'prompts and the old password may not '
'be accessible.')
except Exception as error:
log_error(error)
exit(1)
else:
# Log that a password change is not necessary at this time
log_info('Password change not necessary at this time as'
' the expiration date is %s', formatted_expiration_time)
exit(0)
LAPS = macOSLAPS()
LAPS.connect_to_ad()
LAPS.password_check()