LSMS/scripts/monitor_ssh_authorized_keys.py

372 lines
12 KiB
Python
Raw Normal View History

2021-12-27 13:52:26 +01:00
#!/usr/bin/env python3
# written by sqall
# twitter: https://twitter.com/sqall01
# blog: https://h4des.org
# github: https://github.com/sqall01
#
2021-12-30 20:48:17 +01:00
# Licensed under the MIT License.
2021-12-27 13:52:26 +01:00
"""
Short summary:
Monitor ~/.ssh/authorized_keys for changes to detect malicious backdoor attempts.
Requirements:
None
"""
import os
import json
import stat
import socket
from typing import List, Tuple, Dict, Any
from lib.alerts import raise_alert_alertr, raise_alert_mail
# Read configuration and library functions.
try:
from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR
from config.monitor_ssh_authorized_keys import ACTIVATED
STATE_DIR = os.path.join(os.path.dirname(__file__), STATE_DIR, os.path.basename(__file__))
except:
ALERTR_FIFO = None
FROM_ADDR = None
TO_ADDR = None
ACTIVATED = True
STATE_DIR = os.path.join("/tmp", os.path.basename(__file__))
MAIL_SUBJECT = "[Security] Monitoring SSH authorized_keys on host '%s'" % socket.gethostname()
class MonitorSSHException(Exception):
def __init__(self, msg: str):
self._msg = msg
def __str__(self):
return self._msg
def _get_home_dirs_from_passwd() -> List[Tuple[str, str]]:
user_home_list = []
try:
with open("/etc/passwd", 'rt') as fp:
for line in fp:
line_split = line.split(":")
user_home_list.append((line_split[0], line_split[5]))
except Exception as e:
raise MonitorSSHException(str(e))
return user_home_list
def _get_system_ssh_data() -> List[Dict[str, Any]]:
ssh_data = []
user_home_list = _get_home_dirs_from_passwd()
for user, home in user_home_list:
# Monitor "authorized_keys2" too since SSH also checks this file for keys (even though it is deprecated).
for authorized_file_name in ["authorized_keys", "authorized_keys2"]:
authorized_keys_file = os.path.join(home, ".ssh", authorized_file_name)
if os.path.isfile(authorized_keys_file):
ssh_user_data = {"user": user,
"authorized_keys_file": authorized_keys_file,
"authorized_keys_entries": _parse_authorized_keys_file(authorized_keys_file)}
ssh_data.append(ssh_user_data)
return ssh_data
def _load_ssh_data() -> List[Dict[str, Any]]:
state_file = os.path.join(STATE_DIR, "state")
ssh_data = []
if os.path.isfile(state_file):
data = None
try:
with open(state_file, 'rt') as fp:
data = fp.read()
if data is None:
raise MonitorSSHException("Read state data is None.")
ssh_data = json.loads(data)
except Exception as e:
raise MonitorSSHException("State data: '%s'; Exception: '%s'" % (str(data), str(e)))
return ssh_data
def _output_error(msg: str):
# Decide where to output results.
print_output = False
if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None:
print_output = True
if print_output:
print(msg)
else:
hostname = socket.gethostname()
message = "Error monitoring SSH authorized_keys on host '%s': %s" \
% (hostname, msg)
if ALERTR_FIFO:
optional_data = dict()
optional_data["error"] = True
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
def _parse_authorized_keys_file(authorized_keys_file: str) -> List[str]:
entries = set()
try:
with open(authorized_keys_file, 'rt') as fp:
for line in fp:
entries.add(line.strip())
except Exception as e:
raise MonitorSSHException("Unable to parse file '%s'; Exception: '%s'" % (authorized_keys_file, str(e)))
return list(entries)
def _store_ssh_data(ssh_data: List[Dict[str, Any]]):
# Create state dir if it does not exist.
if not os.path.exists(STATE_DIR):
os.makedirs(STATE_DIR)
state_file = os.path.join(STATE_DIR, "state")
with open(state_file, 'wt') as fp:
fp.write(json.dumps(ssh_data))
os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE)
def monitor_ssh_authorized_keys():
# Decide where to output results.
print_output = False
if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None:
print_output = True
if not ACTIVATED:
if print_output:
print("Module deactivated.")
return
stored_ssh_data = []
curr_ssh_data = []
try:
stored_ssh_data = _load_ssh_data()
curr_ssh_data = _get_system_ssh_data()
except Exception as e:
_output_error(str(e))
return
# Check if any authorized_keys file is world writable.
for curr_entry in curr_ssh_data:
authorized_keys_file = curr_entry["authorized_keys_file"]
file_stat = os.stat(authorized_keys_file)
if file_stat.st_mode & stat.S_IWOTH:
hostname = socket.gethostname()
message = "SSH authorized_keys file for user '%s' is world writable on host '%s'." \
% (curr_entry["user"], hostname)
if print_output:
print(message)
print("#" * 80)
if ALERTR_FIFO:
optional_data = dict()
optional_data["username"] = curr_entry["user"]
optional_data["hostname"] = hostname
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
# Compare stored data with current one.
for stored_entry in stored_ssh_data:
# Extract current entry belonging to the same user.
curr_user_entry = None
for curr_entry in curr_ssh_data:
if stored_entry["user"] == curr_entry["user"]:
curr_user_entry = curr_entry
break
if curr_user_entry is None:
hostname = socket.gethostname()
message = "SSH authorized_keys file for user '%s' was deleted on host '%s'." \
% (stored_entry["user"], hostname)
if print_output:
print(message)
print("#" * 80)
if ALERTR_FIFO:
optional_data = dict()
optional_data["username"] = stored_entry["user"]
optional_data["hostname"] = hostname
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
continue
# Check authorized_keys path has changed.
if stored_entry["authorized_keys_file"] != curr_user_entry["authorized_keys_file"]:
hostname = socket.gethostname()
message = "SSH authorized_keys location for user '%s' changed from '%s' to '%s' on host '%s'." \
% (stored_entry["user"],
stored_entry["authorized_keys_file"],
curr_user_entry["authorized_keys_file"],
hostname)
if print_output:
print(message)
print("#" * 80)
if ALERTR_FIFO:
optional_data = dict()
optional_data["username"] = stored_entry["user"]
optional_data["from"] = stored_entry["authorized_keys_file"]
optional_data["to"] = curr_user_entry["authorized_keys_file"]
optional_data["hostname"] = hostname
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
# Check authorized_key was removed.
for authorized_key in stored_entry["authorized_keys_entries"]:
if authorized_key not in curr_user_entry["authorized_keys_entries"]:
hostname = socket.gethostname()
message = "SSH authorized_keys entry was removed on host '%s'.\n\n" % hostname
message += "Entry: %s" % authorized_key
if print_output:
print(message)
print("#" * 80)
if ALERTR_FIFO:
optional_data = dict()
optional_data["username"] = stored_entry["user"]
optional_data["authorized_keys_entry"] = authorized_key
optional_data["hostname"] = hostname
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
# Check authorized_key was added.
for authorized_key in curr_user_entry["authorized_keys_entries"]:
if authorized_key not in stored_entry["authorized_keys_entries"]:
hostname = socket.gethostname()
message = "SSH authorized_keys entry was added on host '%s'.\n\n" % hostname
message += "Entry: %s" % authorized_key
if print_output:
print(message)
print("#" * 80)
if ALERTR_FIFO:
optional_data = dict()
optional_data["username"] = stored_entry["user"]
optional_data["authorized_keys_entry"] = authorized_key
optional_data["hostname"] = hostname
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
for curr_entry in curr_ssh_data:
found = False
for stored_entry in stored_ssh_data:
if curr_entry["user"] == stored_entry["user"]:
found = True
break
if not found:
hostname = socket.gethostname()
message = "New authorized_keys file was added for user '%s' on host '%s'.\n\n" \
% (curr_entry["user"], hostname)
message += "Entries:\n"
for authorized_key in curr_entry["authorized_keys_entries"]:
message += authorized_key
message += "\n"
if print_output:
print(message)
print("#" * 80)
if ALERTR_FIFO:
optional_data = dict()
optional_data["username"] = curr_entry["user"]
optional_data["authorized_keys_entries"] = curr_entry["authorized_keys_entries"]
optional_data["hostname"] = hostname
optional_data["message"] = message
raise_alert_alertr(ALERTR_FIFO,
optional_data)
if FROM_ADDR is not None and TO_ADDR is not None:
raise_alert_mail(FROM_ADDR,
TO_ADDR,
MAIL_SUBJECT,
message)
try:
_store_ssh_data(curr_ssh_data)
except Exception as e:
_output_error(str(e))
if __name__ == '__main__':
monitor_ssh_authorized_keys()