forked from extern/SSH-Snake
109 lines
3.9 KiB
Python
Executable File
109 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# <https://github.com/MegaManSec/SSH-Snake>
|
|
# By Joshua Rogers <https://joshua.hu/>
|
|
# GPL 3, of course.
|
|
|
|
import re
|
|
import heapq
|
|
from collections import defaultdict
|
|
import argparse
|
|
|
|
def indirect_get_connected_nodes(graph, interesting_host, ignore_dest_user):
|
|
connected_nodes = set()
|
|
sentinel = '__SENTINEL__'
|
|
|
|
heap = [(interesting_host, sentinel)]
|
|
while heap:
|
|
current_node, parent_node = heapq.heappop(heap)
|
|
|
|
if current_node not in connected_nodes:
|
|
connected_nodes.add(current_node)
|
|
if parent_node is not sentinel:
|
|
heapq.heappush(heap, (parent_node, sentinel))
|
|
if current_node in graph:
|
|
for connection in graph[current_node]:
|
|
if ignore_dest_user:
|
|
node = connection[4] # dest_host
|
|
else:
|
|
node = f"{connection[3]}@{connection[4]}" #dest_host@dest_user
|
|
heapq.heappush(heap, (node, current_node))
|
|
|
|
return connected_nodes
|
|
|
|
def build_lookup_table(input_lines, ignore_dest_user):
|
|
graph = defaultdict(set)
|
|
|
|
for line in input_lines:
|
|
line = line.strip()
|
|
line = re.sub(r"^\[?\d+\]?\s*", "", line)
|
|
|
|
prev_dest_host = None
|
|
|
|
if ": " in line or "]->" not in line or not line[-1].isdigit():
|
|
continue
|
|
|
|
pattern = re.compile(r"(\w+)@(\d+\.\d+\.\d+\.\d+)(\[[^\]]+\])->(?=(\w+)@(\d+\.\d+\.\d+\.\d+))")
|
|
matches = re.finditer(pattern, line)
|
|
for match in matches:
|
|
user, host, path, dest_user, dest_host = match.groups()
|
|
|
|
if host == "(127.0.0.1)" or host == "127.0.0.1":
|
|
if prev_dest_host is not None:
|
|
host = prev_dest_host
|
|
|
|
if dest_host == "(127.0.0.1)" or dest_host == "127.0.0.1":
|
|
dest_host = host
|
|
prev_dest_host = dest_host
|
|
else:
|
|
prev_dest_host = None
|
|
|
|
line_to_add = (user, host, path, dest_user, dest_host)
|
|
|
|
if ignore_dest_user:
|
|
graph[host].add(line_to_add)
|
|
else:
|
|
graph[f"{user}@{host}"].add(line_to_add)
|
|
|
|
return graph
|
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = argparse.ArgumentParser(description="Find all of the destinations that a source 'host' or 'user@host' can directly or indirectly access.")
|
|
parser.add_argument("--file", help="Path to a file containing the output of SSH-Snake.")
|
|
parser.add_argument("--src", help="The host or destination that we are interested in finding out from which other hosts can be connected to. This may either be in the form of 'host' or 'user@host'.")
|
|
parser.add_argument("--mode", choices=['directly', 'indirectly'], help="Specify whether to search for directly connected hosts or indirectly connected hosts. Note: 'indirectly' includes 'directly' entries.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
file_path = args.file
|
|
interesting_host = args.src
|
|
mode = args.mode
|
|
|
|
ignore_dest_user = False
|
|
|
|
if not any(vars(args).values()) or mode not in ("directly", "indirectly"):
|
|
parser.print_help()
|
|
exit()
|
|
|
|
if '@' not in interesting_host:
|
|
ignore_dest_user = True
|
|
|
|
with open(file_path, 'r') as file:
|
|
input_lines = file.readlines()
|
|
|
|
lookup_table = build_lookup_table(input_lines, ignore_dest_user)
|
|
|
|
if interesting_host in lookup_table:
|
|
print(f"{interesting_host} is able to connect {mode} to:\n")
|
|
if mode == "indirectly":
|
|
result = indirect_get_connected_nodes(lookup_table, interesting_host, ignore_dest_user)
|
|
for dest in result:
|
|
print(dest)
|
|
else:
|
|
for entry in lookup_table[interesting_host]:
|
|
user, host, path, dest_user, dest_host = entry
|
|
print(f"{user}@{host}{path} -> {dest_user}@{dest_host}")
|
|
else:
|
|
print(f"{interesting_host} cannot connect {mode} to any other destinations.")
|