IntermedioPowerShellWindowsSOCforensicsrespuesta a incidentespersistencia

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.

MalwareIntel Research··15 min lectura

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

Preguntas frecuentes

Artículos relacionados

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.