我该如何在PowerShell中防止变量注入?

4
我又被一个最近关于PowerShell的问题的评论触动了,评论作者是@Ansgar Wiechers:不要使用Invoke-Expression,这与我脑海中长期存在的安全问题相关,需要提问。
强烈的声明(参考 Invoke-Expression被认为是有害的文章)表明调用一个可以覆盖变量的脚本是有害的。
此外,PSScriptAnalyzer也建议不要使用Invoke-Expression,请参见AvoidUsingInvokeExpression规则
但是,我曾经自己使用过一种技巧来更新递归脚本中的公共变量,它实际上可以覆盖任何父级范围中的值,代码非常简单:
([Ref]$ParentVariable).Value = $NewValue

据我所知,一个潜在的恶意脚本也可以使用此技术来注入变量,无论如何调用都可以实现... 请考虑以下“恶意”Inject.ps1脚本:
([Ref]$MyValue).Value = 456
([Ref]$MyString).Value = 'Injected string'
([Ref]$MyObject).Value = [PSCustomObject]@{Name = 'Injected'; Value = 'Object'}

我的Test.ps1脚本:

$MyValue = 123
$MyString = "MyString"
$MyObject = [PSCustomObject]@{Name = 'My'; Value = 'Object'}
.\Inject.ps1
Write-Host $MyValue
Write-Host $MyString
Write-Host $MyObject

结果:

456
Injected string
@{Name=Injected; Value=Object}

正如您所看到的,在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

是否有一种方法可以完全隔离调用的脚本/命令,以防止其覆盖当前作用域中的变量?
如果没有,那么以任何方式调用脚本是否会被视为安全风险?


3
@BoogaRoo:这不仅仅是关于“变量”的问题,它通常是防止不受信任的代码意外操纵,可能会影响函数和别名。即使对于变量,为了防止潜在的操纵,使用$private:来创建所有变量也是不切实际的。此外,其他经过验证的代码可能依赖于变量不是私有的。至于Invoke-Command:不幸的是,文档是错误的:默认情况下,本地调用发生在子范围中,但是您可以使用-NoNewScope选择进入同一范围执行。 - mklement0
1
@BoogaRoo 幸运的是,文档已经被纠正了。 - mklement0
1个回答

5

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

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

警告运行未知代码有两个问题:

  • 主要是因为可能直接执行不需要或破坏性的操作[1]
  • 其次,可能会恶意修改调用者的状态(变量等),这是以下解决方案唯一保护的方面

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

  • 子进程中运行代码:

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

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

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

    • 通过Start-ThreadJob cmdlet(随PowerShell [Core] 6+一起发布;在Windows PowerShell中,可以通过类似于
      Install-Module -Scope CurrentUser ThreadJob的方式安装from the PowerShell Gallery)作为线程作业运行;例如:

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

      • [powershell]::Create().AddScript('./someUntrustedScript.ps1').Invoke()
      • 请注意,您需要额外的工作来获取除成功流以外的输出流,特别是错误流的输出;此外,在完成命令时应调用PowerShell实例上的.Dispose()

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

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

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


js2010提到了其他不太理想的选择:

  • Start-Process(基于子进程,仅支持文本参数和输出)

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

  • 使用“回环远程”(-ComputerName localhost)的Invoke-Command在理论上也是一种选择,但这将导致子进程和基于HTTP的通信的双重开销;此外,您的计算机必须设置为远程,您必须以管理员身份运行。


[1] 一种缓解这个问题的方法是在计算字符串时,限制可以调用的命令、语句、类型等,可以通过使用PowerShell SDK结合语言模式和/或显式构建初始会话状态来实现。有关使用SDK和语言模式的示例,请参见此答案


哦,这是一个笨拙的方法来获取start-process的输出。start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'; import-clixml out.xml - js2010
@js2010 或许更加简洁的方式是将 -RedirectStandardOutput out.xml 与 CLI 的 -of XML 参数结合起来;然而,总体来说,这只是 Start-Job 更加繁琐的替代方法,并且仅限于将输入作为文本传递(除非您通过 stdin 和 -if XML 提供 CLIXML 输入)。 - mklement0
感谢更新,Start-Thread 正是我想引起你注意的地方(通过点赞回答),因为我发现它现在确实是 PowerShell 核心的一部分。我实际上正在调查的重点是它有助于防止变量注入(我所询问的内容),但并不适用于一般的代码注入,请参见恶意代码注入保护 #12377 - iRon
是的,Start-ThreadJob 是 PowerShell [Core] 6+ 的一部分(我已更新答案以使其更清晰)。我会查看 GitHub 问题。 - mklement0
1
@iRon,我还添加了一条注释到答案中,以明确那里呈现的隔离技术仅防止调用者状态的操纵,但并不普遍防止执行不需要的命令。 - mklement0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接