From da0cc4e4b0cd87ca5227dfe9bbfcf0106a23dca6 Mon Sep 17 00:00:00 2001 From: Carrie Roberts Date: Sat, 27 Apr 2024 11:31:21 -0600 Subject: [PATCH] Revert "Atomic sudo" (#193) * Revert "Atomic sudo" * fix linting issues --------- Co-authored-by: Hare Sudhan --- Invoke-AtomicRedTeam.psd1 | 197 +++-- Private/AtomicClassSchema.ps1 | 110 +-- Private/Invoke-ExecuteCommand.ps1 | 13 +- Public/Get-PreferredIPAddress.ps1 | 1 - Public/Invoke-AtomicRunner.ps1 | 523 +++++++------ Public/Invoke-AtomicTest.ps1 | 9 +- Public/Invoke-KickoffAtomicRunner.ps1 | 114 +-- Public/Invoke-SetupAtomicRunner.ps1 | 289 ++++--- Public/New-Atomic.ps1 | 1030 ++++++++++++------------- Public/Set-Sudo.ps1 | 33 - 10 files changed, 1125 insertions(+), 1194 deletions(-) delete mode 100644 Public/Set-Sudo.ps1 diff --git a/Invoke-AtomicRedTeam.psd1 b/Invoke-AtomicRedTeam.psd1 index b62446a..686c6b0 100644 --- a/Invoke-AtomicRedTeam.psd1 +++ b/Invoke-AtomicRedTeam.psd1 @@ -1,99 +1,98 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'Invoke-AtomicRedTeam.psm1' - - # Version number of this module. - ModuleVersion = '2.1.0' - - # ID used to uniquely identify this module - GUID = '8f492621-18f8-432e-9532-b1d54d3e90bd' - - # Author of this module - Author = 'Casey Smith @subTee, Josh Rickard @MSAdministrator, Carrie Roberts @OrOneEqualsOne, Matt Graeber @mattifestation' - - # Company or vendor of this module - CompanyName = 'Red Canary, Inc.' - - # Copyright statement for this module - Copyright = '(c) 2021 Red Canary. All rights reserved.' - - # Description of the functionality provided by this module - Description = 'A PowerShell module that runs Atomic Red Team tests from yaml definition files.' - - # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.0' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('powershell-yaml') - - # Script files (.ps1) that are run in the caller's environment prior to importing this module. - # AtomicClassSchema.ps1 needs to be present in the caller's scope in order for the built-in classes to surface properly. - ScriptsToProcess = @('Private\AtomicClassSchema.ps1', 'Public\config.ps1') - - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - 'Invoke-AtomicTest', - 'Get-AtomicTechnique', - 'New-AtomicTechnique', - 'New-AtomicTest', - 'New-AtomicTestInputArgument', - 'New-AtomicTestDependency', - 'Start-AtomicGUI', - 'Stop-AtomicGUI', - 'Set-Sudo' - 'Invoke-SetupAtomicRunner', - 'Invoke-GenerateNewSchedule', - 'Invoke-RefreshExistingSchedule', - 'Invoke-AtomicRunner', - 'Get-Schedule', - 'Invoke-KickoffAtomicRunner', - 'Get-PreferredIPAddress' - ) - - # Variables to export from this module - VariablesToExport = '*' - - NestedModules = @( - "Public\Default-ExecutionLogger.psm1", - "Public\Attire-ExecutionLogger.psm1", - "Public\Syslog-ExecutionLogger.psm1", - "Public\WinEvent-ExecutionLogger.psm1" - ) - - # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('Security', 'Defense') - - # A URL to the license for this module. - LicenseUri = 'https://github.com/redcanaryco/invoke-atomicredteam/blob/master/LICENSE.txt' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/redcanaryco/invoke-atomicredteam' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - ReleaseNotes = @' -1.0.2 ------ -* Add support for custom execution loggers - -1.0.1 ------ -* Adding 'powershell-yaml' to RequiredModules in the module manifest - -1.0.0 ------ -* Initial release for submission to the PowerShell Gallery -'@ - - } # End of PSData hashtable - - } # End of PrivateData hashtable -} +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Invoke-AtomicRedTeam.psm1' + + # Version number of this module. + ModuleVersion = '2.1.0' + + # ID used to uniquely identify this module + GUID = '8f492621-18f8-432e-9532-b1d54d3e90bd' + + # Author of this module + Author = 'Casey Smith @subTee, Josh Rickard @MSAdministrator, Carrie Roberts @OrOneEqualsOne, Matt Graeber @mattifestation' + + # Company or vendor of this module + CompanyName = 'Red Canary, Inc.' + + # Copyright statement for this module + Copyright = '(c) 2021 Red Canary. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'A PowerShell module that runs Atomic Red Team tests from yaml definition files.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('powershell-yaml') + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # AtomicClassSchema.ps1 needs to be present in the caller's scope in order for the built-in classes to surface properly. + ScriptsToProcess = @('Private\AtomicClassSchema.ps1', 'Public\config.ps1') + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Invoke-AtomicTest', + 'Get-AtomicTechnique', + 'New-AtomicTechnique', + 'New-AtomicTest', + 'New-AtomicTestInputArgument', + 'New-AtomicTestDependency', + 'Start-AtomicGUI', + 'Stop-AtomicGUI', + 'Invoke-SetupAtomicRunner', + 'Invoke-GenerateNewSchedule', + 'Invoke-RefreshExistingSchedule', + 'Invoke-AtomicRunner', + 'Get-Schedule', + 'Invoke-KickoffAtomicRunner', + 'Get-PreferredIPAddress' + ) + + # Variables to export from this module + VariablesToExport = '*' + + NestedModules = @( + "Public\Default-ExecutionLogger.psm1", + "Public\Attire-ExecutionLogger.psm1", + "Public\Syslog-ExecutionLogger.psm1", + "Public\WinEvent-ExecutionLogger.psm1" + ) + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Security', 'Defense') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/redcanaryco/invoke-atomicredteam/blob/master/LICENSE.txt' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/redcanaryco/invoke-atomicredteam' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = @' +1.0.2 +----- +* Add support for custom execution loggers + +1.0.1 +----- +* Adding 'powershell-yaml' to RequiredModules in the module manifest + +1.0.0 +----- +* Initial release for submission to the PowerShell Gallery +'@ + + } # End of PSData hashtable + + } # End of PrivateData hashtable +} diff --git a/Private/AtomicClassSchema.ps1 b/Private/AtomicClassSchema.ps1 index f106883..1204803 100644 --- a/Private/AtomicClassSchema.ps1 +++ b/Private/AtomicClassSchema.ps1 @@ -1,55 +1,55 @@ -class AtomicDependency { - [String] $description - [String] $prereq_command - [String] $get_prereq_command -} - -class AtomicInputArgument { - [String] $description - [String] $type - [String] $default -} - -class AtomicExecutorBase { - [String] $name - [Bool] $elevation_required - - # Implemented to facilitate improved PS object display - [String] ToString() { - return $this.Name - } -} - -class AtomicExecutorDefault : AtomicExecutorBase { - [String] $command - [String] $cleanup_command -} - -class AtomicExecutorManual : AtomicExecutorBase { - [String] $steps - [String] $cleanup_command -} - -class AtomicTest { - [String] $name - [String] $auto_generated_guid - [String] $description - [String[]] $supported_platforms - # I wish this didn't have to be a hashtable but I don't - # want to change the schema and introduce a breaking change. - [Hashtable] $input_arguments - [String] $dependency_executor_name - [AtomicDependency[]] $dependencies - [AtomicExecutorBase] $executor - - # Implemented to facilitate improved PS object display - [String] ToString() { - return $this.name - } -} - -class AtomicTechnique { - [String[]] $attack_technique - [String] $display_name - [AtomicTest[]] $atomic_tests -} +class AtomicDependency { + [String] $description + [String] $prereq_command + [String] $get_prereq_command +} + +class AtomicInputArgument { + [String] $description + [String] $type + [String] $default +} + +class AtomicExecutorBase { + [String] $name + [Bool] $elevation_required + + # Implemented to facilitate improved PS object display + [String] ToString() { + return $this.Name + } +} + +class AtomicExecutorDefault : AtomicExecutorBase { + [String] $command + [String] $cleanup_command +} + +class AtomicExecutorManual : AtomicExecutorBase { + [String] $steps + [String] $cleanup_command +} + +class AtomicTest { + [String] $name + [String] $auto_generated_guid + [String] $description + [String[]] $supported_platforms + # I wish this didn't have to be a hashtable but I don't + # want to change the schema and introduce a breaking change. + [Hashtable] $input_arguments + [String] $dependency_executor_name + [AtomicDependency[]] $dependencies + [AtomicExecutorBase] $executor + + # Implemented to facilitate improved PS object display + [String] ToString() { + return $this.name + } +} + +class AtomicTechnique { + [String[]] $attack_technique + [String] $display_name + [AtomicTest[]] $atomic_tests +} diff --git a/Private/Invoke-ExecuteCommand.ps1 b/Private/Invoke-ExecuteCommand.ps1 index 1fb0a1c..13686a6 100644 --- a/Private/Invoke-ExecuteCommand.ps1 +++ b/Private/Invoke-ExecuteCommand.ps1 @@ -1,18 +1,11 @@ -function Invoke-ExecuteCommand ($finalCommand, $executor, $elevationreq, $can_sudo, $executionPlatform, $TimeoutSeconds, $session = $null, $interactive) { +function Invoke-ExecuteCommand ($finalCommand, $executor, $executionPlatform, $TimeoutSeconds, $session = $null, $interactive) { $null = @( if ($null -eq $finalCommand) { return 0 } $finalCommand = $finalCommand.trim() Write-Verbose -Message 'Invoking Atomic Tests using defined executor' if ($executor -eq "command_prompt" -or $executor -eq "sh" -or $executor -eq "bash") { - if (($executor -eq "sh" -or $executor -eq "bash") -and ($elevationreq -eq $true) -and ($can_sudo -eq $true)) { - $execExe = "$(which sudo)" - $execPrefix = "$(which $executor) -c" - } - else { - $execExe = $executor - $execPrefix = "-c" - } - + $execPrefix = "-c" + $execExe = $executor if ($executor -eq "command_prompt") { $execPrefix = "/c"; $execExe = "cmd.exe"; diff --git a/Public/Get-PreferredIPAddress.ps1 b/Public/Get-PreferredIPAddress.ps1 index dcf7af6..b8e0bed 100644 --- a/Public/Get-PreferredIPAddress.ps1 +++ b/Public/Get-PreferredIPAddress.ps1 @@ -12,4 +12,3 @@ function Get-PreferredIPAddress($isWindows) { return '' } } - diff --git a/Public/Invoke-AtomicRunner.ps1 b/Public/Invoke-AtomicRunner.ps1 index 6d3620a..40b4a67 100755 --- a/Public/Invoke-AtomicRunner.ps1 +++ b/Public/Invoke-AtomicRunner.ps1 @@ -1,264 +1,259 @@ -. "$PSScriptRoot\Invoke-RunnerScheduleMethods.ps1" - -function Invoke-AtomicRunner { - [CmdletBinding( - SupportsShouldProcess = $true, - PositionalBinding = $false, - ConfirmImpact = 'Medium')] - Param( - [Parameter(Mandatory = $false)] - [switch] - $ShowDetails, - - [Parameter(Mandatory = $false)] - [switch] - $CheckPrereqs, - - [Parameter(Mandatory = $false)] - [switch] - $GetPrereqs, - - [Parameter(Mandatory = $false)] - [switch] - $Cleanup, - - [Parameter(Mandatory = $false)] - [switch] - $ShowDetailsBrief, - - [Parameter(Mandatory = $false)] - [String] - $LoggingModule, - - [Parameter(Mandatory = $false)] - $ListOfAtomics, - - [parameter(Mandatory = $false)] - [ValidateRange(0, [int]::MaxValue)] - [int] $PauseBetweenAtomics, - - [parameter(Mandatory = $false)] - [switch] $scheduledTaskCleanup, - - [Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)] - $OtherArgs - ) - Begin { } - Process { - - function Get-GuidFromHostName( $basehostname ) { - $guid = [System.Net.Dns]::GetHostName() -replace $($basehostname + "-"), "" - - if (!$guid) { - LogRunnerMsg "Hostname has not been updated or could not parse out the Guid: " + $guid - return - } - - # Confirm hostname contains a guid - [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$' - - if ($guid -match $guidRegex) { return $guid } else { return "" } - } - - function Invoke-AtomicTestFromScheduleRow ($tr, $Cleanup = $false) { - $theArgs = $tr.InputArgs - if ($theArgs.GetType().Name -ne "Hashtable") { - $tr.InputArgs = ConvertFrom-StringData -StringData $theArgs - } - $sc = $tr.AtomicsFolder - #Run the Test based on if scheduleContext is 'private' or 'public' - if (($sc -eq 'public') -or ($null -eq $sc)) { - Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPublicAtomicsFolder @htvars -Cleanup:$Cleanup -supressPathToAtomicsFolder - } - elseif ($sc -eq 'private') { - Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPrivateAtomicsFolder @htvars -Cleanup:$Cleanup -supressPathToAtomicsFolder - } - if ($timeToPause -gt 0) { - Write-Host "Sleeping for $timeToPause seconds..." - Start-Sleep $timeToPause - } - elseif ($timeToPause -eq 0) { - Write-Host 'Press any key to continue...'; - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); - } - } - - function Rename-ThisComputer ($tr, $basehostname) { - $hash = $tr.auto_generated_guid - - $newHostName = "$basehostname-$hash" - $shouldRename = $true - if ( $newHostName -eq [System.Net.Dns]::GetHostName()) { $shouldRename = $false } - if ($artConfig.verbose) { LogRunnerMsg "Setting hostname to $newHostName" } - - If (Test-Path $artConfig.stopFile) { - LogRunnerMsg "exiting script because $($artConfig.stopFile) exists" - exit - } - if ($IsLinux) { - # Check if linux Host can use sudo without a password. - $can_sudo = Set-Sudo($false) - if($can_sudo){ - if ($shouldRename) { Invoke-Expression $("sudo hostnamectl set-hostname $newHostName") } - Invoke-Expression $("sudo shutdown -r now") - } - if ($shouldRename) { Invoke-Expression $("hostnamectl set-hostname $newHostName") } - Invoke-Expression $("shutdown -r now") - } - if ($IsMacOS) { - if ($shouldRename) { - Invoke-Expression $("/usr/sbin/scutil --set HostName $newHostName") - Invoke-Expression $("/usr/sbin/scutil --set ComputerName $newHostName") - Invoke-Expression $("/usr/sbin/scutil --set LocalHostName $newHostName") - } - Invoke-Expression $("/sbin/shutdown -r now") - } - else { - if ($debug) { LogRunnerMsg "Debug: pretending to rename the computer to $newHostName"; exit } - if (-not $shouldRename) { Restart-Computer -Force } - if ($artConfig.gmsaAccount) { - $retry = $true; $count = 0 - while ($retry) { - # add retry loop to avoid this occassional error "The verification of the MSA failed with error 1355" - Invoke-Command -ComputerName '127.0.0.1' -ConfigurationName 'RenameRunnerEndpoint' -ScriptBlock { Rename-Computer -NewName $Using:newHostName -Force -Restart } - Start-Sleep 120; $count = $count + 1 - LogRunnerMsg "Retrying computer rename $count" - if ($count -gt 15) { $retry = $false } - } - } - else { - $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) - try { - Rename-Computer -NewName $newHostName -Force -DomainCredential $cred -Restart -ErrorAction stop - } - catch { - if ($artConfig.verbose) { LogRunnerMsg $_ } - try { Rename-Computer -NewName $newHostName -Force -LocalCredential $cred -Restart -ErrorAction stop } catch { if ($artConfig.verbose) { LogRunnerMsg $_ } } - } - } - Start-Sleep -seconds 30 - LogRunnerMsg "uh oh, still haven't restarted - should never get to here" - $retry = $true; $count = 0 - while ($retry) { - Restart-Computer -Force - Start-Sleep 300; $count = $count + 1 - LogRunnerMsg "Rename retry $count" - if ($count -gt 60) { $retry = $false } - } - exit - } - - } - - function Get-TimingVariable ($sched) { - $atcount = $sched.Count - if ($null -eq $atcount) { $atcount = 1 } - $scheduleTimeSpanSeconds = $artConfig.scheduleTimeSpan.TotalSeconds - $secondsForAllTestsToComplete = $scheduleTimeSpanSeconds - $sleeptime = ($secondsForAllTestsToComplete / $atcount) - 120 - $artConfig.kickOffDelay.TotalSeconds # 1 minute for restart and 1 minute delay for scheduled task and an optional kickoff delay - if ($sleeptime -lt 120) { $sleeptime = 120 } # minimum 2 minute sleep time - return $sleeptime - } - - # Convert OtherArgs to hashtable so we can pass it through to the call to Invoke-AtomicTest - $htvars = @{} - if ($OtherArgs) { - $OtherArgs | ForEach-Object { - if ($_ -match '^-') { - #New parameter - $lastvar = $_ -replace '^-' - $htvars[$lastvar] = $true - } - else { - #Value - $htvars[$lastvar] = $_ - } - } - } - if ($PSBoundParameters.ContainsKey("PauseBetweenAtomics")) { - $timeToPause = $PauseBetweenAtomics - } - else { - $timeToPause = $null - } - $htvars += [Hashtable]$PSBoundParameters - $htvars.Remove('listOfAtomics') | Out-Null - $htvars.Remove('OtherArgs') | Out-Null - $htvars.Remove('Cleanup') | Out-Null - $htvars.Remove('PauseBetweenAtomics') | Out-Null - $htvars.Remove('scheduledTaskCleanup') | Out-Null - - $schedule = Get-Schedule $listOfAtomics - # If the schedule is empty, end process - if (-not $schedule) { - LogRunnerMsg "No test guid's or enabled tests." - return - } - - # timing variables - $SleepTillCleanup = Get-TimingVariable $schedule - - # Perform cleanup, Showdetails or Prereq stuff for all scheduled items and then exit - if ($Cleanup -or $ShowDetails -or $CheckPrereqs -or $ShowDetailsBrief -or $GetPrereqs -or $listOfAtomics) { - $schedule | ForEach-Object { - Invoke-AtomicTestFromScheduleRow $_ $Cleanup - } - return - } - - # exit if file stop.txt is found - If (Test-Path $artConfig.stopFile) { - LogRunnerMsg "exiting script because $($artConfig.stopFile) does exist" - Write-Host -ForegroundColor Yellow "Exiting script because $($artConfig.stopFile) does exist."; Start-Sleep 10; - exit - } - - # Find current test to run - $guid = Get-GuidFromHostName $artConfig.basehostname - if ([string]::IsNullOrWhiteSpace($guid)) { - LogRunnerMsg "Test Guid ($guid) was null, using next item in the schedule" - } - else { - if ($artConfig.verbose) { LogRunnerMsg "Found Test: $guid specified in hostname" } - $sp = [Collections.Generic.List[Object]]$schedule - $currentIndex = $sp.FindIndex( { $args[0].auto_generated_guid -eq $guid }) - if (($null -ne $currentIndex) -and ($currentIndex -ne -1)) { - $tr = $schedule[$currentIndex] - } - - if ($null -ne $tr) { - if ($scheduledTaskCleanup) { - # Cleanup after running test - Write-Host -Fore cyan "Sleeping for $SleepTillCleanup seconds before cleaning up for $($tr.Technique) $($tr.auto_generated_guid) "; Start-Sleep -Seconds $SleepTillCleanup - Invoke-AtomicTestFromScheduleRow $tr $true - } - else { - # run the atomic test and exit - Invoke-AtomicTestFromScheduleRow $tr - Start-Sleep 3; exit - } - } - else { - LogRunnerMsg "Could not find Test: $guid in schedule. Please update schedule to run this test." - } - } - - # Load next scheduled test before renaming computer - $nextIndex += $currentIndex + 1 - if ($nextIndex -ge ($schedule.count)) { - $tr = $schedule[0] - } - else { - $tr = $schedule[$nextIndex] - } - - if ($null -eq $tr) { - LogRunnerMsg "Could not determine the next row to execute from the schedule, Starting from 1st row"; - $tr = $schedule[0] - } - - #Rename Computer and Restart - Rename-ThisComputer $tr $artConfig.basehostname - - } -} +. "$PSScriptRoot\Invoke-RunnerScheduleMethods.ps1" + +function Invoke-AtomicRunner { + [CmdletBinding( + SupportsShouldProcess = $true, + PositionalBinding = $false, + ConfirmImpact = 'Medium')] + Param( + [Parameter(Mandatory = $false)] + [switch] + $ShowDetails, + + [Parameter(Mandatory = $false)] + [switch] + $CheckPrereqs, + + [Parameter(Mandatory = $false)] + [switch] + $GetPrereqs, + + [Parameter(Mandatory = $false)] + [switch] + $Cleanup, + + [Parameter(Mandatory = $false)] + [switch] + $ShowDetailsBrief, + + [Parameter(Mandatory = $false)] + [String] + $LoggingModule, + + [Parameter(Mandatory = $false)] + $ListOfAtomics, + + [parameter(Mandatory = $false)] + [ValidateRange(0, [int]::MaxValue)] + [int] $PauseBetweenAtomics, + + [parameter(Mandatory = $false)] + [switch] $scheduledTaskCleanup, + + [Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)] + $OtherArgs + ) + Begin { } + Process { + + function Get-GuidFromHostName( $basehostname ) { + $guid = [System.Net.Dns]::GetHostName() -replace $($basehostname + "-"), "" + + if (!$guid) { + LogRunnerMsg "Hostname has not been updated or could not parse out the Guid: " + $guid + return + } + + # Confirm hostname contains a guid + [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$' + + if ($guid -match $guidRegex) { return $guid } else { return "" } + } + + function Invoke-AtomicTestFromScheduleRow ($tr, $Cleanup = $false) { + $theArgs = $tr.InputArgs + if ($theArgs.GetType().Name -ne "Hashtable") { + $tr.InputArgs = ConvertFrom-StringData -StringData $theArgs + } + $sc = $tr.AtomicsFolder + #Run the Test based on if scheduleContext is 'private' or 'public' + if (($sc -eq 'public') -or ($null -eq $sc)) { + Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPublicAtomicsFolder @htvars -Cleanup:$Cleanup -supressPathToAtomicsFolder + } + elseif ($sc -eq 'private') { + Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPrivateAtomicsFolder @htvars -Cleanup:$Cleanup -supressPathToAtomicsFolder + } + if ($timeToPause -gt 0) { + Write-Host "Sleeping for $timeToPause seconds..." + Start-Sleep $timeToPause + } + elseif ($timeToPause -eq 0) { + Write-Host 'Press any key to continue...'; + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + } + } + + function Rename-ThisComputer ($tr, $basehostname) { + $hash = $tr.auto_generated_guid + + $newHostName = "$basehostname-$hash" + $shouldRename = $true + if ( $newHostName -eq [System.Net.Dns]::GetHostName()) { $shouldRename = $false } + if ($artConfig.verbose) { LogRunnerMsg "Setting hostname to $newHostName" } + + If (Test-Path $artConfig.stopFile) { + LogRunnerMsg "exiting script because $($artConfig.stopFile) exists" + exit + } + + if ($IsLinux) { + if ($shouldRename) { Invoke-Expression $("hostnamectl set-hostname $newHostName") } + Invoke-Expression $("shutdown -r now") + } + if ($IsMacOS) { + if ($shouldRename) { + Invoke-Expression $("/usr/sbin/scutil --set HostName $newHostName") + Invoke-Expression $("/usr/sbin/scutil --set ComputerName $newHostName") + Invoke-Expression $("/usr/sbin/scutil --set LocalHostName $newHostName") + } + Invoke-Expression $("/sbin/shutdown -r now") + } + else { + if ($debug) { LogRunnerMsg "Debug: pretending to rename the computer to $newHostName"; exit } + if (-not $shouldRename) { Restart-Computer -Force } + if ($artConfig.gmsaAccount) { + $retry = $true; $count = 0 + while ($retry) { + # add retry loop to avoid this occassional error "The verification of the MSA failed with error 1355" + Invoke-Command -ComputerName '127.0.0.1' -ConfigurationName 'RenameRunnerEndpoint' -ScriptBlock { Rename-Computer -NewName $Using:newHostName -Force -Restart } + Start-Sleep 120; $count = $count + 1 + LogRunnerMsg "Retrying computer rename $count" + if ($count -gt 15) { $retry = $false } + } + } + else { + $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) + try { + Rename-Computer -NewName $newHostName -Force -DomainCredential $cred -Restart -ErrorAction stop + } + catch { + if ($artConfig.verbose) { LogRunnerMsg $_ } + try { Rename-Computer -NewName $newHostName -Force -LocalCredential $cred -Restart -ErrorAction stop } catch { if ($artConfig.verbose) { LogRunnerMsg $_ } } + } + } + Start-Sleep -seconds 30 + LogRunnerMsg "uh oh, still haven't restarted - should never get to here" + $retry = $true; $count = 0 + while ($retry) { + Restart-Computer -Force + Start-Sleep 300; $count = $count + 1 + LogRunnerMsg "Rename retry $count" + if ($count -gt 60) { $retry = $false } + } + exit + } + + } + + function Get-TimingVariable ($sched) { + $atcount = $sched.Count + if ($null -eq $atcount) { $atcount = 1 } + $scheduleTimeSpanSeconds = $artConfig.scheduleTimeSpan.TotalSeconds + $secondsForAllTestsToComplete = $scheduleTimeSpanSeconds + $sleeptime = ($secondsForAllTestsToComplete / $atcount) - 120 - $artConfig.kickOffDelay.TotalSeconds # 1 minute for restart and 1 minute delay for scheduled task and an optional kickoff delay + if ($sleeptime -lt 120) { $sleeptime = 120 } # minimum 2 minute sleep time + return $sleeptime + } + + # Convert OtherArgs to hashtable so we can pass it through to the call to Invoke-AtomicTest + $htvars = @{} + if ($OtherArgs) { + $OtherArgs | ForEach-Object { + if ($_ -match '^-') { + #New parameter + $lastvar = $_ -replace '^-' + $htvars[$lastvar] = $true + } + else { + #Value + $htvars[$lastvar] = $_ + } + } + } + if ($PSBoundParameters.ContainsKey("PauseBetweenAtomics")) { + $timeToPause = $PauseBetweenAtomics + } + else { + $timeToPause = $null + } + $htvars += [Hashtable]$PSBoundParameters + $htvars.Remove('listOfAtomics') | Out-Null + $htvars.Remove('OtherArgs') | Out-Null + $htvars.Remove('Cleanup') | Out-Null + $htvars.Remove('PauseBetweenAtomics') | Out-Null + $htvars.Remove('scheduledTaskCleanup') | Out-Null + + $schedule = Get-Schedule $listOfAtomics + # If the schedule is empty, end process + if (-not $schedule) { + LogRunnerMsg "No test guid's or enabled tests." + return + } + + # timing variables + $SleepTillCleanup = Get-TimingVariable $schedule + + # Perform cleanup, Showdetails or Prereq stuff for all scheduled items and then exit + if ($Cleanup -or $ShowDetails -or $CheckPrereqs -or $ShowDetailsBrief -or $GetPrereqs -or $listOfAtomics) { + $schedule | ForEach-Object { + Invoke-AtomicTestFromScheduleRow $_ $Cleanup + } + return + } + + # exit if file stop.txt is found + If (Test-Path $artConfig.stopFile) { + LogRunnerMsg "exiting script because $($artConfig.stopFile) does exist" + Write-Host -ForegroundColor Yellow "Exiting script because $($artConfig.stopFile) does exist."; Start-Sleep 10; + exit + } + + # Find current test to run + $guid = Get-GuidFromHostName $artConfig.basehostname + if ([string]::IsNullOrWhiteSpace($guid)) { + LogRunnerMsg "Test Guid ($guid) was null, using next item in the schedule" + } + else { + if ($artConfig.verbose) { LogRunnerMsg "Found Test: $guid specified in hostname" } + $sp = [Collections.Generic.List[Object]]$schedule + $currentIndex = $sp.FindIndex( { $args[0].auto_generated_guid -eq $guid }) + if (($null -ne $currentIndex) -and ($currentIndex -ne -1)) { + $tr = $schedule[$currentIndex] + } + + if ($null -ne $tr) { + if ($scheduledTaskCleanup) { + # Cleanup after running test + Write-Host -Fore cyan "Sleeping for $SleepTillCleanup seconds before cleaning up for $($tr.Technique) $($tr.auto_generated_guid) "; Start-Sleep -Seconds $SleepTillCleanup + Invoke-AtomicTestFromScheduleRow $tr $true + } + else { + # run the atomic test and exit + Invoke-AtomicTestFromScheduleRow $tr + Start-Sleep 3; exit + } + } + else { + LogRunnerMsg "Could not find Test: $guid in schedule. Please update schedule to run this test." + } + } + + # Load next scheduled test before renaming computer + $nextIndex += $currentIndex + 1 + if ($nextIndex -ge ($schedule.count)) { + $tr = $schedule[0] + } + else { + $tr = $schedule[$nextIndex] + } + + if ($null -eq $tr) { + LogRunnerMsg "Could not determine the next row to execute from the schedule, Starting from 1st row"; + $tr = $schedule[0] + } + + #Rename Computer and Restart + Rename-ThisComputer $tr $artConfig.basehostname + + } +} diff --git a/Public/Invoke-AtomicTest.ps1 b/Public/Invoke-AtomicTest.ps1 index c4f6644..a567507 100644 --- a/Public/Invoke-AtomicTest.ps1 +++ b/Public/Invoke-AtomicTest.ps1 @@ -429,8 +429,6 @@ function Invoke-AtomicTest { Write-Debug -Message 'Gathering final Atomic test command' - # Check if linux Host can use sudo without a password. - $can_sudo = Set-Sudo($false) if ($CheckPrereqs) { Write-KeyValue "CheckPrereq's for: " $testId @@ -443,12 +441,7 @@ function Invoke-AtomicTest { } Write-KeyValue "GetPrereq's for: " $testId if ( $test.executor.elevation_required -and -not $isElevated) { - if ($can_sudo -eq $true) { - Write-Host -ForegroundColor Yellow "Elevation required but not provided, but host supports passwordless sudo" - } - else{ Write-Host -ForegroundColor Red "Elevation required but not provided" - } } if ($nul -eq $test.dependencies) { Write-KeyValue "No Preqs Defined"; continue } foreach ($dep in $test.dependencies) { @@ -491,7 +484,7 @@ function Invoke-AtomicTest { $startTime = Get-Date $final_command = Merge-InputArgs $test.executor.command $test $InputArgs $PathToPayloads if (Get-Command 'Invoke-ARTPreAtomicHook' -errorAction SilentlyContinue) { Invoke-ARTPreAtomicHook $test $InputArgs } - $res = Invoke-ExecuteCommand $final_command $test.executor.name $test.executor.elevation_required $can_sudo $executionPlatform $TimeoutSeconds $session -Interactive:$Interactive + $res = Invoke-ExecuteCommand $final_command $test.executor.name $executionPlatform $TimeoutSeconds $session -Interactive:$Interactive Write-Host "Exit code: $($res.ExitCode)" if (Get-Command 'Invoke-ARTPostAtomicHook' -errorAction SilentlyContinue) { Invoke-ARTPostAtomicHook $test $InputArgs } $stopTime = Get-Date diff --git a/Public/Invoke-KickoffAtomicRunner.ps1 b/Public/Invoke-KickoffAtomicRunner.ps1 index c02ffb0..284aae2 100644 --- a/Public/Invoke-KickoffAtomicRunner.ps1 +++ b/Public/Invoke-KickoffAtomicRunner.ps1 @@ -1,57 +1,57 @@ -function Invoke-KickoffAtomicRunner { - - #log rotation function - function Rotate-Log { - Param ($logPath, $max_filesize, $max_age) - $datetime = Get-Date -uformat "%Y-%m-%d-%H%M" - - $log = Get-Item $logPath - if ($log.Length / 1MB -ge $max_filesize) { - Write-Host "file named $($log.name) is bigger than $max_filesize MB" - $newname = "$($log.Name)_${datetime}.arclog" - Rename-Item $log.PSPath $newname - Write-Host "Done rotating file" - } - - $logdir_content = Get-ChildItem $artConfig.atomicLogsPath -filter "*.arclog" - $cutoff_date = (get-date).AddDays($max_age) - $logdir_content | ForEach-Object { - if ($_.LastWriteTime -gt $cutoff_date) { - Remove-Item $_ - Write-Host "Removed $($_.PSPath)" - } - } - } - - #Create log files as needed - $all_log_file = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname).txt" - $all_log_file_cleanup = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname)-cleanup.txt" - New-Item $all_log_file -ItemType file -ErrorAction Ignore - New-Item $all_log_file_cleanup -ItemType file -ErrorAction Ignore - New-Item $artConfig.logFile -ItemType File -ErrorAction Ignore - - #Rotate logs based on FileSize and Date max_filesize - $max_filesize = 200 #in MB - $max_file_age = 30 #in days - Rotate-Log $all_log_file $max_filesize $max_file_age - Rotate-Log $all_log_file_cleanup $max_filesize $max_file_age - - Rotate-Log $artConfig.logFile $max_filesize $max_file_age #no need to repeat this. Can reduce further. - - # Optional additional delay before starting - Start-Sleep $artConfig.kickOffDelay.TotalSeconds - - $WorkingDirectory = if ($IsLinux -or $IsMacOS) { "/tmp" } else { $env:TEMP } - $FileName = if ($IsLinux -or $IsMacOS) { "pwsh" } else { "powershell.exe" } - if ($artConfig.debug) { $Arguments = "-Command Invoke-AtomicRunner *>> $all_log_file" } else { $Arguments = "-Command Invoke-AtomicRunner" } - # Invoke the atomic as its own process because we don't want to skip the cleanup and rename process in the event that AV kills the process running the atomic - Start-Process -FilePath $FileName -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory - # Run the cleanup commmands - if ($artConfig.debug) { Invoke-AtomicRunner -scheduledTaskCleanup *>> $all_log_file_cleanup } else { Invoke-AtomicRunner -scheduledTaskCleanup } -} - -function LogRunnerMsg ($message) { - $now = "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date) - Write-Host -fore cyan $message - Add-Content $artConfig.logFile "$now`: $message" -} +function Invoke-KickoffAtomicRunner { + + #log rotation function + function Rotate-Log { + Param ($logPath, $max_filesize, $max_age) + $datetime = Get-Date -uformat "%Y-%m-%d-%H%M" + + $log = Get-Item $logPath + if ($log.Length / 1MB -ge $max_filesize) { + Write-Host "file named $($log.name) is bigger than $max_filesize MB" + $newname = "$($log.Name)_${datetime}.arclog" + Rename-Item $log.PSPath $newname + Write-Host "Done rotating file" + } + + $logdir_content = Get-ChildItem $artConfig.atomicLogsPath -filter "*.arclog" + $cutoff_date = (get-date).AddDays($max_age) + $logdir_content | ForEach-Object { + if ($_.LastWriteTime -gt $cutoff_date) { + Remove-Item $_ + Write-Host "Removed $($_.PSPath)" + } + } + } + + #Create log files as needed + $all_log_file = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname).txt" + $all_log_file_cleanup = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname)-cleanup.txt" + New-Item $all_log_file -ItemType file -ErrorAction Ignore + New-Item $all_log_file_cleanup -ItemType file -ErrorAction Ignore + New-Item $artConfig.logFile -ItemType File -ErrorAction Ignore + + #Rotate logs based on FileSize and Date max_filesize + $max_filesize = 200 #in MB + $max_file_age = 30 #in days + Rotate-Log $all_log_file $max_filesize $max_file_age + Rotate-Log $all_log_file_cleanup $max_filesize $max_file_age + + Rotate-Log $artConfig.logFile $max_filesize $max_file_age #no need to repeat this. Can reduce further. + + # Optional additional delay before starting + Start-Sleep $artConfig.kickOffDelay.TotalSeconds + + $WorkingDirectory = if ($IsLinux -or $IsMacOS) { "/tmp" } else { $env:TEMP } + $FileName = if ($IsLinux -or $IsMacOS) { "pwsh" } else { "powershell.exe" } + if ($artConfig.debug) { $Arguments = "-Command Invoke-AtomicRunner *>> $all_log_file" } else { $Arguments = "-Command Invoke-AtomicRunner" } + # Invoke the atomic as its own process because we don't want to skip the cleanup and rename process in the event that AV kills the process running the atomic + Start-Process -FilePath $FileName -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory + # Run the cleanup commmands + if ($artConfig.debug) { Invoke-AtomicRunner -scheduledTaskCleanup *>> $all_log_file_cleanup } else { Invoke-AtomicRunner -scheduledTaskCleanup } +} + +function LogRunnerMsg ($message) { + $now = "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date) + Write-Host -fore cyan $message + Add-Content $artConfig.logFile "$now`: $message" +} diff --git a/Public/Invoke-SetupAtomicRunner.ps1 b/Public/Invoke-SetupAtomicRunner.ps1 index be3369d..45a9deb 100755 --- a/Public/Invoke-SetupAtomicRunner.ps1 +++ b/Public/Invoke-SetupAtomicRunner.ps1 @@ -1,152 +1,137 @@ -function Invoke-SetupAtomicRunner { - - # ensure running with admin privs - if ($artConfig.OS -eq "windows") { - # auto-elevate on Windows - $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) - $testadmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) - if ($testadmin -eq $false) { - Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition)) - exit $LASTEXITCODE - } - } - else { - # linux and macos check - doesn't auto-elevate - # Check if current user has passwordless sudo privleges. If not, attempt to configure it for current user. - $can_sudo = Set-Sudo($true) - if ($can_sudo -eq $true -and (sudo id -u) -ne 0 ) { - Throw "You must run the Invoke-SetupAtomicRunner script as root" - exit - } - elseif ($can_sudo -eq $false -and (id -u) -ne 0 ) { - Throw "You must run the Invoke-SetupAtomicRunner script as root" - exit - } - } - - if ($artConfig.basehostname.length -gt 15) { Throw "The hostname for this machine (minus the GUID) must be 15 characters or less. Please rename this computer." } - - #create AtomicRunner-Logs directories if they don't exist - New-Item -ItemType Directory $artConfig.atomicLogsPath -ErrorAction Ignore - New-Item -ItemType Directory $artConfig.runnerFolder -ErrorAction Ignore - - if ($artConfig.gmsaAccount) { - Start-Service WinRM - $path = Join-Path $env:ProgramFiles "WindowsPowerShell\Modules\RenameRunner\RoleCapabilities" - New-Item -ItemType Directory $path -ErrorAction Ignore - New-PSSessionConfigurationFile -SessionType RestrictedRemoteServer -GroupManagedServiceAccount $artConfig.gmsaAccount -RoleDefinitions @{ "$($artConfig.user)" = @{ 'RoleCapabilities' = 'RenameRunner' } } -path "$env:Temp\RenameRunner.pssc" - New-PSRoleCapabilityFile -VisibleCmdlets @{ 'Name' = 'Rename-Computer'; 'Parameters' = @{ 'Name' = 'NewName'; 'ValidatePattern' = 'ATOMICSOC.*' }, @{ 'Name' = 'Force' }, @{ 'Name' = 'restart' } } -path "$path\RenameRunner.psrc" - $null = Register-PSSessionConfiguration -name "RenameRunnerEndpoint" -path "$env:Temp\RenameRunner.pssc" -force - Add-LocalGroupMember "administrators" "$($artConfig.gmsaAccount)$" -ErrorAction Ignore - # Make sure WinRM is enabled and set to Automic start (not delayed) - Set-ItemProperty hklm:\\SYSTEM\CurrentControlSet\Services\WinRM -Name Start -Value 2 - Set-ItemProperty hklm:\\SYSTEM\CurrentControlSet\Services\WinRM -Name DelayedAutostart -Value 0 # default is delayed start and that is too slow given our 1 minute delay on our kickoff task - # this registry key must be set to zero for things to work get-itemproperty hklm:\Software\Policies\Microsoft\Windows\WinRM\Service\ - $hklmKey = (get-itemproperty hklm:\Software\Policies\Microsoft\Windows\WinRM\Service -name DisableRunAs -ErrorAction ignore).DisableRunAs - $hkcuKey = (get-itemproperty hkcu:\Software\Policies\Microsoft\Windows\WinRM\Service -name DisableRunAs -ErrorAction ignore).DisableRunAs - if ((1 -eq $hklmKey) -or (1 -eq $hkcuKey)) { Write-Host -ForegroundColor Red "DisableRunAs registry Key will not allow use of the JEA endpoint with a gmsa account" } - if ((Get-ItemProperty hklm:\System\CurrentControlSet\Control\Lsa\ -name DisableDomainCreds).DisableDomainCreds) { Write-Host -ForegroundColor Red "Do not allow storage of passwords and credentials for network authentication must be disabled" } - } - - if ($artConfig.OS -eq "windows") { - - if (Test-Path $artConfig.credFile) { - Write-Host "Credential File $($artConfig.credFile) already exists, not prompting for creation of a new one." - $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) - } - else { - # create credential file for the user since we aren't using a group managed service account - $cred = Get-Credential -UserName $artConfig.user -message "Enter password for $($artConfig.user) in order to create the runner scheduled task" - $cred.Password | ConvertFrom-SecureString | Out-File $artConfig.credFile - - } - - # setup scheduled task that will start the runner after each restart - # local security policy --> Local Policies --> Security Options --> Network access: Do not allow storage of passwords and credentials for network authentication must be disabled - $taskName = "KickOff-AtomicRunner" - Unregister-ScheduledTask $taskName -confirm:$false -ErrorAction Ignore - # Windows scheduled task includes a 20 minutes sleep then restart if the call to Invoke-KickoffAtomicRunner fails - # this occurs occassionally when Windows has issues logging into the runner user's account and logs in as a TEMP user - $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-exec bypass -Command Invoke-KickoffAtomicRunner; Start-Sleep 1200; Restart-Computer -Force" - $taskPrincipal = New-ScheduledTaskPrincipal -UserId $artConfig.user - $delays = @(1, 2, 4, 8, 16, 32, 64) # using multiple triggers as a retry mechanism because the built-in retry mechanism doesn't work when the computer renaming causes AD replication delays - $triggers = @() - foreach ($delay in $delays) { - $trigger = New-ScheduledTaskTrigger -AtStartup - $trigger.Delay = "PT$delay`M" - $triggers += $trigger - } - $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Trigger $triggers -Description "A task that runs 1 minute or later after boot to start the atomic test runner script" - try { - $null = Register-ScheduledTask -TaskName $taskName -InputObject $task -User $artConfig.user -Password $($cred.GetNetworkCredential().password) -ErrorAction Stop - } - catch { - if ($_.CategoryInfo.Category -eq "AuthenticationError") { - # remove the credential file if the password didn't work - Write-Error "The credentials you entered are incorrect. Please run the setup script again and double check the username and password." - Remove-Item $artConfig.credFile - } - else { - Throw $_ - } - } - } - else { - - # sets cronjob string using basepath from config.ps1 - $pwshPath = which pwsh - $job = "@reboot $env:USER sleep 60;$pwshPath -Command Invoke-KickoffAtomicRunner" - $exists = cat /etc/crontab | Select-String -Quiet "KickoffAtomicRunner" - #checks if the Kickoff-AtomicRunner job exists. If not appends it to the system crontab. - if ($null -eq $exists -and $can_sudo -eq $true) { - $(Write-Output "$job" | sudo tee -a /etc/crontab) - write-host "setting cronjob" - } - elseif ($null -eq $exists -and $can_sudo -eq $false) { - $(Write-Output "$job" >> /etc/crontab) - write-host "setting cronjob" - } - else { - write-host "cronjob already exists" - } - } - - # Add Import-Module statement to the PowerShell profile - $root = Split-Path $PSScriptRoot -Parent - if($IsLinux -or $IsMacOS){ - mkdir (Split-Path $PROFILE) - touch $PROFILE - } - $pathToPSD1 = Join-Path $root "Invoke-AtomicRedTeam.psd1" - $importStatement = "Import-Module ""$pathToPSD1"" -Force" - New-Item $PROFILE -ErrorAction Ignore - $profileContent = Get-Content $profile - $line = $profileContent | Select-String ".*import-module.*invoke-atomicredTeam.psd1" | Select-Object -ExpandProperty Line - if ($line) { - $profileContent | ForEach-Object { $_.replace( $line, "$importStatement") } | Set-Content $profile - } - else { - Add-Content $profile $importStatement - } - - # Install the Posh-SYLOG module if we are configured to use it and it is not already installed - if ((-not (Get-Module -ListAvailable "Posh-SYSLOG")) -and [bool]$artConfig.syslogServer -and [bool]$artConfig.syslogPort) { - write-verbose "Posh-SYSLOG" - Install-Module -Name Posh-SYSLOG -Scope CurrentUser -Force - } - - # create the CSV schedule of atomics to run if it doesn't exist - if (-not (Test-Path $artConfig.scheduleFile)) { - Invoke-GenerateNewSchedule - } - - $schedule = Get-Schedule - if ($null -eq $schedule) { - Write-Host -ForegroundColor Yellow "There are no tests enabled on the schedule, set the 'Enabled' column to 'True' for the atomic test that you want to run. The schedule file is found here: $($artConfig.scheduleFile)" - Write-Host -ForegroundColor Yellow "Rerun this setup script after updating the schedule" - } - else { - # Get the prereqs for all of the tests on the schedule - Invoke-AtomicRunner -GetPrereqs - } -} +function Invoke-SetupAtomicRunner { + + # ensure running with admin privs + if ($artConfig.OS -eq "windows") { + # auto-elevate on Windows + $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) + $testadmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + if ($testadmin -eq $false) { + Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition)) + exit $LASTEXITCODE + } + } + else { + # linux and macos check - doesn't auto-elevate + if ((id -u) -ne 0 ) { + Throw "You must run the Invoke-SetupAtomicRunner script as root" + exit + } + } + + if ($artConfig.basehostname.length -gt 15) { Throw "The hostname for this machine (minus the GUID) must be 15 characters or less. Please rename this computer." } + + #create AtomicRunner-Logs directories if they don't exist + New-Item -ItemType Directory $artConfig.atomicLogsPath -ErrorAction Ignore + New-Item -ItemType Directory $artConfig.runnerFolder -ErrorAction Ignore + + if ($artConfig.gmsaAccount) { + Start-Service WinRM + $path = Join-Path $env:ProgramFiles "WindowsPowerShell\Modules\RenameRunner\RoleCapabilities" + New-Item -ItemType Directory $path -ErrorAction Ignore + New-PSSessionConfigurationFile -SessionType RestrictedRemoteServer -GroupManagedServiceAccount $artConfig.gmsaAccount -RoleDefinitions @{ "$($artConfig.user)" = @{ 'RoleCapabilities' = 'RenameRunner' } } -path "$env:Temp\RenameRunner.pssc" + New-PSRoleCapabilityFile -VisibleCmdlets @{ 'Name' = 'Rename-Computer'; 'Parameters' = @{ 'Name' = 'NewName'; 'ValidatePattern' = 'ATOMICSOC.*' }, @{ 'Name' = 'Force' }, @{ 'Name' = 'restart' } } -path "$path\RenameRunner.psrc" + $null = Register-PSSessionConfiguration -name "RenameRunnerEndpoint" -path "$env:Temp\RenameRunner.pssc" -force + Add-LocalGroupMember "administrators" "$($artConfig.gmsaAccount)$" -ErrorAction Ignore + # Make sure WinRM is enabled and set to Automic start (not delayed) + Set-ItemProperty hklm:\\SYSTEM\CurrentControlSet\Services\WinRM -Name Start -Value 2 + Set-ItemProperty hklm:\\SYSTEM\CurrentControlSet\Services\WinRM -Name DelayedAutostart -Value 0 # default is delayed start and that is too slow given our 1 minute delay on our kickoff task + # this registry key must be set to zero for things to work get-itemproperty hklm:\Software\Policies\Microsoft\Windows\WinRM\Service\ + $hklmKey = (get-itemproperty hklm:\Software\Policies\Microsoft\Windows\WinRM\Service -name DisableRunAs -ErrorAction ignore).DisableRunAs + $hkcuKey = (get-itemproperty hkcu:\Software\Policies\Microsoft\Windows\WinRM\Service -name DisableRunAs -ErrorAction ignore).DisableRunAs + if ((1 -eq $hklmKey) -or (1 -eq $hkcuKey)) { Write-Host -ForegroundColor Red "DisableRunAs registry Key will not allow use of the JEA endpoint with a gmsa account" } + if ((Get-ItemProperty hklm:\System\CurrentControlSet\Control\Lsa\ -name DisableDomainCreds).DisableDomainCreds) { Write-Host -ForegroundColor Red "Do not allow storage of passwords and credentials for network authentication must be disabled" } + } + + if ($artConfig.OS -eq "windows") { + + if (Test-Path $artConfig.credFile) { + Write-Host "Credential File $($artConfig.credFile) already exists, not prompting for creation of a new one." + $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) + } + else { + # create credential file for the user since we aren't using a group managed service account + $cred = Get-Credential -UserName $artConfig.user -message "Enter password for $($artConfig.user) in order to create the runner scheduled task" + $cred.Password | ConvertFrom-SecureString | Out-File $artConfig.credFile + + } + + # setup scheduled task that will start the runner after each restart + # local security policy --> Local Policies --> Security Options --> Network access: Do not allow storage of passwords and credentials for network authentication must be disabled + $taskName = "KickOff-AtomicRunner" + Unregister-ScheduledTask $taskName -confirm:$false -ErrorAction Ignore + # Windows scheduled task includes a 20 minutes sleep then restart if the call to Invoke-KickoffAtomicRunner fails + # this occurs occassionally when Windows has issues logging into the runner user's account and logs in as a TEMP user + $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-exec bypass -Command Invoke-KickoffAtomicRunner; Start-Sleep 1200; Restart-Computer -Force" + $taskPrincipal = New-ScheduledTaskPrincipal -UserId $artConfig.user + $delays = @(1, 2, 4, 8, 16, 32, 64) # using multiple triggers as a retry mechanism because the built-in retry mechanism doesn't work when the computer renaming causes AD replication delays + $triggers = @() + foreach ($delay in $delays) { + $trigger = New-ScheduledTaskTrigger -AtStartup + $trigger.Delay = "PT$delay`M" + $triggers += $trigger + } + $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Trigger $triggers -Description "A task that runs 1 minute or later after boot to start the atomic test runner script" + try { + $null = Register-ScheduledTask -TaskName $taskName -InputObject $task -User $artConfig.user -Password $($cred.GetNetworkCredential().password) -ErrorAction Stop + } + catch { + if ($_.CategoryInfo.Category -eq "AuthenticationError") { + # remove the credential file if the password didn't work + Write-Error "The credentials you entered are incorrect. Please run the setup script again and double check the username and password." + Remove-Item $artConfig.credFile + } + else { + Throw $_ + } + } + } + else { + # sets cronjob string using basepath from config.ps1 + $pwshPath = which pwsh + $job = "@reboot root sleep 60;$pwshPath -Command Invoke-KickoffAtomicRunner" + $exists = cat /etc/crontab | Select-String -Quiet "KickoffAtomicRunner" + #checks if the Kickoff-AtomicRunner job exists. If not appends it to the system crontab. + if ($null -eq $exists) { + $(Write-Output "$job" >> /etc/crontab) + write-host "setting cronjob" + } + else { + write-host "cronjob already exists" + } + } + + # Add Import-Module statement to the PowerShell profile + $root = Split-Path $PSScriptRoot -Parent + $pathToPSD1 = Join-Path $root "Invoke-AtomicRedTeam.psd1" + $importStatement = "Import-Module ""$pathToPSD1"" -Force" + New-Item $PROFILE -ErrorAction Ignore + $profileContent = Get-Content $profile + $line = $profileContent | Select-String ".*import-module.*invoke-atomicredTeam.psd1" | Select-Object -ExpandProperty Line + if ($line) { + $profileContent | ForEach-Object { $_.replace( $line, "$importStatement") } | Set-Content $profile + } + else { + Add-Content $profile $importStatement + } + + # Install the Posh-SYLOG module if we are configured to use it and it is not already installed + if ((-not (Get-Module -ListAvailable "Posh-SYSLOG")) -and [bool]$artConfig.syslogServer -and [bool]$artConfig.syslogPort) { + write-verbose "Posh-SYSLOG" + Install-Module -Name Posh-SYSLOG -Scope CurrentUser -Force + } + + # create the CSV schedule of atomics to run if it doesn't exist + if (-not (Test-Path $artConfig.scheduleFile)) { + Invoke-GenerateNewSchedule + } + + $schedule = Get-Schedule + if ($null -eq $schedule) { + Write-Host -ForegroundColor Yellow "There are no tests enabled on the schedule, set the 'Enabled' column to 'True' for the atomic test that you want to run. The schedule file is found here: $($artConfig.scheduleFile)" + Write-Host -ForegroundColor Yellow "Rerun this setup script after updating the schedule" + } + else { + # Get the prereqs for all of the tests on the schedule + Invoke-AtomicRunner -GetPrereqs + } +} diff --git a/Public/New-Atomic.ps1 b/Public/New-Atomic.ps1 index 57be349..3c9abb4 100644 --- a/Public/New-Atomic.ps1 +++ b/Public/New-Atomic.ps1 @@ -1,515 +1,515 @@ -# The class definitions that these functions rely upon are located in Private\AtomicClassSchema.ps1 - -function New-AtomicTechnique { - <# -.SYNOPSIS - -Specifies a new atomic red team technique. The output of this function is designed to be piped directly to ConvertTo-Yaml, eliminating the need to work with YAML directly. - -.PARAMETER AttackTechnique - -Specifies one or more MITRE ATT&CK techniques that to which this technique applies. Per MITRE naming convention, an attack technique should start with "T" followed by a 4 digit number. The MITRE sub-technique format is also supported: TNNNN.NNN - -.PARAMETER DisplayName - -Specifies the name of the technique as defined by ATT&CK. Example: 'Audio Capture' - -.PARAMETER AtomicTests - -Specifies one or more atomic tests. Atomic tests are created using the New-AtomicTest function. - -.EXAMPLE - -$InputArg1 = New-AtomicTestInputArgument -Name filename -Description 'location of the payload' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.dll' -$InputArg2 = New-AtomicTestInputArgument -Name source -Description 'location of the source code to compile' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.cs' - -$AtomicTest1 = New-AtomicTest -Name 'InstallUtil uninstall method call' -Description 'Executes the Uninstall Method' -SupportedPlatforms Windows -InputArguments @($InputArg1, $InputArg2) -ExecutorType CommandPrompt -ExecutorCommand @' -C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:#{filename} #{source} -C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U #{filename} -'@ - -# Note: the input arguments are identical for atomic test #1 and #2 -$AtomicTest2 = New-AtomicTest -Name 'InstallUtil GetHelp method call' -Description 'Executes the Help property' -SupportedPlatforms Windows -InputArguments @($InputArg1, $InputArg2) -ExecutorType CommandPrompt -ExecutorCommand @' -C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:#{filename} #{source} -C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /? #{filename} -'@ - -$AtomicTechnique = New-AtomicTechnique -AttackTechnique T1118 -DisplayName InstallUtil -AtomicTests $AtomicTest1, $AtomicTest2 - -# Everything is ready to convert to YAML now! -$AtomicTechnique | ConvertTo-Yaml | Out-File T1118.yaml - -.OUTPUTS - -AtomicTechnique - -Outputs an object representing an atomic technique. - -The output of New-AtomicTechnique is designed to be piped to ConvertTo-Yaml. -#> - - [CmdletBinding()] - [OutputType([AtomicTechnique])] - param ( - [Parameter(Mandatory)] - [String[]] - $AttackTechnique, - - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $DisplayName, - - [Parameter(Mandatory)] - [AtomicTest[]] - [ValidateNotNull()] - $AtomicTests - ) - - $AtomicTechniqueInstance = [AtomicTechnique]::new() - - foreach ($Technique in $AttackTechnique) { - # Attack techniques should match the MITRE ATT&CK [sub-]technique format. - # This is not a requirement so just warn the user. - if ($Technique -notmatch '^(?-i:T\d{4}(\.\d{3}){0,1})$') { - Write-Warning "The following supplied attack technique does not start with 'T' followed by a four digit number: $Technique" - } - } - - $AtomicTechniqueInstance.attack_technique = $AttackTechnique - $AtomicTechniqueInstance.display_name = $DisplayName - $AtomicTechniqueInstance.atomic_tests = $AtomicTests - - return $AtomicTechniqueInstance -} - -function New-AtomicTest { - <# -.SYNOPSIS - -Specifies an atomic test. - -.PARAMETER Name - -Specifies the name of the test that indicates how it tests the technique. - -.PARAMETER Description - -Specifies a long form description of the test. Markdown is supported. - -.PARAMETER SupportedPlatforms - -Specifies the OS/platform on which the test is designed to run. The following platforms are currently supported: Windows, macOS, Linux. - -A single test can support multiple platforms. - -.PARAMETER ExecutorType - -Specifies the the framework or application in which the test should be executed. The following executor types are currently supported: CommandPrompt, Sh, Bash, PowerShell. - -- CommandPrompt: The Windows Command Prompt, aka cmd.exe - Requires the -ExecutorCommand argument to contain a multi-line script that will be preprocessed and then executed by cmd.exe. - -- PowerShell: PowerShell - Requires the -ExecutorCommand argument to contain a multi-line PowerShell scriptblock that will be preprocessed and then executed by powershell.exe - -- Sh: Linux's bourne shell - Requires the -ExecutorCommand argument to contain a multi-line script that will be preprocessed and then executed by sh. - -- Bash: Linux's bourne again shell - Requires the -ExecutorCommand argument to contain a multi-line script that will be preprocessed and then executed by bash. - -.PARAMETER ExecutorElevationRequired - -Specifies that the test must run with elevated privileges. - -.PARAMETER ExecutorSteps - -Specifies a manual list of steps to execute. This should be specified when the atomic test cannot be executed in an automated fashion, for example when GUI steps are involved that cannot be automated. - -.PARAMETER ExecutorCommand - -Specifies the command to execute as part of the atomic test. This should be specified when the atomic test can be executed in an automated fashion. - -The -ExecutorType specified will dictate the command specified, e.g. PowerShell scriptblock code when the "PowerShell" ExecutorType is specified. - -.PARAMETER ExecutorCleanupCommand - -Specifies the command to execute if there are any artifacts that need to be cleaned up. - -.PARAMETER InputArguments - -Specifies one or more input arguments. Input arguments are defined using the New-AtomicTestInputArgument function. - -.PARAMETER DependencyExecutorType - -Specifies an override execution type for dependencies. By default, dependencies are executed using the framework specified in -ExecutorType. - -In most cases, 'PowerShell' is specified as a dependency executor type when 'CommandPrompt' is specified as an executor type. - -.PARAMETER Dependencies - -Specifies one or more dependencies. Dependencies are defined using the New-AtomicTestDependency function. - -.EXAMPLE - -$InputArg1 = New-AtomicTestInputArgument -Name filename -Description 'location of the payload' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.dll' -$InputArg2 = New-AtomicTestInputArgument -Name source -Description 'location of the source code to compile' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.cs' - -$AtomicTest = New-AtomicTest -Name 'InstallUtil uninstall method call' -Description 'Executes the Uninstall Method' -SupportedPlatforms Windows -InputArguments $InputArg1, $InputArg2 -ExecutorType CommandPrompt -ExecutorCommand @' -C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:#{filename} #{source} -C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U #{filename} -'@ - -.OUTPUTS - -AtomicTest - -Outputs an object representing an atomic test. This object is intended to be supplied to the New-AtomicTechnique -AtomicTests parameter. - -The output of New-AtomicTest can be piped to ConvertTo-Yaml. The resulting output can be added to an existing atomic technique YAML doc. -#> - - [CmdletBinding(DefaultParameterSetName = 'AutomatedExecutor')] - [OutputType([AtomicTest])] - param ( - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $Name, - - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $Description, - - [Parameter(Mandatory)] - [String[]] - [ValidateSet('Windows', 'macOS', 'Linux')] - $SupportedPlatforms, - - [Parameter(Mandatory, ParameterSetName = 'AutomatedExecutor')] - [String] - [ValidateSet('CommandPrompt', 'Sh', 'Bash', 'PowerShell')] - $ExecutorType, - - [Switch] - $ExecutorElevationRequired, - - [Parameter(Mandatory, ParameterSetName = 'ManualExecutor')] - [String] - [ValidateNotNullOrEmpty()] - $ExecutorSteps, - - [Parameter(Mandatory, ParameterSetName = 'AutomatedExecutor')] - [String] - [ValidateNotNullOrEmpty()] - $ExecutorCommand, - - [String] - [ValidateNotNullOrEmpty()] - $ExecutorCleanupCommand, - - [AtomicInputArgument[]] - $InputArguments, - - [String] - [ValidateSet('CommandPrompt', 'Sh', 'Bash', 'PowerShell')] - $DependencyExecutorType, - - [AtomicDependency[]] - $Dependencies - ) - - $AtomicTestInstance = [AtomicTest]::new() - - $AtomicTestInstance.name = $Name - $AtomicTestInstance.description = $Description - $AtomicTestInstance.supported_platforms = $SupportedPlatforms | ForEach-Object { $_.ToLower() } - - $StringsWithPotentialInputArgs = New-Object -TypeName 'System.Collections.Generic.List`1[String]' - - switch ($PSCmdlet.ParameterSetName) { - 'AutomatedExecutor' { - $ExecutorInstance = [AtomicExecutorDefault]::new() - $ExecutorInstance.command = $ExecutorCommand - $StringsWithPotentialInputArgs.Add($ExecutorCommand) - } - - 'ManualExecutor' { - $ExecutorInstance = [AtomicExecutorManual]::new() - $ExecutorInstance.steps = $ExecutorSteps - $StringsWithPotentialInputArgs.Add($ExecutorSteps) - } - } - - switch ($ExecutorType) { - 'CommandPrompt' { $ExecutorInstance.name = 'command_prompt' } - default { $ExecutorInstance.name = $ExecutorType.ToLower() } - } - - if ($ExecutorCleanupCommand) { - $ExecutorInstance.cleanup_command = $ExecutorCleanupCommand - $StringsWithPotentialInputArgs.Add($ExecutorCleanupCommand) - } - - if ($ExecutorElevationRequired) { $ExecutorInstance.elevation_required = $True } - - if ($Dependencies) { - foreach ($Dependency in $Dependencies) { - $StringsWithPotentialInputArgs.Add($Dependency.description) - $StringsWithPotentialInputArgs.Add($Dependency.prereq_command) - $StringsWithPotentialInputArgs.Add($Dependency.get_prereq_command) - } - } - - if ($DependencyExecutorType) { - switch ($DependencyExecutorType) { - 'CommandPrompt' { $AtomicTestInstance.dependency_executor_name = 'command_prompt' } - default { $AtomicTestInstance.dependency_executor_name = $DependencyExecutorType.ToLower() } - } - } $AtomicTestInstance.dependencies = $Dependencies - - [Hashtable] $InputArgHashtable = @{ } - - if ($InputArguments.Count) { - # Determine if any of the input argument names repeat. They must be unique. - $InputArguments | Group-Object -Property Name | Where-Object { $_.Count -gt 1 } | ForEach-Object { - Write-Error "There are $($_.Count) instances of the $($_.Name) input argument. Input argument names must be unique." - return - } - - # Convert each input argument to a hashtable where the key is the Name property. - - foreach ($InputArg in $InputArguments) { - # Create a copy of the passed input argument that doesn't include the "Name" property. - # Passing in a shallow copy adversely affects YAML serialization for some reason. - $NewInputArg = [AtomicInputArgument]::new() - $NewInputArg.default = $InputArg.default - $NewInputArg.description = $InputArg.description - $NewInputArg.type = $InputArg.type - - $InputArgHashtable[$InputArg.Name] = $NewInputArg - } - - $AtomicTestInstance.input_arguments = $InputArgHashtable - } - - # Extract all specified input arguments from executor and any dependencies. - $Regex = [Regex] '#\{(?[^}]+)\}' - [String[]] $InputArgumentNamesFromExecutor = $StringsWithPotentialInputArgs | - ForEach-Object { $Regex.Matches($_) } | - Select-Object -ExpandProperty Groups | - Where-Object { $_.Name -eq 'ArgName' } | - Select-Object -ExpandProperty Value | - Sort-Object -Unique - - - # Validate that all executor arguments are defined as input arguments - if ($InputArgumentNamesFromExecutor.Count) { - $InputArgumentNamesFromExecutor | ForEach-Object { - if ($InputArgHashtable.Keys -notcontains $_) { - Write-Error "The following input argument was specified but is not defined: '$_'" - return - } - } - } - - # Validate that all defined input args are utilized at least once in the executor. - if ($InputArgHashtable.Keys.Count) { - $InputArgHashtable.Keys | ForEach-Object { - if ($InputArgumentNamesFromExecutor -notcontains $_) { - # Write a warning since this scenario is not considered a breaking change - Write-Warning "The following input argument is defined but not utilized: '$_'." - } - } - } - - $AtomicTestInstance.executor = $ExecutorInstance - - return $AtomicTestInstance -} - -function New-AtomicTestDependency { - <# -.SYNOPSIS - -Specifies a new dependency that must be met prior to execution of an atomic test. - -.PARAMETER Description - -Specifies a human-readable description of the dependency. This should be worded in the following form: SOMETHING must SOMETHING - -.PARAMETER PrereqCommand - -Specifies commands to check if prerequisites for running this test are met. - -For the "command_prompt" executor, if any command returns a non-zero exit code, the pre-requisites are not met. - -For the "powershell" executor, all commands are run as a script block and the script block must return 0 for success. - -.PARAMETER GetPrereqCommand - -Specifies commands to meet this prerequisite or a message describing how to meet this prereq - -More specifically, this command is designed to satisfy either of the following conditions: - -1) If a prerequisite is not met, perform steps necessary to satify the prerequisite. Such a command should be implemented when prerequisites can be satisfied in an automated fashion. -2) If a prerequisite is not met, inform the user what the steps are to satisfy the prerequisite. Such a message should be presented to the user in the case that prerequisites cannot be satisfied in an automated fashion. - -.EXAMPLE - -$Dependency = New-AtomicTestDependency -Description 'Folder to zip must exist (#{input_file_folder})' -PrereqCommand 'test -e #{input_file_folder}' -GetPrereqCommand 'echo Please set input_file_folder argument to a folder that exists' - -.OUTPUTS - -AtomicDependency - -Outputs an object representing an atomic test dependency. This object is intended to be supplied to the New-AtomicTest -Dependencies parameter. - -Note: due to a bug in PowerShell classes, the get_prereq_command property will not display by default. If all fields must be explicitly displayed, they can be viewed by piping output to "Select-Object description, prereq_command, get_prereq_command". -#> - - [CmdletBinding()] - [OutputType([AtomicDependency])] - param ( - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $Description, - - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $PrereqCommand, - - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $GetPrereqCommand - ) - - $DependencyInstance = [AtomicDependency]::new() - - $DependencyInstance.description = $Description - $DependencyInstance.prereq_command = $PrereqCommand - $DependencyInstance.get_prereq_command = $GetPrereqCommand - - return $DependencyInstance -} - -function New-AtomicTestInputArgument { - <# -.SYNOPSIS - -Specifies an input to an atomic test that is a requirement to run the test (think of these like function arguments). - -.PARAMETER Name - -Specifies the name of the input argument. This must be lowercase and can optionally, have underscores. The input argument name is what is specified as arguments within executors and dependencies. - -.PARAMETER Description - -Specifies a human-readable description of the input argument. - -.PARAMETER Type - -Specifies the data type of the input argument. The following data types are supported: Path, Url, String, Integer, Float. If an alternative data type must be supported, use the -TypeOverride parameter. - -.PARAMETER TypeOverride - -Specifies an unsupported input argument data type. Specifying this parameter should not be common. - -.PARAMETER Default - -Specifies a default value for an input argument if one is not specified via the Invoke-AtomicTest -InputArgs parameter. - -.EXAMPLE - -$AtomicInputArgument = New-AtomicTestInputArgument -Name 'rar_exe' -Type Path -Description 'The RAR executable from Winrar' -Default '%programfiles%\WinRAR\Rar.exe' - -.OUTPUTS - -AtomicInputArgument - -Outputs an object representing an atomic test input argument. This object is intended to be supplied to the New-AtomicTest -InputArguments parameter. -#> - - [CmdletBinding(DefaultParameterSetName = 'PredefinedType')] - [OutputType([AtomicInputArgument])] - param ( - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $Name, - - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $Description, - - [Parameter(Mandatory, ParameterSetName = 'PredefinedType')] - [String] - [ValidateSet('Path', 'Url', 'String', 'Integer', 'Float')] - $Type, - - [Parameter(Mandatory, ParameterSetName = 'TypeOverride')] - [String] - [ValidateNotNullOrEmpty()] - $TypeOverride, - - [Parameter(Mandatory)] - [String] - [ValidateNotNullOrEmpty()] - $Default - ) - - if ($Name -notmatch '^(?-i:[0-9a-z_]+)$') { - Write-Error "Input argument names must be lowercase and optionally, contain underscores. Input argument name supplied: $Name" - return - } - - $AtomicInputArgInstance = [AtomicInputArgument]::new() - - $AtomicInputArgInstance.description = $Description - $AtomicInputArgInstance.default = $Default - - if ($Type) { - $AtomicInputArgInstance.type = $Type - - # Validate input argument types when it makes sense to do so. - switch ($Type) { - 'Url' { - if (-not [Uri]::IsWellFormedUriString($Type, [UriKind]::RelativeOrAbsolute)) { - Write-Warning "The specified Url is not properly formatted: $Type" - } - } - - 'Integer' { - if (-not [Int]::TryParse($Type, [Ref] $null)) { - Write-Warning "The specified Int is not properly formatted: $Type" - } - } - - 'Float' { - if (-not [Double]::TryParse($Type, [Ref] $null)) { - Write-Warning "The specified Float is not properly formatted: $Type" - } - } - - # The following supported data types do not make sense to validate: - # 'Path' { } - # 'String' { } - } - } - else { - $AtomicInputArgInstance.type = $TypeOverride - } - - # Add Name as a note property since the Name property cannot be defined in the AtomicInputArgument - # since it must be stored as a hashtable where the name is the key. Fortunately, ConvertTo-Yaml - # won't convert note properties during serialization. - $InputArgument = Add-Member -InputObject $AtomicInputArgInstance -MemberType NoteProperty -Name Name -Value $Name -PassThru - - return $InputArgument -} +# The class definitions that these functions rely upon are located in Private\AtomicClassSchema.ps1 + +function New-AtomicTechnique { + <# +.SYNOPSIS + +Specifies a new atomic red team technique. The output of this function is designed to be piped directly to ConvertTo-Yaml, eliminating the need to work with YAML directly. + +.PARAMETER AttackTechnique + +Specifies one or more MITRE ATT&CK techniques that to which this technique applies. Per MITRE naming convention, an attack technique should start with "T" followed by a 4 digit number. The MITRE sub-technique format is also supported: TNNNN.NNN + +.PARAMETER DisplayName + +Specifies the name of the technique as defined by ATT&CK. Example: 'Audio Capture' + +.PARAMETER AtomicTests + +Specifies one or more atomic tests. Atomic tests are created using the New-AtomicTest function. + +.EXAMPLE + +$InputArg1 = New-AtomicTestInputArgument -Name filename -Description 'location of the payload' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.dll' +$InputArg2 = New-AtomicTestInputArgument -Name source -Description 'location of the source code to compile' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.cs' + +$AtomicTest1 = New-AtomicTest -Name 'InstallUtil uninstall method call' -Description 'Executes the Uninstall Method' -SupportedPlatforms Windows -InputArguments @($InputArg1, $InputArg2) -ExecutorType CommandPrompt -ExecutorCommand @' +C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:#{filename} #{source} +C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U #{filename} +'@ + +# Note: the input arguments are identical for atomic test #1 and #2 +$AtomicTest2 = New-AtomicTest -Name 'InstallUtil GetHelp method call' -Description 'Executes the Help property' -SupportedPlatforms Windows -InputArguments @($InputArg1, $InputArg2) -ExecutorType CommandPrompt -ExecutorCommand @' +C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:#{filename} #{source} +C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /? #{filename} +'@ + +$AtomicTechnique = New-AtomicTechnique -AttackTechnique T1118 -DisplayName InstallUtil -AtomicTests $AtomicTest1, $AtomicTest2 + +# Everything is ready to convert to YAML now! +$AtomicTechnique | ConvertTo-Yaml | Out-File T1118.yaml + +.OUTPUTS + +AtomicTechnique + +Outputs an object representing an atomic technique. + +The output of New-AtomicTechnique is designed to be piped to ConvertTo-Yaml. +#> + + [CmdletBinding()] + [OutputType([AtomicTechnique])] + param ( + [Parameter(Mandatory)] + [String[]] + $AttackTechnique, + + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $DisplayName, + + [Parameter(Mandatory)] + [AtomicTest[]] + [ValidateNotNull()] + $AtomicTests + ) + + $AtomicTechniqueInstance = [AtomicTechnique]::new() + + foreach ($Technique in $AttackTechnique) { + # Attack techniques should match the MITRE ATT&CK [sub-]technique format. + # This is not a requirement so just warn the user. + if ($Technique -notmatch '^(?-i:T\d{4}(\.\d{3}){0,1})$') { + Write-Warning "The following supplied attack technique does not start with 'T' followed by a four digit number: $Technique" + } + } + + $AtomicTechniqueInstance.attack_technique = $AttackTechnique + $AtomicTechniqueInstance.display_name = $DisplayName + $AtomicTechniqueInstance.atomic_tests = $AtomicTests + + return $AtomicTechniqueInstance +} + +function New-AtomicTest { + <# +.SYNOPSIS + +Specifies an atomic test. + +.PARAMETER Name + +Specifies the name of the test that indicates how it tests the technique. + +.PARAMETER Description + +Specifies a long form description of the test. Markdown is supported. + +.PARAMETER SupportedPlatforms + +Specifies the OS/platform on which the test is designed to run. The following platforms are currently supported: Windows, macOS, Linux. + +A single test can support multiple platforms. + +.PARAMETER ExecutorType + +Specifies the the framework or application in which the test should be executed. The following executor types are currently supported: CommandPrompt, Sh, Bash, PowerShell. + +- CommandPrompt: The Windows Command Prompt, aka cmd.exe + Requires the -ExecutorCommand argument to contain a multi-line script that will be preprocessed and then executed by cmd.exe. + +- PowerShell: PowerShell + Requires the -ExecutorCommand argument to contain a multi-line PowerShell scriptblock that will be preprocessed and then executed by powershell.exe + +- Sh: Linux's bourne shell + Requires the -ExecutorCommand argument to contain a multi-line script that will be preprocessed and then executed by sh. + +- Bash: Linux's bourne again shell + Requires the -ExecutorCommand argument to contain a multi-line script that will be preprocessed and then executed by bash. + +.PARAMETER ExecutorElevationRequired + +Specifies that the test must run with elevated privileges. + +.PARAMETER ExecutorSteps + +Specifies a manual list of steps to execute. This should be specified when the atomic test cannot be executed in an automated fashion, for example when GUI steps are involved that cannot be automated. + +.PARAMETER ExecutorCommand + +Specifies the command to execute as part of the atomic test. This should be specified when the atomic test can be executed in an automated fashion. + +The -ExecutorType specified will dictate the command specified, e.g. PowerShell scriptblock code when the "PowerShell" ExecutorType is specified. + +.PARAMETER ExecutorCleanupCommand + +Specifies the command to execute if there are any artifacts that need to be cleaned up. + +.PARAMETER InputArguments + +Specifies one or more input arguments. Input arguments are defined using the New-AtomicTestInputArgument function. + +.PARAMETER DependencyExecutorType + +Specifies an override execution type for dependencies. By default, dependencies are executed using the framework specified in -ExecutorType. + +In most cases, 'PowerShell' is specified as a dependency executor type when 'CommandPrompt' is specified as an executor type. + +.PARAMETER Dependencies + +Specifies one or more dependencies. Dependencies are defined using the New-AtomicTestDependency function. + +.EXAMPLE + +$InputArg1 = New-AtomicTestInputArgument -Name filename -Description 'location of the payload' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.dll' +$InputArg2 = New-AtomicTestInputArgument -Name source -Description 'location of the source code to compile' -Type Path -Default 'PathToAtomicsFolder\T1118\src\T1118.cs' + +$AtomicTest = New-AtomicTest -Name 'InstallUtil uninstall method call' -Description 'Executes the Uninstall Method' -SupportedPlatforms Windows -InputArguments $InputArg1, $InputArg2 -ExecutorType CommandPrompt -ExecutorCommand @' +C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:#{filename} #{source} +C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U #{filename} +'@ + +.OUTPUTS + +AtomicTest + +Outputs an object representing an atomic test. This object is intended to be supplied to the New-AtomicTechnique -AtomicTests parameter. + +The output of New-AtomicTest can be piped to ConvertTo-Yaml. The resulting output can be added to an existing atomic technique YAML doc. +#> + + [CmdletBinding(DefaultParameterSetName = 'AutomatedExecutor')] + [OutputType([AtomicTest])] + param ( + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $Name, + + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $Description, + + [Parameter(Mandatory)] + [String[]] + [ValidateSet('Windows', 'macOS', 'Linux')] + $SupportedPlatforms, + + [Parameter(Mandatory, ParameterSetName = 'AutomatedExecutor')] + [String] + [ValidateSet('CommandPrompt', 'Sh', 'Bash', 'PowerShell')] + $ExecutorType, + + [Switch] + $ExecutorElevationRequired, + + [Parameter(Mandatory, ParameterSetName = 'ManualExecutor')] + [String] + [ValidateNotNullOrEmpty()] + $ExecutorSteps, + + [Parameter(Mandatory, ParameterSetName = 'AutomatedExecutor')] + [String] + [ValidateNotNullOrEmpty()] + $ExecutorCommand, + + [String] + [ValidateNotNullOrEmpty()] + $ExecutorCleanupCommand, + + [AtomicInputArgument[]] + $InputArguments, + + [String] + [ValidateSet('CommandPrompt', 'Sh', 'Bash', 'PowerShell')] + $DependencyExecutorType, + + [AtomicDependency[]] + $Dependencies + ) + + $AtomicTestInstance = [AtomicTest]::new() + + $AtomicTestInstance.name = $Name + $AtomicTestInstance.description = $Description + $AtomicTestInstance.supported_platforms = $SupportedPlatforms | ForEach-Object { $_.ToLower() } + + $StringsWithPotentialInputArgs = New-Object -TypeName 'System.Collections.Generic.List`1[String]' + + switch ($PSCmdlet.ParameterSetName) { + 'AutomatedExecutor' { + $ExecutorInstance = [AtomicExecutorDefault]::new() + $ExecutorInstance.command = $ExecutorCommand + $StringsWithPotentialInputArgs.Add($ExecutorCommand) + } + + 'ManualExecutor' { + $ExecutorInstance = [AtomicExecutorManual]::new() + $ExecutorInstance.steps = $ExecutorSteps + $StringsWithPotentialInputArgs.Add($ExecutorSteps) + } + } + + switch ($ExecutorType) { + 'CommandPrompt' { $ExecutorInstance.name = 'command_prompt' } + default { $ExecutorInstance.name = $ExecutorType.ToLower() } + } + + if ($ExecutorCleanupCommand) { + $ExecutorInstance.cleanup_command = $ExecutorCleanupCommand + $StringsWithPotentialInputArgs.Add($ExecutorCleanupCommand) + } + + if ($ExecutorElevationRequired) { $ExecutorInstance.elevation_required = $True } + + if ($Dependencies) { + foreach ($Dependency in $Dependencies) { + $StringsWithPotentialInputArgs.Add($Dependency.description) + $StringsWithPotentialInputArgs.Add($Dependency.prereq_command) + $StringsWithPotentialInputArgs.Add($Dependency.get_prereq_command) + } + } + + if ($DependencyExecutorType) { + switch ($DependencyExecutorType) { + 'CommandPrompt' { $AtomicTestInstance.dependency_executor_name = 'command_prompt' } + default { $AtomicTestInstance.dependency_executor_name = $DependencyExecutorType.ToLower() } + } + } $AtomicTestInstance.dependencies = $Dependencies + + [Hashtable] $InputArgHashtable = @{ } + + if ($InputArguments.Count) { + # Determine if any of the input argument names repeat. They must be unique. + $InputArguments | Group-Object -Property Name | Where-Object { $_.Count -gt 1 } | ForEach-Object { + Write-Error "There are $($_.Count) instances of the $($_.Name) input argument. Input argument names must be unique." + return + } + + # Convert each input argument to a hashtable where the key is the Name property. + + foreach ($InputArg in $InputArguments) { + # Create a copy of the passed input argument that doesn't include the "Name" property. + # Passing in a shallow copy adversely affects YAML serialization for some reason. + $NewInputArg = [AtomicInputArgument]::new() + $NewInputArg.default = $InputArg.default + $NewInputArg.description = $InputArg.description + $NewInputArg.type = $InputArg.type + + $InputArgHashtable[$InputArg.Name] = $NewInputArg + } + + $AtomicTestInstance.input_arguments = $InputArgHashtable + } + + # Extract all specified input arguments from executor and any dependencies. + $Regex = [Regex] '#\{(?[^}]+)\}' + [String[]] $InputArgumentNamesFromExecutor = $StringsWithPotentialInputArgs | + ForEach-Object { $Regex.Matches($_) } | + Select-Object -ExpandProperty Groups | + Where-Object { $_.Name -eq 'ArgName' } | + Select-Object -ExpandProperty Value | + Sort-Object -Unique + + + # Validate that all executor arguments are defined as input arguments + if ($InputArgumentNamesFromExecutor.Count) { + $InputArgumentNamesFromExecutor | ForEach-Object { + if ($InputArgHashtable.Keys -notcontains $_) { + Write-Error "The following input argument was specified but is not defined: '$_'" + return + } + } + } + + # Validate that all defined input args are utilized at least once in the executor. + if ($InputArgHashtable.Keys.Count) { + $InputArgHashtable.Keys | ForEach-Object { + if ($InputArgumentNamesFromExecutor -notcontains $_) { + # Write a warning since this scenario is not considered a breaking change + Write-Warning "The following input argument is defined but not utilized: '$_'." + } + } + } + + $AtomicTestInstance.executor = $ExecutorInstance + + return $AtomicTestInstance +} + +function New-AtomicTestDependency { + <# +.SYNOPSIS + +Specifies a new dependency that must be met prior to execution of an atomic test. + +.PARAMETER Description + +Specifies a human-readable description of the dependency. This should be worded in the following form: SOMETHING must SOMETHING + +.PARAMETER PrereqCommand + +Specifies commands to check if prerequisites for running this test are met. + +For the "command_prompt" executor, if any command returns a non-zero exit code, the pre-requisites are not met. + +For the "powershell" executor, all commands are run as a script block and the script block must return 0 for success. + +.PARAMETER GetPrereqCommand + +Specifies commands to meet this prerequisite or a message describing how to meet this prereq + +More specifically, this command is designed to satisfy either of the following conditions: + +1) If a prerequisite is not met, perform steps necessary to satify the prerequisite. Such a command should be implemented when prerequisites can be satisfied in an automated fashion. +2) If a prerequisite is not met, inform the user what the steps are to satisfy the prerequisite. Such a message should be presented to the user in the case that prerequisites cannot be satisfied in an automated fashion. + +.EXAMPLE + +$Dependency = New-AtomicTestDependency -Description 'Folder to zip must exist (#{input_file_folder})' -PrereqCommand 'test -e #{input_file_folder}' -GetPrereqCommand 'echo Please set input_file_folder argument to a folder that exists' + +.OUTPUTS + +AtomicDependency + +Outputs an object representing an atomic test dependency. This object is intended to be supplied to the New-AtomicTest -Dependencies parameter. + +Note: due to a bug in PowerShell classes, the get_prereq_command property will not display by default. If all fields must be explicitly displayed, they can be viewed by piping output to "Select-Object description, prereq_command, get_prereq_command". +#> + + [CmdletBinding()] + [OutputType([AtomicDependency])] + param ( + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $Description, + + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $PrereqCommand, + + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $GetPrereqCommand + ) + + $DependencyInstance = [AtomicDependency]::new() + + $DependencyInstance.description = $Description + $DependencyInstance.prereq_command = $PrereqCommand + $DependencyInstance.get_prereq_command = $GetPrereqCommand + + return $DependencyInstance +} + +function New-AtomicTestInputArgument { + <# +.SYNOPSIS + +Specifies an input to an atomic test that is a requirement to run the test (think of these like function arguments). + +.PARAMETER Name + +Specifies the name of the input argument. This must be lowercase and can optionally, have underscores. The input argument name is what is specified as arguments within executors and dependencies. + +.PARAMETER Description + +Specifies a human-readable description of the input argument. + +.PARAMETER Type + +Specifies the data type of the input argument. The following data types are supported: Path, Url, String, Integer, Float. If an alternative data type must be supported, use the -TypeOverride parameter. + +.PARAMETER TypeOverride + +Specifies an unsupported input argument data type. Specifying this parameter should not be common. + +.PARAMETER Default + +Specifies a default value for an input argument if one is not specified via the Invoke-AtomicTest -InputArgs parameter. + +.EXAMPLE + +$AtomicInputArgument = New-AtomicTestInputArgument -Name 'rar_exe' -Type Path -Description 'The RAR executable from Winrar' -Default '%programfiles%\WinRAR\Rar.exe' + +.OUTPUTS + +AtomicInputArgument + +Outputs an object representing an atomic test input argument. This object is intended to be supplied to the New-AtomicTest -InputArguments parameter. +#> + + [CmdletBinding(DefaultParameterSetName = 'PredefinedType')] + [OutputType([AtomicInputArgument])] + param ( + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $Name, + + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $Description, + + [Parameter(Mandatory, ParameterSetName = 'PredefinedType')] + [String] + [ValidateSet('Path', 'Url', 'String', 'Integer', 'Float')] + $Type, + + [Parameter(Mandatory, ParameterSetName = 'TypeOverride')] + [String] + [ValidateNotNullOrEmpty()] + $TypeOverride, + + [Parameter(Mandatory)] + [String] + [ValidateNotNullOrEmpty()] + $Default + ) + + if ($Name -notmatch '^(?-i:[0-9a-z_]+)$') { + Write-Error "Input argument names must be lowercase and optionally, contain underscores. Input argument name supplied: $Name" + return + } + + $AtomicInputArgInstance = [AtomicInputArgument]::new() + + $AtomicInputArgInstance.description = $Description + $AtomicInputArgInstance.default = $Default + + if ($Type) { + $AtomicInputArgInstance.type = $Type + + # Validate input argument types when it makes sense to do so. + switch ($Type) { + 'Url' { + if (-not [Uri]::IsWellFormedUriString($Type, [UriKind]::RelativeOrAbsolute)) { + Write-Warning "The specified Url is not properly formatted: $Type" + } + } + + 'Integer' { + if (-not [Int]::TryParse($Type, [Ref] $null)) { + Write-Warning "The specified Int is not properly formatted: $Type" + } + } + + 'Float' { + if (-not [Double]::TryParse($Type, [Ref] $null)) { + Write-Warning "The specified Float is not properly formatted: $Type" + } + } + + # The following supported data types do not make sense to validate: + # 'Path' { } + # 'String' { } + } + } + else { + $AtomicInputArgInstance.type = $TypeOverride + } + + # Add Name as a note property since the Name property cannot be defined in the AtomicInputArgument + # since it must be stored as a hashtable where the name is the key. Fortunately, ConvertTo-Yaml + # won't convert note properties during serialization. + $InputArgument = Add-Member -InputObject $AtomicInputArgInstance -MemberType NoteProperty -Name Name -Value $Name -PassThru + + return $InputArgument +} diff --git a/Public/Set-Sudo.ps1 b/Public/Set-Sudo.ps1 deleted file mode 100644 index ea33f89..0000000 --- a/Public/Set-Sudo.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -function Set-Sudo ($set_sudo) { - - $ErrorActionPreference = "Stop" - $env:SUDO_ASKPASS="/bin/false" - - try { - if ((sudo -A whoami) -and ((sudo grep -r $env:USER /etc/sudoers | grep NOPASSWD:ALL) -or (sudo grep -r $env:USER /etc/sudoers.d | grep NOPASSWD:ALL))){ - - if($set_sudo){ - Write-Host "Passwordless logon already configured.`n" - } - $nopassword_enabled = $true - - } - elseif ($set_sudo -eq $true){ - - Write-Host "Configuring Passwordless logon...`n" - Write-Output "$env:USER ALL=(ALL) NOPASSWD:ALL" > /tmp/90-$env:USER-sudo-access - sudo install -m 440 /tmp/90-$env:USER-sudo-access /etc/sudoers.d/90-$env:USER-sudo-access - rm -f /tmp/90-$env:USER-sudo-access - $nopassword_enabled = $true - } - else { - write-host "Host not configured for passwordless logon" - $nopassword_enabled = $false - } - } - catch { - write-host "Error configuring passwordless logon" - $nopassword_enabled = $false - } -return $nopassword_enabled -}