Identifying #iamroot Issues With Osquery: Blank Password Vuln in MacOS 10.13.1

Blog Author
Doug Wilson

Update: Following this article's original publication, Apple released a somewhat confusing set of security updates, which invalidates some of the original content I had shared. I have posted a follow-up here and updated the version number in the determination query in this article.

Tuesday’s event of a vulnerability in macOS High Sierra (tagged #iamroot by some) was a great chance to explore the utility of using osquery in response to a previously unknown security threat. [See this post for other macos malware identification tips]

If you are not familiar with it, osquery is a tool developed and open-sourced by Facebook for querying endpoints for security and DevOps purposes. osquery has since spawned a growing community, and is one of the most active open-source security projects on github.

TL;DR for those seeking immediate help

 

Using osquery to investigate

Installing osquery and getting it running are outside the scope of this blog post. If you have osquery installed, you can tell the os version of your macOS computers through a simple query of the os version table in osquery:

osquery> select * from os_version;
|  name     | version | major | minor | patch | build | platform |
| Mac OS X | 10.13.1 | 10 | 13 | 1 | 17B48 | darwin |

A build version for 10.13.1 that is not “17B1002” is a computer that is potentially
vulnerable if manual mitigation steps have not been taken. (n.b. osquery still refers to all versions of macOS in the 10.x family as OS X)

UPDATE — Apple has released a second version of Security Update 2017–001, and the correct build number is now 17B1003 (note the last digit change) for the purposes of this exercise.

In a distributed fleet management system for osquery (such as Uptycs, or other solutions), a query along the lines of

select * from os_version where major='10' and minor = '13' and patch = '1' and build <> '17B1002'

deployed against all of your macOS endpoints would yield a report of all the machines that still need to be patched.

With the patch out now, the most direct method to a solution is to apply that patch. osquery can be used to see what remaining computers in your enterprise still need to be addressed. But Tuesday, before the patch went out, it was interesting seeing how we could use the flexibility of osquery to find out the information we needed to identify vulnerable machines, as well as see if manual mitigation had already taken place either by proactive users or IT staff.

The longer version of the story

After the news broke on twitter about the vulnerability, the osquery community developed solutions together in the osquery slack team that allowed osquery users to determine if manual mitigation had already taken place or machines were vulnerable within a short time of the discovery. This was well before a patch was even available.

After the initial discovery of the vulnerability, many pointed out a quick work around: enable the root account and set a password on it before an attacker did. This is something users were doing ad hoc, IT and security teams might want to do, but either way it was something you’d want to be able to track and see the status of in your organization.

At first glance the problem and workaround appear to be something outside the scope of osquery. When I was trying to look at the root account on my own Mac, I didn’t see what I felt I needed to try to investigate this issue (n.b. several of the query results following are truncated for clarity, also, I was running osqueryi using sudo to get the query results below):

osquery> select * from users where username = ‘root’;
| uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid |
| 0 | 0 | 0 | 0 | root | System Administrator | /var/root | /bin/sh | <truncated> |
Looking at this output, there’s nothing here about if the user is disabled, or if the password is set. Is it even possible for us to proceed? Part of the power of osquery is its extreme flexibility, and the fact that it can query various configuration files as well as directly querying settings on macOS. So all hope is not lost, we just have to get creative.

\If you use the macOS Directory Services command line utility (dscl) you can get the information if you know where and how to look.

The command

sudo dscl . -read /Users/root Password

will give the following outputs:

Root account set to disabled gives

Password: *

Root account set to enabled gives

Password: ********


You can also use this same tool to see when the password has last been set (which is important for this issue, as if it has never been set and you are on 10.13.1 and unpatched, you are at risk of an attacker being able to log in as root with no credentials).

sudo dscl . -read /Users/root accountPolicyData

yields something like this if the password has not been set

dsAttrTypeNative:accountPolicyData:
<?xml version=”1.0" encoding=”UTF-8"?>
<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=”1.0">
<dict>
<key>creationTime</key>
<real>1477541088.3076141</real>
<key>failedLoginCount</key>
<integer>0</integer>
<key>failedLoginTimestamp</key>
<integer>0</integer>
</dict>
</plist>
If the password has been set, you will get something like
dsAttrTypeNative:accountPolicyData:
<?xml version=”1.0" encoding=”UTF-8"?>
<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=”1.0">
<dict>
<key>creationTime</key>
<real>1477541088.3076141</real>
<key>failedLoginCount</key>
<integer>0</integer>
<key>failedLoginTimestamp</key>
<integer>0</integer>
<key>passwordLastSetTime</key>
<real>1511906347.545491</real>
</dict>
</plist>


Note the additional key of passwordLastSetTime in the dict for the account with the password set.

So, how to get this in osquery? Surely we don’t have the time and resources to write a new table for osquery while responding to this issue? Of course not — but we also don’t have to!

osquery has the ability to query configuration files on macOS known as blists, or property lists. These often store configuration information for users and programs on macOS. A little detective work on google finds out that the directory service settings are indeed stored in user plists!

The plist query in osquery requires a target path, or it will not run, because it needs to know which particular plist you are querying. If we query the plist table in osquery and supply the path of the file in question

osquery> select * from plist where path = ‘/var/db/dslocal/nodes/Default/users/root.plist’;

we will see a ton of data. plists are mainly key/value pairs. I’m not going to show all of the output of that command here, as it’s a lot of screen space. Let’s try to clean that up a bit first by just looking at the keys available in this plist, rather than all the values.

osquery> select key from plist where path = ‘/var/db/dslocal/nodes/Default/users/root.plist’;
+ — — — — — — — — — — — -+
| key |
+ — — — — — — — — — — — -+
| shell |
| HeimdalSRPKey |
| uid |
| accountPolicyData |
| smb_sid |
| realname |
| generateduid |
| gid |
| home |
| home |
| passwd |
| _writers_passwd |
| record_daemon_version |
| name |
| name |
+ — — — — — — — — — — — -+

OK — that’s interesting. Some of those keys look very much like the same information we were getting from the dscl utility above! Let’s dig in on them and see what we get

osquery> select key, value, subkey from plist where path = ‘/var/db/dslocal/nodes/Default/users/root.plist’ and key = ‘passwd’;
+ — — — — + — — — -+ — — — — +
| key | value | subkey |
+ — — — — + — — — -+ — — — — +
| passwd | * | |
+ — — — — + — — — -+ — — — — +

well, hey, that looks familiar! This is the same key/value we saw from the dscl tool.

We can now see the passwd value, which will tell us if the root account is enabled or not. If we can also see if the password has a set date (which we got above from accountPolicyData) then we are in business!

Let’s try that (abbreviating output a bit for clarity):

osquery> select key, valuefrom plist where path =
‘/var/db/dslocal/nodes/Default/users/root.plist’ and key
= ‘accountPolicyData’;
| key               | value |
| accountPolicyData | PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGl
uZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBw
bGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb
20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9
uPSIxLjAiPgo8ZGljdD4KCTxrZXk+Y3JlYXRpb25UaW1lPC9rZXk+Cgk8c
mVhbD4xNDc3NTQxMDg4LjMwNzYxNDE8L3JlYWw+Cgk8a2V5PmZhaWxlZExv
Z2luQ291bnQ8L2tleT4KCTxpbnRlZ2VyPjA8L2ludGVnZXI+Cgk8a2V5PmZ
haWxlZExvZ2luVGltZXN0YW1wPC9rZXk+Cgk8aW50ZWdlcj4wPC9pbnRlZ2
VyPgoJPGtleT5wYXNzd29yZExhc3RTZXRUaW1lPC9rZXk+Cgk8cmVhbD4xN
TExOTE3NDI0LjY3MzUwNzwvcmVhbD4KPC9kaWN0Pgo8L3BsaXN0Pgo= | |

Woah. That’s a little crazy. Quick experimentation shows that this is base64 encoded, and sure enough, it gives us the data we need if decode it.

osquery> select from_base64(value) from plist where path = ‘/var/db/dslocal/nodes/Default/users/root.plist’ and key = ‘accountPolicyData’;

| from_base64(value) |

| <?xml version=”1.0" encoding=”UTF-8"?>
<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=”1.0">
<dict>
<key>creationTime</key>
<real>1477541088.3076141</real>
<key>failedLoginCount</key>
<integer>0</integer>
<key>failedLoginTimestamp</key>
<integer>0</integer>
<key>passwordLastSetTime</key>
<real>1511917424.673507</real>
</dict>
</plist>|

So, it turns out that by pointing osquery at plist files, we can determine most of the same information we got from the dscl utility, when we combine that with knowing where to look.

Pulling it all together

I would like to give a shout out to Teddy Reed, who currently leads the osquery efforts at Facebook for contributing his considerable SQL chops on this. He pulled all the parts of the puzzle together into a single query on Tuesday faster (and slicker) than I could. Most of my “final answer” is derived from a query he offered up in the osquery slack team. I’ve added a few tweaks to it, but most of what follows next was his doing.

This final query identifies machines that might be at risk (by looking at the os version and the build number), and then combines that with looking at the status of the accounts on that machine, showing if those account are enabled or disabled, and if they have a password set or not. This lets you see what machines might be vulnerable, and if a manual mitigation has already been attempted or not.

select os_version.version, os_version.build, u.username as account_username, case when from_base64(p2.value) like '%passwordLastSet%' then 1 else 0 end as password_set, case when p1.value = '*' then 0 else 1 end as account_enabled, u.shell from plist p1, plist p2, os_version, (select * from users where directory != '/var/empty') u where (p1.path = '/var/db/dslocal/nodes/Default/users/' || username || '.plist' and p1.key = 'passwd') and (p2.path = '/var/db/dslocal/nodes/Default/users/' || username || '.plist' and p2.key = 'accountPolicyData') and os_version.version = '10.13.1' and os_version.build <> '17B1002';

This will give you output along the lines of (edited slightly to fit the screen)

+ — — — — + — — — + — — — — — — + — — — — — — — + — — — — — — — - +
| version | build | username | password_set | account_enabled |
| 10.13.1 | 17B48 | doug-test | 1 | 1 |
| 10.13.1 | 17B48 | root | 1 | 1 |
| 10.13.1 | 17B48 | testuser123 | 1 | 1 |
+ — — — — + — — — + — — — — — — + — — — — — — — + — — — — — — — - +

In this case, the os version is vulnerable, BUT the manual mitigation had been put into place.

If you want to only view the root account in the above output, you can constrain the query by adding another condition ( (select * from users where directory != '/var/empty' and username = 'root') ) . That’s the beauty of working in SQL — you can easily change the query for your needs.

n.b. We did discover that some accounts (if they are old enough) may show not having a password set just as root did not. The explanation for this is in Patrick Wardle’s article above, but basically if an account did not have a shadow hash value computed, it would appear to not have a password set (even though it does). Updating the password resolves this. I also did not update the query to be “pretty” as I wanted folks to be able to just cut/paste without potentially introducing formatting errors.

UPDATE: In the additional fixes that Apple released, they have not only re-disabled root, but have removed the accountPolicyData attribute from the root plist file if it was set. Thus root will not appear in the above query after you have applied both versions of the Security Update 2017–001 patch. Please see my follow-on article if you are still troubleshooting this issue using osquery.

In Conclusion

osquery shows again how it is a powerful and flexible open-source tool. oquery allows you to ask questions and get answers, and make determinations and take action without having to wait on a vendor or a patch.

Once the patch is published, osquery continues to still be useful in checking on the status of your machines and being able to see where users may have taken action themselves.

In this post, we saw that even though there was not a existing osquery table dedicated to those particular user attributes we cared about, we were able to find out the information we needed due to the flexibility of osquery and being able to write our own SQL queries.

Taking that knowledge and combining it with a distributed system to query your endpoints (such as Uptycs or another fleet manager of your choice) allows you to quickly develop a solution to poll your enterprise for a unique and previously unseen threat. With osquery, your staff can be well on the way to tracking down vulnerable machines and mitigating long before the vendor releases a fix (even when they release it in less than 24 hours!).

Bonus round

For IT or security staff out there who thought “Are my users sharing out their desktops remotely??” during this event, you can also see if any of your mac users have desktop sharing (or other types of sharing) enabled very easily if you are using osquery. This is a built in table, and a simple query lets you see the settings equivalent to how they are in the system preference panel:

osquery> .mode line             <-- makes display easier
osquery> select * from sharing_preferences;
screen_sharing = 0
file_sharing = 0
printer_sharing = 0
remote_login = 0
remote_management = 0
remote_apple_events = 0
internet_sharing = 0
bluetooth_sharing = 0
disc_sharing = 0

Thanks for reading. If you have comments about this article, or questions about osquery, drop me a line!