PowerShell para Windows Forensics: Scripts Esenciales para el SOC
Scripts PowerShell esenciales para Windows forensics en el SOC: enumeración de procesos sospechosos, análisis de logons, detección de persistencia (tareas programadas, servicios, claves de registro), auditoría de conexiones de red y timeline de archivos.
PowerShell: el bisturí del analista Windows
En entornos Windows, PowerShell es la herramienta más potente para forensics y respuesta a incidentes. A diferencia de las herramientas GUI (Event Viewer, Task Scheduler, Resource Monitor), PowerShell permite: automatizar la recopilación de datos, ejecutar los mismos scripts en decenas de equipos simultáneamente, y generar informes estructurados listos para analizar.
Este artículo presenta cinco scripts esenciales que cubren las tareas forenses más frecuentes: enumeración de procesos sospechosos, análisis de logons, búsqueda de mecanismos de persistencia, auditoría de conexiones de red, y creación de timelines de archivos.
Cmdlets esenciales para forensics
Antes de los scripts completos, los cmdlets que todo analista debe conocer:
Get-WinEvent: leer Event Logs
# Logons exitosos de las últimas 24 horas
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
StartTime = (Get-Date).AddHours(-24)
} | Select-Object TimeCreated, Id, Message -First 20
# Logs de PowerShell (Script Block Logging)
Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PowerShell/Operational'
Id = 4104
} -MaxEvents 50
Get-Process: procesos en ejecución
# Todos los procesos con detalles
Get-Process | Select-Object Name, Id, CPU, WorkingSet64,
Path, StartTime | Sort-Object CPU -Descending
# Procesos sin path (sospechoso si no es un proceso del sistema)
Get-Process | Where-Object { $_.Path -eq $null -and $_.Id -ne 0 }
Get-NetTCPConnection: conexiones de red activas
# Conexiones establecidas a IPs externas
Get-NetTCPConnection -State Established |
Where-Object { $_.RemoteAddress -notmatch '^(10\.|172\.(1[6-9]|2|3[01])\.|192\.168\.|127\.|::1|0\.0\.0\.0)' } |
Select-Object LocalPort, RemoteAddress, RemotePort, OwningProcess,
@{N='Process';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Name}}
Get-ScheduledTask: tareas programadas
# Tareas programadas activas (no de Microsoft)
Get-ScheduledTask | Where-Object {
$_.State -eq 'Ready' -and
$_.TaskPath -notmatch '\\Microsoft\\'
} | Select-Object TaskName, TaskPath, State,
@{N='Actions';E={($_.Actions | ForEach-Object { $_.Execute }) -join '; '}}
Script 1: Enumeración de procesos sospechosos
Este script identifica procesos que presentan indicadores de actividad maliciosa: ejecución desde directorios temporales, procesos sin firma digital, nombres que imitan procesos legítimos, y consumo anómalo de recursos.
<#
.SYNOPSIS
Enumera procesos sospechosos en un sistema Windows.
.DESCRIPTION
Analiza los procesos en ejecución buscando indicadores de
actividad maliciosa: paths sospechosos, procesos sin firma,
nombres masquerading, y consumo anómalo.
.NOTES
Requiere privilegios de administrador para resultados completos.
#>
function Get-SuspiciousProcesses {
[CmdletBinding()]
param(
[int]$CpuThresholdPercent = 80,
[int]$MemoryThresholdMB = 500
)
$findings = @()
# Directorios donde los procesos legítimos NO suelen ejecutarse
$suspiciousPaths = @(
"$env:TEMP",
"$env:TMP",
"$env:APPDATA",
"$env:LOCALAPPDATA\Temp",
"C:\Users\Public",
"C:\PerfLogs",
"C:\Windows\Temp"
)
# Procesos legítimos de Windows que suelen ser suplantados
$legitimateSystemProcs = @{
'svchost.exe' = 'C:\Windows\System32\svchost.exe'
'csrss.exe' = 'C:\Windows\System32\csrss.exe'
'lsass.exe' = 'C:\Windows\System32\lsass.exe'
'services.exe' = 'C:\Windows\System32\services.exe'
'smss.exe' = 'C:\Windows\System32\smss.exe'
'wininit.exe' = 'C:\Windows\System32\wininit.exe'
'explorer.exe' = 'C:\Windows'
}
$processes = Get-Process -IncludeUserName -ErrorAction SilentlyContinue
foreach ($proc in $processes) {
$reasons = @()
# 1. Proceso sin path (potencialmente inyectado)
if (-not $proc.Path -and $proc.Id -ne 0 -and $proc.Id -ne 4) {
$reasons += "Sin path de ejecucion"
}
# 2. Ejecución desde directorio temporal
if ($proc.Path) {
foreach ($suspPath in $suspiciousPaths) {
if ($proc.Path -like "$suspPath*") {
$reasons += "Ejecutando desde directorio temporal: $suspPath"
break
}
}
}
# 3. Name masquerading (nombre legítimo, path incorrecto)
$procName = $proc.ProcessName + ".exe"
if ($legitimateSystemProcs.ContainsKey($procName) -and $proc.Path) {
$expectedPath = $legitimateSystemProcs[$procName]
if (-not $proc.Path.StartsWith($expectedPath)) {
$reasons += "Name masquerading: se espera en $expectedPath, encontrado en $($proc.Path)"
}
}
# 4. Proceso sin firma digital
if ($proc.Path -and (Test-Path $proc.Path)) {
try {
$sig = Get-AuthenticodeSignature -FilePath $proc.Path -ErrorAction SilentlyContinue
if ($sig.Status -ne 'Valid') {
$reasons += "Sin firma digital valida (Status: $($sig.Status))"
}
} catch {
# Algunos archivos del sistema no permiten verificar firma
}
}
# 5. Consumo anómalo de CPU
if ($proc.CPU -gt $CpuThresholdPercent) {
$reasons += "CPU alto: $([math]::Round($proc.CPU, 2))%"
}
# 6. Consumo anómalo de memoria
$memMB = [math]::Round($proc.WorkingSet64 / 1MB, 2)
if ($memMB -gt $MemoryThresholdMB) {
$reasons += "Memoria alta: ${memMB} MB"
}
# Registrar si hay razones de sospecha
if ($reasons.Count -gt 0) {
$findings += [PSCustomObject]@{
ProcessName = $proc.ProcessName
PID = $proc.Id
Path = $proc.Path
User = $proc.UserName
StartTime = $proc.StartTime
CPU = [math]::Round($proc.CPU, 2)
MemoryMB = $memMB
Reasons = $reasons -join ' | '
RiskLevel = if ($reasons.Count -ge 3) { 'HIGH' }
elseif ($reasons.Count -ge 2) { 'MEDIUM' }
else { 'LOW' }
}
}
}
return $findings | Sort-Object RiskLevel -Descending
}
# Ejecutar y mostrar resultados
$suspicious = Get-SuspiciousProcesses
$suspicious | Format-Table -AutoSize -Wrap
# Exportar a CSV
$suspicious | Export-Csv -Path "suspicious_processes_$(Get-Date -Format 'yyyyMMdd_HHmm').csv" -NoTypeInformation
Write-Host "`nProcesos sospechosos encontrados: $($suspicious.Count)" -ForegroundColor Yellow
Script 2: Análisis de logons recientes
Este script extrae y analiza los logons del sistema para detectar accesos sospechosos: logons fuera de horario, logons tipo 10 (RDP) desde IPs desconocidas, múltiples logons fallidos seguidos de uno exitoso (posible brute force), y cuentas de servicio con logons interactivos.
<#
.SYNOPSIS
Analiza logons recientes para detectar actividad sospechosa.
.DESCRIPTION
Procesa Event IDs 4624 (exitoso) y 4625 (fallido) del Security log.
Detecta brute force, logons fuera de horario, RDP sospechoso,
y cuentas de servicio con logons interactivos.
#>
function Analyze-RecentLogons {
[CmdletBinding()]
param(
[int]$HoursBack = 24,
[int]$BruteForceThreshold = 5,
[string[]]$BusinessHours = @("08:00", "19:00"),
[string[]]$KnownRDPSources = @() # IPs permitidas para RDP
)
$startTime = (Get-Date).AddHours(-$HoursBack)
$findings = @()
# --- Logons exitosos (4624) ---
$successLogons = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
StartTime = $startTime
} -ErrorAction SilentlyContinue | ForEach-Object {
$xml = [xml]$_.ToXml()
$data = @{}
foreach ($d in $xml.Event.EventData.Data) {
$data[$d.Name] = $d.'#text'
}
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
TargetUser = $data['TargetUserName']
TargetDomain = $data['TargetDomainName']
LogonType = [int]$data['LogonType']
SourceIP = $data['IpAddress']
SourceHost = $data['WorkstationName']
LogonProcess = $data['LogonProcessName']
AuthPackage = $data['AuthenticationPackageName']
}
}
# --- Logons fallidos (4625) ---
$failedLogons = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = $startTime
} -ErrorAction SilentlyContinue | ForEach-Object {
$xml = [xml]$_.ToXml()
$data = @{}
foreach ($d in $xml.Event.EventData.Data) {
$data[$d.Name] = $d.'#text'
}
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
TargetUser = $data['TargetUserName']
SourceIP = $data['IpAddress']
FailureCode = $data['SubStatus']
}
}
# Tipos de logon reference
$logonTypes = @{
2 = 'Interactive (local)'
3 = 'Network'
4 = 'Batch'
5 = 'Service'
7 = 'Unlock'
8 = 'NetworkCleartext'
9 = 'NewCredentials'
10 = 'RemoteInteractive (RDP)'
11 = 'CachedInteractive'
}
# DETECCION 1: Brute force (muchos fallidos + un exitoso)
$failedBySource = $failedLogons | Group-Object SourceIP
foreach ($group in $failedBySource) {
if ($group.Count -ge $BruteForceThreshold) {
$sourceIP = $group.Name
$successAfterFail = $successLogons | Where-Object {
$_.SourceIP -eq $sourceIP -and
$_.TimeCreated -gt ($group.Group | Sort-Object TimeCreated | Select-Object -Last 1).TimeCreated
}
$status = if ($successAfterFail) { "BRUTE_FORCE_EXITOSO" } else { "BRUTE_FORCE_ACTIVO" }
$findings += [PSCustomObject]@{
Type = $status
Severity = if ($successAfterFail) { 'CRITICAL' } else { 'HIGH' }
SourceIP = $sourceIP
FailedCount = $group.Count
TargetUsers = ($group.Group | Select-Object -ExpandProperty TargetUser -Unique) -join ', '
SuccessUser = if ($successAfterFail) { $successAfterFail[0].TargetUser } else { 'N/A' }
TimeRange = "$($group.Group[0].TimeCreated) - $(($group.Group | Select-Object -Last 1).TimeCreated)"
}
}
}
# DETECCION 2: RDP desde IPs no conocidas
$rdpLogons = $successLogons | Where-Object { $_.LogonType -eq 10 }
foreach ($rdp in $rdpLogons) {
if ($rdp.SourceIP -and $rdp.SourceIP -ne '-' -and
$rdp.SourceIP -notin $KnownRDPSources) {
$findings += [PSCustomObject]@{
Type = 'RDP_DESCONOCIDO'
Severity = 'HIGH'
SourceIP = $rdp.SourceIP
TargetUser = $rdp.TargetUser
TimeCreated = $rdp.TimeCreated
Detail = "Logon RDP desde IP no autorizada"
}
}
}
# DETECCION 3: Logons fuera de horario laboral
$startHour = [int]$BusinessHours[0].Split(':')[0]
$endHour = [int]$BusinessHours[1].Split(':')[0]
$offHours = $successLogons | Where-Object {
$_.LogonType -in @(2, 10, 11) -and
($_.TimeCreated.Hour -lt $startHour -or $_.TimeCreated.Hour -ge $endHour) -and
$_.TargetUser -notmatch '(SYSTEM|LOCAL SERVICE|NETWORK SERVICE|\$)'
}
if ($offHours.Count -gt 0) {
$findings += [PSCustomObject]@{
Type = 'LOGON_FUERA_HORARIO'
Severity = 'MEDIUM'
Count = $offHours.Count
Users = ($offHours | Select-Object -ExpandProperty TargetUser -Unique) -join ', '
Detail = "Logons interactivos fuera de horario laboral"
}
}
# Resumen
Write-Host "`n=== ANALISIS DE LOGONS ($HoursBack horas) ===" -ForegroundColor Cyan
Write-Host "Logons exitosos: $($successLogons.Count)"
Write-Host "Logons fallidos: $($failedLogons.Count)"
Write-Host "Sesiones RDP: $($rdpLogons.Count)"
Write-Host "Hallazgos: $($findings.Count)" -ForegroundColor $(if ($findings.Count -gt 0) {'Yellow'} else {'Green'})
return $findings
}
$logonFindings = Analyze-RecentLogons -HoursBack 48
$logonFindings | Format-List
Script 3: Búsqueda de persistencia
La persistencia es la técnica MITRE ATT&CK mas utilizada por malware. Este script revisa los tres mecanismos de persistencia más comunes en Windows: tareas programadas, servicios, y claves de registro Run/RunOnce.
<#
.SYNOPSIS
Busca mecanismos de persistencia en Windows.
.DESCRIPTION
Revisa Scheduled Tasks, Services y Registry Run Keys
para detectar persistencia maliciosa.
#>
function Hunt-Persistence {
[CmdletBinding()]
param()
$findings = @()
# ===== 1. SCHEDULED TASKS SOSPECHOSAS =====
Write-Host "`n[1/3] Analizando Scheduled Tasks..." -ForegroundColor Cyan
$tasks = Get-ScheduledTask | Where-Object {
$_.State -ne 'Disabled' -and
$_.TaskPath -notmatch '\\Microsoft\\' -and
$_.TaskName -notmatch '^(GoogleUpdate|MicrosoftEdgeUpdate|OneDrive)'
}
foreach ($task in $tasks) {
$info = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath -ErrorAction SilentlyContinue
$actions = $task.Actions | ForEach-Object {
"$($_.Execute) $($_.Arguments)"
}
$suspicious = $false
$reasons = @()
foreach ($action in $actions) {
if ($action -match '(powershell|cmd|wscript|cscript|mshta|rundll32|regsvr32|certutil|bitsadmin)') {
$suspicious = $true
$reasons += "Ejecuta interprete/LOLBin: $action"
}
if ($action -match '(\\Temp\\|\\AppData\\|\\Users\\Public|http://|https://)') {
$suspicious = $true
$reasons += "Path o URL sospechosa: $action"
}
}
if ($suspicious) {
$findings += [PSCustomObject]@{
Category = 'ScheduledTask'
Name = $task.TaskName
Path = $task.TaskPath
State = $task.State
Actions = $actions -join ' ; '
LastRun = $info.LastRunTime
NextRun = $info.NextRunTime
Reasons = $reasons -join ' | '
Severity = 'HIGH'
}
}
}
# ===== 2. SERVICIOS SOSPECHOSOS =====
Write-Host "[2/3] Analizando Services..." -ForegroundColor Cyan
$services = Get-WmiObject -Class Win32_Service | Where-Object {
$_.State -eq 'Running' -and
$_.PathName -and
$_.PathName -notmatch '(\\Windows\\|\\Program Files|svchost\.exe)'
}
foreach ($svc in $services) {
$reasons = @()
$pathName = $svc.PathName
# Servicios ejecutando desde directorios temporales
if ($pathName -match '(\\Temp\\|\\AppData\\|\\Users\\Public)') {
$reasons += "Ejecutando desde directorio temporal"
}
# Servicios con cmd o powershell en el path
if ($pathName -match '(cmd\.exe|powershell\.exe)') {
$reasons += "Servicio usando interprete de comandos"
}
# Servicio sin descripción (común en malware)
if (-not $svc.Description -or $svc.Description -eq '') {
$reasons += "Sin descripcion"
}
if ($reasons.Count -gt 0) {
$findings += [PSCustomObject]@{
Category = 'Service'
Name = $svc.Name
DisplayName = $svc.DisplayName
Path = $svc.PathName
StartMode = $svc.StartMode
Account = $svc.StartName
Reasons = $reasons -join ' | '
Severity = 'HIGH'
}
}
}
# ===== 3. REGISTRY RUN KEYS =====
Write-Host "[3/3] Analizando Registry Run Keys..." -ForegroundColor Cyan
$runKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'
)
foreach ($key in $runKeys) {
if (-not (Test-Path $key)) { continue }
$entries = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
$props = $entries.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
}
foreach ($prop in $props) {
$value = $prop.Value
$reasons = @()
if ($value -match '(powershell|cmd|wscript|mshta|rundll32|regsvr32)') {
$reasons += "Ejecuta interprete/LOLBin"
}
if ($value -match '(\\Temp\\|\\AppData\\Local\\Temp|\\Users\\Public)') {
$reasons += "Referencia a directorio temporal"
}
if ($value -match '(http://|https://|ftp://)') {
$reasons += "Contiene URL"
}
if ($reasons.Count -gt 0) {
$findings += [PSCustomObject]@{
Category = 'RegistryRunKey'
Key = $key
Name = $prop.Name
Value = $value
Reasons = $reasons -join ' | '
Severity = 'HIGH'
}
}
}
}
# Resumen
Write-Host "`n=== RESULTADOS PERSISTENCIA ===" -ForegroundColor Cyan
Write-Host "Tasks sospechosas: $(($findings | Where-Object Category -eq 'ScheduledTask').Count)"
Write-Host "Services sospechosos: $(($findings | Where-Object Category -eq 'Service').Count)"
Write-Host "Run Keys sospechosas: $(($findings | Where-Object Category -eq 'RegistryRunKey').Count)"
Write-Host "Total hallazgos: $($findings.Count)" -ForegroundColor $(if ($findings.Count -gt 0) {'Red'} else {'Green'})
return $findings
}
$persistence = Hunt-Persistence
$persistence | Format-Table Category, Name, Severity, Reasons -AutoSize -Wrap
$persistence | Export-Csv "persistence_hunt_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Script 4: Auditoría de conexiones de red
Este script analiza las conexiones TCP activas, identifica procesos que se comunican con IPs externas, y genera un informe de conexiones sospechosas.
<#
.SYNOPSIS
Audita conexiones de red TCP activas.
.DESCRIPTION
Identifica procesos con conexiones a IPs externas,
detecta puertos de C2 comunes, y genera un informe.
#>
function Audit-NetworkConnections {
[CmdletBinding()]
param()
# Puertos comúnmente usados por C2/malware
$suspiciousPorts = @(
4444, # Metasploit default
5555, # Android ADB / algunos RATs
8080, # HTTP alternativo / C2
8443, # HTTPS alternativo / C2
1234, # Varios RATs
9999, # Varios RATs
6666, # Varios trojans
31337, # Back Orifice
12345, # NetBus
50050 # Cobalt Strike
)
$connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
Where-Object {
$_.RemoteAddress -ne '127.0.0.1' -and
$_.RemoteAddress -ne '::1' -and
$_.RemoteAddress -ne '0.0.0.0'
} | ForEach-Object {
$proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
$isPrivate = $_.RemoteAddress -match '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)'
[PSCustomObject]@{
LocalAddress = $_.LocalAddress
LocalPort = $_.LocalPort
RemoteAddress = $_.RemoteAddress
RemotePort = $_.RemotePort
PID = $_.OwningProcess
ProcessName = $proc.Name
ProcessPath = $proc.Path
IsExternal = -not $isPrivate
IsSuspPort = $_.RemotePort -in $suspiciousPorts
State = $_.State
}
}
# Filtrar solo conexiones externas
$external = $connections | Where-Object { $_.IsExternal }
# Identificar conexiones sospechosas
$suspicious = $external | Where-Object {
$_.IsSuspPort -or
$_.ProcessPath -match '(\\Temp\\|\\AppData\\)' -or
-not $_.ProcessPath -or
$_.ProcessName -match '(powershell|cmd|wscript|rundll32|regsvr32)'
}
# Resumen por proceso
$byProcess = $external | Group-Object ProcessName |
Select-Object @{N='Process';E={$_.Name}},
Count,
@{N='RemoteIPs';E={($_.Group | Select-Object -ExpandProperty RemoteAddress -Unique) -join ', '}},
@{N='Ports';E={($_.Group | Select-Object -ExpandProperty RemotePort -Unique) -join ', '}}
Write-Host "`n=== AUDITORIA DE CONEXIONES ===" -ForegroundColor Cyan
Write-Host "Total conexiones establecidas: $($connections.Count)"
Write-Host "Conexiones externas: $($external.Count)"
Write-Host "Conexiones sospechosas: $($suspicious.Count)" -ForegroundColor $(if ($suspicious.Count -gt 0) {'Red'} else {'Green'})
Write-Host "`nConexiones por proceso:" -ForegroundColor Cyan
$byProcess | Sort-Object Count -Descending | Format-Table -AutoSize
if ($suspicious.Count -gt 0) {
Write-Host "Conexiones SOSPECHOSAS:" -ForegroundColor Red
$suspicious | Format-Table ProcessName, RemoteAddress, RemotePort, ProcessPath -AutoSize
}
return @{
All = $connections
External = $external
Suspicious = $suspicious
ByProcess = $byProcess
}
}
$netAudit = Audit-NetworkConnections
$netAudit.External | Export-Csv "network_audit_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Script 5: Timeline de archivos modificados
Durante una investigación, una de las primeras preguntas es: "¿qué archivos se crearon o modificaron durante la ventana del ataque?" Este script genera una timeline de actividad en el sistema de archivos.
<#
.SYNOPSIS
Genera una timeline de archivos creados o modificados recientemente.
.DESCRIPTION
Busca archivos nuevos o modificados en directorios clave del sistema
dentro de una ventana temporal configurable.
#>
function Get-FileTimeline {
[CmdletBinding()]
param(
[int]$HoursBack = 24,
[string[]]$TargetPaths = @(
"$env:SystemRoot\System32",
"$env:SystemRoot\SysWOW64",
"$env:SystemRoot\Temp",
"$env:TEMP",
"$env:APPDATA",
"$env:LOCALAPPDATA",
"$env:USERPROFILE\Downloads",
"$env:USERPROFILE\Desktop",
"$env:ProgramData",
"C:\Users\Public"
),
[string[]]$SuspiciousExtensions = @(
'.exe', '.dll', '.ps1', '.bat', '.cmd', '.vbs',
'.js', '.wsf', '.hta', '.scr', '.pif', '.lnk',
'.msi', '.jar', '.py', '.reg'
)
)
$cutoff = (Get-Date).AddHours(-$HoursBack)
$results = @()
foreach ($path in $TargetPaths) {
if (-not (Test-Path $path)) { continue }
Write-Host " Escaneando: $path" -ForegroundColor DarkGray
try {
$files = Get-ChildItem -Path $path -Recurse -File -ErrorAction SilentlyContinue |
Where-Object {
$_.LastWriteTime -ge $cutoff -or
$_.CreationTime -ge $cutoff
}
foreach ($file in $files) {
$isSuspExt = $file.Extension.ToLower() -in $SuspiciousExtensions
$isNew = $file.CreationTime -ge $cutoff
$isModified = $file.LastWriteTime -ge $cutoff -and -not $isNew
# Verificar firma digital para ejecutables
$sigStatus = 'N/A'
if ($file.Extension -in @('.exe', '.dll', '.ps1')) {
$sig = Get-AuthenticodeSignature -FilePath $file.FullName -ErrorAction SilentlyContinue
$sigStatus = if ($sig) { $sig.Status.ToString() } else { 'Unknown' }
}
$results += [PSCustomObject]@{
Timestamp = if ($isNew) { $file.CreationTime } else { $file.LastWriteTime }
Action = if ($isNew) { 'CREATED' } else { 'MODIFIED' }
FullPath = $file.FullName
Extension = $file.Extension
SizeMB = [math]::Round($file.Length / 1MB, 2)
Suspicious = $isSuspExt
Signature = $sigStatus
Owner = try { (Get-Acl $file.FullName).Owner } catch { 'Unknown' }
}
}
} catch {
Write-Warning "Error escaneando ${path}: $_"
}
}
$sorted = $results | Sort-Object Timestamp -Descending
$suspiciousFiles = $sorted | Where-Object { $_.Suspicious }
Write-Host "`n=== TIMELINE DE ARCHIVOS ($HoursBack horas) ===" -ForegroundColor Cyan
Write-Host "Total archivos nuevos/modificados: $($sorted.Count)"
Write-Host "Archivos con extension sospechosa: $($suspiciousFiles.Count)" -ForegroundColor $(if ($suspiciousFiles.Count -gt 0) {'Yellow'} else {'Green'})
if ($suspiciousFiles.Count -gt 0) {
Write-Host "`nArchivos sospechosos:" -ForegroundColor Yellow
$suspiciousFiles | Format-Table Timestamp, Action, FullPath, SizeMB, Signature -AutoSize -Wrap
}
return $sorted
}
$timeline = Get-FileTimeline -HoursBack 48
$timeline | Export-Csv "file_timeline_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Write-Host "`nTimeline exportada a CSV."
Remoting para respuesta a incidentes
En un incidente real, necesitas ejecutar estos scripts en múltiples equipos simultáneamente. PowerShell Remoting permite hacerlo sin iniciar sesión interactiva en cada uno.
Configurar WinRM
# En el equipo objetivo (requiere admin)
Enable-PSRemoting -Force
# Verificar que el servicio está activo
Get-Service WinRM
Ejecutar scripts remotamente
# Ejecutar en un equipo
Invoke-Command -ComputerName "SRV-DC01" -ScriptBlock {
Get-SuspiciousProcesses
} -Credential (Get-Credential)
# Ejecutar en múltiples equipos
$targets = @("WS-001", "WS-002", "SRV-DC01", "SRV-FILE01")
$results = Invoke-Command -ComputerName $targets -ScriptBlock {
# Script de persistencia
Hunt-Persistence
} -Credential $cred
# Agrupar resultados por equipo
$results | Group-Object PSComputerName | ForEach-Object {
Write-Host "`n=== $($_.Name) ===" -ForegroundColor Cyan
$_.Group | Format-Table Category, Name, Severity -AutoSize
}
Script de triaje remoto completo
function Invoke-RemoteTriage {
param(
[string[]]$ComputerNames,
[PSCredential]$Credential
)
$triageScript = {
$results = @{
ComputerName = $env:COMPUTERNAME
Timestamp = Get-Date -Format 'o'
Processes = (Get-Process | Where-Object { $_.Path -match '\\Temp\\' }).Count
Connections = (Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
Where-Object { $_.RemoteAddress -notmatch '^(10\.|172\.|192\.168\.|127\.)' }).Count
FailedLogons = (Get-WinEvent -FilterHashtable @{
LogName = 'Security'; Id = 4625;
StartTime = (Get-Date).AddHours(-24)
} -ErrorAction SilentlyContinue).Count
RunKeys = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -ErrorAction SilentlyContinue).PSObject.Properties.Count
}
[PSCustomObject]$results
}
$triageResults = Invoke-Command -ComputerName $ComputerNames `
-ScriptBlock $triageScript `
-Credential $Credential `
-ErrorAction SilentlyContinue
$triageResults | Format-Table -AutoSize
return $triageResults
}
Próximos pasos
Con Python y PowerShell cubiertos, tienes un arsenal completo para automatizar las tareas forenses más comunes del SOC. Los siguientes artículos de la serie profundizan en temas avanzados: pipelines de enrichment automático, correlación de alertas con Machine Learning básico, integración con MISP y OpenCTI, y construcción de un toolkit SOC personalizado.
Recursos
- PowerShell Documentation. Referencia oficial de Microsoft.
- SANS PowerShell Cheat Sheet. Referencia rápida de cmdlets para seguridad.
- MITRE ATT&CK Persistence. Técnicas de persistencia documentadas en el framework.
- PowerShell Empire. Framework de post-explotación, útil para entender qué buscar en las investigaciones.
- Get-WinEvent Tips. Guía avanzada de consultas al Event Log.
- LOLBAS Project. Catálogo de Living Off The Land Binaries usados por atacantes.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Python para SOC Analysts: Fundamentos y Primeros Scripts de Seguridad
Python para Análisis de Logs: Parsear, Filtrar y Correlacionar Eventos
SOAR: Qué Es, Arquitectura y Diseño de Playbooks de Automatización
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.