convert to gitea
This commit is contained in:
BIN
ADLockUser.exe
Normal file
BIN
ADLockUser.exe
Normal file
Binary file not shown.
217
AccountUnlocker.ps1
Normal file
217
AccountUnlocker.ps1
Normal file
@ -0,0 +1,217 @@
|
||||
#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
|
||||
76
README.md
Normal file
76
README.md
Normal file
@ -0,0 +1,76 @@
|
||||
# AD 계정 잠금 해제 툴 (AD Account Unlocker)
|
||||
|
||||
Active Directory(AD) 환경에서 잠긴 사용자 계정을 관리하기 위한 PowerShell 기반 GUI 툴. 잠긴 계정 목록 조회, 잠금 해제, 잠금 출처 추적 기능을 제공한다.
|
||||
|
||||
## 주요 기능
|
||||
|
||||
* **잠긴 AD 계정 목록 조회**: 도메인 내 잠긴 모든 사용자 계정 목록 실시간 조회.
|
||||
* **간편한 계정 잠금 해제**: 선택한 사용자의 계정 잠금을 버튼 클릭으로 해제.
|
||||
* **계정 잠금 출처 추적**: 계정 잠금을 유발한 컴퓨터(IP 또는 이름)를 추적하여 원인 파악 지원.
|
||||
* **사용자 검색**: 표시 이름, 로그온 ID 등으로 전체 AD 사용자 검색 기능 제공.
|
||||
* **직관적인 다크 모드 GUI**: 관리자 편의를 위한 다크 테마 인터페이스.
|
||||
* **실시간 작업 로그**: 모든 작업의 성공 및 실패 여부를 로그 창에 실시간 표시.
|
||||
|
||||
## 사용 전제 조건
|
||||
|
||||
1. **Active Directory 모듈**: 스크립트 실행 컴퓨터에 `ActiveDirectory` PowerShell 모듈 설치 필요. (RSAT의 일부)
|
||||
2. **실행 권한**:
|
||||
* AD 사용자 조회 및 계정 잠금 해제 권한.
|
||||
* 도메인 컨트롤러(DC)의 보안 이벤트 로그(Event ID 4740, 4625) 원격 조회 권한.
|
||||
3. **실행 환경**: 도메인에 가입된 Windows PC.
|
||||
4. **.NET Framework**: .NET Framework 4.5 이상 필요. (최신 Windows에 기본 설치됨)
|
||||
|
||||
## 사용법
|
||||
|
||||
### 방법 1: PowerShell 스크립트(.ps1) 직접 실행
|
||||
|
||||
1. `AccountUnlocker.ps1` 스크립트 파일을 PC에 저장.
|
||||
2. PowerShell을 **관리자 권한으로** 실행.
|
||||
3. 필요 시 PowerShell 실행 정책 변경: `Set-ExecutionPolicy RemoteSigned -Scope Process`
|
||||
4. 스크립트가 있는 경로로 이동 후 아래와 같이 실행.
|
||||
```powershell
|
||||
.\AccountUnlocker.ps1
|
||||
```
|
||||
|
||||
### 방법 2: 실행 파일(.exe)로 패키징
|
||||
|
||||
`PS2EXE` 모듈을 사용하여 스크립트를 단일 실행 파일로 변환 가능.
|
||||
|
||||
**1단계: PS2EXE 모듈 설치 (최초 1회)**
|
||||
|
||||
관리자 권한 PowerShell에서 아래 명령어로 `PS2EXE` 설치.
|
||||
|
||||
```powershell
|
||||
Install-Module -Name PS2EXE
|
||||
```
|
||||
|
||||
**2단계: 스크립트 패키징**
|
||||
|
||||
스크립트 경로에서 아래 명령어로 `.exe` 파일 생성.
|
||||
|
||||
```powershell
|
||||
Invoke-PS2EXE -InputFile ".\AccountUnlocker.ps1" -OutputFile ".\ADLockUser.exe" -noConsole -title "AD 계정 잠 금 해제 툴" -product "AD Management Tools" -company "TigErJin" -requireAdmin
|
||||
```
|
||||
* `-noConsole`: 콘솔 창 없이 GUI만 실행.
|
||||
* `-title`, `-product`, `-company`: 파일 속성 정보 지정.
|
||||
* `-requireAdmin`: 실행 시 관리자 권한 자동 요구.
|
||||
|
||||
**3단계: 프로그램 실행**
|
||||
|
||||
생성된 `ADLockUser.exe` 파일 실행.
|
||||
|
||||
## 스크립트 주요 로직 설명
|
||||
|
||||
### `Get-LockedADAccounts`
|
||||
* 도메인의 PDC(Primary Domain Controller) 에뮬레이터에 쿼리하여 가장 정확한 잠긴 계정 목록을 가져온다.
|
||||
* `Search-ADAccount -LockedOut`으로 잠긴 계정을 찾고 `Get-ADUser`로 상세 정보(표시 이름, 마지막 로그온 등)를 추가 조회.
|
||||
|
||||
### `Unlock-ADUserAccount`
|
||||
* 선택된 계정의 `SamAccountName`을 받아 PDC를 대상으로 `Unlock-ADAccount` cmdlet을 실행.
|
||||
* 성공/실패 여부를 반환하여 GUI에 로그를 기록.
|
||||
|
||||
### `Get-ADLockoutEvent` (잠금 출처 추적)
|
||||
이 기능은 2단계에 걸쳐 잠금 원인을 추적한다.
|
||||
1. **1단계 (Event ID 4740 조회)**: 모든 DC를 대상으로 보안 로그에서 **Event ID 4740 (계정 잠김)** 이벤트를 검색. 이를 통해 잠금 시간, 발생 DC, 호출자 컴퓨터 이름을 확인.
|
||||
|
||||
2. **2단계 (Event ID 4625 조회)**: 4740 이벤트의 '호출자 컴퓨터 이름'이 명확하지 않을 경우, 해당 DC에서 잠금 직전의 **Event ID 4625 (로그온 실패)** 이벤트를 추가 검색. 이 로그의 네트워크 주소(IP) 정보를 통해 실제 인증 실패가 발생한 출처를 특정.
|
||||
Reference in New Issue
Block a user