Refactor(ADN): Retructuración del ecosistema ADN para unificar herramientas independientes. Fusión de w-zombi como adn/sondas y ns8-candados como adn/seguridad. Renombrado de comando/ a cli/ para consistencia y plan/parser.rb a parsers/plan.rb
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/adn/comando/cierre.rb — Subcomando para cierre de jornada laboral (DB-First)
|
||||
# tools/adn/cli/cierre.rb — Subcomando para cierre de jornada laboral (DB-First)
|
||||
# ==============================================================================
|
||||
|
||||
require 'date'
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/adn/comando/conocimiento.rb — Subcomando para gestionar la Base de Conocimiento del ADN
|
||||
# tools/adn/cli/conocimiento.rb — Subcomando para gestionar la Base de Conocimiento del ADN
|
||||
# ==============================================================================
|
||||
# Inyecta nuevos documentos y los referencia automáticamente en el 00_indice.md
|
||||
# cumpliendo con el principio de Armonía Integral.
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/adn/comando/inicio.rb — Subcomando para inicio de jornada laboral (DB-First)
|
||||
# tools/adn/cli/inicio.rb — Subcomando para inicio de jornada laboral (DB-First)
|
||||
# ==============================================================================
|
||||
|
||||
require 'date'
|
||||
@@ -31,10 +31,10 @@ require 'yaml'
|
||||
|
||||
# Cargar módulo PlanParser
|
||||
begin
|
||||
require_relative '../plan/parser'
|
||||
require_relative '../parsers/plan'
|
||||
rescue LoadError => e
|
||||
puts "⚠ Advertencia: No se pudo cargar PlanParser: #{e.message}"
|
||||
puts " Asegúrate de que tools/adn/plan/parser.rb existe"
|
||||
puts " Asegúrate de que tools/adn/parsers/plan.rb existe"
|
||||
end
|
||||
|
||||
module ADN
|
||||
@@ -7,7 +7,7 @@ rutas:
|
||||
proyectos: "../../proyectos"
|
||||
logs: "../../logs/adn"
|
||||
tmp: "../../tmp"
|
||||
ns8_candados: "../ns8-candados/ns8-candados.rb"
|
||||
ns8_candados: "../seguridad/ns8-candados.rb"
|
||||
validador:
|
||||
watch_mode: false
|
||||
strict_mode: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/adn/plan/parser.rb — Parser para seguimiento de planes ADN
|
||||
# tools/adn/parsers/plan.rb — Parser para seguimiento de planes ADN
|
||||
# ============================================================================
|
||||
# Este módulo proporciona funcionalidad para parsear, actualizar y generar
|
||||
# reportes del plan de trabajo ADN BITACORAs.
|
||||
+12
-12
@@ -46,13 +46,13 @@ require_relative 'core/logger'
|
||||
require_relative 'core/eventos'
|
||||
require_relative 'core/triggers'
|
||||
require_relative 'core/validador'
|
||||
require_relative 'comando/db'
|
||||
require_relative 'comando/plan'
|
||||
require_relative 'comando/backup'
|
||||
require_relative 'comando/triggers'
|
||||
require_relative 'comando/conocimiento'
|
||||
require_relative 'comando/cierre'
|
||||
require_relative 'comando/inicio'
|
||||
require_relative 'cli/db'
|
||||
require_relative 'cli/plan'
|
||||
require_relative 'cli/backup'
|
||||
require_relative 'cli/triggers'
|
||||
require_relative 'cli/conocimiento'
|
||||
require_relative 'cli/cierre'
|
||||
require_relative 'cli/inicio'
|
||||
require_relative 'core/conciliador'
|
||||
|
||||
require_relative 'core/colores'
|
||||
@@ -61,11 +61,11 @@ require_relative 'core/colores'
|
||||
# El logger está implementado en adn/logger.rb
|
||||
ADNLogger = ADN::Logger
|
||||
|
||||
require_relative 'comando/validador'
|
||||
require_relative 'comando/contexto'
|
||||
require_relative 'comando/generar'
|
||||
require_relative 'comando/salud'
|
||||
require_relative 'comando/ayuda'
|
||||
require_relative 'cli/validador'
|
||||
require_relative 'cli/contexto'
|
||||
require_relative 'cli/generar'
|
||||
require_relative 'cli/salud'
|
||||
require_relative 'cli/ayuda'
|
||||
|
||||
# ─── CLI Principal ──────────────────────────────────────────────────────
|
||||
class ADNCLI
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"sudo": "eyJpdiI6IkgzUnpoNHVGeWVqQUVGSDQiLCJ0YWciOiI3TmhPcjRGVFo3TFZHTUpNREVncXlBPT0iLCJkYXRhIjoiQlptVmtYbGdEcjFYWkxpZ21nPT0ifQ==",
|
||||
"rsa": "eyJpdiI6IkpoTmpZRWZMeE12eWhXSVMiLCJ0YWciOiJUUnJGY05qRlVoOTl2REplTms0ZU5RPT0iLCJkYXRhIjoiNkFIWCsyYm9ndzZKbGM3c0hRPT0ifQ==",
|
||||
"domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||
"admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||
"win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==",
|
||||
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0="
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# 🔐 ns8-candados: Gestión de Secretos y Autorización SSH
|
||||
|
||||
## Resumen Ejecutivo
|
||||
`ns8-candados` es una herramienta de seguridad robusta diseñada para centralizar, cifrar y gestionar secretos operativos del ecosistema `srv-ns8`. Utiliza el algoritmo industrial **AES-256-GCM** para garantizar la integridad y confidencialidad de las credenciales. Su arquitectura implementa un sistema de **Candado Temporal (MFA)** que requiere una autorización explícita para liberar secretos, protegiendo la información incluso si el servidor se ve comprometido.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Análisis Estructural
|
||||
- **Tipo de Proyecto**: Utilidad de seguridad CLI escrita en Ruby.
|
||||
- **Organización**: Estructura modular centrada en la protección del dato:
|
||||
- `Cripto`: Motor criptográfico AES-256-GCM con IV aleatorio.
|
||||
- `Boveda`: Persistencia cifrada en `.boveda.json`.
|
||||
- **MFA / Autorización**: Sistema de sesiones temporales basadas en archivos con permisos restringidos.
|
||||
- **Archivos Fundamentales**:
|
||||
- `ns8-candados.rb`: Lógica central.
|
||||
- `.master.key` (Local): La semilla de cifrado (permisos 600).
|
||||
- `.boveda.json`: Almacén de secretos cifrados (permisos 600).
|
||||
- `.session`: Token de autorización temporal (vida: 5 min, permisos 600).
|
||||
|
||||
## ⚙️ Configuración y Dependencias
|
||||
- **Tecnologías**: Ruby, OpenSSL.
|
||||
- **Seguridad de Archivos**: Todos los archivos sensibles se manejan con `chmod 600`, asegurando que **solo el propietario** de la herramienta pueda operar.
|
||||
- **Comandos de Gestión**:
|
||||
- `ruby ns8-candados.rb authorize`: **Abre el candado**. Habilita el acceso a secretos durante 5 minutos.
|
||||
- `ruby ns8-candados.rb cerrar`: **Cierra el candado** manualmente eliminando la sesión activa.
|
||||
- `ruby ns8-candados.rb list`: Lista las claves guardadas (requiere autorización).
|
||||
- `ruby ns8-candados.rb get <clave>`: Recupera el valor descifrado (requiere autorización).
|
||||
|
||||
## 🛡️ Auditoría (logging)
|
||||
La herramienta mantiene un archivo `access.log` en su directorio local que registra de forma inmutable:
|
||||
- **Timestamp**: Fecha y hora exacta de la acción.
|
||||
- **Usuario**: El usuario del sistema operativo que ejecutó el comando.
|
||||
- **Acción**: `AUTHORIZE`, `GET`, `LOCK`, `DENIED` o `EXPIRED`.
|
||||
- **Detalle**: Nombre de la clave solicitada (solo en comandos `get`).
|
||||
|
||||
El archivo tiene permisos `600` para asegurar que solo el propietario pueda auditar los accesos.
|
||||
|
||||
## 🧠 Contexto para IA
|
||||
- **Flujo Obligatorio**: Los agentes de IA DEBEN ejecutar `authorize` antes de intentar recuperar cualquier clave.
|
||||
- **Captura Segura**: Capturar siempre la salida en variables silenciadas:
|
||||
```bash
|
||||
ruby tools/ns8-candados/ns8-candados.rb authorize
|
||||
PASS=$(ruby tools/ns8-candados/ns8-candados.rb get clave)
|
||||
# Operar y luego cerrar:
|
||||
ruby tools/ns8-candados/ns8-candados.rb cerrar
|
||||
```
|
||||
|
||||
## 🚀 Próximos Pasos
|
||||
|
||||
### Propuestas de Mejora
|
||||
- **Auditoría**: Implementar un `access.log` para trazar quién y cuándo abrió el candado.
|
||||
- **Notificación**: Enviar un mensaje (ej. Telegram/Mail) cada vez que se ejecute `authorize`.
|
||||
|
||||
---
|
||||
*Generado siguiendo docs/prompt/documentacion.md*
|
||||
Executable
+319
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ns8-candados — Gestión unificada de secretos y autorización SSH
|
||||
# ==============================================================================
|
||||
# 🤖 INSTRUCCIONES PARA LA IA (AGENTES EXTERNOS):
|
||||
# Esta herramienta permite obtener credenciales seguras sin exponerlas en el chat.
|
||||
# PARA USARLA CORRECTAMENTE (SIN FILTRAR SECRETOS):
|
||||
# Ejecuta el comando y asigna su salida a una variable silenciada:
|
||||
# PASS=$(ruby tools/ns8-candados/ns8-candados.rb get nombre_clave)
|
||||
# sshpass -p "$PASS" ssh usuario@servidor
|
||||
# NUNCA ejecutes `ruby ns8-candados.rb get clave` directamente en run_command sin
|
||||
# capturar o redirigir el output, porque el secreto se filtrará en la bitácora del LLM.
|
||||
# ==============================================================================
|
||||
# Uso: ruby tools/ns8-candados/ns8-candados.rb <comando> [args]
|
||||
#
|
||||
# Comandos:
|
||||
# abrir Autorizar SSH (cargar llave con passphrase)
|
||||
# encrypt <txt> Cifrar texto plano
|
||||
# decrypt <tok> Descifrar payload
|
||||
# get <clave> Obtener secreto de la bóveda (salida: solo el valor)
|
||||
# run <cl> <vr> <cmd> Ejecutar comando con secreto en variable de entorno <vr>
|
||||
# set <clave> Guardar secreto (pide input interactivo, sin eco)
|
||||
# rm <clave> Eliminar secreto de la bóveda
|
||||
# list Listar claves disponibles en la bóveda
|
||||
|
||||
require 'openssl'
|
||||
require 'base64'
|
||||
require 'json'
|
||||
require 'io/console'
|
||||
require 'fileutils'
|
||||
|
||||
# ─── Rutas ───────────────────────────────────────────────────────────
|
||||
TOOL_DIR = File.expand_path(__dir__)
|
||||
MASTER_KEY = File.join(TOOL_DIR, '.master.key')
|
||||
OLD_KEY = File.expand_path('../../.master.key', TOOL_DIR)
|
||||
BOVEDA = File.join(TOOL_DIR, '.boveda.json')
|
||||
SESION = File.join(TOOL_DIR, '.session')
|
||||
ACCESS_LOG = File.join(TOOL_DIR, 'access.log')
|
||||
|
||||
# ─── Transición de Seguridad ─────────────────────────────────────────
|
||||
if File.exist?(OLD_KEY) && !File.exist?(MASTER_KEY)
|
||||
FileUtils.mv(OLD_KEY, MASTER_KEY)
|
||||
FileUtils.chmod(0600, MASTER_KEY)
|
||||
end
|
||||
|
||||
# ─── Colores ─────────────────────────────────────────────────────────
|
||||
module C
|
||||
RESET = "\e[0m"
|
||||
GREEN = "\e[32m"
|
||||
CYAN = "\e[36m"
|
||||
YELLOW = "\e[33m"
|
||||
RED = "\e[31m"
|
||||
DIM = "\e[2m"
|
||||
end
|
||||
|
||||
# ─── Motor Criptográfico (AES-256-GCM) ──────────────────────────────
|
||||
class Cripto
|
||||
ALGORITHM = 'aes-256-gcm'
|
||||
|
||||
def initialize(key_content)
|
||||
@key = Digest::SHA256.digest(key_content.strip)
|
||||
end
|
||||
|
||||
def encrypt(plaintext)
|
||||
cipher = OpenSSL::Cipher.new(ALGORITHM).tap do |c|
|
||||
c.encrypt
|
||||
c.key = @key
|
||||
end
|
||||
iv = cipher.random_iv
|
||||
ciphertext = cipher.update(plaintext.to_s) + cipher.final
|
||||
payload = { iv: b64(iv), tag: b64(cipher.auth_tag), data: b64(ciphertext) }
|
||||
Base64.strict_encode64(payload.to_json)
|
||||
end
|
||||
|
||||
def decrypt(token)
|
||||
p = JSON.parse(Base64.strict_decode64(token), symbolize_names: true)
|
||||
cipher = OpenSSL::Cipher.new(ALGORITHM).tap do |c|
|
||||
c.decrypt
|
||||
c.key = @key
|
||||
c.iv = db64(p[:iv])
|
||||
c.auth_tag = db64(p[:tag])
|
||||
end
|
||||
cipher.update(db64(p[:data])) + cipher.final
|
||||
rescue => e
|
||||
abort "#{C::RED}✗ Error al descifrar: #{e.message}#{C::RESET}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def b64(d) = Base64.strict_encode64(d)
|
||||
def db64(d) = Base64.strict_decode64(d)
|
||||
end
|
||||
|
||||
# ─── Bóveda ──────────────────────────────────────────────────────────
|
||||
class Boveda
|
||||
def initialize(path)
|
||||
@path = path
|
||||
@data = File.exist?(path) ? JSON.parse(File.read(path)) : {}
|
||||
end
|
||||
|
||||
def keys = @data.keys
|
||||
def get(k) = @data[k]
|
||||
def set(k, v) = (@data[k] = v) && save!
|
||||
def rm(k) = @data.delete(k) && save!
|
||||
def empty? = @data.empty?
|
||||
|
||||
private
|
||||
|
||||
def save!
|
||||
File.write(@path, JSON.pretty_generate(@data) + "\n")
|
||||
FileUtils.chmod(0600, @path)
|
||||
end
|
||||
end
|
||||
|
||||
# ─── Master Key ──────────────────────────────────────────────────────
|
||||
def cargar_master_key
|
||||
unless File.exist?(MASTER_KEY)
|
||||
require 'securerandom'
|
||||
File.write(MASTER_KEY, SecureRandom.hex(32))
|
||||
FileUtils.chmod(0600, MASTER_KEY)
|
||||
$stderr.puts "#{C::YELLOW}⚠ Nueva clave maestra creada en local: #{MASTER_KEY}#{C::RESET}"
|
||||
end
|
||||
File.read(MASTER_KEY)
|
||||
end
|
||||
|
||||
# ─── Auditoría ───────────────────────────────────────────────────────
|
||||
def registrar_acceso(accion, detalle = '')
|
||||
usuario = ENV['USER'] || ENV['USERNAME'] || 'unknown'
|
||||
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
||||
log_entry = "[#{timestamp}] - user:#{usuario} - action:#{accion} - #{detalle}\n"
|
||||
|
||||
File.open(ACCESS_LOG, 'a') do |f|
|
||||
f.write(log_entry)
|
||||
end
|
||||
FileUtils.chmod(0600, ACCESS_LOG) if File.exist?(ACCESS_LOG)
|
||||
end
|
||||
|
||||
# ─── Gestión de Sesión (MFA) ─────────────────────────────────────────
|
||||
def autorizar!
|
||||
File.write(SESION, Time.now.to_i.to_s)
|
||||
FileUtils.chmod(0600, SESION)
|
||||
registrar_acceso('AUTHORIZE', 'Candado abierto (Sesión iniciada)')
|
||||
$stderr.puts "#{C::GREEN}✓ Autorización concedida (Válida por 5 minutos).#{C::RESET}"
|
||||
end
|
||||
|
||||
def cerrar_sesion!
|
||||
if File.exist?(SESION)
|
||||
FileUtils.rm_f(SESION)
|
||||
registrar_acceso('LOCK', 'Candado cerrado (Sesión finalizada)')
|
||||
$stderr.puts "#{C::YELLOW}🔒 Sesión cerrada. Candado puesto.#{C::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
def verificar_sesion!
|
||||
unless File.exist?(SESION)
|
||||
registrar_acceso('DENIED', 'Intento de acceso sin autorización')
|
||||
abort "#{C::RED}✗ Error: El candado está puesto. Ejecutá 'ruby #{File.basename($0)} authorize' para abrirlo.#{C::RESET}"
|
||||
end
|
||||
|
||||
inicio = File.read(SESION).to_i
|
||||
if Time.now.to_i - inicio > 300 # 5 minutos
|
||||
cerrar_sesion!
|
||||
registrar_acceso('EXPIRED', 'Sesión expirada automáticamente')
|
||||
abort "#{C::RED}✗ Sesión expirada. Por seguridad, volvé a autorizar.#{C::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
# ─── Comandos ────────────────────────────────────────────────────────
|
||||
|
||||
def cmd_abrir
|
||||
puts "#{C::CYAN}🔐 ns8-candados: Autorización SSH#{C::RESET}"
|
||||
puts "#{C::DIM}Limpiando identidades previas...#{C::RESET}"
|
||||
system('ssh-add -D > /dev/null 2>&1')
|
||||
|
||||
puts "#{C::YELLOW}🔑 Ingresá tu passphrase:#{C::RESET}"
|
||||
system('ssh-add')
|
||||
|
||||
system('ssh-add -l > /dev/null 2>&1')
|
||||
unless $?.success?
|
||||
abort "#{C::RED}✗ Sin identidades SSH cargadas. Abortando.#{C::RESET}"
|
||||
end
|
||||
|
||||
puts "#{C::GREEN}🔓 SSH autorizado. Las identidades permanecen activas mientras dure la sesión del agente.#{C::RESET}"
|
||||
end
|
||||
|
||||
def cmd_encrypt(cripto, texto)
|
||||
abort "#{C::RED}✗ Falta texto a cifrar.#{C::RESET}" if texto.nil? || texto.empty?
|
||||
puts cripto.encrypt(texto)
|
||||
end
|
||||
|
||||
def cmd_decrypt(cripto, token)
|
||||
abort "#{C::RED}✗ Falta payload a descifrar.#{C::RESET}" if token.nil? || token.empty?
|
||||
puts cripto.decrypt(token)
|
||||
end
|
||||
|
||||
def cmd_get(cripto, boveda, clave)
|
||||
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
|
||||
verificar_sesion!
|
||||
token = boveda.get(clave)
|
||||
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil?
|
||||
|
||||
registrar_acceso('GET', "Clave: #{clave}")
|
||||
puts cripto.decrypt(token)
|
||||
end
|
||||
|
||||
def cmd_run(cripto, boveda, clave, env_var, comando)
|
||||
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
|
||||
abort "#{C::RED}✗ Falta nombre de variable de entorno.#{C::RESET}" if env_var.nil?
|
||||
abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if comando.nil? || comando.empty?
|
||||
|
||||
verificar_sesion!
|
||||
token = boveda.get(clave)
|
||||
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil?
|
||||
|
||||
secret = cripto.decrypt(token)
|
||||
registrar_acceso('RUN', "Clave: #{clave}, Comando: #{comando.is_a?(Array) ? comando.join(' ') : comando}")
|
||||
|
||||
# Ejecutar el comando inyectando la variable
|
||||
ENV[env_var] = secret
|
||||
begin
|
||||
# Si comando es un string, lo ejecutamos vía shell, si es array, directo
|
||||
if comando.is_a?(Array)
|
||||
system(*comando)
|
||||
else
|
||||
system(comando)
|
||||
end
|
||||
exit $?.exitstatus || 0
|
||||
ensure
|
||||
ENV.delete(env_var)
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_set(cripto, boveda, clave)
|
||||
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
|
||||
$stderr.print "#{C::YELLOW}🔑 Valor para '#{clave}': #{C::RESET}"
|
||||
valor = $stdin.noecho(&:gets)&.chomp
|
||||
$stderr.puts
|
||||
abort "#{C::RED}✗ Valor vacío.#{C::RESET}" if valor.nil? || valor.empty?
|
||||
|
||||
boveda.set(clave, cripto.encrypt(valor))
|
||||
$stderr.puts "#{C::GREEN}✓ Clave '#{clave}' guardada en la bóveda.#{C::RESET}"
|
||||
end
|
||||
|
||||
def cmd_rm(boveda, clave)
|
||||
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
|
||||
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" unless boveda.get(clave)
|
||||
boveda.rm(clave)
|
||||
$stderr.puts "#{C::GREEN}✓ Clave '#{clave}' eliminada.#{C::RESET}"
|
||||
end
|
||||
|
||||
def cmd_list(boveda)
|
||||
if boveda.empty?
|
||||
$stderr.puts "#{C::DIM}Bóveda vacía.#{C::RESET}"
|
||||
return
|
||||
end
|
||||
$stderr.puts "#{C::CYAN}🔐 Claves en la bóveda:#{C::RESET}"
|
||||
boveda.keys.each { |k| $stderr.puts " #{C::GREEN}•#{C::RESET} #{k}" }
|
||||
end
|
||||
|
||||
def cmd_help
|
||||
puts <<~HELP
|
||||
#{C::CYAN}🔐 ns8-candados — Gestión de secretos#{C::RESET}
|
||||
|
||||
#{C::GREEN}Uso:#{C::RESET} ruby ns8-candados.rb <comando> [args]
|
||||
|
||||
#{C::YELLOW}Comandos:#{C::RESET}
|
||||
authorize Abrir candado (autorización temporal 5 min)
|
||||
cerrar Cerrar candado manual (eliminar sesión)
|
||||
abrir Autorizar SSH (cargar passphrase)
|
||||
encrypt <texto> Cifrar texto plano → token
|
||||
decrypt <token> Descifrar token → texto plano
|
||||
get <clave> Obtener secreto de la bóveda
|
||||
run <cl> <vr> <cmd> Ejecutar comando con secreto en variable <vr>
|
||||
set <clave> Guardar secreto (input seguro)
|
||||
rm <clave> Eliminar secreto
|
||||
list Listar claves disponibles
|
||||
|
||||
#{C::YELLOW}Ejemplos:#{C::RESET}
|
||||
# Autorizar para operar
|
||||
ruby ns8-candados.rb authorize
|
||||
|
||||
#{C::DIM}# Usar de forma segura inyectando en el entorno (recomendado)#{C::RESET}
|
||||
ruby ns8-candados.rb run admindasu PASS "sshpass -e ssh user@host"
|
||||
|
||||
#{C::DIM}# Usar en scripts (salida limpia - menos seguro)#{C::RESET}
|
||||
PASS=$(ruby ns8-candados.rb get admindasu)
|
||||
sshpass -p "$PASS" ssh user@host "cmd"
|
||||
HELP
|
||||
end
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────────────
|
||||
comando = ARGV[0]
|
||||
arg = ARGV[1]
|
||||
|
||||
case comando
|
||||
when 'authorize'
|
||||
autorizar!
|
||||
when 'cerrar'
|
||||
cerrar_sesion!
|
||||
when 'encrypt', 'decrypt', 'get', 'run', 'set', 'rm', 'list'
|
||||
cripto = Cripto.new(cargar_master_key)
|
||||
boveda = Boveda.new(BOVEDA)
|
||||
case comando
|
||||
when 'encrypt' then cmd_encrypt(cripto, arg)
|
||||
when 'decrypt' then cmd_decrypt(cripto, arg)
|
||||
when 'get' then cmd_get(cripto, boveda, arg)
|
||||
when 'run' then cmd_run(cripto, boveda, arg, ARGV[2], ARGV[3..].join(' '))
|
||||
when 'set' then cmd_set(cripto, boveda, arg)
|
||||
when 'rm' then cmd_rm(boveda, arg)
|
||||
when 'list' then cmd_list(boveda)
|
||||
end
|
||||
when 'help', '-h', '--help', nil
|
||||
cmd_help
|
||||
else
|
||||
$stderr.puts "#{C::RED}✗ Comando desconocido: '#{comando}'#{C::RESET}"
|
||||
cmd_help
|
||||
exit 1
|
||||
end
|
||||
@@ -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 @@
|
||||
OpenSSH-Win64.zip
|
||||
@@ -0,0 +1,115 @@
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ZOMBI_URL = "http://10.0.10.8:8000"
|
||||
|
||||
function Log($msg) {
|
||||
$ts = Get-Date -Format "HH:mm:ss"
|
||||
Write-Host "[$ts] $msg" -ForegroundColor Cyan
|
||||
try { Invoke-RestMethod -Uri "$ZOMBI_URL/log" -Method Post -Body @{msg="[$env:COMPUTERNAME] $msg"} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||
}
|
||||
|
||||
$DEST = "C:\SysDasuten"
|
||||
$ZIP = "C:\TEMP\runSysDasuten.zip"
|
||||
$SQL_SERVER = "sql-dasuten"
|
||||
$SQL_DB = "sysdasuten"
|
||||
|
||||
Log "=========================================="
|
||||
Log "DEPLOY SYSDASUTEN — $env:COMPUTERNAME"
|
||||
Log "=========================================="
|
||||
|
||||
# --- PASO 1: Descargar ZIP ---
|
||||
if (-not (Test-Path $DEST\Sistema\DasutenSQL.exe)) {
|
||||
Log "PASO 1: Descargando runSysDasuten.zip (~268 MB)..."
|
||||
New-Item -ItemType Directory -Path "C:\TEMP" -Force | Out-Null
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$ZOMBI_URL/payloads/runSysDasuten.zip" -OutFile $ZIP -UseBasicParsing -ErrorAction Stop
|
||||
Log "Descarga completada: $(((Get-Item $ZIP).Length / 1MB).ToString('N0')) MB"
|
||||
} catch {
|
||||
Log "ERROR descarga: $($_.Exception.Message)"
|
||||
return
|
||||
}
|
||||
|
||||
# --- PASO 2: Extraer ---
|
||||
Log "PASO 2: Extrayendo a $DEST..."
|
||||
New-Item -ItemType Directory -Path $DEST -Force | Out-Null
|
||||
Expand-Archive -Path $ZIP -DestinationPath "C:\" -Force
|
||||
# El zip contiene runSysDasuten/ como raiz, renombrar
|
||||
if (Test-Path "C:\runSysDasuten") {
|
||||
Copy-Item -Path "C:\runSysDasuten\*" -Destination $DEST -Recurse -Force
|
||||
Remove-Item "C:\runSysDasuten" -Recurse -Force
|
||||
}
|
||||
Remove-Item $ZIP -Force -ErrorAction SilentlyContinue
|
||||
Log "Extraido OK."
|
||||
} else {
|
||||
Log "PASO 1-2: SKIP — Ya existe $DEST\Sistema\DasutenSQL.exe"
|
||||
}
|
||||
|
||||
# --- PASO 3: Modificar Kermet.ini ---
|
||||
Log "PASO 3: Configurando Kermet.ini..."
|
||||
$iniPaths = @("$DEST\Kermet.ini", "$DEST\Sistema\Kermet.ini")
|
||||
foreach ($ini in $iniPaths) {
|
||||
if (Test-Path $ini) {
|
||||
$content = Get-Content $ini -Raw
|
||||
$content = $content -replace 'SERVER=srvFENIX', "SERVER=$SQL_SERVER"
|
||||
$content = $content -replace 'SERVER=172\.16\.9\.204\\SQL2017', "SERVER=$SQL_SERVER"
|
||||
Set-Content $ini $content -Force
|
||||
Log "Actualizado: $ini -> SERVER=$SQL_SERVER"
|
||||
}
|
||||
}
|
||||
|
||||
# --- PASO 4: Instalar Fuentes ---
|
||||
Log "PASO 4: Instalando fuentes..."
|
||||
$fontDir = "$DEST\Fonts\Fonts"
|
||||
if (Test-Path $fontDir) {
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$fontsFolder = $shell.NameSpace(0x14) # Windows Fonts folder
|
||||
Get-ChildItem "$fontDir\*.ttf" | ForEach-Object {
|
||||
$fontPath = $_.FullName
|
||||
$fontName = $_.Name
|
||||
if (-not (Test-Path "C:\Windows\Fonts\$fontName")) {
|
||||
Copy-Item $fontPath "C:\Windows\Fonts\" -Force
|
||||
$regKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
|
||||
New-ItemProperty -Path $regKey -Name $fontName -Value $fontName -PropertyType String -Force | Out-Null
|
||||
Log "Fuente instalada: $fontName"
|
||||
} else {
|
||||
Log "Fuente ya existe: $fontName"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log "WARN: No se encontro directorio de fuentes"
|
||||
}
|
||||
|
||||
# --- PASO 5: Verificar firewall SQL en sql-dasuten ---
|
||||
Log "PASO 5: Probando conectividad SQL..."
|
||||
$tcpTest = Test-NetConnection -ComputerName $SQL_SERVER -Port 1433 -WarningAction SilentlyContinue
|
||||
if ($tcpTest.TcpTestSucceeded) {
|
||||
Log "Conexion TCP a ${SQL_SERVER}:1433 OK"
|
||||
} else {
|
||||
Log "WARN: No se puede conectar a ${SQL_SERVER}:1433 — verificar firewall"
|
||||
}
|
||||
|
||||
# --- PASO 6: Probar conexion SQL con sqlcmd (si existe) ---
|
||||
$sqlcmd = Get-Command sqlcmd -ErrorAction SilentlyContinue
|
||||
if ($sqlcmd) {
|
||||
Log "PASO 6: Probando SQL con sqlcmd..."
|
||||
$result = sqlcmd -S $SQL_SERVER -E -Q "SELECT DB_NAME() AS db_actual; SELECT name FROM sys.databases WHERE name = '$SQL_DB'" -W 2>&1
|
||||
Log "SQL Result: $result"
|
||||
} else {
|
||||
Log "PASO 6: SKIP — sqlcmd no disponible (normal en Win10), probar con DasutenSQL.exe"
|
||||
}
|
||||
|
||||
# --- PASO 7: Crear acceso directo ---
|
||||
Log "PASO 7: Creando acceso directo en Escritorio..."
|
||||
$desktopPath = [Environment]::GetFolderPath('CommonDesktopDirectory')
|
||||
$shortcutPath = "$desktopPath\DasutenSQL.lnk"
|
||||
$WshShell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $WshShell.CreateShortcut($shortcutPath)
|
||||
$shortcut.TargetPath = "$DEST\Sistema\DasutenSQL.exe"
|
||||
$shortcut.WorkingDirectory = "$DEST\Sistema"
|
||||
$shortcut.Description = "Sistema DASUTEN"
|
||||
$shortcut.Save()
|
||||
Log "Acceso directo creado: $shortcutPath"
|
||||
|
||||
Log "=========================================="
|
||||
Log "DEPLOY SYSDASUTEN COMPLETADO"
|
||||
Log "Ejecutar desde: $DEST\Sistema\DasutenSQL.exe"
|
||||
Log "=========================================="
|
||||
@@ -0,0 +1,115 @@
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ZOMBI_URL = "http://10.0.10.8:8000"
|
||||
|
||||
function Log($msg) {
|
||||
$ts = Get-Date -Format "HH:mm:ss"
|
||||
Write-Host "[$ts] $msg" -ForegroundColor Cyan
|
||||
try { Invoke-RestMethod -Uri "$ZOMBI_URL/log" -Method Post -Body @{msg="[$env:COMPUTERNAME] $msg"} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||
}
|
||||
|
||||
$DEST = "C:\SysDasuten"
|
||||
$ZIP = "C:\TEMP\runSysDasuten.zip"
|
||||
$SQL_SERVER = "sql-dasuten"
|
||||
$SQL_DB = "sysdasuten"
|
||||
|
||||
Log "=========================================="
|
||||
Log "DEPLOY SYSDASUTEN — $env:COMPUTERNAME"
|
||||
Log "=========================================="
|
||||
|
||||
# --- PASO 1: Descargar ZIP ---
|
||||
if (-not (Test-Path $DEST\Sistema\DasutenSQL.exe)) {
|
||||
Log "PASO 1: Descargando runSysDasuten.zip (~268 MB)..."
|
||||
New-Item -ItemType Directory -Path "C:\TEMP" -Force | Out-Null
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$ZOMBI_URL/payloads/runSysDasuten.zip" -OutFile $ZIP -UseBasicParsing -ErrorAction Stop
|
||||
Log "Descarga completada: $(((Get-Item $ZIP).Length / 1MB).ToString('N0')) MB"
|
||||
} catch {
|
||||
Log "ERROR descarga: $($_.Exception.Message)"
|
||||
return
|
||||
}
|
||||
|
||||
# --- PASO 2: Extraer ---
|
||||
Log "PASO 2: Extrayendo a $DEST..."
|
||||
New-Item -ItemType Directory -Path $DEST -Force | Out-Null
|
||||
Expand-Archive -Path $ZIP -DestinationPath "C:\" -Force
|
||||
# El zip contiene runSysDasuten/ como raiz, renombrar
|
||||
if (Test-Path "C:\runSysDasuten") {
|
||||
Copy-Item -Path "C:\runSysDasuten\*" -Destination $DEST -Recurse -Force
|
||||
Remove-Item "C:\runSysDasuten" -Recurse -Force
|
||||
}
|
||||
Remove-Item $ZIP -Force -ErrorAction SilentlyContinue
|
||||
Log "Extraido OK."
|
||||
} else {
|
||||
Log "PASO 1-2: SKIP — Ya existe $DEST\Sistema\DasutenSQL.exe"
|
||||
}
|
||||
|
||||
# --- PASO 3: Modificar Kermet.ini ---
|
||||
Log "PASO 3: Configurando Kermet.ini..."
|
||||
$iniPaths = @("$DEST\Kermet.ini", "$DEST\Sistema\Kermet.ini")
|
||||
foreach ($ini in $iniPaths) {
|
||||
if (Test-Path $ini) {
|
||||
$content = Get-Content $ini -Raw
|
||||
$content = $content -replace 'SERVER=srvFENIX', "SERVER=$SQL_SERVER"
|
||||
$content = $content -replace 'SERVER=172\.16\.9\.204\\SQL2017', "SERVER=$SQL_SERVER"
|
||||
Set-Content $ini $content -Force
|
||||
Log "Actualizado: $ini -> SERVER=$SQL_SERVER"
|
||||
}
|
||||
}
|
||||
|
||||
# --- PASO 4: Instalar Fuentes ---
|
||||
Log "PASO 4: Instalando fuentes..."
|
||||
$fontDir = "$DEST\Fonts\Fonts"
|
||||
if (Test-Path $fontDir) {
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$fontsFolder = $shell.NameSpace(0x14) # Windows Fonts folder
|
||||
Get-ChildItem "$fontDir\*.ttf" | ForEach-Object {
|
||||
$fontPath = $_.FullName
|
||||
$fontName = $_.Name
|
||||
if (-not (Test-Path "C:\Windows\Fonts\$fontName")) {
|
||||
Copy-Item $fontPath "C:\Windows\Fonts\" -Force
|
||||
$regKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
|
||||
New-ItemProperty -Path $regKey -Name $fontName -Value $fontName -PropertyType String -Force | Out-Null
|
||||
Log "Fuente instalada: $fontName"
|
||||
} else {
|
||||
Log "Fuente ya existe: $fontName"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log "WARN: No se encontro directorio de fuentes"
|
||||
}
|
||||
|
||||
# --- PASO 5: Verificar firewall SQL en sql-dasuten ---
|
||||
Log "PASO 5: Probando conectividad SQL..."
|
||||
$tcpTest = Test-NetConnection -ComputerName $SQL_SERVER -Port 1433 -WarningAction SilentlyContinue
|
||||
if ($tcpTest.TcpTestSucceeded) {
|
||||
Log "Conexion TCP a ${SQL_SERVER}:1433 OK"
|
||||
} else {
|
||||
Log "WARN: No se puede conectar a ${SQL_SERVER}:1433 — verificar firewall"
|
||||
}
|
||||
|
||||
# --- PASO 6: Probar conexion SQL con sqlcmd (si existe) ---
|
||||
$sqlcmd = Get-Command sqlcmd -ErrorAction SilentlyContinue
|
||||
if ($sqlcmd) {
|
||||
Log "PASO 6: Probando SQL con sqlcmd..."
|
||||
$result = sqlcmd -S $SQL_SERVER -E -Q "SELECT DB_NAME() AS db_actual; SELECT name FROM sys.databases WHERE name = '$SQL_DB'" -W 2>&1
|
||||
Log "SQL Result: $result"
|
||||
} else {
|
||||
Log "PASO 6: SKIP — sqlcmd no disponible (normal en Win10), probar con DasutenSQL.exe"
|
||||
}
|
||||
|
||||
# --- PASO 7: Crear acceso directo ---
|
||||
Log "PASO 7: Creando acceso directo en Escritorio..."
|
||||
$desktopPath = [Environment]::GetFolderPath('CommonDesktopDirectory')
|
||||
$shortcutPath = "$desktopPath\DasutenSQL.lnk"
|
||||
$WshShell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $WshShell.CreateShortcut($shortcutPath)
|
||||
$shortcut.TargetPath = "$DEST\Sistema\DasutenSQL.exe"
|
||||
$shortcut.WorkingDirectory = "$DEST\Sistema"
|
||||
$shortcut.Description = "Sistema DASUTEN"
|
||||
$shortcut.Save()
|
||||
Log "Acceso directo creado: $shortcutPath"
|
||||
|
||||
Log "=========================================="
|
||||
Log "DEPLOY SYSDASUTEN COMPLETADO"
|
||||
Log "Ejecutar desde: $DEST\Sistema\DasutenSQL.exe"
|
||||
Log "=========================================="
|
||||
@@ -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 "=========================================================="
|
||||
return
|
||||
@@ -0,0 +1,49 @@
|
||||
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 OPENSSH (con fix TEMP)"
|
||||
Log-Msg "=========================================================="
|
||||
|
||||
try {
|
||||
# Fix TEMP: usar carpeta de sistema en vez de la del usuario
|
||||
$env:TEMP = "C:\Windows\Temp"
|
||||
$env:TMP = "C:\Windows\Temp"
|
||||
Log-Msg "TEMP redirigido a C:\Windows\Temp"
|
||||
|
||||
$cap = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
|
||||
if ($cap.State -eq 'Installed') {
|
||||
Log-Msg "OpenSSH Server ya instalado."
|
||||
} else {
|
||||
Log-Msg "Instalando OpenSSH.Server..."
|
||||
Add-WindowsCapability -Online -Name $cap.Name
|
||||
Log-Msg "Instalado OK."
|
||||
}
|
||||
|
||||
Set-Service sshd -StartupType Automatic
|
||||
Start-Service sshd -ErrorAction SilentlyContinue
|
||||
Log-Msg "Servicio sshd configurado e iniciado."
|
||||
|
||||
# Puerto 7022
|
||||
$cfg = "C:\ProgramData\ssh\sshd_config"
|
||||
$lines = Get-Content $cfg | Where-Object { $_ -notmatch '^\s*#?\s*Port\s+\d+' }
|
||||
@("Port 7022") + $lines | Set-Content $cfg -Force
|
||||
New-NetFirewallRule -Name 'SSH-7022' -DisplayName 'SSH 7022' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 7022 -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
Stop-Service sshd -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Service sshd
|
||||
Log-Msg "OpenSSH configurado en puerto 7022"
|
||||
|
||||
} catch {
|
||||
Log-Msg "ERROR: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "OPERACION COMPLETADA"
|
||||
Log-Msg "=========================================================="
|
||||
@@ -0,0 +1,83 @@
|
||||
$ErrorActionPreference = "Continue"
|
||||
$logFile = "C:\ssh_install.log"
|
||||
|
||||
function Log($msg) {
|
||||
$ts = Get-Date -Format "HH:mm:ss"
|
||||
$line = "[$ts] $msg"
|
||||
Write-Host $line -ForegroundColor Cyan
|
||||
Add-Content -Path $logFile -Value $line
|
||||
try { (New-Object Net.WebClient).DownloadString("http://10.0.10.8:8000/log?msg=$msg") | Out-Null } catch {}
|
||||
}
|
||||
|
||||
Log "=========================================="
|
||||
Log "OPENSSH LEGACY INSTALLER (2008R2+)"
|
||||
Log "=========================================="
|
||||
|
||||
$sshDir = "C:\OpenSSH-Win64"
|
||||
$zipPath = "C:\OpenSSH-Win64.zip"
|
||||
$sshUrl = "http://10.0.10.8:8000/payloads/OpenSSH-Win64.zip"
|
||||
|
||||
try {
|
||||
if (Test-Path "$sshDir\sshd.exe") {
|
||||
Log "OpenSSH ya instalado en $sshDir"
|
||||
} else {
|
||||
Log "Paso 1: Descargando ZIP..."
|
||||
$wc = New-Object System.Net.WebClient
|
||||
$wc.DownloadFile($sshUrl, $zipPath)
|
||||
Log "Descarga OK ($zipPath)"
|
||||
|
||||
Log "Paso 2: Extrayendo ZIP..."
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$zip = $shell.NameSpace($zipPath)
|
||||
$dest = $shell.NameSpace("C:\")
|
||||
$dest.CopyHere($zip.Items(), 0x14)
|
||||
Log "Extraido en $sshDir"
|
||||
}
|
||||
|
||||
Log "Paso 3: Instalando servicio sshd..."
|
||||
cd $sshDir
|
||||
powershell -ExecutionPolicy Bypass -File "$sshDir\install-sshd.ps1"
|
||||
Log "Servicio instalado"
|
||||
|
||||
Log "Paso 4: Generando host keys..."
|
||||
if (!(Test-Path "$sshDir\ssh_host_rsa_key")) {
|
||||
& "$sshDir\ssh-keygen.exe" -A
|
||||
Log "Host keys generadas"
|
||||
} else {
|
||||
Log "Host keys ya existen"
|
||||
}
|
||||
|
||||
Log "Paso 5: Configurando puerto 7022..."
|
||||
$cfg = "$sshDir\sshd_config"
|
||||
if (Test-Path "$sshDir\sshd_config_default") {
|
||||
Copy-Item "$sshDir\sshd_config_default" $cfg -Force
|
||||
}
|
||||
if (Test-Path $cfg) {
|
||||
$lines = Get-Content $cfg | Where-Object { $_ -notmatch '^\s*#?\s*Port\s+\d+' }
|
||||
$newContent = @("Port 7022") + $lines
|
||||
$newContent | Set-Content $cfg -Force
|
||||
} else {
|
||||
"Port 7022" | Set-Content $cfg
|
||||
}
|
||||
Log "Config: Port 7022"
|
||||
|
||||
Log "Paso 6: Firewall..."
|
||||
netsh advfirewall firewall add rule name="SSH-7022" dir=in action=allow protocol=TCP localport=7022
|
||||
Log "Firewall actualizado"
|
||||
|
||||
Log "Paso 7: Iniciando servicio..."
|
||||
Set-Service sshd -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
Start-Service sshd -ErrorAction Stop
|
||||
Log "Servicio sshd INICIADO en puerto 7022"
|
||||
|
||||
} catch {
|
||||
Log "ERROR: $($_.Exception.Message)"
|
||||
Log "LINEA: $($_.InvocationInfo.ScriptLineNumber)"
|
||||
}
|
||||
|
||||
Log "=========================================="
|
||||
Log "FIN. Log guardado en $logFile"
|
||||
Log "=========================================="
|
||||
Write-Host ""
|
||||
Write-Host "Presiona ENTER para cerrar..." -ForegroundColor Yellow
|
||||
Read-Host
|
||||
@@ -0,0 +1,106 @@
|
||||
$ErrorActionPreference = "Continue"
|
||||
$env:TEMP = "C:\Windows\Temp"
|
||||
$env:TMP = "C:\Windows\Temp"
|
||||
|
||||
function Log($msg) {
|
||||
$ts = Get-Date -Format "HH:mm:ss"
|
||||
Write-Host "[$ts] $msg" -ForegroundColor Cyan
|
||||
try { Invoke-RestMethod -Uri "http://10.0.10.8:8000/log" -Method Post -Body @{msg="[$env:COMPUTERNAME] $msg"} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||
}
|
||||
|
||||
$hn = $env:COMPUTERNAME
|
||||
$os = (Get-WmiObject Win32_OperatingSystem).Caption
|
||||
$sshPort = 7022
|
||||
|
||||
Log "=========================================="
|
||||
Log "W-ZOMBI SETUP — $hn"
|
||||
Log "OS: $os"
|
||||
Log "=========================================="
|
||||
|
||||
# --- PASO 1: Detectar e instalar OpenSSH ---
|
||||
$sshInstalled = $false
|
||||
|
||||
# Verificar si sshd ya existe
|
||||
if (Get-Service sshd -ErrorAction SilentlyContinue) {
|
||||
Log "PASO 1: sshd ya existe."
|
||||
$sshInstalled = $true
|
||||
} elseif ($os -match "200[38]|Vista|XP") {
|
||||
# Legacy: Win32-OpenSSH manual
|
||||
Log "PASO 1: OS Legacy ($os) — instalando Win32-OpenSSH..."
|
||||
$zip = "C:\OpenSSH-Win64.zip"
|
||||
$dir = "C:\OpenSSH-Win64"
|
||||
try {
|
||||
(New-Object Net.WebClient).DownloadFile("http://10.0.10.8:8000/payloads/OpenSSH-Win64.zip", $zip)
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$shell.NameSpace("C:\").CopyHere($shell.NameSpace($zip).Items(), 0x14)
|
||||
& "$dir\install-sshd.ps1" 2>&1 | Out-Null
|
||||
& "$dir\ssh-keygen.exe" -A 2>&1 | Out-Null
|
||||
Set-Service sshd -StartupType Automatic
|
||||
$sshInstalled = $true
|
||||
Log "Win32-OpenSSH instalado OK."
|
||||
} catch {
|
||||
Log "ERROR Legacy: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
# Moderno: Add-WindowsCapability (Win10/2016+)
|
||||
Log "PASO 1: OS Moderno — instalando via Add-WindowsCapability..."
|
||||
try {
|
||||
$cap = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
|
||||
if ($cap.State -ne 'Installed') {
|
||||
Add-WindowsCapability -Online -Name $cap.Name | Out-Null
|
||||
}
|
||||
Set-Service sshd -StartupType Automatic
|
||||
Start-Service sshd -ErrorAction SilentlyContinue
|
||||
$sshInstalled = $true
|
||||
Log "OpenSSH Server instalado OK."
|
||||
} catch {
|
||||
Log "ERROR Moderno: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $sshInstalled) {
|
||||
Log "FALLO: No se pudo instalar SSH. Abortando."
|
||||
return
|
||||
}
|
||||
|
||||
# --- PASO 2: Configurar puerto 7022 ---
|
||||
Log "PASO 2: Configurando puerto $sshPort..."
|
||||
|
||||
$cfgPaths = @("C:\ProgramData\ssh\sshd_config", "C:\OpenSSH-Win64\sshd_config")
|
||||
$cfgPath = $cfgPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
|
||||
if ($cfgPath) {
|
||||
$lines = Get-Content $cfgPath | Where-Object { $_ -notmatch '^\s*#?\s*Port\s+\d+' }
|
||||
@("Port $sshPort") + $lines | Set-Content $cfgPath -Force
|
||||
Log "Config: $cfgPath -> Port $sshPort"
|
||||
} else {
|
||||
Log "WARN: No se encontro sshd_config"
|
||||
}
|
||||
|
||||
# --- PASO 3: Firewall ---
|
||||
Log "PASO 3: Firewall..."
|
||||
try {
|
||||
New-NetFirewallRule -Name "SSH-$sshPort" -DisplayName "SSH $sshPort" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort $sshPort -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {
|
||||
netsh advfirewall firewall add rule name="SSH-$sshPort" dir=in action=allow protocol=TCP localport=$sshPort 2>&1 | Out-Null
|
||||
}
|
||||
Log "Firewall: puerto $sshPort abierto"
|
||||
|
||||
# --- PASO 4: Reiniciar sshd ---
|
||||
Log "PASO 4: Reiniciando sshd..."
|
||||
Stop-Service sshd -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Service sshd -ErrorAction SilentlyContinue
|
||||
Log "sshd reiniciado."
|
||||
|
||||
# --- PASO 5: Verificar ---
|
||||
$listening = netstat -an | Select-String ":$sshPort\s+.*LISTEN"
|
||||
if ($listening) {
|
||||
Log "VERIFICADO: $hn escuchando en puerto $sshPort"
|
||||
} else {
|
||||
Log "WARN: Puerto $sshPort no detectado en netstat"
|
||||
}
|
||||
|
||||
Log "=========================================="
|
||||
Log "SETUP COMPLETADO — $hn : $sshPort"
|
||||
Log "=========================================="
|
||||
@@ -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
|
||||
@@ -0,0 +1,42 @@
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Log($msg) {
|
||||
$ts = Get-Date -Format "HH:mm:ss"
|
||||
Write-Host "[$ts] $msg" -ForegroundColor Cyan
|
||||
try { Invoke-RestMethod -Uri "http://10.0.10.8:8000/log" -Method Post -Body @{msg="[$env:COMPUTERNAME] $msg"} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||
}
|
||||
|
||||
Log "=========================================="
|
||||
Log "SQL SYSADMIN FIX — Agregando DASUTEN\admindasu"
|
||||
Log "=========================================="
|
||||
|
||||
# Paso 1: Parar SQL Server
|
||||
Log "PASO 1: Deteniendo MSSQLSERVER..."
|
||||
net stop MSSQLSERVER /y 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Paso 2: Iniciar en modo single-user
|
||||
Log "PASO 2: Iniciando en modo single-user..."
|
||||
net start MSSQLSERVER /m 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
# Paso 3: Agregar sysadmin
|
||||
Log "PASO 3: Agregando DASUTEN\admindasu como sysadmin..."
|
||||
$result = sqlcmd -S localhost -E -Q "ALTER SERVER ROLE sysadmin ADD MEMBER [DASUTEN\admindasu]; SELECT 'SYSADMIN_OK' AS resultado;" -W 2>&1
|
||||
Log "Resultado: $result"
|
||||
|
||||
# Paso 4: Reiniciar normal
|
||||
Log "PASO 4: Reiniciando SQL Server en modo normal..."
|
||||
net stop MSSQLSERVER /y 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 3
|
||||
net start MSSQLSERVER 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
# Paso 5: Verificar
|
||||
Log "PASO 5: Verificando acceso..."
|
||||
$check = sqlcmd -S localhost -E -Q "SELECT SYSTEM_USER AS usuario, IS_SRVROLEMEMBER('sysadmin') AS es_sysadmin" -W 2>&1
|
||||
Log "Verificacion: $check"
|
||||
|
||||
Log "=========================================="
|
||||
Log "SQL SYSADMIN FIX COMPLETADO"
|
||||
Log "=========================================="
|
||||
@@ -0,0 +1,46 @@
|
||||
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 "CONFIGURANDO SSH EN PUERTO 7022"
|
||||
Log-Msg "=========================================================="
|
||||
|
||||
$cfg = "C:\ProgramData\ssh\sshd_config"
|
||||
|
||||
try {
|
||||
# Leer config, quitar cualquier linea Port anterior
|
||||
$lines = Get-Content $cfg | Where-Object { $_ -notmatch '^\s*#?\s*Port\s+\d+' }
|
||||
|
||||
# Agregar Port 7022 al inicio
|
||||
$lines = @("Port 7022") + $lines
|
||||
|
||||
# Escribir config limpia
|
||||
$lines | Set-Content $cfg -Force
|
||||
Log-Msg "sshd_config actualizado: Port 7022"
|
||||
|
||||
# Firewall: agregar 7022, quitar 22
|
||||
New-NetFirewallRule -Name 'SSH-7022' -DisplayName 'SSH 7022' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 7022 -ErrorAction SilentlyContinue | Out-Null
|
||||
Remove-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue
|
||||
Log-Msg "Firewall actualizado: 7022 abierto, 22 cerrado"
|
||||
|
||||
# Reiniciar servicio
|
||||
Stop-Service sshd -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Service sshd
|
||||
Log-Msg "Servicio sshd reiniciado OK"
|
||||
|
||||
$port = (Get-Content $cfg | Select-String "^Port").ToString().Trim()
|
||||
Log-Msg "Verificacion: $port"
|
||||
} catch {
|
||||
Log-Msg "ERROR: $_"
|
||||
}
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "OPERACION COMPLETADA"
|
||||
Log-Msg "=========================================================="
|
||||
return
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
chcp 65001 >nul 2>&1
|
||||
title W-ZOMBI Agent - Conectado a srv-ns8
|
||||
color 0A
|
||||
|
||||
echo ======================================================
|
||||
echo W-ZOMBI Agent — Esperando ordenes de srv-ns8
|
||||
echo ======================================================
|
||||
echo.
|
||||
echo Este agente se queda abierto y ejecuta lo que
|
||||
echo srv-ns8 le envie. NO cerrar esta ventana.
|
||||
echo.
|
||||
echo Servidor: http://10.0.10.8:8000
|
||||
echo ======================================================
|
||||
echo.
|
||||
|
||||
:LOOP
|
||||
echo [%TIME%] Consultando payload activo...
|
||||
|
||||
REM Descargar y ejecutar el payload activo via PowerShell
|
||||
powershell -ExecutionPolicy Bypass -Command ^
|
||||
"$ErrorActionPreference='Continue'; " ^
|
||||
"try { " ^
|
||||
" $script = (New-Object Net.WebClient).DownloadString('http://10.0.10.8:8000/payloads/activo.ps1'); " ^
|
||||
" if ($script -and $script.Trim() -ne '') { " ^
|
||||
" Write-Host '[ZOMBI] Ejecutando payload...' -ForegroundColor Green; " ^
|
||||
" Invoke-Expression $script " ^
|
||||
" } else { " ^
|
||||
" Write-Host '[ZOMBI] Sin payload pendiente.' -ForegroundColor DarkGray " ^
|
||||
" } " ^
|
||||
"} catch { " ^
|
||||
" Write-Host '[ZOMBI] Sin conexion a srv-ns8, reintentando...' -ForegroundColor Yellow " ^
|
||||
"}"
|
||||
|
||||
echo.
|
||||
echo [%TIME%] Esperando 15 segundos... (CTRL+C para salir)
|
||||
timeout /t 15 /nobreak >nul
|
||||
goto LOOP
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/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 "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://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
|
||||
}
|
||||
}
|
||||
} 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
|
||||
Reference in New Issue
Block a user