[CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$Operation, [Parameter(Mandatory = $true)][string]$InputPath, [string]$DestinationPath = "", [ValidateSet("None", "Path")][string]$BackupMode = "None", [string]$BackupPath = "", [Parameter(Mandatory = $true)][string]$ResultPath, [Parameter(Mandatory = $true)][string]$SupportPath ) $ErrorActionPreference = "Stop" . $SupportPath $sourceCreatedByCaller = $false $sourceReadVerified = $false $sourceModifyVerified = $false $sourceRenameVerified = $false $sourceDeleteAccessVerified = $false $sourceReplaceAccessVerified = $false $replaceInvoked = $false $sourceExistedBeforeAttack = $null $destinationExistedBeforeAttack = $null $backupExistedBeforeAttack = $null $inputFullPath = "" $destinationFullPath = "" $backupFullPath = "" $backupArgumentWasNativeNull = $false function Get-ProbeMetadata { param( [Parameter(Mandatory = $true)][string]$Status, [Exception]$Exception ) $inputOwnedByCaller = $false if ([IO.File]::Exists($InputPath)) { try { $callerSid = [Security.Principal.WindowsIdentity]::GetCurrent().User $ownerSid = [IO.File]::GetAccessControl($InputPath).GetOwner( [Security.Principal.SecurityIdentifier] ) $inputOwnedByCaller = $callerSid.Equals($ownerSid) } catch { $inputOwnedByCaller = $false } } $details = if ($null -ne $Exception) { Get-MinerOpsNativeExceptionDetails -Exception $Exception } else { $null } return [ordered]@{ status = $Status operation = $Operation # Retain these two outer-exception fields for compatibility with # earlier native-harness result readers. exception_type = if ($null -ne $Exception) { $Exception.GetType().FullName } else { "" } hresult = if ($null -ne $details) { $details.OuterHResult } else { 0 } outer_exception_type = if ($null -ne $details) { $details.OuterExceptionType } else { "" } outer_hresult = if ($null -ne $details) { $details.OuterHResult } else { 0 } leaf_exception_type = if ($null -ne $details) { $details.LeafExceptionType } else { "" } leaf_hresult = if ($null -ne $details) { $details.LeafHResult } else { 0 } native_error = if ($null -ne $details) { $details.NativeError } else { $null } exception_chain_types = if ($null -ne $details) { @($details.ExceptionChainTypes) } else { @() } exception_chain_truncated = if ($null -ne $details) { [bool]$details.ExceptionChainTruncated } else { $false } source_exists = [IO.File]::Exists($InputPath) destination_exists = if ( [string]::IsNullOrWhiteSpace($DestinationPath) ) { $null } else { [IO.File]::Exists($DestinationPath) } input_owned_by_caller = $inputOwnedByCaller source_created_by_caller = $sourceCreatedByCaller source_read_verified = $sourceReadVerified source_modify_verified = $sourceModifyVerified source_rename_verified = $sourceRenameVerified source_delete_access_verified = $sourceDeleteAccessVerified source_replace_access_verified = $sourceReplaceAccessVerified input_full_path = $inputFullPath destination_full_path = $destinationFullPath backup_mode = $BackupMode backup_full_path = $backupFullPath backup_argument_was_native_null = $backupArgumentWasNativeNull source_existed_before_attack = $sourceExistedBeforeAttack destination_existed_before_attack = $destinationExistedBeforeAttack backup_existed_before_attack = $backupExistedBeforeAttack replace_overload = "System.IO.File.Replace(String,String,String)" replace_invoked = $replaceInvoked } } function Write-ProbeResult { param([Parameter(Mandatory = $true)]$Value) $json = $Value | ConvertTo-Json -Compress [IO.File]::WriteAllText($ResultPath, $json) } $exception = $null $status = "succeeded" try { switch ($Operation) { "read" { [void][IO.File]::ReadAllText($InputPath) } "create" { [IO.File]::WriteAllText($InputPath, "untrusted") } "modify" { [IO.File]::WriteAllText($InputPath, "untrusted") } "delete" { [IO.File]::Delete($InputPath) } "rename" { [IO.File]::Move($InputPath, $DestinationPath) } "prepare_swap" { if ([IO.File]::Exists($InputPath)) { throw [ArgumentException]::new( "swap source unexpectedly existed before caller creation" ) } $renameProof = "$InputPath.rename-proof-$([guid]::NewGuid().ToString('N'))" [IO.File]::WriteAllText( $InputPath, "attacker-controlled-swap-source" ) $sourceCreatedByCaller = $true $sourceReadVerified = [IO.File]::ReadAllText($InputPath) -ceq "attacker-controlled-swap-source" [IO.File]::AppendAllText($InputPath, "-modified") $sourceModifyVerified = [IO.File]::ReadAllText($InputPath) -ceq "attacker-controlled-swap-source-modified" [IO.File]::Move($InputPath, $renameProof) [IO.File]::Move($renameProof, $InputPath) $sourceRenameVerified = [IO.File]::Exists($InputPath) -and -not [IO.File]::Exists($renameProof) $requiredRights = [Security.AccessControl.FileSystemRights]::ReadData -bor [Security.AccessControl.FileSystemRights]::WriteData -bor [Security.AccessControl.FileSystemRights]::Delete -bor [Security.AccessControl.FileSystemRights]::ChangePermissions -bor [Security.AccessControl.FileSystemRights]::Synchronize $rightsProbe = [IO.FileStream]::new( $InputPath, [IO.FileMode]::Open, $requiredRights, [IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete, 4096, [IO.FileOptions]::None ) try { $sourceDeleteAccessVerified = $true $sourceReplaceAccessVerified = $true } finally { $rightsProbe.Dispose() } } "swap" { $inputFullPath = [IO.Path]::GetFullPath($InputPath) $destinationFullPath = [IO.Path]::GetFullPath($DestinationPath) if ($inputFullPath.Equals( $destinationFullPath, [StringComparison]::OrdinalIgnoreCase )) { throw [ArgumentException]::new( "swap source and destination are the same path" ) } $sourceExistedBeforeAttack = [IO.File]::Exists($inputFullPath) $destinationExistedBeforeAttack = [IO.File]::Exists($destinationFullPath) if (-not $sourceExistedBeforeAttack) { throw [IO.FileNotFoundException]::new( "swap source does not exist", $inputFullPath ) } if (-not $destinationExistedBeforeAttack) { throw [IO.FileNotFoundException]::new( "swap destination does not exist", $destinationFullPath ) } if ($BackupMode -ceq "None") { if (-not [string]::IsNullOrEmpty($BackupPath)) { throw [ArgumentException]::new( "backup path was supplied while backup mode is None" ) } # A bare PowerShell $null is coerced to String.Empty for this # .NET string parameter. NullString is the explicit native # null required to request no ReplaceFile backup. $backupArgumentWasNativeNull = $true } else { throw [ArgumentException]::new( "backup-producing swap fixtures are prohibited because " + "they can expose protected destination content" ) } $replaceInvoked = $true [IO.File]::Replace( $inputFullPath, $destinationFullPath, [NullString]::Value ) } default { throw [ArgumentException]::new("unknown operation") } } } catch { $exception = $_.Exception $status = Get-MinerOpsNativeExceptionStatus ` -Operation $Operation -Exception $exception } $result = Get-ProbeMetadata -Status $status -Exception $exception Write-ProbeResult -Value $result exit (Get-MinerOpsNativeStatusExitCode -Status $status)