248 lines
8.9 KiB
PowerShell
248 lines
8.9 KiB
PowerShell
[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)
|