[Tools] W-Zombi refactorizado: Ruby servidor, payloads modulares, migrado a tools/
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# 🧟 W-Zombi: Toolkit de Inyección Lateral para Windows VMs
|
||||
|
||||
> Técnica documentada en la bitácora del [25/02/2026](../../bitacoras/2026-02-25.md) (Hito DB01).
|
||||
|
||||
## Problema
|
||||
|
||||
La consola HTML5/VNC de Proxmox **no soporta copiar/pegar** texto. Esto impide provisionar comandos complejos en VMs Windows recién creadas que aún no tienen SSH.
|
||||
|
||||
## Solución
|
||||
|
||||
Arquitectura C2 liviana que sortea la limitación:
|
||||
|
||||
```
|
||||
srv-ns8 (10.0.10.8) VM Windows (VNC)
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ servidor.rb │ ← HTTP :8000 ── │ zombi.ps1 (loop) │
|
||||
│ (Ruby / WEBrick) │ │ Consulta activo.ps1 │
|
||||
│ │ ── payload ────→ │ cada 10 seg │
|
||||
│ payloads/activo.ps1 │ │ Ejecuta y reporta │
|
||||
│ (hot-swap por IA) │ ← POST /log ── │ telemetría al server │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## Uso
|
||||
|
||||
### 1. En srv-ns8 (la IA o el operador):
|
||||
```bash
|
||||
ruby tools/w-zombi/servidor.rb
|
||||
```
|
||||
|
||||
### 2. En la VM Windows (tipear manualmente en VNC — PowerShell Admin):
|
||||
```powershell
|
||||
iwr 10.0.10.8:8000/zombi.ps1 -useb|iex
|
||||
```
|
||||
|
||||
### 3. Cambiar payload activo:
|
||||
```bash
|
||||
# Copiar un payload pre-armado
|
||||
cp tools/w-zombi/payloads/install_ssh.ps1 tools/w-zombi/payloads/activo.ps1
|
||||
|
||||
# O la IA modifica activo.ps1 directamente
|
||||
```
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
tools/w-zombi/
|
||||
├── servidor.rb ← Servidor HTTP Ruby (WEBrick)
|
||||
├── README.md ← Este archivo
|
||||
└── payloads/
|
||||
├── activo.ps1 ← Payload que ejecuta la VM (hot-swap)
|
||||
├── install_ssh.ps1 ← Instalar OpenSSH Server nativo
|
||||
└── sonda.ps1 ← Diagnóstico y relevamiento del sistema
|
||||
```
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Servidor **efímero** — solo se levanta durante la operación.
|
||||
- Solo escucha en la red interna (`10.0.10.x` / `10.0.100.x`).
|
||||
- Una vez instalado SSH, este mecanismo deja de ser necesario.
|
||||
@@ -0,0 +1 @@
|
||||
Write-Host 'W-Zombi: Sin payload activo. La IA lo actualizara pronto.' -ForegroundColor Yellow
|
||||
@@ -0,0 +1,40 @@
|
||||
function Log-Msg {
|
||||
param([string]$Message)
|
||||
Write-Host $Message -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-RestMethod -Uri "http://10.0.10.8:8000/log" -Method Post -Body @{msg=$Message} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "INSTALANDO Y HABILITANDO OPENSSH SERVER NATIVO"
|
||||
Log-Msg "=========================================================="
|
||||
try {
|
||||
$sshCheck = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
|
||||
if ($sshCheck.State -ne 'Installed') {
|
||||
Log-Msg "Instalando caracteristica OpenSSH.Server (esto puede tomar un minuto)..."
|
||||
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 | Out-Null
|
||||
} else {
|
||||
Log-Msg "OpenSSH.Server ya se encuentra instalado."
|
||||
}
|
||||
|
||||
Log-Msg "Configurando servicio sshd en Inicio Automatico..."
|
||||
Set-Service -Name sshd -StartupType 'Automatic'
|
||||
Start-Service sshd -ErrorAction SilentlyContinue
|
||||
|
||||
if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue)) {
|
||||
Log-Msg "Abriendo puerto TCP 22 en el Firewall local..."
|
||||
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
|
||||
}
|
||||
|
||||
$ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object InterfaceAlias -notmatch 'Loopback').IPAddress
|
||||
Log-Msg "OpenSSH Server instalado y corriendo en el puerto 22."
|
||||
Log-Msg "Conectar via: ssh Administrador@$($ip[0])"
|
||||
} catch {
|
||||
Log-Msg "Fallo al instalar SSH: $_"
|
||||
}
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "OPERACION COMPLETADA. Ahora SSH esta disponible."
|
||||
Log-Msg "=========================================================="
|
||||
exit
|
||||
@@ -0,0 +1,48 @@
|
||||
function Log-Msg {
|
||||
param([string]$Message)
|
||||
Write-Host $Message -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-RestMethod -Uri "http://10.0.10.8:8000/log" -Method Post -Body @{msg=$Message} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "SONDA DE DIAGNOSTICO — RELEVAMIENTO DEL SISTEMA"
|
||||
Log-Msg "=========================================================="
|
||||
|
||||
Log-Msg "Hostname: $env:COMPUTERNAME"
|
||||
Log-Msg "OS: $((Get-CimInstance Win32_OperatingSystem).Caption)"
|
||||
Log-Msg "Arquitectura: $((Get-CimInstance Win32_OperatingSystem).OSArchitecture)"
|
||||
|
||||
# Red
|
||||
$ips = Get-NetIPAddress -AddressFamily IPv4 | Where-Object InterfaceAlias -notmatch 'Loopback'
|
||||
foreach ($ip in $ips) {
|
||||
Log-Msg "Red [$($ip.InterfaceAlias)]: $($ip.IPAddress)/$($ip.PrefixLength)"
|
||||
}
|
||||
|
||||
# Dominio
|
||||
try {
|
||||
$domain = (Get-CimInstance Win32_ComputerSystem).Domain
|
||||
$inDomain = (Get-CimInstance Win32_ComputerSystem).PartOfDomain
|
||||
Log-Msg "Dominio: $domain (Unido: $inDomain)"
|
||||
} catch {
|
||||
Log-Msg "Dominio: No disponible"
|
||||
}
|
||||
|
||||
# Servicios clave
|
||||
$servicios = @('sshd', 'MSSQLSERVER', 'NTDS', 'DNS')
|
||||
foreach ($svc in $servicios) {
|
||||
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
|
||||
if ($s) {
|
||||
Log-Msg "Servicio [$svc]: $($s.Status)"
|
||||
}
|
||||
}
|
||||
|
||||
# Firewall SSH
|
||||
$rule = Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue
|
||||
Log-Msg "Firewall SSH (TCP 22): $(if ($rule) { 'Habilitado' } else { 'No configurado' })"
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "SONDA COMPLETADA."
|
||||
Log-Msg "=========================================================="
|
||||
exit
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# W-Zombi Servidor — Servidor HTTP de inyección lateral para VMs Windows
|
||||
# Uso: ruby servidor.rb [opciones]
|
||||
#
|
||||
# Sirve archivos del directorio payloads/ y recibe telemetría POST desde las VMs.
|
||||
# El archivo payloads/activo.ps1 es el que consumirá el loop C2 de la VM.
|
||||
|
||||
require 'webrick'
|
||||
require 'json'
|
||||
require 'fileutils'
|
||||
|
||||
TOOL_DIR = File.expand_path(__dir__)
|
||||
PAYLOADS = File.join(TOOL_DIR, 'payloads')
|
||||
LOG_FILE = File.join(TOOL_DIR, 'telemetria.log')
|
||||
PORT = (ARGV.find { |a| a.match?(/^\d+$/) } || 8000).to_i
|
||||
|
||||
# Colores ANSI
|
||||
C = { reset: "\e[0m", green: "\e[32m", cyan: "\e[36m", yellow: "\e[33m", red: "\e[31m", dim: "\e[2m" }
|
||||
|
||||
def banner
|
||||
puts "#{C[:green]}╔══════════════════════════════════════════════════╗#{C[:reset]}"
|
||||
puts "#{C[:green]}║ 🧟 W-ZOMBI Servidor — Inyección Lateral ║#{C[:reset]}"
|
||||
puts "#{C[:green]}╚══════════════════════════════════════════════════╝#{C[:reset]}"
|
||||
puts "#{C[:cyan]}Puerto: #{C[:yellow]}#{PORT}#{C[:reset]}"
|
||||
puts "#{C[:cyan]}Payloads: #{C[:yellow]}#{PAYLOADS}/#{C[:reset]}"
|
||||
puts "#{C[:cyan]}Telemetría:#{C[:yellow]}#{LOG_FILE}#{C[:reset]}"
|
||||
puts "#{C[:dim]}─────────────────────────────────────────────────────#{C[:reset]}"
|
||||
|
||||
# Listar payloads disponibles
|
||||
Dir.glob(File.join(PAYLOADS, '*.ps1')).sort.each do |f|
|
||||
name = File.basename(f)
|
||||
activo = name == 'activo.ps1' ? " #{C[:green]}◀ ACTIVO#{C[:reset]}" : ''
|
||||
puts " 📄 #{C[:cyan]}#{name}#{C[:reset]}#{activo}"
|
||||
end
|
||||
puts "#{C[:dim]}─────────────────────────────────────────────────────#{C[:reset]}"
|
||||
puts "#{C[:yellow]}En la VM (PowerShell Admin):#{C[:reset]}"
|
||||
puts " #{C[:green]}iwr 10.0.10.8:#{PORT}/zombi.ps1 -useb|iex#{C[:reset]}"
|
||||
puts "#{C[:dim]}─────────────────────────────────────────────────────#{C[:reset]}"
|
||||
puts ''
|
||||
end
|
||||
|
||||
def timestamp
|
||||
Time.now.strftime('%H:%M:%S')
|
||||
end
|
||||
|
||||
def log_telemetry(msg)
|
||||
line = "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{msg}"
|
||||
File.open(LOG_FILE, 'a') { |f| f.puts(line) }
|
||||
puts "#{C[:cyan]}[#{timestamp}] 📡 TELEMETRÍA:#{C[:reset]} #{msg}"
|
||||
end
|
||||
|
||||
# Crear directorio de payloads si no existe
|
||||
FileUtils.mkdir_p(PAYLOADS)
|
||||
|
||||
# Si no hay activo.ps1, crear uno vacío señalizador
|
||||
activo = File.join(PAYLOADS, 'activo.ps1')
|
||||
unless File.exist?(activo)
|
||||
File.write(activo, "Write-Host 'W-Zombi: Sin payload activo. La IA lo actualizara pronto.' -ForegroundColor Yellow\n")
|
||||
end
|
||||
|
||||
# Servidor HTTP
|
||||
server = WEBrick::HTTPServer.new(
|
||||
Port: PORT,
|
||||
Logger: WEBrick::Log.new('/dev/null'),
|
||||
AccessLog: []
|
||||
)
|
||||
|
||||
# Ruta principal: sirve el loader (zombi.ps1) dinámicamente
|
||||
server.mount_proc '/zombi.ps1' do |_req, res|
|
||||
# Genera un loader que apunta al payload activo
|
||||
loader = <<~PS1
|
||||
Write-Host "======================================================" -ForegroundColor Green
|
||||
Write-Host " ZOMBI C2 LOOP — Conectado a srv-ns8:#{PORT} " -ForegroundColor Green
|
||||
Write-Host "======================================================" -ForegroundColor Green
|
||||
Write-Host "CTRL+C para detener." -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
while ($true) {
|
||||
try {
|
||||
$payload = Invoke-RestMethod -Uri "http://10.0.10.8:#{PORT}/payloads/activo.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
if ($payload -and $payload.Trim() -ne "") {
|
||||
try {
|
||||
Invoke-Expression $payload
|
||||
} catch {
|
||||
Write-Host ">>> ERROR EN PAYLOAD <<<" -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
for ($i = 10; $i -gt 0; $i--) {
|
||||
Write-Host -NoNewline "`r[ZOMBI] Esperando payload... $i seg "
|
||||
if ([console]::KeyAvailable) {
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
Write-Host -NoNewline "`r `r"
|
||||
}
|
||||
PS1
|
||||
res['Content-Type'] = 'text/plain'
|
||||
res.body = loader
|
||||
puts "#{C[:green]}[#{timestamp}] 🧟 Loader descargado por VM#{C[:reset]}"
|
||||
end
|
||||
|
||||
# Servir payloads estáticos
|
||||
server.mount('/payloads', WEBrick::HTTPServlet::FileHandler, PAYLOADS)
|
||||
|
||||
# Endpoint de telemetría (POST /log)
|
||||
server.mount_proc '/log' do |req, res|
|
||||
if req.request_method == 'POST'
|
||||
body = req.body || ''
|
||||
msg = if body.include?('=')
|
||||
URI.decode_www_form(body).to_h['msg'] || body
|
||||
else
|
||||
body
|
||||
end
|
||||
log_telemetry(msg) unless msg.strip.empty?
|
||||
end
|
||||
res['Content-Type'] = 'text/plain'
|
||||
res.body = 'OK'
|
||||
end
|
||||
|
||||
# Señal de parada limpia
|
||||
trap('INT') do
|
||||
puts "\n#{C[:yellow]}[#{timestamp}] Servidor detenido.#{C[:reset]}"
|
||||
server.shutdown
|
||||
end
|
||||
|
||||
banner
|
||||
server.start
|
||||
Reference in New Issue
Block a user