PowerShell 技能连载 - Linux 如何保护安全字符串(实际未保护)

当您在非 Windows 系统上的 PowerShell 中将对象序列化为 XML 时,即通过使用 Export-CliXml,所有 SecureString 数据仅被编码,而不被加密。这是根本的区别,也是安全代码一旦移植到非 Windows 操作系统后可能变得不安全的原因。

例如,如果序列化凭据,则结果仅在 Windows 操作系统上是安全的:

1
Get-Credential | Export-Clixml -Path $env:temp\secret.xml

在 XML 文件中,您可以看到 Export-CliXml 如何更改 SecureString 的表示形式并将其转换为十六进制值的列表。在 Windows 上,SecureString 数据已安全加密,并且只有加密数据的人员(并且正在使用该计算机时)才能读取。在 Linux 和 macOS 上并非如此。由于此处缺少 Windows 加密API,因此仅对 SecureString 进行编码而不是加密。

要在非 Windows 操作系统上解码序列化 XML 内容中的纯文本内容,请尝试如下代码:

1
2
3
$secret = '53007500700065007200530065006300720065007400' $bytes = $secret -split '(?<=\G.{2})(?=.)' |ForEach-Object {
[Convert]::ToByte($_, 16)}
$plain = [Text.Encoding]::Unicode.GetString($bytes)$plain

它采用编码字符串,将其分成两对,将十六进制值转换为十进制值,并通过相应的 Unicode 解码器运行结果字节。结果就是原始的密文。

简而言之,请记住,仅在 Windows 操作系统上对序列化的 SecureString 进行安全加密。在 Linux 和 macOS 上,将敏感数据发送到 Export-CliXml 不会对其进行保护。

PowerShell 技能连载 - 快速初始化多个PowerShell控制台

假设您是许多领域的管理员,例如,Azure、SharePoint、SQL、Microsoft 365。对于每种环境,您可能需要运行一些先决条件,登录某些系统并运行一些命令,直到您的 PowerShell 环境准备好为止。

这是一个简单的策略,可以帮助您自动启动不同的PowerShell控制台。在新文件夹中,放置一个具有以下内容的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# safely serialize a credential
$credPath = "$PSScriptRoot\secret.xml"
$exists = Test-Path -Path $credPath
if (!$exists)
{
$cred = Get-Credential
$cred | Export-Clixml -Path $credPath
}

# define your different environments
$action = @{
'Azure' = "$PSScriptRoot\azure.ps1"
'Teams' = "$PSScriptRoot\teams.ps1"
'Office' = "$PSScriptRoot\office.ps1"
}

# run a new PowerShell for each environment, and run the
# associated "spin-up" script:
$action.Keys | ForEach-Object {
$path = $action[$_]
Start-Process -FilePath powershell -ArgumentList "-noexit -noprofile -executionpolicy bypass -file ""$path"""
}

该示例启动了三个 PowerShell 控制台,并为每个控制台运行一个单独的启动脚本。通过将脚本 azure.ps1,teams.ps1 和 office.ps1 添加到您的文件夹,您现在可以定义初始化和准备每个控制台所需的代码。您还可以通过从任何其他脚本中读取主凭据来使用公共凭据。这是一个例子:

1
2
3
4
# set the console title bar
$host.UI.RawUI.WindowTitle = 'Administering Office'
# read the common credential from file
$cred = Import-Clixml -Path "$PSScriptRoot\secret.xml"

PowerShell 技能连载 - 识别组成员身份

如果您的脚本需要知道当前用户是否是给定组的成员,那么最快且耗费最少资源的方法是使用如下代码:

1
2
3
4
5
6
$token = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$xy = 'S-1-5-64-36'
if ($token.Groups -contains $xy)
{
"You're in this group."
}

当当前用户是由 SID S-1-5-64-36 标识的组的直接或间接成员时,该示例将执行代码(将该 SID 替换为所需的组的 SID)。

这段代码访问用户已经存在并且始终有权访问的访问令牌。无需额外耗时进行 AD 查询,并且嵌套组成员身份也没有问题。访问令牌具有当前用户所属的直接和间接组的完整列表。

所有组均按 SID 列出,这很有意义。再次解析 SID 名称既耗时又毫无意义。如果您只想知道用户是否是组的成员,则只需找出该组的 SID(即使用 Get-AdGroup),然后在上述方法中使用此 SID 即可。

PowerShell 技能连载 - 查找系统路径

有时,脚本需要知道用户桌面或开始菜单等的路径。这些路径可能会有所不同,尤其是在用户使用 OneDrive 时。要找到当前系统路径,请使用以下代码:

1
2
PS> [Environment]::GetFolderPath('Desktop')
C:\Users\tobia\OneDrive\Desktop

您可以使用 [System.Environment+SpecialFolder] 类型中定义的所有常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
PS> [Enum]::GetNames([System.Environment+SpecialFolder])
Desktop
Programs
MyDocuments
Personal
Favorites
Startup
Recent
SendTo
StartMenu
MyMusic
MyVideos
DesktopDirectory
MyComputer
NetworkShortcuts
Fonts
Templates
CommonStartMenu
CommonPrograms
CommonStartup
CommonDesktopDirectory
ApplicationData
PrinterShortcuts
LocalApplicationData
InternetCache
Cookies
History

CommonApplicationData
Windows
System
ProgramFiles
MyPictures
UserProfile
SystemX86
ProgramFilesX86
CommonProgramFiles
CommonProgramFilesX86
CommonTemplates
CommonDocuments
CommonAdminTools
AdminTools
CommonMusic
CommonPictures
CommonVideos
Resources
LocalizedResources
CommonOemLinks
CDBurning

如果您知道该路径,并且想知道是否有可以使用的已注册系统路径,请尝试相反的方法并转储所有文件夹常量及其关联的路径:

1
2
3
4
5
6
7
8
9
[Enum]::GetNames([System.Environment+SpecialFolder]) |
ForEach-Object {
# ...for each, create a new object with the constant, the associated path
# and the code required to get that path
[PSCustomObject]@{
Name = $_
Path = [Environment]::GetFolderPath($_)
}
}

结果看起来像这样:

Name                   Path
----                   ----
Desktop                C:\Users\tobia\OneDrive\Desktop
Programs               C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Start Menu\Pr...
MyDocuments            C:\Users\tobia\OneDrive\Dokumente
Personal               C:\Users\tobia\OneDrive\Dokumente
Favorites              C:\Users\tobia\Favorites
Startup                C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Start Menu\Pr...
Recent                 C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Recent
SendTo                 C:\Users\tobia\AppData\Roaming\Microsoft\Windows\SendTo
StartMenu              C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Start Menu
MyMusic                C:\Users\tobia\Music
MyVideos               C:\Users\tobia\Videos
DesktopDirectory       C:\Users\tobia\OneDrive\Desktop
MyComputer
NetworkShortcuts       C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Network Short...
Fonts                  C:\WINDOWS\Fonts
Templates              C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Templates
CommonStartMenu        C:\ProgramData\Microsoft\Windows\Start Menu
CommonPrograms         C:\ProgramData\Microsoft\Windows\Start Menu\Programs
CommonStartup          C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
CommonDesktopDirectory C:\Users\Public\Desktop
ApplicationData        C:\Users\tobia\AppData\Roaming
PrinterShortcuts       C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Printer Short...
LocalApplicationData   C:\Users\tobia\AppData\Local
InternetCache          C:\Users\tobia\AppData\Local\Microsoft\Windows\INetCache
Cookies                C:\Users\tobia\AppData\Local\Microsoft\Windows\INetCookies
History                C:\Users\tobia\AppData\Local\Microsoft\Windows\History
CommonApplicationData  C:\ProgramData
Windows                C:\WINDOWS
System                 C:\WINDOWS\system32
ProgramFiles           C:\Program Files
MyPictures             C:\Users\tobia\OneDrive\Bilder
UserProfile            C:\Users\tobia
SystemX86              C:\WINDOWS\SysWOW64
ProgramFilesX86        C:\Program Files (x86)
CommonProgramFiles     C:\Program Files\Common Files
CommonProgramFilesX86  C:\Program Files (x86)\Common Files
CommonTemplates        C:\ProgramData\Microsoft\Windows\Templates
CommonDocuments        C:\Users\Public\Documents
CommonAdminTools       C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Administr...
AdminTools             C:\Users\tobia\AppData\Roaming\Microsoft\Windows\Start Menu\Pr...
CommonMusic            C:\Users\Public\Music
CommonPictures         C:\Users\Public\Pictures
CommonVideos           C:\Users\Public\Videos
Resources              C:\WINDOWS\resources
LocalizedResources
CommonOemLinks
CDBurning              C:\Users\tobia\AppData\Local\Microsoft\Windows\Burn\Burn   #

PowerShell 技能连载 - 使用 NTFS 流(第 5 部分)

在前面的技能中,我们研究了 NTFS 流,并发现 Windows 如何使用“区域信息”流标记下载的文件。您还学习了使用 Unblock-File 从文件中删除此限制。

在最后一部分中,我们做了相反的事情,查找从不受信任的来源下载的文件。例如,此行列出了所有附加了 Zone.Identifier 流的文件:

1
2
3
4
$path = "$env:userprofile\Downloads"

Get-ChildItem -Path $Path -file |
Where-Object { @(Get-Item -Path $_.FullName -Stream *).Stream -contains 'Zone.Identifier' }

所有这些文件都来自 Windows 认为不一定可信的来源。

要了解更多信息,您必须阅读附件中的信息流。这段代码揭示了“下载”文件夹中所有“区域信息”流的全部内容:

1
2
3
4
5
6
7
$path = "$env:userprofile\Downloads"

Get-ChildItem -Path $Path -file |
Where-Object { @(Get-Item -Path $_.FullName -Stream *).Stream -contains 'Zone.Identifier' } |
ForEach-Object {
Get-Content -Path $_.FullName -Stream Zone.Identifier
}

显然,该信息包含有关引用和来源的信息,因此您可以还原此信息并找出在“下载”文件夹中找到的所有内容的来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$path = "$env:userprofile\Downloads"

Get-ChildItem -Path $Path -file |
Where-Object { @(Get-Item -Path $_.FullName -Stream *).Stream -contains 'Zone.Identifier' } |
ForEach-Object {
$info = Get-Content -Path $_.FullName -Stream Zone.Identifier
[PSCustomObject]@{
Name = $_.Name
Referrer = @(($info -like 'ReferrerUrl=*').Split('='))[-1]
HostUrl = @(($info -like 'HostUrl=*').Split('='))[-1]
Path = $_.FullName
}
} |
Out-GridView

这使您可以很好地了解下载的原始来源。

PowerShell 技能连载 - 使用 NTFS 流(第 4 部分)

每当您从 Internet(或其他不受信任的来源)下载文件并将其存储在 NTFS 驱动器上时,Windows 就会使用区域标识符对这些文件进行静默标记。例如,这就是为什么 PowerShell 拒绝执行从域外部下载的脚本的原因。

您实际上可以查看区域标识符。只要确保您从 Internet 下载文件并将其存储在 NTFS 驱动器上即可。接下来,使用此行查看区域标识符:

1
Get-Content -Path C:\users\tobia\Downloads\Flyer2021.rar -Stream Zone.Identifier

如果存在该流,则您会看到类似以下信息:

[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://shop.laserkino.de/
HostUrl=https://www.somecompany/Flyer2021.rar

它暴露了文件的来源以及从中检索文件的远程区域的类型。如果没有附加到文件的区域信息,则上述命令将引发异常。

若要从文件中删除区域信息(并删除所有限制),请使用 Unblock-File cmdlet。例如,要取消阻止“下载”文件夹中的所有文件,请尝试以下操作:

1
Get-ChildItem -Path C:\users\tobia\Downloads\ -File | Unblock-File -WhatIf

删除 -WhatIf 参数以实际删除该保护流。

PowerShell 技能连载 - 使用 NTFS 流(第 3 部分)

在上一个技能中,我们解释了 NTFS 流是如何工作的。但是,不可能发现隐藏文件流的名称。在 PowerShell 5及更高版本中,大多数访问文件系统的 cmdlet 都新增了一个名为 -Stream 的新参数。有了它,现在访问 NTFS 流变得很简单,因此现在可以重写以前脚本中使用路径名称中的冒号表示的示例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# create a sample file
$desktop = [Environment]::GetFolderPath('Desktop')
$path = Join-Path -Path $desktop -ChildPath 'testfile.txt'
'Test' | Out-File -FilePath $Path

# attach hidden info to the file
'this is hidden' | Set-Content -Path $path -Stream myHiddenStream

# get hidden info from the file
Get-Content -Path $path -Stream myHiddenStream

# remove hidden streams
Remove-Item -Path $Path -Stream myHiddenStream

# show file
explorer /select,$Path

现在,还可以查看(并发现)隐藏的 NTFS 流。让我们创建一个带有一堆流的示例文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
# create a sample file
$desktop = [Environment]::GetFolderPath('Desktop')
$path = Join-Path -Path $desktop -ChildPath 'testfile.txt'
'Test' | Out-File -FilePath $Path

# attach hidden info to the file
'this is hidden' | Set-Content -Path $path -Stream myHiddenStream
'more info' | Set-Content -Path $path -Stream additionalInfo
'anotherone' | Set-Content -Path $path -Stream 'blanks work, too'
'last' | Set-Content -Path $path -Stream finalStream

# find stream names:
Get-Item -Path $Path -Stream * | Select-Object -Property Stream, Length

现在,Get-Item 可以暴露 NTFS 流,并且输出可能如下所示:

Stream           Length
------           ------
:$DATA               14
additionalInfo       11
blanks work, too     12
finalStream           6
myHiddenStream       16

如您所见,您现在可以发现所有流的名称。流 “:$DATA” 表示文件的“可见的”主要内容。

PowerShell 技能连载 - 使用 NTFS 流(第 2 部分)

在上一个技巧中,我们解释了 NTFS 流如何存储有关文件的其他数据,这引发了一个问题,即如何删除此类流或首先发现隐藏的 NTFS 流。

要删除隐藏的命名流,请使用 Remove-Item——就像您要删除整个文件一样。这是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# create a sample file
$path = "$env:temp\test.txt"
'Test' | Out-File -FilePath $Path

# attach hidden info to the file
'this is hidden' | Set-Content -Path "${path}:myHiddenStream"

# get hidden info from the file
Get-Content -Path "${path}:myHiddenStream"

# remove hidden streams
Remove-Item -Path "${path}:myHiddenStream"

# stream is gone, this raises an error:
Get-Content -Path "${path}:myHiddenStream"

# file with main stream is still there:
explorer /select,$Path

尽管您可以像创建单个文件一样创建和删除 NTFS 流,只需添加一个冒号和流名称即可,但没有找到流名称的简单方法。至少不是我们在这里访问流的方式。在第 3 部分中,我们最终将发现隐藏的流名称。

PowerShell 技能连载 - 使用 NTFS 流(第 1 部分)

在 NTFS 文件系统上,您可以将其他信息存储在隐藏的文件流中。传统上,PowerShell 通过冒号访问文件流,因此这会将隐藏的文本信息附加到纯文本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
# create a sample file
$desktop = [Environment]::GetFolderPath('Desktop')
$path = Join-Path -Path $desktop -ChildPath 'testfile.txt'
'Test' | Out-File -FilePath $Path

# attach hidden info to the file
'this is hidden' | Set-Content -Path "${path}:myHiddenStream"

# attach even more hidden info to the file
'this is also hidden' | Set-Content -Path "${path}:myOtherHiddenStream"

# show file
explorer /select,$Path

该代码首先确定到您的桌面的路径,然后创建一个示例纯文本文件。

接下来,它在名为 “myHiddenStream” 和 “myOtherHiddenStream” 的两个流中添加隐藏信息。当您在资源管理器中查看文件时,这些流仍然不可见。

PowerShell 仍然可以像下面这样访问这些流:

1
2
3
# get hidden info from the file
Get-Content -Path "${path}:myHiddenStream"
Get-Content -Path "${path}:myOtherHiddenStream"

请注意,这些流仅存在于使用 NTFS 文件系统的存储中。当您将这些文件复制到其他文件系统时,即使用 exFat 将它们复制到USB记忆棒中,Windows 将显示一个警告对话框,提示所有流将被删除。

PowerShell 技能连载 - 观看德国电视节目

德国公共广播公司保留着丰富的电视档案,并允许用户通过 Web 界面观看其节目。通常无法下载节目或轻松找到其下载 URL。

以下脚本下载了所有节目及其网络位置的非官方目录:

1
2
3
4
# download the German mediathek database as JSON file
$path = "$env:temp\tv.json"
$url = 'http://www.mediathekdirekt.de/good.json'
Invoke-RestMethod -Uri $url -UseBasicParsing -OutFile $path

下载该文件后,您可以使用其他脚本来显示选择对话框,并浏览要观看的电视节目。然后,您可以选择一个或多个节目,并要求 PowerShell 将这些视频自动下载到您的计算机上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$Path = "$env:temp\tv.json"

$data = Get-Content -Path $Path -Raw |
ConvertFrom-Json |
ForEach-Object { $_ } |
ForEach-Object {
# define a string describing the video. This string will be shown in a grid view window
$title = '{0,5} [{2}] "{1}" ({3})' -f ([Object[]]$_)
# add the original data to the string so when the user select a video,
# the details i.e. download URL is still available
$title | Add-Member -MemberType NoteProperty -Name Data -Value $_ -PassThru
}

$data |
Sort-Object |
Out-GridView -Title 'Select Video(s)' -OutputMode Multiple |
ForEach-Object {
# take the download URL from the attached original data
$url = $_.Data[5]
$filename = Split-Path -Path $url -Leaf
$filepath = Join-Path -Path $env:temp -ChildPath $filename
$title = 'Video download {0} ({1})' -f $_.Data[1], $_.Data[0]
Start-BitsTransfer -Description $title -Source $url -Destination $filepath
# you can use a simple web request as well in case BITS isn't available
# Invoke-WebRequest -Uri $url -OutFile $filepath -UseBasicParsing

# open video in associated player
Invoke-Item -Path $filepath
}