Когда появляется необходимость сделать работу на компьютере пользователя, по его заявке, иногда бывает сложно узнать имя компьютера для подключения к нему. В этой статье будет рассмотрена возможность сбора всех активных сеансов пользователей (тех кто прошел аутентификацию на определенном компьютере) на всех компьютерах AD. Конечно, есть сторонние программы, которые решают такие проблемы и встроенные средства, но тут будет рассмотрена реализация через Powershell. Такой список так же хорошо подойдет для аудита, если понадобится быстро узнать кто пользовался компьютером и когда.
Как получить список залогиненных пользователей
В Powershell есть несколько методов возврата списка активных пользователей. Каждый из способов имеет свои минусы и поэтому, в зависимости от цели, какой-то способ будет удобнее использовать локально (при входе пользователя), а какой-то удаленно. В случае с WMI это так и не получилось сделать, так нет класса, который бы хранил все нужные значение и работал бы везде.
Локально при входе пользователя
Самый простой способ - это поместить скрипт в планировщик задач, который будет выполняться при входе пользователя в систему. Этот способ не подразумевает удаленной работы. Почти полный скрипт выглядит так:
[PSCustomObject]@{
UserName=$env:username;
ComputerName=$env:computername;
Date=(Get-Date -Format 'dd/MM/yyyy hh:mm:ss');
}
Можно создать политику, которая будет запускать скрипт при входе и экспортировать в CSV. Сам файл CSV может находится на удаленном компьютере. Следующая команда сделает это записав данные в конец существующий файл (т.е. не перезапишет файл):
$date = [PSCustomObject]@{
UserName=$env:username;
ComputerName=$env:computername;
Date=(Get-Date -Format 'dd/MM/yyyy hh:mm:ss');
}
$data | Export-Csv -Path '\\Ad1\c$\users_logged.csv' -NoOverwrite -NoTypeInformation -Append
Получится примерно следующий вид файла:
Как создавать команды и функции в Powershell вызывать их и передавать параметры
Системная утилита quser.exe
Мы можем использовать системную программу quser, которая возвращает имя текущего пользователя и время его входа. Выглядит это так:
quser
Quser так же может работать удаленно используя следующий синтаксис:
quser /server:localhost
quser /server:127.0.0.1
Первая проблема этого способа - это то, что quser работает через RPC. Если вы выполняете команду удаленно, а порты не открыты, вы получите ошибки:
- Error 0x000006BA enumerating sessionnames
- Error [1722]:The RPC server is unavailable.
Для примера, следующие команды исправят эти проблемы на одном компьютере, но скорее всего вы будете менять настройки через политики. Так же обратите внимание, что правило устанавливается на "Any", а не только "Domain":
# Должно хватить первых двух команд
New-NetFirewallRule -Profile Any -DisplayName "Open Port 135 RPC" -Direction Inbound -Protocol TCP -LocalPort 135
New-NetFirewallRule -Profile Any -DisplayName "Open Port 445 RPC" -Direction Inbound -Protocol TCP -LocalPort 445
New-NetFirewallRule -Profile Any -DisplayName "Open Port 135 RPC" -Direction Inbound -Protocol UDP -LocalPort 135
New-NetFirewallRule -Profile Any -DisplayName "Open Port 445 RPC" -Direction Inbound -Protocol UDP -LocalPort 445
Вторая проблема - если вы будете выполнять команду через Invoke-Command, то могут быть проблемы с кодировками:
И третья проблема - у нас возвращается строка, а не объект. Что бы мы могли все эти данные, в дальнейшем, экспортировать (например в CSV), мы должны ее парсить. Это можно сделать так:
# Имя компьютера, к которому подключаться
$computer_name = "localhost"
# Выполняем удаленный запрос
$query = quser /server:$computer_name
# Убираем первую строку из вывода, заменяем два пробела запятыми
$query = ($query -split '\n')[1] -replace '\s{2,}',','
# Преобразуем в массив
$result = $query -split ','
Далее нам нужно преобразовать все в специальный массив - PSCustomObject, т.к. только он может быть экспортирован в CSV и представляет собой более удобный вывод:
$user_logged = [PSCustomObject]@{
ComputerName = $computer_name;
UserName = $result[0].substring(1);
DateLogged = $result[-1];
}
Метод substring убирает первый символ, так как программа возвращает имя пользователя либо с пробелом начали или символом ">".
Такой скрипт мы можем объединить в один командлет, который сможет работать локально и удаленно:
function Get-UserLogged {
# Эта часть принимает имена компьютеров через конвейер (значение по умолчанию localhost)
[cmdletbinding()]
Param (
[parameter(ValueFromPipeline=$True)]
[string]
# Имя компьютера, к которому подключаться
$ComputerName = $env:computername
)
process {
# Выполняем удаленный запрос и
# игнорируем выключенные компьютеры
$query = quser /server:$ComputerName 2>Null
# Проверка ошибок с доступностью портов, протоколов
if ($query -like "*Error*"){
$UserName = ""
# Ошибка будет отображаться в этом поле, в одну строку
$DateLogged = $query -replace '\n',' '
}
elseif ($query -ne $Null ){
# Убираем первую строку из вывода,
# заменяем два пробела запятыми
$query = ($query -split '\n')[1] -replace '\s{2,}',','
# Преобразуем в массив
$result = $query -split ','
# разделяем массив на объекты
$UserName = $result[0].substring(1);
$DateLogged = $result[-1];
}
else {
$UserName = ""
# Если ответ $querry равен Null,
# то будет выводиться следующее сообщение
$DateLogged = "Компьютер выключен или пользователь не вошел в систему"
}
# На некоторых компьютерах появляется надпись "Отсутствует"
# способ ниже уберет ее, если появится
$DateLogged = $DateLogged -replace 'отсутствует ',''
# Добавляем все объекты в массив типа PSCustomObject
$user_logged = [PSCustomObject]@{
ComputerName = $ComputerName;
UserName = $UserName;
DateLogged = $DateLogged;
}
return $user_logged
}
}
В функцию добавлено несколько деталей:
- Функция имеет атрибут "ComputerName" в которую можно передать имя компьютера. Т.к. эта переменная является строкой мы не можем использовать одновременно несколько (конвейер передает по одному);
- По умолчанию "ComputerName" выполняет $env:computername, что возвращает имя текущего компьютера;
- Часть команды "quser /server:$ComputerName 2>Null " будет исключать некоторые ошибки, которые связаны с выключенными компьютерами. Иначе - будут выводиться красные сообщения мешающие выводу;
- Добавлено несколько условий, которые различают логические ошибки (например фаервол), физические (компьютер выключен) и условие в случае если все хорошо;
Мы можем вызывать скрипт несколькими приемами:
# Получение данных со всего AD
(Get-ADComputer -Filter *).Name | Get-UserLogged
# Получение данных с локального компьютера
Get-UserLogged
# Получение данных с одного удаленного компьютера
Get-UserLogged -ComputerName 'CL5'
# Так делать нельзя
Get-UserLogged -ComputerName 'CL5','AD1'
Отмечу, что в случаях выключенных компьютеров запросы идут очень долго (около 2-3 секунд на компьютер). Способа снизить конкретно таймаут - я не знаю. Один из вариантов ускорить работу - фильтровать вывод с Get-ADComputer исключая отключенные учетные записи компьютеров. Так же можно попробовать использовать параллелизм.
Легче всего такой скрипт запускать на компьютерах пользователей, по событию входа в систему. На примере ниже я экспортирую эти данные в единый, для всех пользователей, файл CSV расположенный в доступности для пользователей:
Get-UserLogged | Export-Csv -Path '\\Ad1\c$\users_logged.csv' -NoOverwrite -NoTypeInformation -Append
Сам файл будет выглядеть так:
Отмечу следующие моменты:
- Можно увидеть разницу во времени и датах. У меня одна ОС с американской локализацией, а другая с русской. В принципе у вас таких проблем быть не должно, но можно исправить через Get-Date (парсингом даты);
- Из-за предыдущего пункта у меня бывали проблемы с кодировками, но они самоустранились быстрее, чем я смог предпринять действия.
Через файл ntuser.dat
Каждый раз, как пользователь входит в систему все его настройки загружаются из файла ntuser.dat, который находится в домашнем каталоге 'C:\Users\UserName\'. При выходе из системы все настройки записываются в этот же файл. То есть мы можем получить имя пользователя по дате изменения этого файла.
В этом примере вернутся все каталоги пользователей:
$home_dirs = (Get-ChildItem -Path 'C:\Users\').FullName
$home_dirs
Получим даты изменения файлов 'ntuser.dat':
$home_dirs = (Get-ChildItem -Path 'C:\Users\').FullName
$home_dirs | Get-ChildItem -Filter 'ntuser.dat' -Force
Извлечем из пути имя пользователя и уберем лишние колонки:
$home_dirs = (Get-ChildItem -Path 'C:\Users\').FullName
# Поместим корректные данные в этот массив
$new_object = @()
foreach ($dir in $home_dirs){
# Получим директории, где есть файл
$result = Get-ChildItem -Path $dir -Filter 'ntuser.dat' -Force
if ($result){
# Создаем из полного пути массив и берем последний элемент (имя пользователя)
$user_name = ($dir -split '\\')[-1]
# Получаем время изменения файла
$logoff_date = $result.LastWriteTime
# Помещаем все данные в этот объект
$new_object += [PSCustomObject]@{
UserName = $user_name;
Computer = $env:computername;
LogoffDate = $logoff_date;
}
}
}
$new_object
Как вы знаете к большинству компьютеров можно подключится используя следующие пути:
# Вернет корень диска C
\\ComputerName\C$
Это же мы можем использовать с командой Get-ChildItem. Соединим все это в функцию:
function Get-UserLogged{
# Эта часть принимает имена компьютеров через конвейер (значение по умолчанию localhost)
[cmdletbinding()]
Param (
[parameter(ValueFromPipeline=$True)]
[string]
# Имя компьютера, к которому подключаться
$ComputerName = $env:computername
)
process {
# Форматируем строку
$path = '\\{0}\C$\Users\' -f $ComputerName
$home_dirs = (Get-ChildItem -Path $path ).FullName
# Поместим корректные данные в этот массив
$new_object = @()
foreach ($dir in $home_dirs){
# Получим директории, где есть файл
$result = Get-ChildItem -Path $dir -Filter 'ntuser.dat' -Force
if ($result){
# Создаем из полного пути массив и берем последний элемент (имя пользователя)
$user_name = ($dir -split '\\')[-1]
# Получаем время изменения файла
$logoff_date = $result.LastWriteTime
# Помещаем все данные в этот объект
$new_object += [PSCustomObject]@{
UserName = $user_name;
Computer = $ComputerName;
LogoffDate = $logoff_date;
}
}
}
return $new_object
}
}
Далее мы можем использовать команду в таких вариациях:
# Получение данных со всего AD
(Get-ADComputer -Filter *).Name | Get-UserLogged -ErrorAction SilentlyContinue
# Получение данных с локального компьютера
Get-UserLogged
# Получение данных с одного удаленного компьютера
Get-UserLogged -ComputerName 'CL5'
Ключ '-ErrorAction SilentlyContinue' нужен для игнорирования ошибок связанных с выключенными компьютерами. Если его не написать вы получите ошибки формата:
- Get-ChildItem : Cannot find path '\\CL2\C$\Users\' because it does not exist.
Отмечу несколько моментов:
- В отличие от первого скрипта Get-ChildItem может принимать массивы. Изменив строки на массивы вы можете немного ускорить работу скрипта. То есть вы можете написать
"Get-Childitem -Path '\\Computer1\C$\Users', '\\Computer2\C$\Users' "; - LogoffDate - это отдельный тип данных даты и времени, а это значит, например, что мы можем увидеть кто вышел за последний час/сутки. Пример будет ниже.
- Если вы выполняете команды типа 'Invoke-Command' (удаленные команды) - они тоже могут изменить файл ntuser.dat. То есть вы возможно захотите исключить часть пользователей из финального списка. Пример ниже.
Представим, что мы захотим сформировать список из тех пользователей, которые выполнили выход за последний час. Это можно сделать так:
Get-UserLogged | Where-Object -Property LogoffDate -LT ((Get-Date).AddHours(-1))
Исключить пользователей мы можем так же:
$exclude = @('Admin', 'Administrator')
Get-UserLogged | where-object -Property UserName -NotIn $exclude
Экспорт для Excel аналогичен предыдущему примеру:
$computers = (Get-ADComputer -Filter *).Name
$users = $computers | Get-UserLogged -ErrorAction SilentlyContinue
$users | Export-Csv -Path 'C:\users_logged.csv' -NoOverwrite -NoTypeInformation -Append
Get-Date - примеры работы с датой в Powershell
Через WMI
У WMI есть множество классов, которые хранят значения времени входа пользователей и их времена. Например класс 'Win32_NetworkLoginProfile' тоже хранит их, но при удаленном использовании - вернет время входа только локальных пользователей, а не доменных. Класс 'Win32_UserProfile' - тоже хранит 'LastUseTime', но это время не обозначает именно процесс успешного входа после ввода логина и пароля. Класс 'win32_computersystem' отображает только имя пользователя.
Я пробовал и другие классы, но каждый со своими проблемами. Решения так и не нашел.
...
Подписывайтесь на наш Telegram канал
Теги: #powershell #ad