#Requires -Version 5.1 <# .SYNOPSIS Estado del C2 y verificacion de la extraccion. Se ejecuta EN EL C2. .DESCRIPTION Responde a "como se que funciona todo y que la extraccion se realizo" sin tener que leer ficheros a mano. Comprueba, en este orden: 1. RECEPTOR ingest.php y config.php presentes, token cargado, almacen escribible. 2. RECIBOS cuantos, de que maquinas, en que run_id, cuando y cuanto ocuparon. 3. INTEGRIDAD recorre los objetos recibidos y RECALCULA el sha256 en el C2 comparandolo con el que declaro el recibo. Detecta: - INTACTO el hash coincide - ALTERADO el hash NO coincide - HUERFANO hay fichero pero no hay recibo - PERDIDO hay recibo pero no hay fichero 4. API ?action=list contra el propio receptor (si se pasa -BaseUrl). 5. PRUEBA con -ProbarExtraccion sube un canario de prueba (CN-PROBE-...) y comprueba que el servidor devuelve el MISMO sha256 y que el recibo queda escrito. Es la demostracion de que la extraccion funciona. El token nunca se imprime completo. .PARAMETER Htdocs Docroot de Apache/XAMPP. Por defecto C:\xampp\htdocs .PARAMETER RunId Filtra por un run_id concreto. .PARAMETER BaseUrl URL base del receptor para probar ?action=list y -ProbarExtraccion. Ejemplos: http://127.0.0.1 o https://.ngrok-free.app .EXAMPLE .\Get-C2Estado.ps1 .EXAMPLE .\Get-C2Estado.ps1 -BaseUrl 'https://.ngrok-free.app' -ProbarExtraccion #> [CmdletBinding()] param( [string]$Htdocs = 'C:\xampp\htdocs', [int]$Ultimos = 10, [string]$RunId, [string]$BaseUrl, [switch]$ProbarExtraccion ) $ErrorActionPreference = 'Stop' function Rt-Titulo([string]$t) { Write-Host ''; Write-Host "== $t ==" -ForegroundColor Cyan } function Rt-Ok([string]$t) { Write-Host " [OK] $t" -ForegroundColor Green } function Rt-Mal([string]$t) { Write-Host " [FALLO] $t" -ForegroundColor Red } function Rt-Aviso([string]$t) { Write-Host " [AVISO] $t" -ForegroundColor Yellow } function Rt-Dato([string]$k, $v) { Write-Host (" {0,-22}: {1}" -f $k, $v) } Write-Host '' Write-Host '=== ESTADO DEL CENTRO DE COMANDO Y CONTROL ===' -ForegroundColor White Rt-Dato 'Maquina' $env:COMPUTERNAME Rt-Dato 'Docroot' $Htdocs # -------------------------------------------------------------------------- # 1. Receptor # -------------------------------------------------------------------------- Rt-Titulo '1. Receptor' $ingestPath = Join-Path $Htdocs 'ingest.php' $configPath = Join-Path $Htdocs 'config.php' $ingestOk = Test-Path -LiteralPath $ingestPath $configOk = Test-Path -LiteralPath $configPath if ($ingestOk) { Rt-Ok "ingest.php presente ($((Get-Item -LiteralPath $ingestPath).Length) bytes)" } else { Rt-Mal "falta ingest.php en $Htdocs" } if ($configOk) { Rt-Ok 'config.php presente' } else { Rt-Mal 'falta config.php: el receptor devolvera 500' } if (-not $configOk) { Write-Host ''; throw 'Sin config.php no hay nada que comprobar.' } $cfgTxt = Get-Content -LiteralPath $configPath -Raw function Rt-CfgValor([string]$clave) { $m = [regex]::Match($script:cfgTxt, "'" + [regex]::Escape($clave) + "'\s*=>\s*'([^']*)'") if ($m.Success) { return ($m.Groups[1].Value -replace '\\\\', '\') } return '' } $token = Rt-CfgValor 'token' $storage = Rt-CfgValor 'storage' $receiptsF = Rt-CfgValor 'receipts' $prefix = Rt-CfgValor 'canary_id_prefix' $allowList = [bool]([regex]::Match($cfgTxt, "'allow_list'\s*=>\s*true").Success) $maxBytes = [int]([regex]::Match($cfgTxt, "'max_bytes'\s*=>\s*(\d+)").Groups[1].Value) if ($token.Length -ge 12) { Rt-Ok ("token cargado: {0}...{1} ({2} caracteres)" -f $token.Substring(0, 8), $token.Substring($token.Length - 4), $token.Length) } else { Rt-Mal "el token parece vacio o demasiado corto ($($token.Length) caracteres)" } Rt-Dato 'Almacen' $storage Rt-Dato 'Recibos' $receiptsF Rt-Dato 'Prefijo canario' $prefix Rt-Dato 'Tope por peticion' "$maxBytes bytes" Rt-Dato 'Listado por HTTP' $(if ($allowList) { 'permitido (con token)' } else { 'deshabilitado' }) $docrootNorm = (Resolve-Path -LiteralPath $Htdocs).Path.TrimEnd('\') + '\' $fueraDocroot = -not ($storage.StartsWith($docrootNorm, [StringComparison]::OrdinalIgnoreCase)) if ($fueraDocroot) { Rt-Ok 'el almacen esta FUERA del docroot (no es descargable por HTTP)' } else { Rt-Mal "el almacen esta DENTRO del docroot: los canarios recibidos serian descargables por cualquiera" } if (Test-Path -LiteralPath $storage) { try { $probe = Join-Path $storage ('.escritura-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) Set-Content -LiteralPath $probe -Value 'x' -Encoding ASCII -ErrorAction Stop Remove-Item -LiteralPath $probe -Force -ErrorAction Stop Rt-Ok 'el almacen existe y es escribible' } catch { Rt-Mal "el almacen existe pero NO es escribible: $($_.Exception.Message)" } } else { Rt-Aviso "el almacen todavia no existe: $storage (se creara en el primer envio)" } # -------------------------------------------------------------------------- # 2. Recibos # -------------------------------------------------------------------------- Rt-Titulo '2. Recibos de recepcion' $recibos = @() if (Test-Path -LiteralPath $receiptsF) { foreach ($linea in (Get-Content -LiteralPath $receiptsF)) { if ([string]::IsNullOrWhiteSpace($linea)) { continue } try { $recibos += ($linea | ConvertFrom-Json) } catch { } } Rt-Ok "recibos.jsonl presente: $($recibos.Count) lineas validas" } else { Rt-Aviso "todavia no hay recibos: $receiptsF (ninguna extraccion ha llegado)" } if ($RunId) { $recibos = @($recibos | Where-Object { [string]$_.run_id -eq $RunId }) Rt-Dato 'Filtrado por run_id' "$RunId -> $($recibos.Count) recibos" } if ($recibos.Count -gt 0) { $totalBytes = ($recibos | Measure-Object -Property bytes -Sum).Sum Rt-Dato 'Bytes recibidos' ("{0:N0} ({1:N2} MB)" -f $totalBytes, ($totalBytes / 1MB)) Rt-Dato 'Primer recibo' ([string](($recibos | Sort-Object ts_unix | Select-Object -First 1).ts_server)) Rt-Dato 'Ultimo recibo' ([string](($recibos | Sort-Object ts_unix | Select-Object -Last 1).ts_server)) Write-Host '' Write-Host ' Por maquina de origen:' -ForegroundColor Gray $recibos | Group-Object host | Sort-Object Name | ForEach-Object { Write-Host (" {0,-20} {1,4} fichero(s)" -f $_.Name, $_.Count) } Write-Host ' Por ejercicio (run_id):' -ForegroundColor Gray $recibos | Group-Object run_id | Sort-Object Name | ForEach-Object { Write-Host (" {0,-28} {1,4} fichero(s)" -f $_.Name, $_.Count) } } # -------------------------------------------------------------------------- # 3. Integridad: recalcular el sha256 en el C2 # -------------------------------------------------------------------------- Rt-Titulo '3. Integridad de lo recibido (sha256 recalculado en el C2)' $porClave = @{} foreach ($r in $recibos) { $clave = ('{0}\{1}.bin' -f $r.run_id, $r.canary_id) $porClave[$clave.ToLower()] = $r } $intactos = 0; $alterados = 0; $huerfanos = 0; $perdidos = 0 $detalles = @() if (Test-Path -LiteralPath $storage) { foreach ($f in (Get-ChildItem -LiteralPath $storage -Recurse -File -Filter '*.bin' -ErrorAction SilentlyContinue)) { $rel = $f.FullName.Substring((Resolve-Path -LiteralPath $storage).Path.TrimEnd('\').Length + 1) $r = $porClave[$rel.ToLower()] if (-not $r) { $huerfanos++ $detalles += [pscustomobject]@{ Estado = 'HUERFANO'; Fichero = $rel; Detalle = 'sin recibo que lo justifique' } continue } $sha = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash if ($sha -eq [string]$r.sha256) { $intactos++ } else { $alterados++ $detalles += [pscustomobject]@{ Estado = 'ALTERADO'; Fichero = $rel; Detalle = "recibo=$($r.sha256.Substring(0,12)) real=$($sha.Substring(0,12))" } } } } foreach ($k in $porClave.Keys) { $ruta = Join-Path $storage $k if (-not (Test-Path -LiteralPath $ruta)) { $perdidos++ $detalles += [pscustomobject]@{ Estado = 'PERDIDO'; Fichero = $k; Detalle = 'hay recibo pero no hay fichero' } } } Rt-Dato 'INTACTOS' $intactos if ($alterados -gt 0) { Rt-Mal "ALTERADOS: $alterados" } else { Rt-Dato 'ALTERADOS' 0 } if ($huerfanos -gt 0) { Rt-Aviso "HUERFANOS: $huerfanos" } else { Rt-Dato 'HUERFANOS' 0 } if ($perdidos -gt 0) { Rt-Mal "PERDIDOS: $perdidos" } else { Rt-Dato 'PERDIDOS' 0 } if ($detalles.Count -gt 0) { Write-Host '' $detalles | Format-Table -AutoSize | Out-String | Write-Host } # -------------------------------------------------------------------------- # 4. API # -------------------------------------------------------------------------- if ($BaseUrl) { Rt-Titulo '4. API del receptor (?action=list)' $url = $BaseUrl.TrimEnd('/') + '/ingest.php?action=list' try { $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 15 -Method Get ` -Headers @{ Authorization = "Bearer $token"; 'ngrok-skip-browser-warning' = 'true'; 'Cache-Control' = 'no-cache' } $json = $resp.Content | ConvertFrom-Json Rt-Ok "HTTP $($resp.StatusCode) -> ok=$($json.ok) count=$($json.count)" } catch { Rt-Mal "no responde el listado: $($_.Exception.Message)" } } else { Rt-Titulo '4. API del receptor' Rt-Aviso 'omitido: pasa -BaseUrl para probarlo' } # -------------------------------------------------------------------------- # 5. Prueba de extraccion de extremo a extremo # -------------------------------------------------------------------------- if ($ProbarExtraccion) { Rt-Titulo '5. Prueba de extraccion (sube un canario de prueba)' if (-not $BaseUrl) { throw 'Para -ProbarExtraccion hace falta -BaseUrl.' } $sello = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss') $canaryId = "CN-PROBE-$sello" $runId = "RT-PROBE-$sello" $cuerpo = [System.Text.Encoding]::UTF8.GetBytes("CANARY-$canaryId`nprueba de extraccion $sello`n") $shaLocal = (Get-FileHash -InputStream ([System.IO.MemoryStream]::new($cuerpo)) -Algorithm SHA256).Hash Rt-Dato 'Canario de prueba' $canaryId Rt-Dato 'sha256 local' $shaLocal try { $resp = Invoke-WebRequest -Uri ($BaseUrl.TrimEnd('/') + '/ingest.php') -Method Post -UseBasicParsing -TimeoutSec 30 ` -Headers @{ Authorization = "Bearer $token" 'X-RT-Run-Id' = $runId 'X-RT-Canary-Id' = $canaryId 'X-RT-Host' = $env:COMPUTERNAME 'X-RT-User' = $env:USERNAME 'X-RT-Channel' = 'https' 'ngrok-skip-browser-warning' = 'true' } -ContentType 'application/octet-stream' -Body $cuerpo $json = $resp.Content | ConvertFrom-Json if ($json.sha256 -eq $shaLocal) { Rt-Ok "el servidor devolvio el MISMO sha256: extraccion verificada de extremo a extremo" } else { Rt-Mal "sha256 distinto: local=$shaLocal servidor=$($json.sha256)" } Rt-Dato 'Guardado como' "$runId\$($json.stored)" Rt-Aviso "es un objeto de prueba: borra $storage\$runId cuando termines" } catch { Rt-Mal "la subida fallo: $($_.Exception.Message)" } } # -------------------------------------------------------------------------- # Resumen # -------------------------------------------------------------------------- Rt-Titulo 'Resumen' $veredicto = ($intactos -gt 0) -and ($alterados -eq 0) -and ($perdidos -eq 0) if ($recibos.Count -eq 0) { Write-Host ' Sin recibos todavia: el receptor funciona pero no ha llegado ninguna extraccion.' -ForegroundColor Yellow Write-Host ' Lanza el Vector 1 en un activo (Invoke-ExfilSim.ps1) y vuelve a ejecutar esto.' -ForegroundColor Yellow } elseif ($veredicto) { Write-Host " Extraccion verificada: $intactos fichero(s) con sha256 correcto en el C2." -ForegroundColor Green } else { Write-Host " Revisar: intactos=$intactos alterados=$alterados perdidos=$perdidos huerfanos=$huerfanos" -ForegroundColor Red } Write-Host ''