From 22384463e24f16931257a56a3b65d06ec00671a5 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Fri, 10 Oct 2025 07:03:10 -0700 Subject: [PATCH] are you human or are you dancer, this was just fun to add. 2fa human check to x: commands --- README.md | 2 +- modules/filemon.py | 62 +++++++++++++++++++++++++++++++++++----------- modules/log.py | 2 +- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0e4ee9e..e13024e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo ### File Monitor Alerts - **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel. - **News File**: On request of news, the contents of the file are returned. Can also call multiple news sources or files. -- **Shell Command Access**: Pass commands via DM directly to the host OS +- **Shell Command Access**: Pass commands via DM directly to the host OS with replay protection. ### Data Reporting - **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md). diff --git a/modules/filemon.py b/modules/filemon.py index faa6334..98e1d33 100644 --- a/modules/filemon.py +++ b/modules/filemon.py @@ -89,42 +89,74 @@ def call_external_script(message, script="script/runShell.sh"): logger.warning(f"FileMon: Error calling external script: {e}") return None +xCmd2factor = True # Enable 2FA for x: commands +waitingXroom = {} # {message_from_id: (expected_answer, original_command, timestamp)} +xCmd2factor_timeout = 100 # seconds def handleShellCmd(message, message_from_id, channel_number, isDM, deviceID): if not allowXcmd: return "x: command is disabled" - if str(message_from_id) not in bbs_admin_list: logger.warning(f"FileMon: Unauthorized x: command attempt from {message_from_id}") return "x: command not authorized" - if not isDM: return "x: command not authorized in group chat" - - if enable_runShellCmd: - # clean up the command input + + # 2FA logic + if xCmd2factor: + timeNOW = datetime.utcnow() + # If user is waiting for 2FA, treat message as answer + if message_from_id in waitingXroom: + answer = message[2:].strip() if message.lower().startswith("x:") else message.strip() + expected, orig_command, ts = waitingXroom[message_from_id] + if timeNOW - ts > timedelta(seconds=xCmd2factor_timeout): + del waitingXroom[message_from_id] + return "x: 2FA timed out, please try again" + if answer == str(expected): + del waitingXroom[message_from_id] + # Run the original command + try: + logger.info(f"FileMon: Running shell command from {message_from_id}: {orig_command}") + result = subprocess.run(orig_command, shell=True, capture_output=True, text=True, timeout=10, start_new_session=True) + output = result.stdout.strip() + return output if output else "x: command executed with no output" + except Exception as e: + logger.warning(f"FileMon: Error running shell command: {e}") + logger.debug(f"FileMon: This command is not good for use over the mesh network") + return "x: error running command" + else: + return "x: 2FA incorrect, try again" + # If not waiting, treat as new command and issue challenge if message.lower().startswith("x:"): - command = message[2:] - if command.startswith(" "): - command = command[1:] - command = command.strip() + command = message[2:].strip() + # Generate two random numbers, seed with message_from_id and time of day + seed = timeNOW.hour + hash(str(message_from_id)) + rnd = random.Random(seed) + a = rnd.randint(10, 99) + b = rnd.randint(10, 99) + expected = a + b + waitingXroom[message_from_id] = (expected, command, timeNOW) + return f"x: 2FA required.\nReply `x: answer`\nWhat is {a} + {b}? " + else: + return "x: invalid command format" + + # If we reach here, 2FA is disabled or passed + if enable_runShellCmd: + if message.lower().startswith("x:"): + command = message[2:].strip() else: return "x: invalid command format" - # Run the shell command as a subprocess try: logger.info(f"FileMon: Running shell command from {message_from_id}: {command}") result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10, start_new_session=True) output = result.stdout.strip() - if output: - return output + return output if output else "x: command executed with no output" except Exception as e: logger.warning(f"FileMon: Error running shell command: {e}") logger.debug(f"FileMon: This command is not good for use over the mesh network") + return "x: error running command" else: logger.debug("FileMon: x: command is disabled by no enable_runShellCmd") return "x: command is disabled" - - return "x: command executed with no output" - def initNewsSources(): #check for the files _news.txt and add to the newsHeadlines list global newsSourcesList diff --git a/modules/log.py b/modules/log.py index f0ac37a..b1d6394 100644 --- a/modules/log.py +++ b/modules/log.py @@ -1,7 +1,7 @@ import logging from logging.handlers import TimedRotatingFileHandler import re -from datetime import datetime +from datetime import datetime, timedelta from modules.settings import * # if LOGGING_LEVEL is not set in settings.py, default to DEBUG if not LOGGING_LEVEL: