29 November 2017

Why Gets You Root

Patrick Wardle

> tracking down the cause of a serious authentication flaw

Aloha it’s Patrick, Chief Security Researcher at Synack. In my free time, I also run a small OS X security website objective-see.com, where I share my personal OS X security tools and blog about OS X security and coding topics. Below is one such post originally published on my site…Read and enjoy!


In case you haven’t heard the news, there is a massive security flaw in macOS, which allows anybody to log into the root account with a blank password. The flaw was discovered by Lemi Orhan Ergin (@lemiorhan):

I was intrigued by this bug, so decided to track down its root cause! That is to say, what is the underlying reason for the bug?

Digging Deeper
First, let’s look at what’s happening at a high level. When a user (or attacker) attempts to log into an account that is not currently enabled (i.e. root), the system will create that account with whatever password the user specifies…even if that password is blank. This is why to perform this attack via the UI, you have to click on ‘Unlock’ twice:

Diving in, let’s see what’s going on behind the scenes.

When a user (or an attacker) tries to authenticate to such an account this is handled by the opendirectory daemon (opendirectoryd). Due to time constraints, we’re going to jump forward a bit to show an informative callstack:

Upon receiving a mach XPC message, opendirectoryd invokes the odm_RecordVerifyPassword method, which in turn calls od_verify_crypt_password. The odm_RecordVerifyPassword method is implemented in the PlistFile binary. This is a bundle that is loaded from /System/Library/OpenDirectory/Modules/PlistFile.bundle into opendirectoryd.

Starting with the odm_RecordVerifyPassword function, it invokes an unnamed method, ‘sub_826b’. This subroutine first invokes another helper function, ‘sub_826b’, to “read shadowhash data from” from the account that the user (or attacker) is trying to log in to. For enabled accounts (such as the user account) this read will succeed as this data exists. Note: this data can be viewed from the terminal via the dscl . -read /Users/<user> command or directly by reading it from /private/var/db/dslocal/nodes/Default/users/<user>.

For disabled accounts, (such as root account that is being targeted), this information is not present, so this function will fail! This is important 🙂

When hash/shadow hash information is not found, an ‘else’ clause is executed:

After reading in the password, it invokes the od_verify_crypt_password function to verify that the password passed in by the user (or attacker) matches that password for the account. So for example if we try to log into the (disabled) root account with ‘hunter2’, od_verify_crypt_password is invoke with ‘*’ and ‘hunter2’:

If we step over the call, it returns 0x1 in al….interesting!

Since a non-zero value was returned, execution continues with a call to various methods such as sub_13d00. As the debug log statments in the decompilation show, these will perform an upgrade from a crypt password to a shadowhash or securetoken:

“found crypt password in user-record – upgrading to shadowhash or securetoken”

However, if we look at what these ‘upgrade’ subroutines are called with, it’s with the password we provided (i.e. ‘hunter2’)!

This password is then saved for the account (i.e. for root), and thus the user (or attacker) can log in! #fail

Let’s recap:

  • For accounts that are disabled (i.e. don’t have ‘shadowhash’ data), macOS will attempt to perform an upgrade
  • During this upgrade, od_verify_crypt_password returns a non-zero value
  • The user (or attacked) specified password is then ‘upgraded’ and saved for the account

Obviously od_verify_crypt_password should fail (maybe it does and the check on the return code for 0x0 is just inverted?). I’ll continue to dig into this 🙂