如何防止 PowerShell 中的变量注入?

iRo*_*Ron 4 powershell code-injection

@Ansgar Wiechers 最近对 PowerShell 问题的评论再次触动了我的心:DO NOT use Invoke-Expression关于一个安全问题,我长期以来一直在脑海中思考,需要问一下。

强烈的声明(参考Invoke-Expression 被认为是有害的文章)表明,调用可以覆盖变量的脚本被认为是有害的。

另外,PSScriptAnalyzer建议不要使用Invoke-Expression,请参阅避免使用InvokeExpression 规则

但我自己曾经使用过一种技术来更新递归脚本中的公共变量,该变量实际上可以覆盖其任何父级作用域中的值,就像这样简单:

([Ref]$ParentVariable).Value = $NewValue
Run Code Online (Sandbox Code Playgroud)

据我所知,潜在的恶意脚本也可以使用这种技术在任何情况下注入变量,无论它是如何调用的......

考虑以下“恶意”Inject.ps1脚本:

([Ref]$MyValue).Value = 456
([Ref]$MyString).Value = 'Injected string'
([Ref]$MyObject).Value = [PSCustomObject]@{Name = 'Injected'; Value = 'Object'}
Run Code Online (Sandbox Code Playgroud)

我的Test.ps1脚本:

$MyValue = 123
$MyString = "MyString"
$MyObject = [PSCustomObject]@{Name = 'My'; Value = 'Object'}
.\Inject.ps1
Write-Host $MyValue
Write-Host $MyString
Write-Host $MyObject
Run Code Online (Sandbox Code Playgroud)

结果:

456
Injected string
@{Name=Injected; Value=Object}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,Test.ps1作用域中的所有三个变量都被脚本覆盖Inject.ps1。这也可以使用 Invoke-Command cmdlet 来完成,无论我是否将变量的范围设置为Private

New-Variable -Name MyValue -Value 123 -Scope Private
$MyString = "MyString"
$MyObject = [PSCustomObject]@{Name = 'My'; Value = 'Object'}
Invoke-Command {
    ([Ref]$MyValue).Value = 456
    ([Ref]$MyString).Value = 'Injected string'
    ([Ref]$MyObject).Value = [PSCustomObject]@{Name = 'Injected'; Value = 'Object'}
}
Write-Host $MyValue
Write-Host $MyString
Write-Host $MyObject
Run Code Online (Sandbox Code Playgroud)

有没有办法完全隔离调用的脚本/命令以覆盖当前范围内的变量?
如果不是,这是否可以被视为以任何方式调用脚本的安全风险?

mkl*_*nt0 5

反对使用 use 的建议Invoke-Expression主要是为了防止意外执行代码(代码注入)。

如果您调用一段 PowerShell 代码(无论是直接还是通过Invoke-Expression),它确实可以(可能是恶意)操纵父作用域,包括全局作用域。
请注意,这种潜在的操作不仅限于变量:例如,也可以修改函数别名。

警告:运行未知代码在两个方面存在问题:

  • 主要是因为有可能直接执行不需要的/破坏性的操作[1]
  • 其次,对于恶意修改调用者状态(变量,...)的可能性,这是下面的解决方案要防范的唯一方面

为了提供所需的隔离,您有两个基本选择:

  • 在子进程中运行代码:

    • 通过启动另一个 PowerShell 实例;例如(在 Windows PowerShell 中使用powershell而不是使用pwsh):

      • pwsh -c { ./someUntrustedScript.ps1 }
    • 通过启动后台作业;例如:

      • Start-Job { ./someUntrustedScript.ps1 } | Receive-Job -Wait -AutoRemove
  • 在同一进程的单独线程中运行代码:

    • 作为线程作业,通过Start-ThreadJobcmdlet(随 PowerShell [Core] 6+ 一起提供;在 Windows PowerShell 中,可以使用类似的工具从 PowerShell Gallery
      Install-Module -Scope CurrentUser ThreadJob安装);例如:

      • Start-ThreadJob { ./someUntrustedScript.ps1 } | Receive-Job -Wait -AutoRemove
    • 通过 PowerShell SDK创建新的运行空间;例如:

      • [powershell]::Create().AddScript('./someUntrustedScript.ps1').Invoke()
      • 请注意,您必须做额外的工作才能获取成功流以外的输出流,尤其是错误流的输出另外,.Dispose()应在命令完成后在 PowerShell 实例上调用。

基于子进程的解决方案将很慢并且在您可以返回的数据类型方面受到限制(由于涉及序列化/反序列化),但它提供了针对调用的代码使进程崩溃的隔离。

基于线程的作业速度要快得多,可以返回任何数据类型,但可能会使整个进程崩溃。

在所有情况下,您都必须从调用者传递被调用代码需要访问的任何值作为参数,或者对于后台作业和线程作业,也可以通过$using:范围说明符传递。


js2010提到了其他不太理想的替代方案:

  • Start-Process(基于子进程,带有纯文本参数和输出)

  • PowerShell Workflows,已过时(它们没有移植到 PowerShell Core,将来也不会)。

  • 假设使用Invoke-Command“环回远程处理”( -ComputerName localhost) 也是一种选择,但这样会带来子进程和基于 HTTP 的通信的双重开销;此外,您的计算机必须设置为远程处理,并且您必须以提升权限运行(以管理员身份)。


[1]缓解该问题的一种方法是限制在评估字符串时允许调用哪些命令、语句、类型... ,这可以通过PowerShell SDK结合语言模式和/或通过显式构建初始会话状态。有关 SDK 使用语言模式的示例,请参阅此答案。