Monday, July 25, 2016

Misconfigured Service ACL Elevation of Privilege Vulnerability in Win10 IoT Core Build 14393

As of this writing, the latest public preview of Windows 10 IoT Core (build 14393) suffers from an elevation of privilege vulnerability via a misconfigured service ACL. The InputService service which run as SYSTEM grants authenticated users (i.e. members of the “NT AUTHORITY\Authenticated Users” group) SERVICE_ALL_ACCESS access rights, allowing an unprivileged, authenticated user to change the binary path of the service and gain elevated code execution upon restarting the service.

For reference, you can validate that InputService runs as SYSTEM with the following commands:

$InputService = Get-CimInstance -ClassName Win32_Service -Filter 'Name = "InputService"'

$InputService.StartName

Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$($InputService.ProcessId)" | Invoke-CimMethod -MethodName GetOwner

This trivial vulnerability was discovered while running the Get-CSVulnerableServicePermission function in CimSweep against an IoT Core instance running on my Raspberry Pi 2. CimSweep is designed to perform incident response and hunt operations entirely over WMI/CIM.

Exploitation

I wrote a proof of concept exploit that simply adds an unprivileged user to the Administrators group.

While this is a classic service misconfiguration vulnerability, there are several caveats to be mindful of when exploiting it on Win 10 IoT Core. First of all, IoT Core is designed to be managed remotely and for that, you are given two remote management options: PowerShell Remoting and SSH. I chose to use PowerShell Remoting in my PoC exploit primarily to point out that the default SDDL for PowerShell Remoting in IoT Core requires that users be members of the “Remote Management Users” group. Additional information on administering IoT Core with PowerShell can be found here. For reference, the default SDDL for PowerShell Remoting can be obtained and interpreted with the following command:

Get-PSSessionConfiguration -Name microsoft.powershell | Select-Object -ExpandProperty Permission

There is no such group membership requirement for SSH. Hopefully, at a future point, SSH endpoints on Windows will have the granular security controls that PowerShell Remoting offers via the PSSessionConfiguration cmdlets. Some additional caveats were that when I remoted in as an unprivileged user, I did not have sufficient privileges to use the Service cmdlets (Get-Service, Set-Service, etc.) or CIM cmdlets (Get-CimInstance, Invoke-CimMethod, etc.) in order to change the service configuration. Fortunately, sc.exe presented no such restrictions.

Conclusion

While this is by no means a “sexy” vulnerability, the fact that such a trivial vulnerability was present in a modern Windows OS tells me that perhaps Win 10 IoT Core isn’t getting the security scrutiny of other Windows operating systems. I hope that many of the same security controls and mitigations will eventually be applied to IoT Core if the plan is for this to be the operating system that drives critical infrastructure.

Lastly, if you’re attending Black Hat USA 2016, you should plan on attending Paul Sabanal’s (@polsab) talk on Windows 10 IoT Core!

Disclosure Timeline

May 22, 2016 – Vulnerability reported to MSRC
May 23, 2016 – MSRC opened a case number for the issue.
July 20, 2016 – Follow-up email sent to MSRC asking for a status update. No response received
July 25, 2016 – Decision made to release the vulnerability details

Friday, January 1, 2016

Properly Retrieving Win32 API Error Codes in PowerShell

Having worked with Win32 API functions enough in PowerShell using P/Invoke and reflection, I was constantly annoyed by the fact that I was often unable to correctly capture the correct error code from a function that sets its error code (by calling SetLastError) prior to returning to the caller despite setting SetLastError to True in the DllImportAttribute.

Consider the following, simple code that calls CopyFile within kernel32.dll:

$MethodDefinition = @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);
'@

$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -Namespace 'Win32' -PassThru

# Perform an invalid copy
$CopyResult = $Kernel32::CopyFile('C:\foo2', 'C:\foo1', $True)

# Retrieve the last error for CopyFile. The following error is expected:
# "The system cannot find the file specified"
$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

# An incorrect error is retrieved:
# "The system could not find the environment option that was entered"
# Grrrrrrrrrrrrrrrrr...

$LastError

I knew that you needed to retrieve the last error code immediately after a call to a Win32 function so naturally, I would have expected the correct error code. The one returned was consistently nonsensical, however. I don’t really know how I thought to try the following but I finally figured out how to properly capture the correct error code after an unmanaged function call – capture the error code on the same line (i.e. immediately after a semicolon). Apparently, the simple act of progressing to the next line in a PowerShell console is enough for your thread to set a different error code…

The following code demonstrates how to accurately capture the last set error code:

$MethodDefinition = @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);
'@

$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -Namespace 'Win32' -PassThru

# Perform an invalid copy
$CopyResult = $Kernel32::CopyFile('C:\foo2', 'C:\foo1', $True);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

# The correct error is retrieved:
# "The system cannot find the file specified"
# Yayyyyyyyyyyy....

$LastError

That’s all. I felt it was necessary to share this as I’m sure others have encountered this issue and were unable to find any solution on the Internet as it pertained to PowerShell.

Happy New Year!