feat(windows): add recoverable existing-agent credential upgrade
This commit is contained in:
1 parent
3cfb0d1abe
commit
97a0139e3d
25 files changed
+11122
-108
No files matched your search
@@ -0,0 +1,247 @@
|
||||
[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)
|
||||
Reference in new issue
Block a user