Bypassing WIA on ADFS

Posted by

As most people who have ever set up Active Directory Federation Services (AD FS) before know, you can enable a feature called Browser SSO which allows clients to seamlessly log on using Kerberos without user interaction. In Windows terms, this is known as Integrated Authentication, Windows Integrated Authentication (WIA), or Integrated Windows Authentication (IWA). Which term you use is not important, but they are almost always used interchangeably, even by Microsoft themselves. In this post, I will be using WIA.

The requirements for WIA in AD FS are the following:

  • The user must be logged in with a domain account
  • The user’s browser must support Integrated Authentication
  • The client must resolve the federation service name to the internal AD FS servers
  • AD FS must have the BrowserSsoEnabled property set to True
  • The WIASupportedUserAgents property in AD FS must include the User Agent of the client’s browser
  • The Federation Service Name must be present as a Service Principal Name on the service accounts that runs AD FS

To check whether you have configured AD FS for WIA, you can run the following (rather convoluted) command in PowerShell on the federation server:

Get-AdfsProperties | Select-Object BrowserSsoEnabled, @{N="WIASupportedUserAgents";E={$_.WIASupportedUserAgents -join "`n"}} | Format-Table -Wrap -AutoSize
The output should look something like this

By default, AD FS supports WIA for most versions of Internet Explorer and Edge. To enable it for Mozilla Firefox and in some cases Chrome, you need to modify the WIASupportedUserAgents to include the User Agent strings used by those browsers.

Now let’s say you have an SAML-enabled application that authenticates users through AD FS, for instance outlook.com. So a user browses to outlook.com in their browser:

Clicking the Sign in button forwards the user to login.live.com, where the user is asked to enter their e-mail address. Now this might not seem like SSO, but is simply because Microsoft cannot figure out how to authenticate you otherwise. Specifically for outlook.com, you can instead browse to outlook.com/<yourdomain>, and it will redirect you to your AD FS farm if you have federation enabled for Office 365.

Actually, it is enough to enter x@domain.com. It only cares about the domain part to redirect you to the correct authentication service.

After a few redirects, you’re logged in!

The odd exception

Recently, I got a request from a customer on how to bypass WIA in AD FS, but for a single client/user. Normally, this is not possible granted that the clients and users are somewhat homogeneous, as AD FS does not distinguish between them. There are a few use cases for this:

  1. Shared/kiosk computers where a service account or equivalent is used to log on to the operating system
  2. When you need to authenticate to an application with an administrative user, but you’re logged on to the client with an unprivileged user (context switching)

Now, if we try this, we’d either be confronted by an AD FS error, or a logon prompt:

Pesky logon prompts

So how can we get redirected to the Forms-Based Authentication endpoint instead of the WIA endpoint? Well, as I wrote in the beginning, one of the criteria for WIA is that the client that you access AD FS from resolves the federation service name (fs.contoso.com, for instance) to the internal AD FS farm. So, if your organization utilizes Web Application Proxy (WAP) servers for external access to AD FS (most do), one solution could be to fool the client that it is outside the internal network. WIA is always disabled when you connect through WAP servers, and authentication will default to FBA.

Easy enough, just modify the C:\Windows\System32\drivers\etc\hosts file to point your federation service name to the external IP:

And presto, we’re now presented with FBA! Problem solved… right?

We did it!

A more… scalable solution

So most administrators shudder with disgust everytime someone mentions the hosts file. One of the reasons is that it doesn’t scale very well, and if forgotten it is a pain when troubleshooting. You can’t distribute it with a GPO, for fear that there are other critical entries in it on certain machines, and overwriting it would be bad. Writing scripts to inject new records in it… well, if you want to spend every single workday with a migraine, go ahead. It is also not possible to modify the hosts file to affect only a single user, so if any other users on the same client still needs SSO, you’re out of luck. There is a reason DNS was invented, and the hosts file should be left for temporary development and test purposes.

So is there another way to force AD FS to redirect a client to the FBA endpoint? Well, we could look at the WIASupportedUserAgents property. A client must send one of the accepted WIA User Agent strings to AD FS to be considered for the WIA endpoint. So what if we can modify it to something not in the list? We can’t remove any entries in the list, as SSO would stop working for everyone else, so our best bet is to look into the client itself.

As it turns out, for Internet Explorer, there is a Group Policy setting that allows you to do just this:

User Configuration \ Administrative Templates \ Windows Components \ Internet Explorer -> Customize user agent string

So what if we change it?

Ok, what’s the result?

Nope, didn’t work

Crap. Instead of FBA we’re redirected back to the Office 365 logon page, but you might get an error message instead. I haven’t looked at the HTTP headers but I figure that the GPO itself doesn’t actually replace the user agent with what you specify, instead adding to it. Hmm…

Well, fortunately AD FS is built on top of the .NET Framework, which means its code is Intermediate Language (IL), and IL code can be decompiled! So I brought out my trusty ILSpy and started digging into the DLL’s of AD FS.

Surely enough, in the Microsoft.IdentityServer.Web.dll assembly I stumbled upon something that looked suspicious:

Now what is this?

Let’s have a look at the code in this method, shall we?

public bool CanPerformIntegratedAuthentication()
{
	bool flag = false;
	foreach (string wIASupportedUserAgent in ServiceState.Current.DefaultAuthenticationPolicy.WIASupportedUserAgents)
	{
		if (string.IsNullOrEmpty(wIASupportedUserAgent))
		{
			continue;
		}
		if (FarmInformation.Current.BehaviourLevel >= FarmBehavior.Threshold && wIASupportedUserAgent.StartsWith(ConditionOperator.REGEXP_MATCH, StringComparison.OrdinalIgnoreCase))
		{
			if (!flag && UserAgentRegexMatch(wIASupportedUserAgent.Substring(ConditionOperator.REGEXP_MATCH.Length, wIASupportedUserAgent.Length - ConditionOperator.REGEXP_MATCH.Length)))
			{
				flag = true;
			}
		}
		else if (wIASupportedUserAgent.IndexOf("MSAuthHost/", StringComparison.OrdinalIgnoreCase) != -1)
		{
			int num = UserAgent.IndexOf("MSAuthHost/", StringComparison.OrdinalIgnoreCase);
			if (num != -1 && wIASupportedUserAgent.IndexOf("/In-Domain", StringComparison.OrdinalIgnoreCase) != -1)
			{
				if (UserAgent.IndexOf("/In-Domain", num, StringComparison.OrdinalIgnoreCase) != -1)
				{
					return true;
				}
				return false;
			}
		}
		else if (UserAgent.IndexOf(wIASupportedUserAgent, StringComparison.OrdinalIgnoreCase) >= 0)
		{
			return true;
		}
	}
	return flag;
}

So it looks like that the method compares the client’s user agent string with each supported user agent in AD FS, as defined by the WIASupportedUserAgents property, after ensuring that the value in AD FS is not null or empty.

if (FarmInformation.Current.BehaviourLevel >= FarmBehavior.Threshold && wIASupportedUserAgent.StartsWith(ConditionOperator.REGEXP_MATCH, StringComparison.OrdinalIgnoreCase))
{
	if (!flag && UserAgentRegexMatch(wIASupportedUserAgent.Substring(ConditionOperator.REGEXP_MATCH.Length, wIASupportedUserAgent.Length - ConditionOperator.REGEXP_MATCH.Length)))
	{
		flag = true;
	}
}

So the first part here is a special case for values in WIASupportedUserAgents that start with “=~”. It doesn’t ever set the flag to false, though, so it’s not useful for us.

else if (wIASupportedUserAgent.IndexOf("MSAuthHost/", StringComparison.OrdinalIgnoreCase) != -1)
{
	int num = UserAgent.IndexOf("MSAuthHost/", StringComparison.OrdinalIgnoreCase);
	if (num != -1 && wIASupportedUserAgent.IndexOf("/In-Domain", StringComparison.OrdinalIgnoreCase) != -1)
	{
		if (UserAgent.IndexOf("/In-Domain", num, StringComparison.OrdinalIgnoreCase) != -1)
		{
			return true;
		}
		return false;
	}
}

The next special case checks if the user agent in AD FS starts with the string “MSAuthHost/” (if you remember the screenshot for the values in WIASupportedUserAgents in the beginning of the post, the first entry is “MSAuthHost/1.0/In-Domain”). Then, it compares the user agent sent by the client with that same string. If it matches, it continues looking in the client’s user agent for the string “/In-Domain”. If it contains that string after the initial MSAuthHost, it returns true and the client gets redirected to the WIA endpoint. But if that second part is anything else than “/In-Domain”… then it returns false, and AD FS determines that the client is not WIA capable.

The last part of the code, the else clause, just compares the user agent to the AD FS values and returns true if it matches.

So let’s put this to the test. First, we modify the GPO setting to contain the string “MSAuthHost/1.0/No-SSO” (the second part can be anything, as long as it isn’t “/In-Domain”):

Setting a lewd value here is an excellent way of trolling people without them knowing it!

Then we browse to outlook.com again, and are redirected to AD FS:

Hey, it worked!

Looks like it works! Now we have a scalable, easily configurable setting that can bypass WIA in AD FS without affecting other users or clients.

As a final note and a warning, however: while this effectively disables WIA for a single user, it will also affect other applications that use Internet Explorer as a browser. For instance, if an application utilizes Internet Explorer as an internal component, then it will be affected as well. This, unfortunately, I have no solution for.

Over and out!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s