PowerShell para auditoría de seguridad:
scripts que todo admin debería tener a mano
Administración de sistemas Windows | unfantasmaenelsistema.com
Un administrador de sistemas pasa la mayor parte del tiempo comprobando que todo está donde debería: los parches aplicados, los grupos privilegiados sin sorpresas, los servicios que arrancan solos justificados, y ninguna tarea programada creada a las tres de la mañana por nadie. Hacer esa comprobación clic a clic por la interfaz gráfica es lento y no deja rastro. Hacerla con PowerShell es cuestión de segundos, es repetible y se puede comparar con la foto de la semana pasada.
Este artículo recopila los bloques de PowerShell que conviene tener guardados para auditar tu propio sistema o el parque que administras: inventario, cuentas, persistencia, red, políticas de auditoría y registro de eventos. No son herramientas de intrusión: son la linterna con la que revisas tu casa. Todos los ejemplos son de solo lectura salvo donde se indique lo contrario.
Qué aprenderás en este artículo
Cómo dejar PowerShell preparado para auditar sin bajar la seguridad del equipo · Inventario de software instalado y estado de parches del sistema · Revisión de usuarios locales, grupos privilegiados y cuentas dormidas · Detección de servicios y tareas programadas sospechosas · Conexiones de red activas correlacionadas con el proceso que las abre · Comprobar la política de auditoría y exprimir el registro de eventos de seguridad · Guardar líneas base y comparar el estado actual con una foto anterior
1. Preparar el terreno sin bajar la guardia
Antes de ejecutar nada conviene entender dos cosas: qué permite tu política de ejecución y cómo dejar constancia de lo que haces. La ExecutionPolicy de PowerShell no es un mecanismo de seguridad frente a un atacante —se salta de diez formas distintas—, pero sí evita ejecuciones accidentales de scripts sin firmar. Para trabajar con tus propios scripts de auditoría, lo razonable es `RemoteSigned` a nivel de máquina y firmar los que uses de forma recurrente.
# Ver la politica efectiva y de donde viene cada nivel Get-ExecutionPolicy -List # Permitir scripts locales sin firma, exigir firma a los descargados de internet Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned # Version de PowerShell: muchas cmdlets de seguridad piden 5.1 o 7+ $PSVersionTable.PSVersion
Segundo: activa la transcripción. Con ella, cada comando que ejecutes y su salida quedan escritos en un fichero de texto con marca de tiempo. Es la diferencia entre «creo que revisé eso» y poder demostrar qué comprobaste y cuándo. En un entorno gestionado se despliega por GPO (*Administrative Templates → Windows Components → Windows PowerShell*), pero también se puede activar para la sesión actual.
# Transcripcion de la sesion actual a un fichero con fecha $log = "C:\Auditoria\transcript_$(Get-Date -Format yyyyMMdd_HHmmss).txt" Start-Transcript -Path $log -NoClobber # ... aqui va toda la auditoria ... Stop-Transcript
Registro que de verdad ayuda en una investigación
Script Block Logging (Event ID 4104) registra el código que PowerShell ejecuta, incluso ofuscado · Module Logging (4103) registra las llamadas a cmdlets con sus parámetros · Ambos se activan por GPO en ‘Windows PowerShell’ y escriben en ‘Microsoft-Windows-PowerShell/Operational’ · Sin estos tres registros (transcripción, script block, module) una intrusión vía PowerShell es casi invisible a posteriori
2. Inventario de software y estado de parches
Lo primero de cualquier auditoría es saber qué hay instalado. Evita `Win32_Product` de WMI: es lentísimo y dispara una reconfiguración de MSI en cada equipo que consultas. La fuente fiable y rápida es el registro de desinstaladores.
# Software instalado (64 y 32 bits) desde el registro, sin tocar WMI $rutas = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' ) Get-ItemProperty $rutas -ErrorAction SilentlyContinue | Where-Object DisplayName | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Sort-Object DisplayName | Export-Csv C:\Auditoria\software.csv -NoTypeInformation -Encoding UTF8
Para el estado de parches interesan dos ángulos: qué actualizaciones están puestas y, sobre todo, cuánto hace de la última. Un equipo sin parchear desde hace tres meses es el hallazgo, no la lista de KB.
# Ultimas actualizaciones instaladas y antiguedad de la mas reciente
$hotfix = Get-HotFix | Sort-Object InstalledOn -Descending
$hotfix | Select-Object -First 10 HotFixID, Description, InstalledOn
$dias = (New-TimeSpan -Start $hotfix[0].InstalledOn -End (Get-Date)).Days
if ($dias -gt 45) { Write-Warning "Ultimo parche hace $dias dias" }
# Estado del servicio de actualizaciones y ultima busqueda correcta
Get-Service wuauserv | Select-Object Status, StartType3. Usuarios, grupos y privilegios
Las cuentas son el terreno donde más rápido se acumulan riesgos: administradores locales que nadie recuerda haber añadido, cuentas de ex-empleados sin deshabilitar, contraseñas que no caducan nunca. En Windows 10/11 y Server 2016+ el módulo `Microsoft.PowerShell.LocalAccounts` cubre lo local.
# Cuentas locales habilitadas, con foco en las que nunca caducan
Get-LocalUser | Where-Object Enabled |
Select-Object Name, LastLogon, PasswordLastSet, PasswordExpires,
@{n='PwdNuncaCaduca';e={ -not $_.PasswordExpires }}
# Miembros del grupo Administradores locales (el hallazgo clasico)
Get-LocalGroupMember -Group 'Administradores' |
Select-Object Name, PrincipalSource, ObjectClassEn un dominio, el equivalente con el módulo `ActiveDirectory` es más revelador todavía. Estas tres consultas destapan la mayoría de los problemas de higiene de cuentas.
# Cuentas activas sin iniciar sesion en 90 dias
$limite = (Get-Date).AddDays(-90)
Search-ADAccount -AccountInactive -DateTime $limite -UsersOnly |
Where-Object Enabled | Select-Object Name, LastLogonDate
# Cuentas con contrasena que no caduca
Get-ADUser -Filter 'PasswordNeverExpires -eq $true -and Enabled -eq $true' -Properties PasswordNeverExpires
# Pertenencia a los grupos mas sensibles
'Domain Admins','Enterprise Admins','Schema Admins' | ForEach-Object {
Get-ADGroupMember $_ | Select-Object @{n='Grupo';e={$_}}, name, objectClass }Interpreta los resultados con contexto
Una cuenta de servicio con ‘PasswordNeverExpires’ puede ser correcta; una cuenta de persona casi nunca lo es · Un administrador local extra puede ser una herramienta de despliegue legítima: contrástalo antes de alarmar · El valor de una auditoría no es la lista cruda, sino comparar ‘lo que hay’ con ‘lo que debería haber’
4. Servicios y tareas programadas sospechosas
La persistencia —que el código vuelva a ejecutarse tras un reinicio— se apoya casi siempre en servicios, tareas programadas o claves de arranque. Auditarlas es buscar lo que se sale de lo normal: rutas raras, binarios sin firmar, ejecutables en carpetas de usuario o temporales.
# Servicios cuyo binario NO esta en Windows\System32 (revisar uno a uno)
Get-CimInstance Win32_Service |
Where-Object { $_.PathName -and $_.PathName -notmatch 'System32' } |
Select-Object Name, StartMode, State, StartName, PathName |
Sort-Object Name
# Binarios de servicio sin firma digital valida
Get-CimInstance Win32_Service | ForEach-Object {
$exe = ($_.PathName -replace '^"([^"]+)".*','$1') -replace '\s+/.*$',''
if (Test-Path $exe) {
$sig = Get-AuthenticodeSignature $exe
if ($sig.Status -ne 'Valid') { [pscustomobject]@{ Servicio=$_.Name; Exe=$exe; Firma=$sig.Status } }
}
}# Tareas programadas activas que ejecutan powershell, cmd, scripts o rutas de usuario
Get-ScheduledTask | Where-Object State -ne 'Disabled' | ForEach-Object {
$acc = $_.Actions.Execute -join ' '
if ($acc -match 'powershell|cmd|wscript|cscript|mshta|\\Users\\|\\Temp\\|\\AppData\\') {
[pscustomobject]@{
Tarea = $_.TaskName
Ruta = $_.TaskPath
Autor = $_.Author
Ejecuta = $acc
Args = $_.Actions.Arguments -join ' '
}
}
} | Format-Table -AutoSizeLos puntos de ejecución automática (claves `Run`, carpeta *Startup*, etc.) los cubre bien Autoruns de Sysinternals, que además tiene salida por línea de comandos (`autorunsc.exe -a * -c -h`) fácil de canalizar a un CSV. Para una revisión rápida sin dependencias, las claves más habituales:
$run = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
)
$run | ForEach-Object { Get-Item $_ -ErrorAction SilentlyContinue } |
Select-Object -ExpandProperty Property -ErrorAction SilentlyContinue5. Red: qué está escuchando y quién lo abrió
Un puerto a la escucha que no reconoces, o una conexión saliente establecida hacia una IP externa desde un proceso que no debería hablar con internet, son señales que merecen un vistazo. La clave es correlacionar la conexión con el proceso y su ruta en disco, no quedarse en el número de puerto.
# Puertos TCP a la escucha, con el proceso y su ejecutable
Get-NetTCPConnection -State Listen |
Select-Object LocalAddress, LocalPort,
@{n='Proceso';e={ (Get-Process -Id $_.OwningProcess).ProcessName }},
@{n='Ruta';e={ (Get-Process -Id $_.OwningProcess).Path }} |
Sort-Object LocalPort
# Conexiones salientes establecidas hacia direcciones publicas
Get-NetTCPConnection -State Established |
Where-Object { $_.RemoteAddress -notmatch '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|::1|fe80)' } |
Select-Object RemoteAddress, RemotePort,
@{n='Proceso';e={ (Get-Process -Id $_.OwningProcess).ProcessName }}Complementa con el estado del firewall: perfiles activos, y sobre todo reglas de entrada que permiten tráfico y que alguien pudo añadir para abrir una puerta.
Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction
Get-NetFirewallRule -Direction Inbound -Action Allow -Enabled True |
Where-Object { $_.DisplayName -notmatch 'Core Networking|mDNS' } |
Select-Object DisplayName, Profile,
@{n='Programa';e={ ($_ | Get-NetFirewallApplicationFilter).Program }}6. Política de auditoría y registro de eventos
De nada sirve revisar los logs si el sistema no está registrando lo que importa. `auditpol` muestra la política de auditoría avanzada efectiva; las subcategorías mínimas que deberían estar en *Success and Failure* son inicio de sesión, gestión de cuentas, cambios de política y creación de procesos.
# Volcado completo de la politica de auditoria efectiva
auditpol /get /category:*
# Comprobacion rapida de subcategorias clave
'Logon','Logoff','User Account Management','Audit Policy Change','Process Creation' |
ForEach-Object { auditpol /get /subcategory:"$_" }Con la auditoría bien configurada, el registro de eventos de seguridad responde a la mayoría de preguntas de una revisión. Estos son los Event ID que más rinden:
| Event ID | Qué indica | Uso en la auditoría |
|---|---|---|
| 4624 / 4625 | Inicio de sesión correcto / fallido | Fuerza bruta, accesos fuera de horario, tipo de logon (10 = RDP) |
| 4720 / 4726 | Cuenta de usuario creada / eliminada | Altas y bajas de cuentas no autorizadas |
| 4728 / 4732 / 4756 | Miembro añadido a grupo (global / local / universal) | Escalada de privilegios vía pertenencia a grupo |
| 4672 | Sesión iniciada con privilegios especiales | Uso de cuentas administrativas: ¿cuántas y cuándo? |
| 4688 | Proceso nuevo creado | Requiere auditoría de creación de procesos; con línea de comandos, oro puro |
| 7045 / 4697 | Servicio nuevo instalado | Persistencia por servicio |
| 1102 | El registro de auditoría fue borrado | Señal de alarma casi inequívoca |
# Inicios de sesion fallidos de las ultimas 24 h, agrupados por cuenta y origen
$desde = (Get-Date).AddDays(-1)
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4625; StartTime=$desde } -ErrorAction SilentlyContinue |
ForEach-Object {
$x = [xml]$_.ToXml()
[pscustomobject]@{
Cuenta = $x.Event.EventData.Data.Where({$_.Name -eq 'TargetUserName'}).'#text'
Origen = $x.Event.EventData.Data.Where({$_.Name -eq 'IpAddress'}).'#text'
}
} | Group-Object Cuenta, Origen | Sort-Object Count -Descending |
Select-Object Count, Name
# Comprobar si el log de seguridad se ha borrado alguna vez
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=1102 } -MaxEvents 5 -ErrorAction SilentlyContinue7. Líneas base: auditar es comparar
Una auditoría aislada te dice cómo está el sistema hoy. El valor real aparece cuando comparas esa foto con una línea base tomada en un momento en que sabías que el equipo estaba limpio. PowerShell hace esto trivial: serializa el estado a disco y compáralo después con `Compare-Object`.
# Tomar la linea base (equipo recien aprovisionado y verificado)
$base = [ordered]@{
Admins = (Get-LocalGroupMember 'Administradores').Name
Servicios= (Get-Service | Where-Object Status -eq 'Running').Name
Tareas = (Get-ScheduledTask | Where-Object State -ne 'Disabled').TaskName
Puertos = (Get-NetTCPConnection -State Listen).LocalPort | Sort-Object -Unique
}
$base | ConvertTo-Json -Depth 4 | Set-Content C:\Auditoria\baseline.json -Encoding UTF8# Semanas despues: comparar el estado actual con la linea base
$base = Get-Content C:\Auditoria\baseline.json -Raw | ConvertFrom-Json
$ahora = (Get-LocalGroupMember 'Administradores').Name
Compare-Object $base.Admins $ahora |
ForEach-Object {
$lado = if ($_.SideIndicator -eq '=>') { 'NUEVO (no estaba en la base)' } else { 'FALTA (estaba en la base)' }
"{0}: {1}" -f $lado, $_.InputObject
}El mismo patrón sirve para servicios, tareas, puertos, software o reglas de firewall. Programa el script de comparación como tarea diaria que te envíe por correo solo las diferencias, y habrás convertido una auditoría manual en una alerta de desviación de configuración.
Ámbito de uso y sentido común
Estos scripts son para auditar sistemas propios o de una organización que te ha encargado su administración; ejecutarlos contra equipos ajenos sin autorización puede constituir acceso ilícito a sistemas informáticos (art. 197 bis del Código Penal español) · Si durante la auditoría encuentras indicios de compromiso real, no sigas «trasteando» en el equipo: documenta, aísla y activa el procedimiento de respuesta a incidentes; cada comando que ejecutas altera marcas de tiempo y memoria · Guarda los CSV y transcripciones en una ubicación con acceso restringido: contienen un mapa detallado de tu infraestructura
Conclusión
La auditoría de seguridad con PowerShell no consiste en memorizar cien comandos, sino en tener media docena de bloques fiables —inventario, cuentas, persistencia, red, política de auditoría y eventos— guardados, versionados y listos para lanzar. La primera vez que los ejecutas obtienes una foto; a partir de la segunda, obtienes tendencias y desviaciones, que es donde están los hallazgos de verdad.
Empieza por lo barato y de alto impacto: activa transcripción y script block logging, toma una línea base de administradores locales, servicios y tareas, y programa una comparación diaria. Con eso solo ya detectarías la mayoría de las técnicas de persistencia más comunes antes de que un atacante tenga tiempo de asentarse.
Referencias y recursos
Microsoft Learn — ‘PowerShell logging’ y ‘Advanced security audit policy settings’ · Sysinternals — Autoruns, Sysmon y Process Explorer, complementos naturales de estos scripts · CIS Benchmarks para Windows — la lista de referencia de qué debería valer cada ajuste · PowerShell ♥ the Blue Team (blog de PowerShell Team) — endurecimiento y registro · Repositorio del proyecto ‘PSGumshoe’ y los módulos de auditoría de la comunidad DFIR
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

