We all know that the battle between sysadmins and power users with admin privileges on the system, is probably a battle which can not be won. At least, not on a technical level. Best strategy, in my opinion, remains to educate people to handle admin privileges with care and accept the high responsibility it entails.
MDM and Mac supervision gives us a wide variety of restrictions we can enforce, however, admins stay admins and there will always be ways to work around some of the restrictions you enforce on your systems. This means that, in my opinion, the only efficient strategy is to create a solid use policy for corporate devices, and link it to HR policies and disciplinary actions for those who do not follow them. Monitoring (and I really mean monitoring, not spying or infringing on privacy), is in my opinion key in this strategy.
That said, I never run away from battles or challenges, in the tech realm that is, so I still wanted to explore the technical possibilities to engage in the fight with power users on the Mac. At least from a monitoring perspective. The main things I’d like to monitor are those computers where the end user has admin privileges, and on a higher level, which user accounts exist on which system.
This may sound as a straight forward thing to do right? Just run some queries to list all local users, and check who is admin. Done. But are you sure? Are you checking the admin privileges the right way? Same goes for the task to list ‘all local end users’!. Let’s have a look!
First of all, let’s focus on our admin accounts. Those are the most important to keep an eye on anyway.
Let’s start with having a look at the built-in Administrators group:
dscl . read /Groups/admin GroupMembership
As you can see the group has the following members:
- jamfadmin (my Jamf Management Account or Managed Admin)
- ttg (my user account which I created during the Setup Assistant)
So, that’s easy right? I now have a list of all the admin accounts on my Mac!
<Problem solved - end of blogpost >
Well, not really! Let’s go through the following exercise:
- Create a standard account on the Mac
- Check the admin group again
- Promote it to admin
- Check the admin group again
- Remove the user from the admin group
- Check System Preferences or try some sudo commands…
As expected, the user is standard, and does not show up as member of the list. Now let’s promote it to Admin. Ok I could just tick the box for ‘Allow user to administer this computer’, but let’s use command line for this as I’ll come back to this specific type of commands later!
dscl . -append /groups/admin GroupMembership otherstandard
Now, both dscl as the Directory Utility app nicely show our new user as member of the admin group, and so does System Preferences:
But let us now do the opposite, and instead of adding the user to the admin group, let’s delete it’s membership again…
dscl . -delete /groups/admin GroupMembership otherstandard
So far all is good. Appending the user to the GroupMembership of the admin group makes it an admin, deleting it demotes it back to standard. BUT, here we are actually changing only one attribute in the directory, the GroupMembership. The problem however is that we are also only querying that single attribute via dscl. System Preferences on the other hand, actually checks multiple things when displaying the admin/standard privileges (and ticking the Allow user to administer this computer checkbox). Nevertheless, in this scenario everything matches our expectations.
Let’s now promote our otherstandard account again, but via the System Preferences instead, by checkin the Allow user to administer this computer checkbox again.
Looks good right? Yeah, looks good to me. But let’s say at some point we don’t like this user to be an admin anymore, so let’s quickly demote it again via command line. An easy oneliner, remotely executed with root privileges in a Jamf Pro policy if you want,… done.
dscl . -delete /groups/admin GroupMembership otherstandard
Just like before, we check our dscl command again and see… ok, the account is gone from the GroupMembership again. All is fine…
But are we sure this account is not an admin anymore? How comes the user is still able to do sudo command then? A sudo jamf recon for instance…
And yes, System Preferences confirms… otherstandard IS STILL ADMIN!
How did this happen? Well, it’s all about the fact that there are multiple ways a user can be a member of a group:
- The accountname listed in the GroupMembership
- Via NestedGroups
- Group is set as PrimaryGroupID
- The UUID listed in the GroupMembers
If we look at the last steps we did above:
- We promoted the account via the Allow user to administer this computer checkmark in System Preferences
- We deleted the account from GroupMembership via dscl
If we have a look at the group in Directory Utility, we can indeed see that the account is not listed in GroupMembership anymore, but we do see 4 UUID’s instead of 3 under GroupMembers:
And the last one in the list is indeed our otherstandard account…
… which is indeed admin according to the System Preferences, which has the Allow user to administer this computer still ticked!
The reason for the above behaviour, is that System Preferences actually adds both the UUID as the ACCOUNT NAME to the group, respectively to the GroupMembership and GroupMembers.
When we untick the Allow user to administer this computer, it also removes BOTH:
The accountname is not listed anymore, and the UUID is also deleted:
However, when we delete the GroupMembership, dscl only does what you ask it to do, and it does not delete the UUID from the GroupMembers. Hence, in view of the ways a user can be member of a group (discussed above), the user still remains admin if the UUID was added via another way before (like via System Preferences).
Note: this means that the following commmand is NOT a garantuee to list all admin accounts on a system!
dscl . read /Groups/admin GroupMembership
Now, let’s take it one step further, and check PrimaryGroup. The ID of the admin group is 80. So let’s see what happens if we change the PrimaryGroup of a standard user to 80.
Changing the PrimaryGroupID of an account to 80, makes it an admin. Our dscl command to check GroupMembership (or GroupMembers if we want to list UUIDs) does NOT pick it up, and what’s more: the Allow user to administer this computer is indeed ticked, but greyed out!
Note: the default group of an account is Staff, with PrimaryGroupID 20, and worth nothing is that when querying this group or checking Directory Utility, we only see root as member. Hence another reason to conclude that querying GroupMembership (or even GroupMembers) is not a good way to list members!
So, what does this learn us? Well, that we have to be careful in how we check if a user is admin or not.
While some organisations limit their end users to standard accounts, others allow them to be admins, and many configurations can depend on the actual username being logged in, there may be situations where users are creating additional admin accounts on their system to find creative ways to get around a certain roadblock or restriction you’ve put in place. Users using other accounts to log into the system may mess up your scripts, reports, profile variables, etc…
Furthermore, some organisations have some kind of tool in place to ‘temporarily elevate standard users to admin’, such as MakeMeAdmin, Privileges, etc… Here we have to be careful that the tool (or you via additional scripting) has something in place which checks if the user does not abuse the temporary power of admin privileges to create another admin account on the system… making the temporary solution pointless.
This all means that you really need to monitor which accounts exist on the system, and which accounts really have admin privileges! Looking at all the above, we know that querying the GroupMembership of the admin group is not enough, and that we would need to check the UUID via GroupMembers and the PrimaryGroupID as well.
Now, that all said, how do we correctly list all admin accounts on the system? There are already so many scripts going around in the community, so this may feel like trying to re-invent the wheel. However, many scripts or oneliners ignore some of the possible manipulations which endusers may be doing to bypass the script. Based on all the above, for instance:
- change the PrimaryGroupID of the admin account
- removing the account from GroupMembership
- changing the UID of the account to something below 500 (many scripts assume the end user has an account with UID above 500 or even higher when using mobile accounts)
- add an underscore in front of the account to work around scripts that assume those are system/service accounts
This means that, taking all the above into consideration, you either need to add multiple checks if you are checking from a groups to user membership point of view (downwards), OR, you check from a users point of view and check if it is member of the admin group (upwards).
To check if a user is a member of the admin group (upwards), we can use dsmemberutil or dseditgroup
dsmemberutil checkmembership -U otherstandard -G admin
dseditgroup -o checkmember -m otherstandard admin
This ensures that the check includes GroupMembership, GroupMembers, PrimaryGroupID in the assessment.
Same goes for manipulating admin privileges. As we saw above we can add/delete users to the admin group via the dscl command with the append/delete flag. I realise that I’m doing this in my script to manage SecureTokens, which does indeed need some clean up and logic enhancement. But in this case the logic is actually ok in my opinion, as I’m checking membership via dseditgroup in the script (and not with dscl). This ensures me that the account is really not an admin via of the above mentioned ways (GroupMembership, GroupMembers, PrimaryGroupID), so adding the account to GroupMembership and removing it again should be ok.
Nevertheless, a better way to manipulate admin privileges is using dseditgroup:
dseditgroup -o edit -a otherstandard -t user admin
dseditgroup -o edit -d otherstandard -t user admin
Using dseditgroup, you are sure to add/delete both the username as the UUID from, respectively, GroupMembership and GroupMembers.
This all said, there is one big consequence of querying the system upwards (check if user is a member of the group instead of checking if the user is listed in the membership), and that is that we need to know all local accounts on the system.
This may sound like an easy thing to do as well right? Jamf Pro already has an inventory field listing local accounts? No?
Sure, but look at what happens when I do this:
Indeed, it only checks for accounts with UID above 500, just like many scripts going around in the community.
In this specific situation I have a user account called otherstandard, which does not show up in the inventory, and which has admin privileges set via its PrimaryGroupID 80.
So, after reading all this, are you sure you are reporting the existing local accounts and admin users correctly across your fleet of Macs? Depending which script you created, or like I do a lot: copy-pasted from the Internet, you may want to check that again!
Note: As mentioned before, some users may add an underscore or hide the account to mimic service/system accounts. That's ok for the Jamf Pro inventory as they are reported correctly (based on UID), however, some scripts may be fooled by that too... so review your scripts and check the logic they use to list user accounts!
As I mentioned, I don’t want to re-invent the wheel, but many scrips I’ve come across do not consider all the caveats listed in this post. Hence I’ve been playing around, mainly to brush up my scripting skills and have some geek time, in order to find another logic catching all the creative ways power users may come up with to walk around the trap.
Don’t take the script into productions, as it’s a work in progress and like I said an attempt to use another strategy and logic to really list all local user accounts. But please have a look and let me know whatever feedback you have: https://github.com/TravellingTechGuy/checkMultipleAccounts
More than happy to accept any comments, suggestions about wrong assumptions, bad practise, mistakes or anything you could think of when looking at the logic. Always happy to learn!
I made a script which calls a custom Jamf Pro policy trigger, an extension attribute listing all local accounts and another one providing the actual number of accounts.
This can be used to take action on Macs which have unauthorised accounts present… for instance: issue a device lock 🙂
DO NOT put it in production… especially not in combination with the device lock API script… I don’t want you to lock your entire fleet of Macs if the logic in the script checking local accounts has false positives!
Final note: Maybe I'm jut overcomplicating things, so I'm waiting for that person throwing a oneliner at me really achieving the same thing. However, I like to look at things from another perspective from time to time and learn by testing things. So please hit me up with any comments or remarks you have. That's how we learn things, no?
That’s it! As always, if you liked the post, hit the like button, tell your friends about it and leave a comment down below!