From bf0823fc02d4beb303ec7ca7192afa6c3af92ff4 Mon Sep 17 00:00:00 2001 From: Ada Isopods Date: Thu, 15 May 2025 07:48:24 -0600 Subject: woahg Okay so I just - Implemented argparse - Universalized the IP filtering logic - Added verbosity settings, finally - Added something called "blank mode" where you can basically pick and choose what kinds of session you want to see - Moved the filtering logic I'd already implemented in low-verbosity mode to support blank mode to its own thing so it's universal among all the verbosity modes - Commented a bunch of stuff - Fixed a bug I ran into where if an extra-long "empty ping" is still open when the program is run it crashes. I just realized I should probably do that for all the session types actually I'm going to bed --- README.md | 29 +++++++-- cowrieder.py | 195 +++++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 153 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 4162bd0..658c4d2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,32 @@ -Script to help make sense of log output from popular SSH honeypot [Cowrie](https://github.com/cowrie/cowrie/). Only tested on my setup. YMMV +Script to help make sense of log output from popular SSH honeypot [Cowrie](https://github.com/cowrie/cowrie/). I think I got carried away with the specialized logging settings. Only tested on my setup. Untested with Telnet. YMMV # Usage ``` -python3 cowrieder.py cowrie.json +cowrieder.py filepath ``` -You can also filter it by IP address: +Options: ``` -python3 cowrieder.py cowrie.json 127.0.0.1 -``` \ No newline at end of file +-h, --help show this help message and exit +-v, --verbose Make the output more verbose (print abridged logs for all logins where commands are run, not just the interesting ones) +-vv, --verboser Make the output even more verbose (print timestamp, event id, and message (if applicable) for all events) +-vvv, --verbosest Make the output even more verboserer (just group the input log by session id and dump it to pretty-printed JSON) +-B, --blank On default and low verbosity, print nothing whatsoever. Override this with other arguments to filter for only the stuff you care about +-P, --pings | With blank mode active, print "empty pings" +-F, --fails | With blank mode active, print failed logins +-L, --logins | With blank mode active, print "login pings" +-C, --commands | With blank mode active, on default verbosity, print "command pings" +-S, --sessions * With blank mode active, on default verbosity, print "interesting" sessions. With -v enabled, this and -C do the same thing. +-a, --stats Print aggregated statistics +-j, --json-stats Dump aggregated statistics as JSON +-i, --filter-ip FILTER_IP + Filter data to a specific IP +``` + +# Terminology + +- "Empty ping": Client connected and made no attempt to log in +- "Login ping": Client logged in successfully and did nothing +- "Command ping": Client logged in successfully and only ran `uname`, `nproc`, and/or `echo`. In my experience "command pings" comprise most insances of a client successfully logging in and then bothering to run any commands \ No newline at end of file diff --git a/cowrieder.py b/cowrieder.py index 7194c20..419dd45 100644 --- a/cowrieder.py +++ b/cowrieder.py @@ -2,6 +2,7 @@ import json import sys import random import os +import argparse def ccgen(seed=""): # Color code generator if(not os.isatty(1)): # Do not output color code at all if output is not a TTY that can accept them @@ -39,69 +40,139 @@ def ppip(addr, spacing = True): # Pretty-print an address with colour and, if it return ccgen(addr) + addr + ccgen() + (" " * (15 - len(addr))) return ccgen(addr) + addr + ccgen() -if not 0 <= 1 < len(sys.argv): # TODO: integrate argparse - print("Provide a path to a Cowrie JSON log file") - exit(1) -filepath = sys.argv[1] +parser = argparse.ArgumentParser( + description="Script to parse JSON logs from popular SSH honeypot Cowrie and print them in a more readable format" +) + +parser.add_argument('filepath') + +parser.add_argument('-v', '--verbose', action='store_true', help="Make the output more verbose (print abridged logs for all logins where commands are run, not just the interesting ones)") +parser.add_argument('-vv', '--verboser', action='store_true', help="Make the output even more verbose (print timestamp, event id, and message (if applicable) for all events)") +parser.add_argument('-vvv', '--verbosest', action='store_true', help="Make the output even more verboserer (just group the input log by session id and dump it to pretty-printed JSON)") + +parser.add_argument('-B', '--blank', action='store_true', help="Print nothing whatsoever. Override this with other arguments to filter for only the stuff you care about") +parser.add_argument('-P', '--pings', action='store_true', help='| With blank mode active, print "empty pings"') +parser.add_argument('-F', '--fails', action='store_true', help='| With blank mode active, print failed logins') +parser.add_argument('-L', '--logins', action='store_true', help='| With blank mode active, print "login pings"') +parser.add_argument('-C', '--commands', action='store_true', help='| With blank mode active, print "command pings"') +parser.add_argument('-S', '--sessions', action='store_true', help='* With blank mode active, print the cool interesting sessions that aren\'t any of that other stuff.') + +parser.add_argument('-i', '--filter-ip', help='Filter data to a specific IP') + +#parser.add_argument('-a', '--stats', action='store_true', help='Print aggregated statistics') +#parser.add_argument('-j', '--json-stats', action='store_true', help='Dump aggregated statistics as JSON') +#print_stats = args.stats or args.json_stats + +args = parser.parse_args() + +filepath = args.filepath + +print_emptypings = True +print_fails = True +print_loginpings = True +print_cmdpings = True +print_sessions = True +if(args.blank): + print_emptypings = args.pings + print_fails = args.fails + print_loginpings = args.logins + print_cmdpings = args.commands + print_sessions = args.sessions -verbosity = 0 -print_log = True -print_stats = True entries = [] -sessions = {} -session_ips = {} -session_timestamps = {} -session_durations = {} -session_ran_command = {} -session_successful_login = {} +sessions = {} # Dict - session_id : list of events in session +session_ips = {} # Dict - session_id : source IP +session_timestamps = {} # Dict - session_id : timestamp of initial connection +session_durations = {} # Dict - session_id : duration +session_ran_command = [] # List of sessions where a command was run +session_successful_login = {} # Dict - session_id : bool: was the login attempt successful + # Sessions where no login attempt was made will not be present +cmdping_commands = ['uname', 'nproc', 'echo'] # List of commands that, if used exclusively, qualify a session as a "command ping" +session_not_cmdpings = [] # List of sessions where a command not in cmdping_commands was run +session_cmdping_commands = {} # Dict - session_id : list of "command ping" commands used in session + + with open(filepath, "r") as logfile: for line in logfile: entry = json.loads(line) - entries.append(entry) - session_id = entry['session'] + + if( + (not args.filter_ip or entry["src_ip"] == args.filter_ip) # If it's not the IP we're filtering for don't even bother man + ): + entries.append(entry) + session_id = entry['session'] + + if(session_id not in sessions): + sessions.update({session_id: [entry]}) + session_ips.update({session_id: entry['src_ip']}) + else: + sessions[session_id].append(entry) - if(session_id not in sessions): - sessions.update({session_id: [entry]}) - session_ips.update({session_id: entry['src_ip']}) - else: - sessions[session_id].append(entry) + if(entry["eventid"] == "cowrie.session.connect" and session_id not in session_timestamps): # Save the timestamp of the session's initial connection + session_timestamps.update({session_id: entry['timestamp']}) - if(entry["eventid"] == "cowrie.session.connect" and session_id not in session_timestamps): # Save the timestamp of the session's initial connection - session_timestamps.update({session_id: entry['timestamp']}) + if(entry["eventid"] == "cowrie.session.closed" and session_id not in session_durations): # Save the session's duration + session_durations.update({session_id: entry['duration']}) - if(entry["eventid"] == "cowrie.session.closed" and session_id not in session_durations): # Save the session's duration - session_durations.update({session_id: entry['duration']}) + if(entry["eventid"] == "cowrie.command.input"): + if(session_id not in session_ran_command): # Save whether any commands were run during the session + session_ran_command.append(session_id) + + argv0 = entry["input"] # Isolate the name of the command + if(" " in argv0): + argv0 = entry["input"][:entry["input"].index(" ")] + + if(argv0 in cmdping_commands): # Determine whether this event qualifies the session as not a command ping + if(session_id not in session_cmdping_commands): + session_cmdping_commands.update({session_id: [argv0]}) + else: + session_cmdping_commands[session_id].append(argv0) + else: + if(session_id not in session_not_cmdpings): + session_not_cmdpings.append(session_id) - if(entry["eventid"] == "cowrie.command.input" and session_id not in session_ran_command): # Save whether any commands were run during the session - session_ran_command.update({session_id: True}) + if(entry["eventid"] == "cowrie.login.success" and session_id not in session_successful_login): # Save if login was a success + session_successful_login.update({session_id: True}) - if(entry["eventid"] == "cowrie.login.success" and session_id not in session_successful_login): # Save if login was a success - session_successful_login.update({session_id: True}) + if(entry["eventid"] == "cowrie.login.failed" and session_id not in session_successful_login): # Save if login failed + session_successful_login.update({session_id: False}) - if(entry["eventid"] == "cowrie.login.failed" and session_id not in session_successful_login): # Save if login failed - session_successful_login.update({session_id: False}) + if(entry["eventid"] == "cowrie.login.success" and session_id in session_successful_login): # Save if login succeeded after failing + session_successful_login[session_id] = True - if(entry["eventid"] == "cowrie.login.success" and session_id in session_successful_login): # Save if login succeeded after failing - session_successful_login[session_id] = True +# Blank Mode logic -filter_ip = "" -if 0 <= 2 < len(sys.argv): - filter_ip = sys.argv[2] +if(args.blank): + newsessions = sessions.copy() -if(verbosity == 2): # High verbosity mode: print timestamp, event type and message (if applicable) for every event. I could probably do better but I'm lazy for session_id in sessions: - ip = session_ips[session_id] - if(ip == filter_ip or not filter_ip): - print("\n--- Session " + ccgen(session_id) + session_id + ccgen() + " from " + ppip(ip, False) +" ---") - for entry in sessions[session_id]: - if(entry['message']): - eventid = entry['eventid'] - print(entry["timestamp"] + " > " + eventid + (" " * (29 - len(eventid))) + ": " + entry['message']) - else: - print(entry["timestamp"] + " > " + entry['eventid']) -elif(verbosity == 3): # Maximum verbosity: literally just pretty print the now-sorted-by-session logs + if( + (not print_emptypings and session_id not in session_successful_login) or # Is empty ping and don't print those + (not print_fails and session_id in session_successful_login and not session_successful_login[session_id]) or # Is failed login and don't print those + (not print_loginpings and session_id in session_successful_login and session_successful_login[session_id] and session_id not in session_ran_command) or # is login ping and don't print those + (not print_cmdpings and session_id in session_ran_command and session_id not in session_not_cmdpings) or # is command ping and don't print those + (not print_sessions and session_id in session_ran_command and session_id in session_not_cmdpings) # is session and don't print those + ): + newsessions.pop(session_id) + + sessions = newsessions + +# Printing logic coming up + +if(args.verboser): # High verbosity mode: print timestamp, event type and message (if applicable) for every event. I could probably do better but I'm lazy + for session_id in sessions: + ip = session_ips[session_id] + print("\n--- Session " + ccgen(session_id) + session_id + ccgen() + " from " + ppip(ip, False) +" ---") + for entry in sessions[session_id]: + if(entry['message']): + eventid = entry['eventid'] + print(entry["timestamp"] + " > " + eventid + (" " * (29 - len(eventid))) + ": " + entry['message']) + else: + print(entry["timestamp"] + " > " + entry['eventid']) + +elif(args.verbosest): # Maximum verbosity: literally just pretty print the now-sorted-by-session logs print(json.dumps(sessions, indent=4)) else: # Default (low) verbosity: just the good stuff @@ -110,10 +181,13 @@ else: # Default (low) verbosity: just the good stuff ip = session_ips[session_id] timestamp = session_timestamps[session_id][:22].replace("T", " ") # clean up the timestamp text all pretty like - if(session_id not in session_successful_login): - print(timestamp + " > " + ppip(ip) + " > " + ccgen("PURPLE") + "Empty ping" + ccgen() + " lasting " + session_durations[session_id] + "s") + if(session_id not in session_successful_login): # Printing logic for empty pings + if(session_id in session_durations): + print(timestamp + " > " + ppip(ip) + " > " + ccgen("PURPLE") + "Empty ping" + ccgen() + " lasting " + session_durations[session_id] + "s") + else: + print(timestamp + " > " + ppip(ip) + " > " + ccgen("PURPLE") + "Empty ping" + ccgen() + " active as of processing") - elif(not session_successful_login[session_id]): + elif(not session_successful_login[session_id]): # Printing logic for failed logins cred_combos = [] for entry in sessions[session_id]: if(entry["eventid"] == "cowrie.login.failed"): @@ -123,27 +197,16 @@ else: # Default (low) verbosity: just the good stuff cred_combos.append((entry["username"], pw)) print(timestamp + " > " + ppip(ip) + " > " + ccgen("RED") +"Failed login" + ccgen() + ": " + str(cred_combos)) - elif(session_id in session_ran_command): - collapse_cmdping = False - pingcmds = [] - if(verbosity != 1): - for entry in sessions[session_id]: - if(entry["eventid"] == "cowrie.command.input"): - argv0 = entry["input"] - if(" " in argv0): - argv0 = entry["input"][:entry["input"].index(" ")] - if(argv0 in ['uname', 'nproc', 'echo']): - pingcmds.append(argv0) - collapse_cmdping = True - - if(collapse_cmdping): + elif(session_id in session_ran_command): # Printing logic for sessions in which a command was run + + if(not args.verbose and session_id not in session_not_cmdpings): # If -v is not enabled, collapse "command pings" to one-liners cred_combos = [] for entry in sessions[session_id]: if(entry["eventid"] == "cowrie.login.failed" or entry["eventid"] == "cowrie.login.success"): cred_combos.append((entry["username"], entry["password"])) - print(timestamp + " > " + ppip(ip) + " > " + ccgen("BLUE") + "Command ping" + ccgen() + ": " + str(cred_combos) + " > " + str(pingcmds)) + print(timestamp + " > " + ppip(ip) + " > " + ccgen("BLUE") + "Command ping" + ccgen() + ": " + str(cred_combos) + " > " + str(session_cmdping_commands[session_id])) - else: + else: # Printing logic for un-collapsed sessions print(timestamp + " > " + ppip(ip) + " > " + ccgen("CYAN") + "Session" + ccgen() + ":") for entry in sessions[session_id]: match(entry["eventid"]): @@ -165,7 +228,7 @@ else: # Default (low) verbosity: just the good stuff print(" | Saved file " + df + " to " + entry["outfile"][:40]) print(" X Disconnected after " + session_durations[session_id] + "s") - else: + else: # Printing logic for successful logins in which no commands were run cred_combos = [] for entry in sessions[session_id]: if(entry["eventid"] == "cowrie.login.failed" or entry["eventid"] == "cowrie.login.success"): -- cgit v1.2.3