173 lines
7.1 KiB
Python
173 lines
7.1 KiB
Python
|
#!/usr/bin/python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
#
|
||
|
# Copyright 2014 The Plaso Project Authors.
|
||
|
# Please see the AUTHORS file for details on individual authors.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
"""This file contains the Mac OS X user plist plugin."""
|
||
|
|
||
|
# TODO: Only plists from Mac OS X 10.8 and 10.9 were tested. Look at other
|
||
|
# versions as well.
|
||
|
|
||
|
import binascii
|
||
|
|
||
|
from binplist import binplist
|
||
|
from dfvfs.file_io import fake_file_io
|
||
|
from dfvfs.path import fake_path_spec
|
||
|
from dfvfs.resolver import context
|
||
|
from xml.etree import ElementTree
|
||
|
|
||
|
from plaso.events import plist_event
|
||
|
from plaso.lib import timelib
|
||
|
from plaso.parsers import plist
|
||
|
from plaso.parsers.plist_plugins import interface
|
||
|
|
||
|
|
||
|
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
|
||
|
|
||
|
|
||
|
class MacUserPlugin(interface.PlistPlugin):
|
||
|
"""Basic plugin to extract timestamp Mac user information."""
|
||
|
|
||
|
NAME = 'plist_macuser'
|
||
|
DESCRIPTION = u'Parser for Mac OS X user plist files.'
|
||
|
|
||
|
# The PLIST_PATH is dynamic, "user".plist is the name of the
|
||
|
# Mac OS X user.
|
||
|
PLIST_KEYS = frozenset([
|
||
|
'name', 'uid', 'home',
|
||
|
'passwordpolicyoptions', 'ShadowHashData'])
|
||
|
|
||
|
_ROOT = u'/'
|
||
|
|
||
|
def Process(
|
||
|
self, parser_context, file_entry=None, parser_chain=None, plist_name=None,
|
||
|
top_level=None, **kwargs):
|
||
|
"""Check if it is a valid Mac OS X system account plist file name.
|
||
|
|
||
|
Args:
|
||
|
parser_context: A parser context object (instance of ParserContext).
|
||
|
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
|
||
|
The default is None.
|
||
|
parser_chain: Optional string containing the parsing chain up to this
|
||
|
point. The default is None.
|
||
|
plist_name: name of the plist file.
|
||
|
top_level: dictionary with the plist file parsed.
|
||
|
"""
|
||
|
super(MacUserPlugin, self).Process(
|
||
|
parser_context, file_entry=file_entry, parser_chain=parser_chain,
|
||
|
plist_name=self.PLIST_PATH, top_level=top_level, **kwargs)
|
||
|
|
||
|
# Generated events:
|
||
|
# name: string with the system user.
|
||
|
# uid: user ID.
|
||
|
# passwordpolicyoptions: XML Plist structures with the timestamp.
|
||
|
# passwordLastSetTime: last time the password was changed.
|
||
|
# lastLoginTimestamp: last time the user was authenticated (*).
|
||
|
# failedLoginTimestamp: last time the user passwd was incorrectly(*).
|
||
|
# failedLoginCount: times of incorrect passwords.
|
||
|
# (*): depending on the situation, these timestamps are reset (0 value).
|
||
|
# It is translated by the library as a 2001-01-01 00:00:00 (COCAO
|
||
|
# zero time representation). If this happens, the event is not yield.
|
||
|
|
||
|
def GetEntries(
|
||
|
self, parser_context, file_entry=None, parser_chain=None, match=None,
|
||
|
**unused_kwargs):
|
||
|
"""Extracts relevant user timestamp entries.
|
||
|
|
||
|
Args:
|
||
|
parser_context: A parser context object (instance of ParserContext).
|
||
|
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
|
||
|
The default is None.
|
||
|
parser_chain: Optional string containing the parsing chain up to this
|
||
|
point. The default is None.
|
||
|
match: Optional dictionary containing keys extracted from PLIST_KEYS.
|
||
|
The default is None.
|
||
|
"""
|
||
|
account = match['name'][0]
|
||
|
uid = match['uid'][0]
|
||
|
cocoa_zero = (
|
||
|
timelib.Timestamp.COCOA_TIME_TO_POSIX_BASE *
|
||
|
timelib.Timestamp.MICRO_SECONDS_PER_SECOND)
|
||
|
# INFO: binplist return a string with the Plist XML.
|
||
|
for policy in match['passwordpolicyoptions']:
|
||
|
xml_policy = ElementTree.fromstring(policy)
|
||
|
for dict_elements in xml_policy.iterfind('dict'):
|
||
|
key_values = [value.text for value in dict_elements.getchildren()]
|
||
|
policy_dict = dict(zip(key_values[0::2], key_values[1::2]))
|
||
|
|
||
|
if policy_dict.get('passwordLastSetTime', 0):
|
||
|
timestamp = timelib.Timestamp.FromTimeString(
|
||
|
policy_dict.get('passwordLastSetTime', '0'))
|
||
|
if timestamp > cocoa_zero:
|
||
|
# Extract the hash password information.
|
||
|
# It is store in the attribure ShadowHasData which is
|
||
|
# a binary plist data; However binplist only extract one
|
||
|
# level of binary plist, then it returns this information
|
||
|
# as a string.
|
||
|
|
||
|
# TODO: change this into a DataRange instead. For this we
|
||
|
# need the file offset and size of the ShadowHashData value data.
|
||
|
resolver_context = context.Context()
|
||
|
fake_file = fake_file_io.FakeFile(
|
||
|
resolver_context, match['ShadowHashData'][0])
|
||
|
fake_file.open(path_spec=fake_path_spec.FakePathSpec(
|
||
|
location=u'ShadowHashData'))
|
||
|
|
||
|
try:
|
||
|
plist_file = binplist.BinaryPlist(file_obj=fake_file)
|
||
|
top_level = plist_file.Parse()
|
||
|
except binplist.FormatError:
|
||
|
top_level = dict()
|
||
|
salted_hash = top_level.get('SALTED-SHA512-PBKDF2', None)
|
||
|
if salted_hash:
|
||
|
password_hash = u'$ml${0:d}${1:s}${2:s}'.format(
|
||
|
salted_hash['iterations'],
|
||
|
binascii.hexlify(salted_hash['salt']),
|
||
|
binascii.hexlify(salted_hash['entropy']))
|
||
|
else:
|
||
|
password_hash = u'N/A'
|
||
|
description = (
|
||
|
u'Last time {0:s} ({1!s}) changed the password: {2!s}').format(
|
||
|
account, uid, password_hash)
|
||
|
event_object = plist_event.PlistTimeEvent(
|
||
|
self._ROOT, u'passwordLastSetTime', timestamp, description)
|
||
|
parser_context.ProduceEvent(
|
||
|
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
||
|
|
||
|
if policy_dict.get('lastLoginTimestamp', 0):
|
||
|
timestamp = timelib.Timestamp.FromTimeString(
|
||
|
policy_dict.get('lastLoginTimestamp', '0'))
|
||
|
description = u'Last login from {0:s} ({1!s})'.format(account, uid)
|
||
|
if timestamp > cocoa_zero:
|
||
|
event_object = plist_event.PlistTimeEvent(
|
||
|
self._ROOT, u'lastLoginTimestamp', timestamp, description)
|
||
|
parser_context.ProduceEvent(
|
||
|
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
||
|
|
||
|
if policy_dict.get('failedLoginTimestamp', 0):
|
||
|
timestamp = timelib.Timestamp.FromTimeString(
|
||
|
policy_dict.get('failedLoginTimestamp', '0'))
|
||
|
description = (
|
||
|
u'Last failed login from {0:s} ({1!s}) ({2!s} times)').format(
|
||
|
account, uid, policy_dict['failedLoginCount'])
|
||
|
if timestamp > cocoa_zero:
|
||
|
event_object = plist_event.PlistTimeEvent(
|
||
|
self._ROOT, u'failedLoginTimestamp', timestamp, description)
|
||
|
parser_context.ProduceEvent(
|
||
|
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
||
|
|
||
|
|
||
|
plist.PlistParser.RegisterPlugin(MacUserPlugin)
|