Files
adlockerinfo/AccountUnlocker.ps1
2025-09-15 13:39:33 +09:00

218 lines
12 KiB
PowerShell

#requires -modules ActiveDirectory
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Xaml, System.Windows.Forms
# AD 모듈 로드
try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Host "[오류] AD 모듈 로드 실패"; exit 1 }
# AD 함수 정의
function Get-LockedADAccounts {
try {
$pdc = (Get-ADDomain).PDCEmulator
$adAccounts = @(Search-ADAccount -LockedOut -Server $pdc | Get-ADUser -Properties Name, SamAccountName, LockedOut, LastLogonDate, BadPwdCount, DisplayName -Server $pdc)
$list = @()
foreach ($account in $adAccounts) {
$lastLogon = if ($account.LastLogonDate -and $account.LastLogonDate.Year -ne 1601) { $account.LastLogonDate } else { $null }
$list += [PSCustomObject]@{
Name = if ([string]::IsNullOrWhiteSpace($account.DisplayName)) { $account.Name } else { $account.DisplayName }
SamAccountName = $account.SamAccountName
LockedOut = $account.LockedOut
LastLogonDate = $lastLogon
BadPwdCount = $account.BadPwdCount
}
}
return ,$list
} catch { Write-Log "AD 조회 오류: $($_.Exception.Message)" "Red"; return $null }
}
function Unlock-ADUserAccount { param([string]$SamAccountName)
try {
$pdc = (Get-ADDomain).PDCEmulator
Unlock-ADAccount -Identity $SamAccountName -Server $pdc
return $true
} catch { Write-Log "잠금 해제 실패: $SamAccountName - $($_.Exception.Message)" "Red"; return $false }
}
function Get-ADLockoutEvent { param([string]$SamAccountName)
$dcs = (Get-ADDomainController -Filter *).Name
$latest = $null
foreach ($dc in $dcs) {
try {
$filter = @{ LogName = 'Security'; ID = 4740; StartTime = (Get-Date).AddDays(-1) }
$event = Get-WinEvent -ComputerName $dc -FilterHashtable $filter -ErrorAction SilentlyContinue |
Where-Object { $_.Properties[0].Value -eq $SamAccountName } |
Sort-Object TimeCreated -Descending | Select-Object -First 1
if ($event -and ($null -eq $latest -or $event.TimeCreated -gt $latest.TimeCreated)) { $latest = $event }
} catch {}
}
if (-not $latest) { return $null }
$lockoutTime = $latest.TimeCreated
$sourceDC = $latest.MachineName
$caller = $latest.Properties[3].Value
if ([string]::IsNullOrWhiteSpace($caller) -or $caller -eq 'S-1-5-18' -or $caller -eq $sourceDC) {
try {
$filter4625 = @{ LogName='Security'; ID=4625; StartTime=$lockoutTime.AddMinutes(-5); EndTime=$lockoutTime }
$fail = Get-WinEvent -ComputerName $sourceDC -FilterHashtable $filter4625 -ErrorAction SilentlyContinue |
Where-Object { ($_.Properties[5].Value -like "*$SamAccountName*") -and ($_.Properties[10].Value -eq '3' -or $_.Properties[10].Value -eq '10') } |
Sort-Object TimeCreated -Descending | Select-Object -First 1
if ($fail) {
$ws = $fail.Properties[13].Value
$ip = $fail.Properties[19].Value
$caller = if ($ws -and $ws -ne '-') { "$ws (IP: $ip)" } else { "IP: $ip" }
} else { $caller = "$sourceDC (자체 발생 - 4625 없음)" }
} catch {}
}
return [PSCustomObject]@{ TimeCreated=$lockoutTime; CallerComputer=$caller; DC=$sourceDC }
}
# 로그 출력
function Write-Log { param([string]$Message,[string]$Color="White")
try { $mainForm.Dispatcher.Invoke({$para=New-Object Windows.Documents.Paragraph;$run=New-Object Windows.Documents.Run($Message);$run.Foreground=(New-Object Windows.Media.BrushConverter).ConvertFromString($Color);$para.Inlines.Add($run);$LogBox.Document.Blocks.Add($para);$LogBox.ScrollToEnd()}) | Out-Null } catch { Write-Host $Message }
}
# XAML GUI 정의 (Dark, DataGrid, 버튼, 로그)
[xml]$xaml=@"
<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Title='AD ' Height='640' Width='960' WindowStartupLocation='CenterScreen' Background='#1E1E1E'>
<Grid Margin='10'>
<Grid.RowDefinitions>
<RowDefinition Height='Auto'/><RowDefinition Height='*'/><RowDefinition Height='Auto'/><RowDefinition Height='160'/>
</Grid.RowDefinitions>
<StackPanel Orientation='Horizontal' Grid.Row='0' Margin='0,0,0,10'>
<Grid Width='260' Height='28' Margin='0,0,10,0'>
<TextBox x:Name='SearchBox' Background='#2D2D30' Foreground='White' FontFamily='Segoe UI' FontSize='13' BorderThickness='0'/>
<TextBlock Text=' ...' Margin='6,0,0,0' VerticalAlignment='Center' Foreground='Gray' IsHitTestVisible='False'>
<TextBlock.Style>
<Style TargetType='TextBlock'>
<Setter Property='Visibility' Value='Collapsed'/>
<Style.Triggers>
<DataTrigger Binding='{Binding Text, ElementName=SearchBox}' Value=''><Setter Property='Visibility' Value='Visible'/></DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Button x:Name='SearchButton' Content='' Width='80' Height='28' Margin='0,0,10,0'/>
<Button x:Name='UnlockButton' Content=' ' Width='130' Height='28' Margin='0,0,10,0'/>
<Button x:Name='CheckSourceButton' Content=' ' Width='130' Height='28'/>
</StackPanel>
<DataGrid x:Name='LockedAccountsDataGrid' Grid.Row='1' AutoGenerateColumns='False' CanUserAddRows='False'
IsReadOnly='True' SelectionMode='Single' AlternatingRowBackground='#2D2D30' Background='#1E1E1E' Foreground='White'
RowBackground='#2D2D30' HorizontalGridLinesBrush='#3E3E42' VerticalGridLinesBrush='#3E3E42' ColumnHeaderHeight='30' RowHeaderWidth='0'>
<DataGrid.Resources>
<Style TargetType='DataGridColumnHeader'>
<Setter Property='Background' Value='#3E3E42'/><Setter Property='Foreground' Value='#DCDCDC'/><Setter Property='FontWeight' Value='Bold'/>
</Style>
<Style TargetType='DataGridCell'>
<Style.Triggers>
<DataTrigger Binding='{Binding LockedOut}' Value='True'>
<Setter Property='Foreground' Value='#FF6A00'/><Setter Property='FontWeight' Value='Bold'/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header=' ' Binding='{Binding Name}' Width='200'/>
<DataGridTextColumn Header=' ID' Binding='{Binding SamAccountName}' Width='150'/>
<DataGridTextColumn Header=' ' Binding='{Binding LockedOut}' Width='100'/>
<DataGridTextColumn Header=' ' Binding='{Binding LastLogonDate, StringFormat=yyyy-MM-dd HH:mm:ss}' Width='200'/>
<DataGridTextColumn Header=' ' Binding='{Binding BadPwdCount}' Width='120'/>
</DataGrid.Columns>
</DataGrid>
<Button x:Name='RefreshButton' Content='' Grid.Row='2' Width='110' Height='28' Margin='0,10,0,10' HorizontalAlignment='Left'/>
<RichTextBox x:Name='LogBox' Grid.Row='3' Background='Black' Foreground='White' IsReadOnly='True' VerticalScrollBarVisibility='Auto'/>
</Grid>
</Window>
"@
$reader = New-Object System.Xml.XmlNodeReader $xaml
$mainForm = [Windows.Markup.XamlReader]::Load($reader)
$reader.Close()
$SearchBox = $mainForm.FindName('SearchBox')
$SearchButton = $mainForm.FindName('SearchButton')
$UnlockButton = $mainForm.FindName('UnlockButton')
$CheckSourceButton = $mainForm.FindName('CheckSourceButton')
$LockedAccountsDataGrid = $mainForm.FindName('LockedAccountsDataGrid')
$RefreshButton = $mainForm.FindName('RefreshButton')
$LogBox = $mainForm.FindName('LogBox')
# 데이터 그리드 채우기
function Populate-DataGrid {
Write-Log "잠긴 계정 목록 가져오는 중..."
$data = Get-LockedADAccounts
if ($data) { $LockedAccountsDataGrid.ItemsSource = $data; Write-Log "잠김 계정 목록 새로고침 완료 (총 $($data.Count)개)" "Cyan" }
else { $LockedAccountsDataGrid.ItemsSource = $null; Write-Log "잠김 계정 없음!" "Red" }
}
$RefreshButton.Add_Click({ Populate-DataGrid })
$SearchButton.Add_Click({
$q=$SearchBox.Text.Trim()
if ([string]::IsNullOrWhiteSpace($q)) { Populate-DataGrid; return }
try {
$pdc=(Get-ADDomain).PDCEmulator
$result=Get-ADUser -Filter "SamAccountName -like '*$q*' -or Name -like '*$q*' -or DisplayName -like '*$q*'" -Server $pdc -Properties LockedOut, LastLogonDate, BadPwdCount, DisplayName |
Select-Object @{n='Name';e={ if ([string]::IsNullOrWhiteSpace($_.DisplayName)){ $_.Name } else { $_.DisplayName } }}, SamAccountName, LockedOut,
@{n='LastLogonDate';e={ $_.LastLogonDate }}, BadPwdCount
$LockedAccountsDataGrid.ItemsSource=@($result)
Write-Log "검색 완료: '$q' ($(@($result).Count)건)" "Cyan"
} catch { Write-Log "검색 실패: $($_.Exception.Message)" "Red" }
})
$UnlockButton.Add_Click({
if ($LockedAccountsDataGrid.SelectedItem) {
$sel=$LockedAccountsDataGrid.SelectedItem
Write-Log "잠금 해제 요청: $($sel.Name) [$($sel.SamAccountName)]" "Yellow"
if (Unlock-ADUserAccount -SamAccountName $sel.SamAccountName) { Write-Log "성공: '$($sel.Name)' 잠금 해제 완료" "Green"; Populate-DataGrid }
} else { Write-Log "계정을 선택하세요." "Orange" }
})
$CheckSourceButton.Add_Click({
if ($LockedAccountsDataGrid.SelectedItem) {
$sel=$LockedAccountsDataGrid.SelectedItem
Write-Log "잠금 출처 조회 중... ($($sel.SamAccountName))" "Yellow"
$info = Get-ADLockoutEvent -SamAccountName $sel.SamAccountName
if ($info) {
$details=@"
: $($sel.Name)
ID: $($sel.SamAccountName)
: $($info.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))
: $($info.CallerComputer)
DC: $($info.DC)
"@
$popup=New-Object Windows.Window
$popup.Title="잠금 출처 정보"; $popup.Width=520; $popup.Height=320; $popup.WindowStartupLocation="CenterOwner"; $popup.Owner=$mainForm; $popup.Background=[Windows.Media.Brushes]::Black
$grid=New-Object Windows.Controls.Grid
$grid.Margin=10
$grid.RowDefinitions.Add((New-Object Windows.Controls.RowDefinition))
$grid.RowDefinitions.Add((New-Object Windows.Controls.RowDefinition -Property @{Height=35}))
$tb=New-Object Windows.Controls.TextBox
$tb.Text=$details; $tb.FontFamily='Consolas'; $tb.FontSize=13; $tb.Foreground=[Windows.Media.Brushes]::White
$tb.Background=[Windows.Media.Brushes]::Black; $tb.TextWrapping='Wrap'; $tb.IsReadOnly=$true
$tb.VerticalScrollBarVisibility='Auto'; $tb.HorizontalScrollBarVisibility='Auto'
$btn=New-Object Windows.Controls.Button
$btn.Content="복사"; $btn.Height=28; $btn.Width=80; $btn.HorizontalAlignment='Right'; $btn.Margin='0,5,0,0'
$btn.Add_Click({ [Windows.Clipboard]::SetText($tb.Text); Write-Log "잠금 출처 정보 복사됨" "Cyan" })
$grid.Children.Add($tb); [Windows.Controls.Grid]::SetRow($tb,0)
$grid.Children.Add($btn); [Windows.Controls.Grid]::SetRow($btn,1)
$popup.Content=$grid; $popup.ShowDialog() | Out-Null
} else { Write-Log "잠금 이벤트 없음" "Orange" }
} else { Write-Log "계정을 선택하세요." "Orange" }
})
Populate-DataGrid
$mainForm.ShowDialog() | Out-Null