Kimsenin sahiplenmediği servis hesaplarını bulun.
Active Directory servis hesaplarınızı envantere alan ve denetçilerin — ve saldırganların — ilk baktığı riskleri işaretleyen salt okunur bir PowerShell script’i. Ajan yok, telemetri yok; sonuçlar makinenizde kalır.
Altı bulgu, tek CSV.
ServicePrincipalName tanımlı her hesap, çevrimdışı parola kırma hedefidir. Script hepsini ortaya çıkarır.
Bir servis hesabında PasswordNeverExpires genellikle parolanın yıllar önce belirlendiği — ve bir yerlerde wiki’de paylaşıldığı — anlamına gelir.
Parolası saklama politikanızdan daha eski olan hesaplar, yapılandırılabilir eşiklerle işaretlenir.
Domain Admins veya diğer ayrıcalıklı gruplarda duran servis hesapları — denetçilerin rapora ilk yazdığı bulgu.
Yakın zamanda oturum açmamış etkin hesaplar: parolayı bulan biri için hâlâ kimlik doğrulayan unutulmuş entegrasyonlar.
Ele geçirilen tek bir hesabı birçok hesaba dönüştüren kısıtsız ve kısıtlı delegasyon ayarları.
- 01Script’i inceleyin
Yaklaşık 150 satırlık, salt okunur Get-AD* sorgularından oluşur. Okuyun — domaininize karşı incelenmemiş bir script asla çalıştırmamalısınız.
- 02Domaine bağlı herhangi bir makineden çalıştırın
RSAT ActiveDirectory modülü ve sıradan okuma erişimi yeterli. Yükseltilmiş yetki yok, AD’de değişiklik yok, ağ çağrısı yok.
- 03CSV’yi önceliklendirin
Sonuçlar risk bayrağı sayısına göre sıralanır. En üstten başlayın: bayat ve süresi dolmayan parolalara sahip ayrıcalıklı hesaplar.
Find-ServiceAccounts.ps1
<#
.SYNOPSIS
Find-ServiceAccounts.ps1 — read-only Active Directory service-account discovery.
.DESCRIPTION
Inventories likely service accounts in Active Directory and flags common
privileged-access risks:
* Accounts with ServicePrincipalNames (kerberoast surface)
* Accounts matching service-style naming patterns (svc*, *service*, sa-*)
* PasswordNeverExpires accounts
* Stale passwords (older than -StalePasswordYears)
* Enabled accounts with no recent logon
* Membership in privileged groups (Domain Admins, etc.)
* Unconstrained / constrained delegation
READ-ONLY: uses Get-AD* cmdlets exclusively. Makes no changes to AD.
No data leaves your network — results are written to a local CSV.
.REQUIREMENTS
RSAT ActiveDirectory PowerShell module; read access to the domain.
.EXAMPLE
.\Find-ServiceAccounts.ps1
.\Find-ServiceAccounts.ps1 -StalePasswordYears 2 -OutputPath .\svc-audit.csv
.NOTES
Provided by Monofor (https://monofor.com) under the MIT license, as-is,
without warranty. Review before running, as you should with any script.
#>
[CmdletBinding()]
param(
[int]$StalePasswordYears = 1,
[int]$StaleLogonDays = 90,
[string]$OutputPath = ".\service-account-inventory.csv",
[string]$Server
)
$ErrorActionPreference = 'Stop'
try {
Import-Module ActiveDirectory
} catch {
Write-Error "RSAT ActiveDirectory module not found. Install RSAT and retry."
exit 1
}
$adParams = @{}
if ($Server) { $adParams['Server'] = $Server }
$stalePwdDate = (Get-Date).AddYears(-$StalePasswordYears)
$staleLogonDate = (Get-Date).AddDays(-$StaleLogonDays)
$privilegedGroups = @(
'Domain Admins', 'Enterprise Admins', 'Schema Admins',
'Administrators', 'Account Operators', 'Server Operators',
'Backup Operators', 'Print Operators'
)
Write-Host "Collecting privileged group membership..." -ForegroundColor Cyan
$privilegedMembers = @{}
foreach ($group in $privilegedGroups) {
try {
Get-ADGroupMember -Identity $group -Recursive @adParams |
Where-Object { $_.objectClass -eq 'user' } |
ForEach-Object {
if (-not $privilegedMembers.ContainsKey($_.SamAccountName)) {
$privilegedMembers[$_.SamAccountName] = @()
}
$privilegedMembers[$_.SamAccountName] += $group
}
} catch {
Write-Verbose "Group not found or unreadable: $group"
}
}
Write-Host "Querying accounts (SPNs, naming patterns, password flags)..." -ForegroundColor Cyan
$properties = @(
'SamAccountName', 'DisplayName', 'Enabled', 'ServicePrincipalName',
'PasswordNeverExpires', 'PasswordLastSet', 'LastLogonDate',
'TrustedForDelegation', 'msDS-AllowedToDelegateTo', 'Description',
'whenCreated'
)
# Union of: SPN accounts + service-style names + password-never-expires.
$filter = {
(ServicePrincipalName -like '*') -or
(SamAccountName -like 'svc*') -or
(SamAccountName -like 'sa-*') -or
(SamAccountName -like '*service*') -or
(PasswordNeverExpires -eq $true)
}
$accounts = Get-ADUser -Filter $filter -Properties $properties @adParams
$results = foreach ($acct in $accounts) {
$flags = New-Object System.Collections.Generic.List[string]
$hasSpn = [bool]$acct.ServicePrincipalName
if ($hasSpn) { $flags.Add('SPN (kerberoastable surface)') }
if ($acct.PasswordNeverExpires) { $flags.Add('PasswordNeverExpires') }
if ($acct.PasswordLastSet -and $acct.PasswordLastSet -lt $stalePwdDate) {
$flags.Add("Password older than $StalePasswordYears y")
}
if ($acct.Enabled -and $acct.LastLogonDate -and $acct.LastLogonDate -lt $staleLogonDate) {
$flags.Add("No logon in $StaleLogonDays d")
}
if ($acct.Enabled -and -not $acct.LastLogonDate) {
$flags.Add('Never logged on')
}
if ($acct.TrustedForDelegation) { $flags.Add('UNCONSTRAINED delegation') }
if ($acct.'msDS-AllowedToDelegateTo') { $flags.Add('Constrained delegation') }
if ($privilegedMembers.ContainsKey($acct.SamAccountName)) {
$flags.Add("PRIVILEGED: $($privilegedMembers[$acct.SamAccountName] -join ', ')")
}
[pscustomobject]@{
SamAccountName = $acct.SamAccountName
DisplayName = $acct.DisplayName
Enabled = $acct.Enabled
HasSPN = $hasSpn
PwdNeverExpires = $acct.PasswordNeverExpires
PasswordLastSet = $acct.PasswordLastSet
LastLogonDate = $acct.LastLogonDate
Created = $acct.whenCreated
Description = $acct.Description
RiskFlags = ($flags -join ' | ')
RiskCount = $flags.Count
}
}
$results = $results | Sort-Object -Property RiskCount -Descending
$results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
$total = ($results | Measure-Object).Count
$privileged = ($results | Where-Object { $_.RiskFlags -match 'PRIVILEGED' } | Measure-Object).Count
$stalePwd = ($results | Where-Object { $_.RiskFlags -match 'older than' } | Measure-Object).Count
$neverExpire = ($results | Where-Object { $_.PwdNeverExpires } | Measure-Object).Count
$delegation = ($results | Where-Object { $_.RiskFlags -match 'delegation' } | Measure-Object).Count
Write-Host ""
Write-Host "=== Service-account inventory summary ===" -ForegroundColor Green
Write-Host (" Candidate service accounts : {0}" -f $total)
Write-Host (" In privileged groups : {0}" -f $privileged)
Write-Host (" Password never expires : {0}" -f $neverExpire)
Write-Host (" Stale passwords (>{0}y) : {1}" -f $StalePasswordYears, $stalePwd)
Write-Host (" Delegation configured : {0}" -f $delegation)
Write-Host ""
Write-Host ("Full inventory written to: {0}" -f (Resolve-Path $OutputPath)) -ForegroundColor Green
Write-Host "Next step: vault and rotate the flagged credentials before an auditor (or an attacker) finds them."
Bir şey mi buldunuz? Şimdi kasaya alın.
Tarama size envanteri verir; çözüm, bu kimlik bilgileri etrafındaki disiplindir — kasaya alın, rotasyona sokun ve erişimi onay ile kayıt arkasına koyun. Monopam tam olarak bunu otomatikleştirir ve bu CSV’yi başlangıç noktası olarak içe aktarır. Konuya yeni misiniz? servis hesabı nedir ve kimlik bilgisi kasalama ile başlayın.
Çalıştırmadan önce.
- Script’i üretim ortamında çalıştırmak güvenli mi?
- Yapısı gereği salt okunurdur — yalnızca Get-AD* cmdlet’leri; Set-, New- veya Remove- çağrısı ve dışarıya ağ trafiği yoktur. Çıktı yerel bir CSV’dir. Yine de her üçüncü taraf script’te olduğu gibi önce inceleyin ve okuma erişimi olan normal (yönetici olmayan) bir hesapla çalıştırın.
- Sonuçlarla ne yapmalıyım?
- İşaretlenen kimlik bilgilerini kasaya alın, bayat olanları rotasyona sokun, mümkün olan yerlerde servis hesaplarını ayrıcalıklı gruplardan çıkarın ve kalanları süre sınırlı, kayıt altındaki erişimin arkasına koyun. Monopam’ın otomatikleştirdiği iş akışı tam olarak budur — ama hangi PAM’i kullanırsanız kullanın bu envanter değerlidir.
- Servis hesapları neden bu kadar önemli?
- Kalıcı ayrıcalık taşırlar, parolaları nadiren döner ve kimse onları sahiplenmez. Olay sonrası raporlar, yanal hareket yolu olarak tekrar tekrar servis hesaplarını gösterir — hem KVKK hem DORA denetimleri tam olarak bu envanteri ister.
- Monofor’a herhangi bir veri gönderiliyor mu?
- Hayır. Script’te telemetri yoktur ve hiçbir ağ çağrısı yapmaz; sonuçlar makinenizdeki CSV’de kalır. İndirilen dosya, bu sayfada gösterilen kodla karşılaştırabileceğiniz düz bir .ps1’dir.
Kimlikleri doğru şekilde
yönetmeye hazır mısınız?
Beş dakikadan kısa sürede tam donanımlı bir deneme ortamı kurun. Kredi kartı yok, satış engeli yok.