This blog post describes an interesting
“feature” added to Windows to support Edge accessing the loopback network
interface. For reference this was on Windows 10 1803 running Edge 42.17134.1.0
as well as verifying on Windows 10 RS5 17713 running 43.17713.1000.0.
I like the concept of the App Container (AC)
sandbox Microsoft introduced in Windows 8. It moved sandboxing on Windows from
restricted tokens which were hard to reason about and required massive cludges
to get working to a reasonably consistent capability based model where you are
heavily limited in what you can do unless you’ve been granted an explicit
capability when your application is started. On Windows 8 this was limited to a
small set of known capabilities. On Windows 10 this has been expanded massively
by effectively allowing an application to define its own capabilities and
enforce them though the normal Windows access control mechanisms.
I’ve been looking at AC more and it's ability
to do network isolation, where access to the network requires being granted
capabilities such as “internetClient”, seems very useful. It’s a little known
fact that even in the most heavily locked down, restricted token sandbox it’s
possible to open network sockets by accessing the raw AFD driver. AC solves this
issue quite well, it doesn’t block access to the AFD driver, instead the
Firewall checks for the capabilities and blocks connecting or accepting
sockets.
One issue does come up with building a generic
sandboxing mechanism this AC network isolation primitive is regardless of what
capabilities you grant it’s not possible for an AC application to access
localhost. For example you might want your sandboxed application to access a
web server on localhost for testing, or use a localhost proxy to MITM the traffic.
Neither of these scenarios can be made to work in an AC sandbox with
capabilities alone.
The likely rationale for blocking localhost is
allowing sandboxed content access can also be a big security risk. Windows runs
quite a few services accessible locally which could be abused, such as the SMB
server. Rather than adding a capability to grant access to localhost, there's
an explicit list of packages exempt from the localhost restriction stored by
the firewall service. You can access or modify this list using the Firewall APIs such as the NetworkIsolationSetAppContainerConfig
function or using the CheckNetIsolation
tool installed with Windows. This behavior seems to be rationalized as
accessing loopback is a developer feature, not something which real
applications should rely on. Curious, I wondered whether I had AC’s already in
the exemption list. You can list all available exemptions by running “CheckNetIsolation
LoopbackExempt -s” on the command line.
On my Windows 10 machine we can see two
exemptions already installed, which is odd for a developer feature which no
applications should be using. The first entry shows “AppContainer NOT FOUND”
which indicates that the registered SID doesn’t correspond to a registered AC.
The second entry shows a very unhelpful name of “001” which at least means it’s
an application on the current system. What’s going on? We can use my NtObjectManager PS module and it's 'Get-NtSid' cmdlet on the
second SID to see if that can resolve a better name.
Ahha, “001” is actually a child AC of the Edge
package, we could have guessed this by looking at the length of the SID, a
normal AC SID had 8 sub authorities, whereas a child has 12, with the extra 4
being added to the end of the base AC SID. Looking back at the unregistered SID
we can see it’s also an Edge AC SID just with a child which isn’t actually
registered. The “001” AC seems to be the one used to host Internet content, at
least based on the browser security whitepaper from X41Sec (see page
54).
This is not exactly surprising. It seems when
Edge was first released it wasn’t possible to access localhost resources at all
(as demonstrated by an IBM help article which instructs
the user to use CheckNetIsolation to
add an exemption). However, at some point in development MS added an about:flags option to enable accessing
localhost, and seems it’s now the default configuration, even though as you can
see in the following screenshot it says enabling can put your device at risk.
What’s interesting though is if you disable
the flags option and restart Edge then the exemption entry is deleted, and
re-enabling it restores the entry again. Why is that a surprise? Well based on
previous knowledge of this exemption feature, such as this blog post by Eric Lawrence you
need admin privileges to change the
exemption list. Perhaps MS have changed that behavior now? Let’s try and add an
exemption using the CheckNetIsolation
tool as a normal user, passing “-a -p=SID” parameters.
I guess they haven’t as adding a new exemption
using the CheckNetIsolation tool
gives us access denied. Now I’m really interested. With Edge being a built-in
application of course there’s plenty of ways that MS could have fudged the
“security” checks to allow Edge to add itself to the list, but where is it?
The simplest location to add the fudge would
be in the RPC service which implements the NetworkIsolationSetAppContainerConfig.
(How do I know there's an RPC service? I just disassembled the API). I took a
guess and assumed the implementation would be hosted in the “Windows Defender
Firewall” service, which is implemented in the MPSSVC DLL. The following is a
simplified version of the RPC server method for the API.
HRESULT
RPC_NetworkIsolationSetAppContainerConfig(handle_t handle,
DWORD dwNumPublicAppCs,
PSID_AND_ATTRIBUTES appContainerSids) {
if (!FwRpcAPIsIsPackageAccessGranted(handle)) {
HRESULT hr;
BOOL developer_mode = FALSE:
IsDeveloperModeEnabled(&developer_mode);
if (developer_mode) {
hr = FwRpcAPIsSecModeAccessCheckForClient(1, handle);
if (FAILED(hr)) {
return hr;
}
}
else
{
hr = FwRpcAPIsSecModeAccessCheckForClient(2, handle);
if (FAILED(hr)) {
return hr;
}
}
}
return FwMoneisAppContainerSetConfig(dwNumPublicAppCs,
appContainerSids);
}
DWORD dwNumPublicAppCs,
PSID_AND_ATTRIBUTES appContainerSids) {
if (!FwRpcAPIsIsPackageAccessGranted(handle)) {
HRESULT hr;
BOOL developer_mode = FALSE:
IsDeveloperModeEnabled(&developer_mode);
if (developer_mode) {
hr = FwRpcAPIsSecModeAccessCheckForClient(1, handle);
if (FAILED(hr)) {
return hr;
}
}
else
{
hr = FwRpcAPIsSecModeAccessCheckForClient(2, handle);
if (FAILED(hr)) {
return hr;
}
}
}
return FwMoneisAppContainerSetConfig(dwNumPublicAppCs,
appContainerSids);
}
What’s immediately obvious is there's a method
call, FwRpcAPIsIsPackageAccessGranted,
which has “Package” in the name which might indicate it’s inspecting some AC
package information. If this call succeeds then the following security checks
are bypassed and the real function FwMoneisAppContainerSetConfig
is called. It's also worth noting that the security checks differ depending on
whether you're in developer mode or not. It turns out that if you have
developer mode enabled then you can also bypass the admin check, which is
confirmation the exemption list was designed primarily as a developer feature.
Anyway let's take a look at FwRpcAPIsIsPackageAccessGranted to see
what it’s checking.
const WCHAR* allowedPackageFamilies[] = {
L"Microsoft.MicrosoftEdge_8wekyb3d8bbwe",
L"Microsoft.MicrosoftEdgeBeta_8wekyb3d8bbwe",
L"Microsoft.zMicrosoftEdge_8wekyb3d8bbwe"
};
HRESULT FwRpcAPIsIsPackageAccessGranted(handle_t handle) {
HANDLE token;
FwRpcAPIsGetAccessTokenFromClientBinding(handle, &token);
WCHAR* package_id;
RtlQueryPackageIdentity(token, &package_id);
WCHAR family_name[0x100];
PackageFamilyNameFromFullName(package_id, family_name)
for (int i = 0;
i < _countof(allowedPackageFamilies);
++i) {
if (wcsicmp(family_name,
allowedPackageFamilies[i]) == 0) {
return S_OK;
}
}
return E_FAIL;
}
L"Microsoft.MicrosoftEdge_8wekyb3d8bbwe",
L"Microsoft.MicrosoftEdgeBeta_8wekyb3d8bbwe",
L"Microsoft.zMicrosoftEdge_8wekyb3d8bbwe"
};
HRESULT FwRpcAPIsIsPackageAccessGranted(handle_t handle) {
HANDLE token;
FwRpcAPIsGetAccessTokenFromClientBinding(handle, &token);
WCHAR* package_id;
RtlQueryPackageIdentity(token, &package_id);
WCHAR family_name[0x100];
PackageFamilyNameFromFullName(package_id, family_name)
for (int i = 0;
i < _countof(allowedPackageFamilies);
++i) {
if (wcsicmp(family_name,
allowedPackageFamilies[i]) == 0) {
return S_OK;
}
}
return E_FAIL;
}
The FwRpcAPIsIsPackageAccessGranted
function gets the caller’s token, queries for the package family name and
then checks it against a hard coded list. If the caller is in the Edge package
(or some beta versions) the function returns success which results in the admin
check being bypassed. The conclusion we can take is this is how Edge is adding
itself to the exemption list, although we also want to check what access is
required to the RPC server. For an ALPC server there’s two security checks,
connecting to the ALPC port and an optional security callback. We could reverse
engineer it from service binary but it is easier just to dump it from the ALPC
server port, again we can use my NtObjectManager
module.
As the RPC service doesn’t specify a name for
the service then the RPC libraries generate a random name of the form
“LRPC-XXXXX”. You would usually use EPMAPPER to find the real name but I just
used a debugger on CheckNetIsolation
to break on NtAlpcConnectPort and
dumped the connection name. Then we just find the handle to that ALPC port in
the service process and dump the security descriptor. The list contains Everyone and all the various network
related capabilities, so any AC process with network access can talk to these
APIs including Edge LPAC. Therefore all Edge processes can access this
capability and add arbitrary packages. The implementation inside Edge is in the
function emodel!SetACLoopbackExemptions.
With this knowledge we can now put together
some code which will exploit this “feature” to add arbitrary exemptions. You can find the
PowerShell script on my Github gist.
Wrap Up
If I was willing to speculate (and I am) I’d
say the reason that MS added localhost access this way is it didn’t require
modifying kernel drivers, it could all be done with changes to user mode
components. Of course the cynic in me thinks this could actually be just there
to make Edge more equal than others, assuming MS ever allowed another web
browser in the App Store. Even a wrapper around the Edge renderer would not be
allowed to add the localhost exemption. It’d be nice to see MS add a capability
to do this in the future, but considering current RS5 builds use this same
approach I’m not hopeful.
Is this a security issue? Well that depends.
On the one hand you could argue the default configuration which allows Internet
facing content to then access localhost is dangerous in itself, they point that
out explicitly in the about:flags
entry. Then again all browsers have this behavior so I’m not sure it’s really
an issue.
The implementation is pretty sloppy and I’m
shocked (well not that shocked) that it passed a security review. To list some
of the issues with it:
●
The package family check isn’t
very restrictive, combined with the weak permissions of the RPC service it
allows any Edge process to add an arbitrary exemption.
●
The exemption isn’t linked to the
calling process, so any SID can be added as an exemption.
While it seems the default is only to allow
the Internet facing ACs access to localhost because of these weaknesses if you
compromised a Flash process (which is child AC “006”) then it could
add itself an exemption and try and attack services listening on localhost. It would make more sense if only the main MicrosoftEdge
process could add the exemptions, not any content process. But what would make
the most sense would be to support this functionality through a capability so
that everyone could take advantage of it rather than implementing it as a
backdoor.