forked from drtaru/BreakGlassAdmin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathBreakglass Admin.sh
396 lines (338 loc) · 14.1 KB
/
Breakglass Admin.sh
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/bin/bash
: HEADER = <<'EOL'
██████╗ ██████╗ ██████╗██╗ ██╗███████╗████████╗███╗ ███╗ █████╗ ███╗ ██╗
██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝████╗ ████║██╔══██╗████╗ ██║
██████╔╝██║ ██║██║ █████╔╝ █████╗ ██║ ██╔████╔██║███████║██╔██╗ ██║
██╔══██╗██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██║╚██╔╝██║██╔══██║██║╚██╗██║
██║ ██║╚██████╔╝╚██████╗██║ ██╗███████╗ ██║ ██║ ╚═╝ ██║██║ ██║██║ ╚████║
╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
Name: Break Glass Admin
Description: Creates/manages a hidden admin account with a random password
Parameters: $1-$3 - Reserved by Jamf (Mount Point, Computer Name, Username)
$4 - The username and (optionally) full name of the admin account
$5 - Which method of creating a password to use (see below)
$6 - Name of extension attribute where password is stored
(e.g. "Breakglass Admin")
$7 - Storage method: Provide BASE64 encoded "user:password" for
storage via API. Otherwise locally stored.
$11- Overrides (optional) - See GitHub for usage
Available Password Methods:
'nato' - Combines words from the NATO phonetic alphabet
(e.g. "WhiskeyTangoFoxtrot")
'wopr' - Like the launch codes in the 80s movie, "Wargames"
[https://www.imdb.com/title/tt0086567]
(e.g. "CPE 1704 TKS")
'xkcd' - Using the system from the XKCD webcomic
(https://xkcd.com/936)
'names' - Same as above but only with the propernames database
'pseudoRandom' - Based on University of Nebraska' LAPS system
(https://github.com/NU-ITS/LAPSforMac)
'custom' (default) - Customizable format with the following defaults
* 16 characters
* 1 Upper case character (min)
* 1 Lower case character (min)
* 1 Digit (min)
* 1 Special character (min)
Optionally you can add a string to specify overrides
in the following format:
N=20;U=3;L=1;D=2;S=0
Latest version and additional notes available at our GitHub
https://github.com/Rocketman-Tech/BreakGlassAdmin
EOL
##
## Create settings from Policy Parameters
##
## User-related components
ADMINUSER=$([ "$4" ] && echo "$4" || echo "breakglass Breakglass Admin")
USERNAME=$(echo "${ADMINUSER}" | sed -nr 's/^([^\ ]+)\ (.*)$/\1/p' )
FULLNAME=$(echo "${ADMINUSER}" | sed -nr 's/^([^\ ]+)\ (.*)$/\2/p' )
FULLNAME=$( [[ $FULLNAME != "" ]] && echo "${FULLNAME}" || echo ${USERNAME} )
## Choose the password generation method
## E.g. nato, wopr, xkcd, names, pseudoRandom
PASSMODE=$([ "$5" ] && echo "$5" || echo "custom")
## Name of the extension attribute to store password
EXTATTR=$([ "$6" ] && echo "$6" || echo "Breakglass Admin")
## API User "Hash" - Base64 encoded "user:password" string for API use
APIHASH=$([ "$7" ] && echo "$7" || echo "")
## Other Main Defaults
## These can either be harcoded here or overriden with $11 (see below)
DEBUG='' ## Default is off.
NUM='' ## Override for each password method's defaults
## nato = 3 words
## xkcd = 4 words
## name = 4 names
## pseudoRandom = 16 characters
HIDDENFLAG="-hiddenUser" ## Set to empty for visible
FORCE="0" ## 1 (true) or 0 (false) - USE WITH EXTREME CAUTION!
## If true and old password is unknown or can't be changed,
## the script will delete the account and re-create it instead.
STOREREMOTE="" ## Set to 'Yes' below -IF- APIHASH is provided
STORELOCAL="" ## Set to 'Yes' below -IF- no APIHASH or overriden
LOCALPATH="/Library/Preferences"
LOCALPREFIX="tech.rocketman"
## Allow for overrides of everything so far...
## If the 11th policy parameter contains an equal sign, run eval on the
## whole thing.
## Example: If $11 is 'NUM=5;HIDDENFLAG=;FORCE=1;STORELOCAL="Yes"', then
## the values of the variables with the same name of those above would change.
## WARNING! This would be HORRIBLE security in a script that remains local
## as any bash-savvy user could inject whatever code they wanted to.
## This danger is LESSENED by the fact that the parameters are
## provided at run-time by Jamf and the script is not stored on
## the computer outside the policy run.
[[ "$11" == *"="* ]] && eval ${11} ## Comment out to disable
## Finalize storage options
if [ ${APIHASH} ]; then
STOREREMOTE="Yes"
APIURL=$(defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url)
SERIAL=$(system_profiler SPHardwareDataType | grep -i serial | grep system | awk '{print $NF}')
else
STORELOCAL="Yes"
fi
if [ $STORELOCAL ]; then
## The local file will use the same name but without anything but letters
## E.g. "Hidden Admin's Password" becomes "HiddenAdminsPassword"
ATTRABBR=$(echo ${EXTATTR} | tr -dc '[:alpha:]')
LOCALEA="${LOCALPATH}/${LOCALPREFIX}.${ATTRABBR}.plist"
fi
##
## Functions
##
function debugLog () {
if [[ ${DEBUG} ]]; then
message=$1
timestamp=$(date +'%H%M%S')
echo "${timestamp}: ${message}" >> /tmp/debug.log
fi
}
function createRandomPassword() {
system=$1
case "$system" in
nato) ## Using NATO Letters (e.g. WhiskeyTangoFoxtrot)
NUM=$([ ${NUM} ] && echo ${NUM} || echo "3")
NATO=(Alpha Bravo Charlie Delta Echo Foxtrot Golf Hotel India Juliet Kilo Lima Mike November Oscar Papa Quebec Romeo Sierra Tango Uniform Victor Whiskey Yankee Zulu)
MAX=${#NATO[@]}
NEWPASS=$(for u in $(jot -r ${NUM} 0 $((${MAX}-1)) ); do echo -n ${NATO[$u]} ; done)
;;
xkcd) ## Using the system from the XKCD webcomic (https://xkcd.com/936/)
NUM=$([ ${NUM} ] && echo ${NUM} || echo "4")
## Get words that are betwen 4 and 6 characters in length, ignoring proper nouns
MAX=$(awk '(length > 3 && length < 9 && /^[a-z]/)' /usr/share/dict/words | wc -l)
CHOICES=$(for u in $(jot -r ${NUM} 0 $((${MAX}-1)) ); do awk '(length > 3 && length < 7 && /^[a-z]/)' /usr/share/dict/words 2>/dev/null | tail +${u} 2>/dev/null | head -1 ; done)
NEWPASS=""
for word in ${CHOICES}; do
first=$(echo $word | cut -c1 | tr '[[:lower:]]' '[[:upper:]]')
rest=$(echo $word | cut -c2-)
NEWPASS=${NEWPASS}${first}${rest}
done
;;
wopr) ## Like the launch codes in the 80s movie "Wargames" (https://www.imdb.com/title/tt0086567)
## (Example "CPE 1704 TKS")
## Fun Fact - The odds of getting the same code as in the movie is roughtly three trillion to one.
PRE=$(jot -nrc -s '' 3 65 90)
NUM=$(jot -nr -s '' 4 0 9)
POST=$(jot -nrc -s '' 3 65 90)
NEWPASS="${PRE} ${NUM} ${POST}"
;;
names) ## Uses the same scheme as above but only with the propernames database
NUM=$([ ${NUM} ] && echo ${NUM} || echo "4")
MAX=$(wc -l /usr/share/dict/propernames | awk '{print $1}')
CHOICES=$(for u in $(jot -r ${NUM} 0 $((${MAX}-1)) ); do tail +${u} /usr/share/dict/propernames 2>/dev/null | head -1 ; done)
NEWPASS=$(echo "${CHOICES}" | tr -d "[:space:]" )
;;
pseudoRandom) ## Based on University of Nebraska' LAPS system (https://github.com/NU-ITS/LAPSforMac)
NUM=$([ ${NUM} ] && echo ${NUM} || echo "16")
## Remove Ambigious characters
NEWPASS=$(openssl rand -base64 100 | tr -d OoIi1lLS | head -c${NUM};echo)
;;
custom* | *) ## Adjustable scheme
## Example: "custom N=16;S=1;D=2;L=3;U=4"
## Defaults
N=16 # Password length
S=1 # Minimum special characters
U=1 # Minimum upper case
L=1 # Minimum lower case
D=1 # Minumum digits
## NOTE:
## If N < S+U+L+D, then N = S+U+L+D
## If N > S+U+L+D, then random characters from the ENTIRE range will be used to fill
## If there are overrides passed in, use them
INPUT=$(echo ${system} | awk '{print $2}')
eval ${INPUT}
## 33-126 - All the printable characters
## 48-57 - Digits
## 65-90 - Upper
## 97-122 - Lower
## Generate the minimums
UC=($([ ${U} -gt 0 ] && echo $(jot -r ${U} 65 90) || echo "")) ## Upper case
LC=($([ ${L} -gt 0 ] && echo $(jot -r ${L} 97 122) || echo "")) ## Lower Case
NC=($([ ${D} -gt 0 ] && echo $(jot -r ${D} 48 57) || echo "")) ## Digits
## Special characters
SN=()
if [ ${S} -gt 0 ]; then
SCNA=({33..47} {58..64} {91..96} {122..126})
for x in $(jot -r ${S} 0 ${#SCNA[@]}); do
SN+=(${SCNA[$x]})
done
fi
## Put the minimums together
ALL=(${UC[@]} ${LC[@]} ${NC[@]} ${SN[@]})
## How many more characters do we need
LO=$(($N-$S-$U-$L-$D))
## Pull any remaining characters from the whole set
if [[ $LO -gt 0 ]]; then
for x in $(jot -r $LO 33 126); do
ALL+=(${x})
done
fi
## Build the password by shuffling the bits
passArray=()
while [ ${#ALL[@]} -gt 0 ]; do
i=$(jot -r 1 0 $(( ${#ALL[@]}-1 )))
passArray+=(${ALL[$i]})
ALL=( ${ALL[@]/${ALL[$i]}} )
done
NEWPASS="$(printf '%x' ${passArray[@]} | xxd -r -p)"
;;
esac
echo ${NEWPASS}
}
function createBreakglassAdmin() {
## Using the built-in jamf tool which beats the old way which doesn't work
## across all OS versions the same way.
echo "Creating ${ADMINUSER}"
jamf createAccount \
-username ${USERNAME} \
-realname "${FULLNAME}" \
-password "${NEWPASS}" \
–home /private/var/${USERNAME} \
–shell “/bin/zsh” \
${HIDDENFLAG} \
-admin \
-suppressSetupAssistant
}
function changePassword() {
## Delete keychain if present
rm -f "~${USERNAME}/Library/Keychains/login.keychain"
## Change password
jamf changePassword -username ${USERNAME} -oldPassword "${OLDPASS}" -password "${NEWPASS}"
## If we are forcing the issue
if [[ $? -ne 0 ]]; then ## Error
echo "ERROR: $?" >> /tmp/debug.log
if [[ ${FORCE} ]]; then
echo "Delete and recreate"
jamf deleteAccount -username ${USERNAME} -deleteHomeDirectory
createBreakglassAdmin
else
## Log it
NEWPASS="EXCEPTION - Password change failed: $?"
fi
fi
}
function getCurrentPassword() {
if [[ $STOREREMOTE ]]; then
## Get the password through the API
CURRENTPASS=$( \
curl -ks \
-H "Authorization: Basic ${APIHASH}" \
-H "Accept: text/xml" \
${APIURL}JSSResource/computers/serialnumber/${SERIAL}/subset/extension_attributes \
| xmllint --xpath "//*[name='${EXTATTR}']/value/text()" - \
2>/dev/null \
)
elif [[ ${STORELOCAL} ]]; then
if [[ -f "${LOCALEA}" ]]; then
CURRENTPASS=$(defaults read "${LOCALEA}" Password 2>/dev/null)
else
CURRENTPASS="EXCEPTION - Local attribute requested but not found"
fi
else
CURRENTPASS="EXCEPTION - No storage method selected" ## This -should- never happen
fi
## Pass it back
echo $CURRENTPASS
}
function storeCurrentPassword() {
if [[ ${STORELOCAL} ]]; then
## If the file doesn't exist, make it and secure it
if [[ ! -f "${LOCALEA}" ]]; then
touch "${LOCALEA}"
chown root:wheel "${LOCALEA}"
chmod 600 "${LOCALEA}"
fi
## Store the password locally for pickup by Recon
/usr/bin/defaults write "${LOCALEA}" Password -string "${NEWPASS}"
fi
if [[ ${STOREREMOTE} ]]; then
# Store the password in Jamf
XML="<computer><extension_attributes><extension_attribute><name>${EXTATTR}</name><value>${NEWPASS}</value></extension_attribute></extension_attributes></computer>"
debugLog "XML: ${XML}"
curl -sk \
-H "Authorization: Basic ${APIHASH}" \
-H "Content-type: application/xml" \
"${APIURL}JSSResource/computers/serialnumber/${SERIAL}" \
-X PUT \
-d "${XML}"
fi
}
##
## Main Script
##
## See if the user exists
EXISTS=$(id ${USERNAME} 2>/dev/null | wc -l | awk '{print $NF}')
## Either way, we'll need a random password
NEWPASS=$(createRandomPassword ${PASSMODE})
debugLog "NewPass: ${NEWPASS}"
## Are we creating the user or changing their password
if [[ $EXISTS -gt 0 ]]; then
debugLog "Exists: Changing"
## Get the existing password
OLDPASS=$(getCurrentPassword)
debugLog "Old: ${OLDPASS}"
## Exception Block
## This was added to handle the computers that had an account prior to enrollment.
## To change a password, we need to know the old one. If there is an issue storing
## or retreiving the password, the issue will be stored for reporting and mitigation.
##
## ADDITIONAL NOTE: If the record for any previous computer is updated with the correct password
## this script will run normally next time and update with a random password
case ${OLDPASS} in
## No password found
"")
debugLog "Old password unknown - create exception"
## The account was created before and is unknown
NEWPASS="EXCEPTION - Unknown password"
;;
## If a previous run had an issue, the 'password' was logged as an 'EXCEPTION'.
EXCEPTION*)
debugLog "Previous exception - ${OLDPASS}"
if [[ ${FORCE} ]]; then
## Request a password change with known bad data to trigger refresh
OLDPASS="NULL"
changePassword
else ## Previous error not resolved. Re-asserting.
NEWPASS=${OLDPASS}
fi
;;
## All is well. Moving on.
*)
debugLog "Changing from ${OLDPASS} to ${NEWPASS}"
## Change the password
changePassword
;;
esac
## End exception block
else ## User does not exist. We are creating it.
## Create the account
debugLog "Creating new admin."
## Create the user
createBreakglassAdmin
fi
## Store the new password
storeCurrentPassword
## Dump and clear the debug log
if [[ -f /tmp/debug.log ]]; then
echo $(cat /tmp/debug.log)
rm /tmp/debug.log
fi
exit 0