Заметки · 15.03.2024

Окно, покажись!

Ещё одна захватывающая история про фокусы с удалёнными хостами в домене. Сегодня коснёмся утилиты PsExec из состава вот этого пакета. При всей популярности утилиты, я с ней сталкиваюсь достаточно редко. В основном для удалённого доступа использую изкоробочные решения, но в данном случае никаких более доступных и простых альтернатив не было. Итак, попробуем запустить программу с графическим интерфейсом на компьютере пользователя в его сеансе, а не где-то под капотом в виде очередного безликого процесса.

В уютном мирке тонких клиентов и удалённых сеансов у каждого пользователя при включении компьютера запускается лаунчер, заменяющий Проводник операционной системы. Обычно на форме лаунчера всего пара кнопок, ведущие куда-то в дебри сервера удалённых приложений или терминальных сессий.

Звучит (да и смотрится вполне) красиво, но в этой истории лаунчер располагался на сетевом ресурсе, который в один прекрасный момент ушёл в оффлайн. Вот здесь-то и начинаются приключения: пока идут работы по возвращению сетевого ресурса в строй, нужно вывести на машине хотя бы окно mstsc.

В случае с PsExec потребуется вот такая функция:

function RemoteRunGUI
{
    Param([Parameter(Mandatory=$true)]$ComputerName,
          [Parameter(Mandatory=$true)]$Command)
    $getuser = quser /server:$ComputerName | ForEach-Object { $_.Trim() -replace "\s+", "," -replace ">","" }
    $parse = $getuser | ConvertFrom-Csv
    $username = $parse.ПОЛЬЗОВАТЕЛЬ
    $idsession = $parse.ID
    Start-Process -FilePath $PSScriptRoot\psexec.exe -ArgumentList "\\$ComputerName -i $idsession -d -s $Command" -NoNewWindow | Out-Null
}

Минусы метода — надо иметь рядом со скриптом тот самый PsExec, а ещё утилита запускает процессы от имени системы удалённого хоста. С последним надо быть особенно аккуратным и понимать последствия.

Если пользователи проходят авторизацию по смарт-картам — приготовьтесь к тому, что mstsc не пропустит пользователей дальше окна ввода пин-кода, ведь локальная система не будет иметь ни малейшего представления о том, кто и куда пытается попасть.

Часть функции, которая про quser и каскад переменных, начиная с $getuser, позаимствована отсюда и изменений не претерпела.

Можно пойти дальше и скачать PsExec, если скрипт его не нашёл:

$exists = Test-Path -Path $PSScriptRoot\psexec.exe
if (!$exists)
{
    $link = "https://download.sysinternals.com/files/PSTools.zip"
    $filename = ([uri]$link).Segments[-1]
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Start-BitsTransfer -Source $link -Destination $PSScriptRoot\$filename
    Expand-Archive $PSScriptRoot\$filename -DestinationPath $PSScriptRoot\temp -Force
    Get-ChildItem $PSScriptRoot\temp -Filter "psexec.exe" -File -Recurse | Move-Item -Destination $PSScriptRoot -Force
    Remove-Item -Path $PSScriptRoot\temp -Recurse -Force
    Remove-Item -Path $PSScriptRoot\$filename -Force
}

Логика конструкции проста: не нашли файл, скачали архив, распаковали во временную папку, переместили нужный файл к скрипту, остальное удалили.

А теперь все куплеты вместе:

# Заголовок окна консоли #
[System.Console]::Title = "Запуск программы с GUI на удалённом хосте"

# Функция запуска программы с GUI на удалённом хосте #
function RemoteRunGUI
{
    Param([Parameter(Mandatory=$true)]$ComputerName,
          [Parameter(Mandatory=$true)]$Command)
    $getuser = quser /server:$ComputerName | ForEach-Object { $_.Trim() -replace "\s+", "," -replace ">","" }
    $parse = $getuser | ConvertFrom-Csv
    $username = $parse.ПОЛЬЗОВАТЕЛЬ
    $idsession = $parse.ID
    Start-Process -FilePath $PSScriptRoot\psexec.exe -ArgumentList "\\$ComputerName -i $idsession -d -s $Command" -NoNewWindow | Out-Null
}

# Скачать PSExec.exe #
$exists = Test-Path -Path $PSScriptRoot\psexec.exe
if (!$exists)
{
    $link = "https://download.sysinternals.com/files/PSTools.zip"
    $filename = ([uri]$link).Segments[-1]
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Start-BitsTransfer -Source $link -Destination $PSScriptRoot\$filename
    Expand-Archive $PSScriptRoot\$filename -DestinationPath $PSScriptRoot\temp -Force
    Get-ChildItem $PSScriptRoot\temp -Filter "psexec.exe" -File -Recurse | Move-Item -Destination $PSScriptRoot -Force
    Remove-Item -Path $PSScriptRoot\temp -Recurse -Force
    Remove-Item -Path $PSScriptRoot\$filename -Force
}

# Запросить у пользователя имя компьютера #
$compname = Read-Host "Имя компьютера"

# Команда для выполнения на удалённом хосте #
$command = "mstsc"

# Вызов функции #
RemoteRunGUI -ComputerName $compname -Command $command

# Ожидание действия #
Read-Host

Добавил переменные $compname и $command для сопровождения параметров функции RemoteRunGUI. И, само собой, никто не мешает запихнуть функцию в перебор и подсовывать имена хостов из любого массива. Например, откуда-нибудь из Active Directory:

Get-ADComputer -Filter * -SearchBase "OU=PC,OU=DEVICES,DC=local,DC=domain,DC=ru" | Select -ExpandProperty Name

Перебор организуем стандартными методами и в итоге получаем:

# Заголовок окна консоли #
[System.Console]::Title = "Запуск программы с GUI на удалённом хосте"

# Импортируем модуль для работы с Active Directory #
Import-Module ActiveDirectory

# Функция запуска программы с GUI на удалённом хосте #
function RemoteRunGUI
{
    Param([Parameter(Mandatory=$true)]$ComputerName,
          [Parameter(Mandatory=$true)]$Command)
    $getuser = quser /server:$ComputerName | ForEach-Object { $_.Trim() -replace "\s+", "," -replace ">","" }
    $parse = $getuser | ConvertFrom-Csv
    $username = $parse.ПОЛЬЗОВАТЕЛЬ
    $idsession = $parse.ID
    Start-Process -FilePath $PSScriptRoot\psexec.exe -ArgumentList "\\$ComputerName -i $idsession -d -s $Command" -NoNewWindow | Out-Null
}

# Скачать PSExec.exe #
$exists = Test-Path -Path $PSScriptRoot\psexec.exe
if (!$exists)
{
    $link = "https://download.sysinternals.com/files/PSTools.zip"
    $filename = ([uri]$link).Segments[-1]
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Start-BitsTransfer -Source $link -Destination $PSScriptRoot\$filename
    Expand-Archive $PSScriptRoot\$filename -DestinationPath $PSScriptRoot\temp -Force
    Get-ChildItem $PSScriptRoot\temp -Filter "psexec.exe" -File -Recurse | Move-Item -Destination $PSScriptRoot -Force
    Remove-Item -Path $PSScriptRoot\temp -Recurse -Force
    Remove-Item -Path $PSScriptRoot\$filename -Force
}

# Команда для выполнения на удалённом хосте #
$command = "mstsc"

# Получим список компьютеров из подразделения Active Directory #
$pcs = Get-ADComputer -Filter * -SearchBase "OU=PC,OU=DEVICES,DC=local,DC=domain,DC=ru" | Select -ExpandProperty Name
foreach($pc in $pcs)
{
    # Вызов функции для переменной $pc #
    RemoteRunGUI -ComputerName $pc -Command $command   
}

# Ожидание действия #
Read-Host

В принципе, завершающий Read-Host тут и не нужен, но иногда уж очень хочется видеть результат работы скрипта.

Теперь дело за малым: вернуть доступ к сетевому ресурсу, безопасности ради ребутнуть на фиг хосты, куда дотянулась функция RemoteRunGUI, чтобы все могли работать по спокойному и привычному распорядку.