From 4ce8db730e47dc5281be57cd2403145e7b0c4349 Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 14:29:41 +0000 Subject: [PATCH 01/10] v.1.1 Chance Based success --- .gitignore | 1 + SSH/config.ini.TEMPLATE | 4 ++++ SSH/ssh_server.py | 18 +++++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4906374..c0053dc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg +dec_Env/ MANIFEST # PyInstaller diff --git a/SSH/config.ini.TEMPLATE b/SSH/config.ini.TEMPLATE index a1b666b..3d9728c 100644 --- a/SSH/config.ini.TEMPLATE +++ b/SSH/config.ini.TEMPLATE @@ -90,5 +90,9 @@ guest = user1 = secretpw user2 = password123 root = * +admin = admin +[authentication] +wildcard_success_rate = 0.3 # 30% chance of success per attempt + diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index 342fdcb..3c73b62 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -13,6 +13,7 @@ import logging import datetime import uuid +import random from base64 import b64encode from operator import itemgetter from langchain_openai import ChatOpenAI @@ -111,15 +112,26 @@ def kbdinit_auth_supported(self) -> bool: return False def validate_password(self, username: str, password: str) -> bool: - pw = accounts.get(username, '*') - - if pw == '*' or (pw != '*' and password == pw): + """Authenticate user, with a chance-based success for wildcard passwords.""" + pw = accounts.get(username, '*') # Get stored password or wildcard (*) + wildcard_success_rate = config['authentication'].getfloat("wildcard_success_rate", 0.3) # Default 30% + + if pw == '*': # Wildcard password logic + if random.random() < wildcard_success_rate: # Success chance + logger.info("Wildcard authentication success", extra={"username": username, "password": password}) + return True + else: + logger.info("Wildcard authentication failed (random chance)", extra={"username": username, "password": password}) + return False + + elif password == pw: # Normal authentication logger.info("Authentication success", extra={"username": username, "password": password}) return True else: logger.info("Authentication failed", extra={"username": username, "password": password}) return False + async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, session: RunnableWithMessageHistory, server: MySSHServer): # Check if the summary has already been generated if server.summary_generated: From 906f5ce175f677e34b48ff652a8a546e1f5baf35 Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 15:18:41 +0000 Subject: [PATCH 02/10] v.1.1 Chance Based success --- SSH/ssh_server.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index 3c73b62..ca6f522 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -111,20 +111,28 @@ def public_key_auth_supported(self) -> bool: def kbdinit_auth_supported(self) -> bool: return False + def validate_password(self, username: str, password: str) -> bool: """Authenticate user, with a chance-based success for wildcard passwords.""" pw = accounts.get(username, '*') # Get stored password or wildcard (*) - wildcard_success_rate = config['authentication'].getfloat("wildcard_success_rate", 0.3) # Default 30% - if pw == '*': # Wildcard password logic - if random.random() < wildcard_success_rate: # Success chance + # Fetch wildcard success rate safely from config, with a default of 30% + try: + wildcard_success_rate = float(config.get('authentication', 'wildcard_success_rate', fallback="0.3")) + except ValueError: + wildcard_success_rate = 0.3 # Default to 30% if there's an issue + + # Wildcard password handling with probability-based success + if pw == '*': + if random.random() < wildcard_success_rate: # Chance-based success logger.info("Wildcard authentication success", extra={"username": username, "password": password}) return True else: logger.info("Wildcard authentication failed (random chance)", extra={"username": username, "password": password}) return False - elif password == pw: # Normal authentication + # Regular password validation + if password == pw: logger.info("Authentication success", extra={"username": username, "password": password}) return True else: @@ -132,6 +140,7 @@ def validate_password(self, username: str, password: str) -> bool: return False + async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, session: RunnableWithMessageHistory, server: MySSHServer): # Check if the summary has already been generated if server.summary_generated: From 50421c4ed6df05853e161b703197e0461915833f Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 15:27:33 +0000 Subject: [PATCH 03/10] Async fix terminal size change error losing conection --- SSH/ssh_server.py | 127 ++++++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index ca6f522..d4ff598 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -200,77 +200,80 @@ async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, server.summary_generated = True -async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: - # This is the main loop for handling SSH client connections. - # Any user interaction should be done here. - - # Give each session a unique name - task_uuid = f"session-{uuid.uuid4()}" - current_task = asyncio.current_task() - current_task.set_name(task_uuid) - - llm_config = {"configurable": {"session_id": task_uuid}} - - try: - if process.command: - # Handle non-interactive command execution - command = process.command - logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content=command)], - "username": process.get_extra_info('username'), - "interactive": False - }, - config=llm_config - ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - else: - # Handle interactive session - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content="ignore this message")], - "username": process.get_extra_info('username'), - "interactive": True - }, - config=llm_config - ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: + # This is the main loop for handling SSH client connections. + # Any user interaction should be done here. - async for line in process.stdin: - line = line.rstrip('\n') - logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + # Give each session a unique name + task_uuid = f"session-{uuid.uuid4()}" + current_task = asyncio.current_task() + current_task.set_name(task_uuid) - # Send the command to the LLM and give the response to the user + llm_config = {"configurable": {"session_id": task_uuid}} + + try: + if process.command: + # Handle non-interactive command execution + command = process.command + logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=command)], + "username": process.get_extra_info('username'), + "interactive": False + }, + config=llm_config + ) + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) + else: + # Handle interactive session llm_response = await with_message_history.ainvoke( { - "messages": [HumanMessage(content=line)], + "messages": [HumanMessage(content="ignore this message")], "username": process.get_extra_info('username'), "interactive": True }, - config=llm_config + config=llm_config ) - if llm_response.content == "XXX-END-OF-SESSION-XXX": - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - return - else: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - - except asyncssh.BreakReceived: - pass - finally: - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - - # Just in case we ever get here, which we probably shouldn't - # process.exit(0) + + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + + try: + async for line in process.stdin: + line = line.rstrip('\n') + logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + + # Send the command to the LLM and give the response to the user + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=line)], + "username": process.get_extra_info('username'), + "interactive": True + }, + config=llm_config + ) + if llm_response.content == "XXX-END-OF-SESSION-XXX": + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) + return + else: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + + except asyncssh.misc.TerminalSizeChanged: + logger.warning("SSH client changed terminal size. Ignoring and continuing.") + pass # Ignore this error and keep running + + except asyncssh.BreakReceived: + pass + finally: + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) async def start_server() -> None: async def process_factory(process: asyncssh.SSHServerProcess) -> None: From deb3e03e5cd32b294ede9da657e68d2c20eb39a4 Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 15:32:14 +0000 Subject: [PATCH 04/10] handle client fix --- SSH/ssh_server.py | 142 +++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index d4ff598..4e3b6de 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -201,79 +201,79 @@ async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, server.summary_generated = True - async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: - # This is the main loop for handling SSH client connections. - # Any user interaction should be done here. - - # Give each session a unique name - task_uuid = f"session-{uuid.uuid4()}" - current_task = asyncio.current_task() - current_task.set_name(task_uuid) - - llm_config = {"configurable": {"session_id": task_uuid}} - - try: - if process.command: - # Handle non-interactive command execution - command = process.command - logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content=command)], - "username": process.get_extra_info('username'), - "interactive": False - }, - config=llm_config - ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - else: - # Handle interactive session - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content="ignore this message")], - "username": process.get_extra_info('username'), - "interactive": True - }, - config=llm_config - ) - - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - - try: - async for line in process.stdin: - line = line.rstrip('\n') - logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) - - # Send the command to the LLM and give the response to the user - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content=line)], - "username": process.get_extra_info('username'), - "interactive": True - }, - config=llm_config - ) - if llm_response.content == "XXX-END-OF-SESSION-XXX": - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - return - else: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - - except asyncssh.misc.TerminalSizeChanged: - logger.warning("SSH client changed terminal size. Ignoring and continuing.") - pass # Ignore this error and keep running - - except asyncssh.BreakReceived: - pass - finally: +async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: + # This is the main loop for handling SSH client connections. + # Any user interaction should be done here. + + # Give each session a unique name + task_uuid = f"session-{uuid.uuid4()}" + current_task = asyncio.current_task() + current_task.set_name(task_uuid) + + llm_config = {"configurable": {"session_id": task_uuid}} + + try: + if process.command: + # Handle non-interactive command execution + command = process.command + logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=command)], + "username": process.get_extra_info('username'), + "interactive": False + }, + config=llm_config + ) + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) await session_summary(process, llm_config, with_message_history, server) process.exit(0) + else: + # Handle interactive session + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content="ignore this message")], + "username": process.get_extra_info('username'), + "interactive": True + }, + config=llm_config + ) + + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + + try: + async for line in process.stdin: + line = line.rstrip('\n') + logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + + # Send the command to the LLM and give the response to the user + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=line)], + "username": process.get_extra_info('username'), + "interactive": True + }, + config=llm_config + ) + if llm_response.content == "XXX-END-OF-SESSION-XXX": + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) + return + else: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + + except asyncssh.misc.TerminalSizeChanged: + logger.warning("SSH client changed terminal size. Ignoring and continuing.") + pass # Ignore this error and keep running + + except asyncssh.BreakReceived: + pass + finally: + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) async def start_server() -> None: async def process_factory(process: asyncssh.SSHServerProcess) -> None: From 6338701a23508e906487ae335f97662209f86f7c Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 16:37:17 +0000 Subject: [PATCH 05/10] Fix broken pipe --- SSH/ssh_server.py | 76 +++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index 4e3b6de..a3a5ac0 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -200,12 +200,7 @@ async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, server.summary_generated = True - async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: - # This is the main loop for handling SSH client connections. - # Any user interaction should be done here. - - # Give each session a unique name task_uuid = f"session-{uuid.uuid4()}" current_task = asyncio.current_task() current_task.set_name(task_uuid) @@ -214,9 +209,9 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) try: if process.command: - # Handle non-interactive command execution command = process.command logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) + llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content=command)], @@ -225,12 +220,17 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) }, config=llm_config ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) + + try: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) + except BrokenPipeError: + logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + await session_summary(process, llm_config, with_message_history, server) process.exit(0) + else: - # Handle interactive session llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content="ignore this message")], @@ -240,41 +240,45 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) config=llm_config ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - try: - async for line in process.stdin: - line = line.rstrip('\n') - logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) - - # Send the command to the LLM and give the response to the user - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content=line)], - "username": process.get_extra_info('username'), - "interactive": True - }, - config=llm_config - ) - if llm_response.content == "XXX-END-OF-SESSION-XXX": - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - return - else: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - - except asyncssh.misc.TerminalSizeChanged: - logger.warning("SSH client changed terminal size. Ignoring and continuing.") - pass # Ignore this error and keep running + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + except BrokenPipeError: + logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + + async for line in process.stdin: + line = line.rstrip('\n') + logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=line)], + "username": process.get_extra_info('username'), + "interactive": True + }, + config=llm_config + ) + + if llm_response.content == "XXX-END-OF-SESSION-XXX": + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) + return + + try: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + except BrokenPipeError: + logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + break # Stop processing if the client has disconnected except asyncssh.BreakReceived: pass + finally: await session_summary(process, llm_config, with_message_history, server) process.exit(0) + async def start_server() -> None: async def process_factory(process: asyncssh.SSHServerProcess) -> None: server = process.get_server() From 53cc6620e07a028266df69211cef282af291857b Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 16:41:11 +0000 Subject: [PATCH 06/10] orig --- SSH/ssh_server.py | 68 ++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index a3a5ac0..342fdcb 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -13,7 +13,6 @@ import logging import datetime import uuid -import random from base64 import b64encode from operator import itemgetter from langchain_openai import ChatOpenAI @@ -111,36 +110,16 @@ def public_key_auth_supported(self) -> bool: def kbdinit_auth_supported(self) -> bool: return False - def validate_password(self, username: str, password: str) -> bool: - """Authenticate user, with a chance-based success for wildcard passwords.""" - pw = accounts.get(username, '*') # Get stored password or wildcard (*) - - # Fetch wildcard success rate safely from config, with a default of 30% - try: - wildcard_success_rate = float(config.get('authentication', 'wildcard_success_rate', fallback="0.3")) - except ValueError: - wildcard_success_rate = 0.3 # Default to 30% if there's an issue - - # Wildcard password handling with probability-based success - if pw == '*': - if random.random() < wildcard_success_rate: # Chance-based success - logger.info("Wildcard authentication success", extra={"username": username, "password": password}) - return True - else: - logger.info("Wildcard authentication failed (random chance)", extra={"username": username, "password": password}) - return False - - # Regular password validation - if password == pw: + pw = accounts.get(username, '*') + + if pw == '*' or (pw != '*' and password == pw): logger.info("Authentication success", extra={"username": username, "password": password}) return True else: logger.info("Authentication failed", extra={"username": username, "password": password}) return False - - async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, session: RunnableWithMessageHistory, server: MySSHServer): # Check if the summary has already been generated if server.summary_generated: @@ -201,6 +180,10 @@ async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, server.summary_generated = True async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: + # This is the main loop for handling SSH client connections. + # Any user interaction should be done here. + + # Give each session a unique name task_uuid = f"session-{uuid.uuid4()}" current_task = asyncio.current_task() current_task.set_name(task_uuid) @@ -209,75 +192,64 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) try: if process.command: + # Handle non-interactive command execution command = process.command logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) - llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content=command)], "username": process.get_extra_info('username'), "interactive": False }, - config=llm_config + config=llm_config ) - - try: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) - except BrokenPipeError: - logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") - + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) await session_summary(process, llm_config, with_message_history, server) process.exit(0) - else: + # Handle interactive session llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content="ignore this message")], "username": process.get_extra_info('username'), "interactive": True }, - config=llm_config + config=llm_config ) - try: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - except BrokenPipeError: - logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) async for line in process.stdin: line = line.rstrip('\n') logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + # Send the command to the LLM and give the response to the user llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content=line)], "username": process.get_extra_info('username'), "interactive": True }, - config=llm_config + config=llm_config ) - if llm_response.content == "XXX-END-OF-SESSION-XXX": await session_summary(process, llm_config, with_message_history, server) process.exit(0) return - - try: + else: process.stdout.write(f"{llm_response.content}") logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - except BrokenPipeError: - logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") - break # Stop processing if the client has disconnected except asyncssh.BreakReceived: pass - finally: await session_summary(process, llm_config, with_message_history, server) process.exit(0) + # Just in case we ever get here, which we probably shouldn't + # process.exit(0) async def start_server() -> None: async def process_factory(process: asyncssh.SSHServerProcess) -> None: From f5fde10568217563a2f31e29bcb57f47a55edfa0 Mon Sep 17 00:00:00 2001 From: slehee Date: Thu, 13 Feb 2025 16:50:11 +0000 Subject: [PATCH 07/10] bypass TerminalSizeChange issue --- SSH/ssh_server.py | 107 ++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index 342fdcb..1786acb 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -13,6 +13,7 @@ import logging import datetime import uuid +import random from base64 import b64encode from operator import itemgetter from langchain_openai import ChatOpenAI @@ -110,16 +111,36 @@ def public_key_auth_supported(self) -> bool: def kbdinit_auth_supported(self) -> bool: return False + def validate_password(self, username: str, password: str) -> bool: - pw = accounts.get(username, '*') - - if pw == '*' or (pw != '*' and password == pw): + """Authenticate user, with a chance-based success for wildcard passwords.""" + pw = accounts.get(username, '*') # Get stored password or wildcard (*) + + # Fetch wildcard success rate safely from config, with a default of 30% + try: + wildcard_success_rate = float(config.get('authentication', 'wildcard_success_rate', fallback="0.3")) + except ValueError: + wildcard_success_rate = 0.3 # Default to 30% if there's an issue + + # Wildcard password handling with probability-based success + if pw == '*': + if random.random() < wildcard_success_rate: # Chance-based success + logger.info("Wildcard authentication success", extra={"username": username, "password": password}) + return True + else: + logger.info("Wildcard authentication failed (random chance)", extra={"username": username, "password": password}) + return False + + # Regular password validation + if password == pw: logger.info("Authentication success", extra={"username": username, "password": password}) return True else: logger.info("Authentication failed", extra={"username": username, "password": password}) return False + + async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, session: RunnableWithMessageHistory, server: MySSHServer): # Check if the summary has already been generated if server.summary_generated: @@ -180,10 +201,6 @@ async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, server.summary_generated = True async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: - # This is the main loop for handling SSH client connections. - # Any user interaction should be done here. - - # Give each session a unique name task_uuid = f"session-{uuid.uuid4()}" current_task = asyncio.current_task() current_task.set_name(task_uuid) @@ -192,64 +209,82 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) try: if process.command: - # Handle non-interactive command execution command = process.command logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) + llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content=command)], "username": process.get_extra_info('username'), "interactive": False }, - config=llm_config + config=llm_config ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) + + try: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) + except BrokenPipeError: + logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + await session_summary(process, llm_config, with_message_history, server) process.exit(0) + else: - # Handle interactive session llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content="ignore this message")], "username": process.get_extra_info('username'), "interactive": True }, - config=llm_config + config=llm_config ) - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + try: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + except BrokenPipeError: + logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + + try: + async for line in process.stdin: + line = line.rstrip('\n') + logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=line)], + "username": process.get_extra_info('username'), + "interactive": True + }, + config=llm_config + ) - async for line in process.stdin: - line = line.rstrip('\n') - logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) + if llm_response.content == "XXX-END-OF-SESSION-XXX": + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) + return - # Send the command to the LLM and give the response to the user - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content=line)], - "username": process.get_extra_info('username'), - "interactive": True - }, - config=llm_config - ) - if llm_response.content == "XXX-END-OF-SESSION-XXX": - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - return - else: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + try: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) + except BrokenPipeError: + logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") + break # Stop processing if the client has disconnected + + except asyncssh.misc.TerminalSizeChanged: + logger.warning("SSH client changed terminal size. Ignoring and continuing.") + await asyncio.sleep(0.1) # Small delay to avoid spamming errors + pass # Continue execution except asyncssh.BreakReceived: pass + finally: await session_summary(process, llm_config, with_message_history, server) process.exit(0) - # Just in case we ever get here, which we probably shouldn't - # process.exit(0) + async def start_server() -> None: async def process_factory(process: asyncssh.SSHServerProcess) -> None: From 3406ba3dbcfdde0b22837d8932c87196ee8a744a Mon Sep 17 00:00:00 2001 From: slehee Date: Fri, 14 Feb 2025 12:41:00 +0000 Subject: [PATCH 08/10] fix random --- SSH/ssh_server.py | 214 +++++++++++++++++++++++++++------------------- 1 file changed, 124 insertions(+), 90 deletions(-) diff --git a/SSH/ssh_server.py b/SSH/ssh_server.py index 1786acb..367963d 100755 --- a/SSH/ssh_server.py +++ b/SSH/ssh_server.py @@ -110,37 +110,34 @@ def public_key_auth_supported(self) -> bool: return False def kbdinit_auth_supported(self) -> bool: return False - - + def validate_password(self, username: str, password: str) -> bool: - """Authenticate user, with a chance-based success for wildcard passwords.""" - pw = accounts.get(username, '*') # Get stored password or wildcard (*) - - # Fetch wildcard success rate safely from config, with a default of 30% - try: - wildcard_success_rate = float(config.get('authentication', 'wildcard_success_rate', fallback="0.3")) - except ValueError: - wildcard_success_rate = 0.3 # Default to 30% if there's an issue - - # Wildcard password handling with probability-based success - if pw == '*': - if random.random() < wildcard_success_rate: # Chance-based success - logger.info("Wildcard authentication success", extra={"username": username, "password": password}) + """Authenticate user, with a chance-based success for wildcard passwords.""" + pw = accounts.get(username, '*') # Get stored password or wildcard (*) + + # Fetch wildcard success rate safely from config, with a default of 30% + try: + wildcard_success_rate = float(config.get('authentication', 'wildcard_success_rate', fallback="0.3")) + except ValueError: + wildcard_success_rate = 0.3 # Default to 30% if there's an issue + + # Wildcard password handling with probability-based success + if pw == '*': + if random.random() < wildcard_success_rate: # Chance-based success + logger.info("Wildcard authentication success", extra={"username": username, "password": password}) + return True + else: + logger.info("Wildcard authentication failed (random chance)", extra={"username": username, "password": password}) + return False + + # Regular password validation + if password == pw: + logger.info("Authentication success", extra={"username": username, "password": password}) return True else: - logger.info("Wildcard authentication failed (random chance)", extra={"username": username, "password": password}) + logger.info("Authentication failed", extra={"username": username, "password": password}) return False - # Regular password validation - if password == pw: - logger.info("Authentication success", extra={"username": username, "password": password}) - return True - else: - logger.info("Authentication failed", extra={"username": username, "password": password}) - return False - - - async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, session: RunnableWithMessageHistory, server: MySSHServer): # Check if the summary has already been generated if server.summary_generated: @@ -201,6 +198,10 @@ async def session_summary(process: asyncssh.SSHServerProcess, llm_config: dict, server.summary_generated = True async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: + # This is the main loop for handling SSH client connections. + # Any user interaction should be done here. + + # Give each session a unique name task_uuid = f"session-{uuid.uuid4()}" current_task = asyncio.current_task() current_task.set_name(task_uuid) @@ -209,82 +210,64 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) try: if process.command: + # Handle non-interactive command execution command = process.command logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False}) - llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content=command)], "username": process.get_extra_info('username'), "interactive": False }, - config=llm_config + config=llm_config ) - - try: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) - except BrokenPipeError: - logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") - + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False}) await session_summary(process, llm_config, with_message_history, server) process.exit(0) - else: + # Handle interactive session llm_response = await with_message_history.ainvoke( { "messages": [HumanMessage(content="ignore this message")], "username": process.get_extra_info('username'), "interactive": True }, - config=llm_config + config=llm_config ) - try: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - except BrokenPipeError: - logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") - - try: - async for line in process.stdin: - line = line.rstrip('\n') - logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) - - llm_response = await with_message_history.ainvoke( - { - "messages": [HumanMessage(content=line)], - "username": process.get_extra_info('username'), - "interactive": True - }, - config=llm_config - ) + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - if llm_response.content == "XXX-END-OF-SESSION-XXX": - await session_summary(process, llm_config, with_message_history, server) - process.exit(0) - return + async for line in process.stdin: + line = line.rstrip('\n') + logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True}) - try: - process.stdout.write(f"{llm_response.content}") - logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) - except BrokenPipeError: - logger.warning("BrokenPipeError: SSH client disconnected mid-session. Ignoring output.") - break # Stop processing if the client has disconnected - - except asyncssh.misc.TerminalSizeChanged: - logger.warning("SSH client changed terminal size. Ignoring and continuing.") - await asyncio.sleep(0.1) # Small delay to avoid spamming errors - pass # Continue execution + # Send the command to the LLM and give the response to the user + llm_response = await with_message_history.ainvoke( + { + "messages": [HumanMessage(content=line)], + "username": process.get_extra_info('username'), + "interactive": True + }, + config=llm_config + ) + if llm_response.content == "XXX-END-OF-SESSION-XXX": + await session_summary(process, llm_config, with_message_history, server) + process.exit(0) + return + else: + process.stdout.write(f"{llm_response.content}") + logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True}) except asyncssh.BreakReceived: pass - finally: await session_summary(process, llm_config, with_message_history, server) process.exit(0) - + # Just in case we ever get here, which we probably shouldn't + # process.exit(0) async def start_server() -> None: async def process_factory(process: asyncssh.SSHServerProcess) -> None: @@ -313,7 +296,7 @@ def filter(self, record): if task: task_name = task.get_name() else: - task_name = "-" + task_name = thread_local.__dict__.get('session_id', '-') record.src_ip = thread_local.__dict__.get('src_ip', '-') record.src_port = thread_local.__dict__.get('src_port', '-') @@ -340,10 +323,10 @@ def get_user_accounts() -> dict: return accounts -def choose_llm(): - llm_provider_name = config['llm'].get("llm_provider", "openai") +def choose_llm(llm_provider: Optional[str] = None, model_name: Optional[str] = None): + llm_provider_name = llm_provider or config['llm'].get("llm_provider", "openai") llm_provider_name = llm_provider_name.lower() - model_name = config['llm'].get("model_name", "gpt-3.5-turbo") + model_name = model_name or config['llm'].get("model_name", "gpt-3.5-turbo") if llm_provider_name == 'openai': llm_model = ChatOpenAI( @@ -395,26 +378,77 @@ def get_prompts(prompt: Optional[str], prompt_file: Optional[str]) -> dict: try: # Parse command line arguments parser = argparse.ArgumentParser(description='Start the SSH honeypot server.') - parser.add_argument('-c', '--config', type=str, default='config.ini', help='Path to the configuration file') + parser.add_argument('-c', '--config', type=str, default=None, help='Path to the configuration file') parser.add_argument('-p', '--prompt', type=str, help='The entire text of the prompt') parser.add_argument('-f', '--prompt-file', type=str, default='prompt.txt', help='Path to the prompt file') + parser.add_argument('-l', '--llm-provider', type=str, help='The LLM provider to use') + parser.add_argument('-m', '--model-name', type=str, help='The model name to use') + parser.add_argument('-t', '--trimmer-max-tokens', type=int, help='The maximum number of tokens to send to the LLM backend in a single request') + parser.add_argument('-s', '--system-prompt', type=str, help='System prompt for the LLM') + parser.add_argument('-P', '--port', type=int, help='The port the SSH honeypot will listen on') + parser.add_argument('-k', '--host-priv-key', type=str, help='The host key to use for the SSH server') + parser.add_argument('-v', '--server-version-string', type=str, help='The server version string to send to clients') + parser.add_argument('-L', '--log-file', type=str, help='The name of the file you wish to write the honeypot log to') + parser.add_argument('-S', '--sensor-name', type=str, help='The name of the sensor, used to identify this honeypot in the logs') + parser.add_argument('-u', '--user-account', action='append', help='User account in the form username=password. Can be repeated.') args = parser.parse_args() - # Check if the config file exists - if not os.path.exists(args.config): - print(f"Error: The specified config file '{args.config}' does not exist.", file=sys.stderr) - sys.exit(1) - - # Always use UTC for logging - logging.Formatter.formatTime = (lambda self, record, datefmt=None: datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).isoformat(sep="T",timespec="milliseconds")) - - # Read our configuration file + # Determine which config file to load config = ConfigParser() - config.read(args.config) + if args.config is not None: + # User explicitly set a config file; error if it doesn't exist. + if not os.path.exists(args.config): + print(f"Error: The specified config file '{args.config}' does not exist.", file=sys.stderr) + sys.exit(1) + config.read(args.config) + else: + default_config = "config.ini" + if os.path.exists(default_config): + config.read(default_config) + else: + # Use defaults when no config file found. + config['honeypot'] = {'log_file': 'ssh_log.log', 'sensor_name': socket.gethostname()} + config['ssh'] = {'port': '8022', 'host_priv_key': 'ssh_host_key', 'server_version_string': 'SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3'} + config['llm'] = {'llm_provider': 'openai', 'model_name': 'gpt-3.5-turbo', 'trimmer_max_tokens': '64000', 'system_prompt': ''} + config['user_accounts'] = {} + + # Override config values with command line arguments if provided + if args.llm_provider: + config['llm']['llm_provider'] = args.llm_provider + if args.model_name: + config['llm']['model_name'] = args.model_name + if args.trimmer_max_tokens: + config['llm']['trimmer_max_tokens'] = str(args.trimmer_max_tokens) + if args.system_prompt: + config['llm']['system_prompt'] = args.system_prompt + if args.port: + config['ssh']['port'] = str(args.port) + if args.host_priv_key: + config['ssh']['host_priv_key'] = args.host_priv_key + if args.server_version_string: + config['ssh']['server_version_string'] = args.server_version_string + if args.log_file: + config['honeypot']['log_file'] = args.log_file + if args.sensor_name: + config['honeypot']['sensor_name'] = args.sensor_name + + # Merge command-line user accounts into the config + if args.user_account: + if 'user_accounts' not in config: + config['user_accounts'] = {} + for account in args.user_account: + if '=' in account: + key, value = account.split('=', 1) + config['user_accounts'][key.strip()] = value.strip() + else: + config['user_accounts'][account.strip()] = '' - # Read the user accounts from the configuration file + # Read the user accounts from the configuration accounts = get_user_accounts() + # Always use UTC for logging + logging.Formatter.formatTime = (lambda self, record, datefmt=None: datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).isoformat(sep="T",timespec="milliseconds")) + # Get the sensor name from the config or use the system's hostname sensor_name = config['honeypot'].get('sensor_name', socket.gethostname()) @@ -436,7 +470,7 @@ def get_prompts(prompt: Optional[str], prompt_file: Optional[str]) -> dict: llm_system_prompt = prompts["system_prompt"] llm_user_prompt = prompts["user_prompt"] - llm = choose_llm() + llm = choose_llm(config['llm'].get("llm_provider"), config['llm'].get("model_name")) llm_sessions = dict() From 010e7a3b57f91b013c4a36f5f3fca1230f44f009 Mon Sep 17 00:00:00 2001 From: slehee Date: Fri, 14 Feb 2025 12:47:49 +0000 Subject: [PATCH 09/10] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c0053dc..904b9f8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg +dec_env/ dec_Env/ MANIFEST From 040548218b109e3ba7055ff1ff723cc04befbef0 Mon Sep 17 00:00:00 2001 From: slehee Date: Fri, 14 Feb 2025 13:06:43 +0000 Subject: [PATCH 10/10] updt config temp --- SSH/config.ini.TEMPLATE | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SSH/config.ini.TEMPLATE b/SSH/config.ini.TEMPLATE index 3d9728c..47b6471 100644 --- a/SSH/config.ini.TEMPLATE +++ b/SSH/config.ini.TEMPLATE @@ -85,6 +85,11 @@ system_prompt = Interpret all inputs as though they were SSH commands and provid # a password by leaving that field blank (e.g., "guest =" on a line by # itself). You can set an account to accept ANY password, including an empty # password, by setting the password to "*" +# If "*" is used, login success is determined by the "wildcard_success_rate" +# setting under the [authentication] section. +# Regular accounts with defined passwords (e.g., "admin = admin123") will +# always require the exact password. + [user_accounts] guest = user1 = secretpw @@ -94,5 +99,7 @@ admin = admin [authentication] -wildcard_success_rate = 0.3 # 30% chance of success per attempt +# Chance-based success rate for wildcard passwords (*). +# A value of 0.3 means a 30% chance of success per attempt. +wildcard_success_rate = 0.3