"""Plist parser plugin for MacOS user plist files.
Fields within the plist key:
name: username.
uid: user identifier (UID).
passwordpolicyoptions: XML Plist structures with the timestamp.
passwordLastSetTime: last time the password was changed.
lastLoginTimestamp: last time the user was authenticated depending on
the situation, these timestamps are reset (0 value). It is translated
by the library as a 2001-01-01 00:00:00 (Cocoa zero time representation).
failedLoginTimestamp: last time the login attempt failed.
failedLoginCount: number of failed loging attempts.
"""
# TODO: Only plists from MacOS 10.8 and 10.9 were tested. Look at other
# versions as well.
import codecs
import plistlib
from xml.parsers import expat
from defusedxml import ElementTree
from dfdatetime import time_elements as dfdatetime_time_elements
from plaso.containers import events
from plaso.parsers import logger
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
[docs]
class MacOSUserEventData(events.EventData):
"""MacOS user event data.
Attributes:
fullname (str): full name.
home_directory (str): path of the home directory.
last_login_attempt_time (dfdatetime.DateTimeValues): date and time of
the last (failed) login attempt.
last_login_time (dfdatetime.DateTimeValues): date and time of the last
login.
last_password_set_time (dfdatetime.DateTimeValues): date and time of the
last password set.
number_of_failed_login_attempts (str): number of failed login attempts.
password_hash (str): password hash.
user_identifier (str): user identifier.
username (str): username.
"""
DATA_TYPE = "macos:user:entry"
[docs]
def __init__(self):
"""Initializes event data."""
super().__init__(data_type=self.DATA_TYPE)
self.fullname = None
self.home_directory = None
self.last_login_attempt_time = None
self.last_login_time = None
self.last_password_set_time = None
self.number_of_failed_login_attempts = None
self.password_hash = None
self.user_identifier = None
self.username = None
[docs]
class MacOSUserPlistPlugin(interface.PlistPlugin):
"""Plist parser plugin for MacOS user plist files."""
NAME = "macuser"
DATA_FORMAT = "MacOS user plist file"
# The PLIST_PATH is dynamic, "user".plist is the name of the
# MacOS user.
PLIST_KEYS = frozenset(
["name", "uid", "home", "passwordpolicyoptions", "ShadowHashData"]
)
_ROOT = "/"
def _GetDateTimeValueFromTimeString(
self, parser_mediator, policy_values, value_name
):
"""Retrieves a date and time value from a time string value.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
policy_values (dict[str, object]): policy values.
value_name (str): name of the value.
Returns:
dfdatetime.TimeElements: date and time or None if not available.
"""
time_string = policy_values.get(value_name)
if not time_string or time_string == "2001-01-01T00:00:00Z":
return None
date_time = dfdatetime_time_elements.TimeElements()
try:
date_time.CopyFromStringISO8601(time_string)
except (TypeError, ValueError):
parser_mediator.ProduceExtractionWarning(
f"unable to parse value: {value_name:s} time string: {time_string!s}"
)
return None
return date_time
# pylint: disable=arguments-differ
def _ParsePlist(self, parser_mediator, match=None, top_level=None, **unused_kwargs):
"""Extracts relevant user timestamp entries.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
match (Optional[dict[str: object]]): keys extracted from PLIST_KEYS.
top_level (Optional[dict[str, object]]): plist top-level item.
"""
if "name" not in match or "uid" not in match:
return
password_hash = None
user_identifier = match["uid"][0]
username = match["name"][0]
shadow_hash_data = match.get("ShadowHashData")
if isinstance(shadow_hash_data, (list, tuple)):
# Extract the hash password information, which is stored in
# the attribute ShadowHashData which is a binary plist data.
try:
property_list = plistlib.loads(shadow_hash_data[0])
except plistlib.InvalidFileException as exception:
parser_mediator.ProduceExtractionWarning(
f"unable to parse ShadowHashData with error: {exception!s}"
)
property_list = {}
salted_hash = property_list.get("SALTED-SHA512-PBKDF2")
if salted_hash:
salt_hex_bytes = codecs.encode(salted_hash["salt"], "hex")
salt_string = codecs.decode(salt_hex_bytes, "ascii")
entropy_hex_bytes = codecs.encode(salted_hash["entropy"], "hex")
entropy_string = codecs.decode(entropy_hex_bytes, "ascii")
number_of_iterations = salted_hash["iterations"]
password_hash = (
f"$ml${number_of_iterations:d}${salt_string:s}${entropy_string:s}"
)
for policy in match.get("passwordpolicyoptions", []):
try:
xml_policy = ElementTree.fromstring(policy)
except (LookupError, ElementTree.ParseError, expat.ExpatError) as exception:
logger.error(
f"Unable to parse XML structure for an user policy, username: "
f"{username:s} and UID: {user_identifier!s}, with error: "
f"{exception!s}"
)
continue
for dict_elements in xml_policy.iterfind("dict"):
key_values = [value.text for value in dict_elements]
# Taking a list and converting it to a dict, using every other item
# as the key and the other one as the value.
policy_dict = dict(zip(key_values[0::2], key_values[1::2]))
event_data = MacOSUserEventData()
event_data.fullname = top_level.get("realname", [None])[0]
event_data.home_directory = top_level.get("home", [None])[0]
event_data.last_login_attempt_time = self._GetDateTimeValueFromTimeString(
parser_mediator, policy_dict, "failedLoginTimestamp"
)
event_data.last_login_time = self._GetDateTimeValueFromTimeString(
parser_mediator, policy_dict, "lastLoginTimestamp"
)
event_data.last_password_set_time = self._GetDateTimeValueFromTimeString(
parser_mediator, policy_dict, "passwordLastSetTime"
)
event_data.number_of_failed_login_attempts = policy_dict.get(
"failedLoginCount", None
)
event_data.password_hash = password_hash
event_data.user_identifier = user_identifier
event_data.username = username
parser_mediator.ProduceEventData(event_data)
plist.PlistParser.RegisterPlugin(MacOSUserPlistPlugin)