1#!/usr/bin/env python
2
3# NOTE: This script is only tested with Python 3.6, Python 2.7, and Python
4# 2.6.9. It cannot work with Python 2.5.x.
5#
6# This script was previously called "ml2john.py".
7
8# Start of library code
9
10"""
11biplist is under BSD license
12
13Copyright (c) 2010, Andrew Wooster
14All rights reserved.
15
16Redistribution and use in source and binary forms, with or without
17modification, are permitted provided that the following conditions are met:
18
19    * Redistributions of source code must retain the above copyright notice,
20      this list of conditions and the following disclaimer.
21    * Redistributions in binary form must reproduce the above copyright
22      notice, this list of conditions and the following disclaimer in the
23      documentation and/or other materials provided with the distribution.
24    * Neither the name of biplist nor the names of its contributors may be
25      used to endorse or promote products derived from this software without
26      specific prior written permission.
27
28THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE"""
38
39"""biplist -- a library for reading and writing binary property list files.
40
41Binary Property List (plist) files provide a faster and smaller serialization
42format for property lists on OS X. This is a library for generating binary
43plists which can be read by OS X, iOS, or other clients.
44
45The API models the plistlib API, and will call through to plistlib when
46XML serialization or deserialization is required.
47
48To generate plists with UID values, wrap the values with the Uid object. The
49value must be an int.
50
51To generate plists with NSData/CFData values, wrap the values with the
52Data object. The value must be a string.
53
54Date values can only be datetime.datetime objects.
55
56The exceptions InvalidPlistException and NotBinaryPlistException may be
57thrown to indicate that the data cannot be serialized or deserialized as
58a binary plist.
59
60Plist generation example:
61
62    from biplist import *
63    from datetime import datetime
64    plist = {'aKey':'aValue',
65             '0':1.322,
66             'now':datetime.now(),
67             'list':[1,2,3],
68             'tuple':('a','b','c')
69             }
70    try:
71        writePlist(plist, "example.plist")
72    except (InvalidPlistException, NotBinaryPlistException), e:
73        print "Something bad happened:", e
74
75Plist parsing example:
76
77    from biplist import *
78    try:
79        plist = readPlist("example.plist")
80        print plist
81    except (InvalidPlistException, NotBinaryPlistException), e:
82        print "Not a plist:", e
83"""
84
85from collections import namedtuple
86import datetime
87import io
88import math
89import plistlib
90from struct import pack, unpack, unpack_from
91import sys
92import time
93
94try:
95    unicode
96    unicodeEmpty = r''
97except NameError:
98    unicode = str
99    unicodeEmpty = ''
100try:
101    long
102except NameError:
103    long = int
104try:
105    {}.iteritems
106    iteritems = lambda x: x.iteritems()
107except AttributeError:
108    iteritems = lambda x: x.items()
109
110__all__ = [
111    'Uid', 'Data', 'readPlist', 'writePlist', 'readPlistFromString',
112    'writePlistToString', 'InvalidPlistException', 'NotBinaryPlistException'
113]
114
115# Apple uses Jan 1, 2001 as a base for all plist date/times.
116apple_reference_date = datetime.datetime.utcfromtimestamp(978307200)
117
118class Uid(object):
119    """Wrapper around integers for representing UID values. This
120       is used in keyed archiving."""
121    integer = 0
122    def __init__(self, integer):
123        self.integer = integer
124
125    def __repr__(self):
126        return "Uid(%d)" % self.integer
127
128    def __eq__(self, other):
129        if isinstance(self, Uid) and isinstance(other, Uid):
130            return self.integer == other.integer
131        return False
132
133    def __cmp__(self, other):
134        return self.integer - other.integer
135
136    def __lt__(self, other):
137        return self.integer < other.integer
138
139    def __hash__(self):
140        return self.integer
141
142    def __int__(self):
143        return int(self.integer)
144
145class Data(bytes):
146    """Wrapper around bytes to distinguish Data values."""
147
148class InvalidPlistException(Exception):
149    """Raised when the plist is incorrectly formatted."""
150
151class NotBinaryPlistException(Exception):
152    """Raised when a binary plist was expected but not encountered."""
153
154def readPlist(pathOrFile):
155    """Raises NotBinaryPlistException, InvalidPlistException"""
156    didOpen = False
157    result = None
158    if isinstance(pathOrFile, (bytes, unicode)):
159        pathOrFile = open(pathOrFile, 'rb')
160        didOpen = True
161    try:
162        reader = PlistReader(pathOrFile)
163        result = reader.parse()
164    except NotBinaryPlistException as e:
165        try:
166            pathOrFile.seek(0)
167            result = None
168            if hasattr(plistlib, 'loads'):
169                contents = None
170                if isinstance(pathOrFile, (bytes, unicode)):
171                    with open(pathOrFile, 'rb') as f:
172                        contents = f.read()
173                else:
174                    contents = pathOrFile.read()
175                result = plistlib.loads(contents)
176            else:
177                result = plistlib.readPlist(pathOrFile)
178            result = wrapDataObject(result, for_binary=True)
179        except Exception as e:
180            raise InvalidPlistException(e)
181    finally:
182        if didOpen:
183            pathOrFile.close()
184    return result
185
186def wrapDataObject(o, for_binary=False):
187    if isinstance(o, Data) and not for_binary:
188        v = sys.version_info
189        if not (v[0] >= 3 and v[1] >= 4):
190            o = plistlib.Data(o)
191    elif isinstance(o, (bytes, plistlib.Data)) and for_binary:
192        if hasattr(o, 'data'):
193            o = Data(o.data)
194    elif isinstance(o, tuple):
195        o = wrapDataObject(list(o), for_binary)
196        o = tuple(o)
197    elif isinstance(o, list):
198        for i in range(len(o)):
199            o[i] = wrapDataObject(o[i], for_binary)
200    elif isinstance(o, dict):
201        for k in o:
202            o[k] = wrapDataObject(o[k], for_binary)
203    return o
204
205def readPlistFromString(data):
206    return readPlist(io.BytesIO(data))
207
208def is_stream_binary_plist(stream):
209    stream.seek(0)
210    header = stream.read(7)
211    if header == b'bplist0':
212        return True
213    else:
214        return False
215
216PlistTrailer = namedtuple('PlistTrailer', 'offsetSize, objectRefSize, offsetCount, topLevelObjectNumber, offsetTableOffset')
217PlistByteCounts = namedtuple('PlistByteCounts', 'nullBytes, boolBytes, intBytes, realBytes, dateBytes, dataBytes, stringBytes, uidBytes, arrayBytes, setBytes, dictBytes')
218
219class PlistReader(object):
220    file = None
221    contents = ''
222    offsets = None
223    trailer = None
224    currentOffset = 0
225    # Used to detect recursive object references.
226    offsetsStack = []
227
228    def __init__(self, fileOrStream):
229        """Raises NotBinaryPlistException."""
230        self.reset()
231        self.file = fileOrStream
232
233    def parse(self):
234        return self.readRoot()
235
236    def reset(self):
237        self.trailer = None
238        self.contents = ''
239        self.offsets = []
240        self.currentOffset = 0
241        self.offsetsStack = []
242
243    def readRoot(self):
244        result = None
245        self.reset()
246        # Get the header, make sure it's a valid file.
247        if not is_stream_binary_plist(self.file):
248            raise NotBinaryPlistException()
249        self.file.seek(0)
250        self.contents = self.file.read()
251        if len(self.contents) < 32:
252            raise InvalidPlistException("File is too short.")
253        trailerContents = self.contents[-32:]
254        try:
255            self.trailer = PlistTrailer._make(unpack("!xxxxxxBBQQQ", trailerContents))
256
257            if pow(2, self.trailer.offsetSize*8) < self.trailer.offsetTableOffset:
258                raise InvalidPlistException("Offset size insufficient to reference all objects.")
259
260            if pow(2, self.trailer.objectRefSize*8) < self.trailer.offsetCount:
261                raise InvalidPlistException("Too many offsets to represent in size of object reference representation.")
262
263            offset_size = self.trailer.offsetSize * self.trailer.offsetCount
264            offset = self.trailer.offsetTableOffset
265
266            if offset + offset_size > pow(2, 64):
267                raise InvalidPlistException("Offset table is excessively long.")
268
269            if self.trailer.offsetSize > 16:
270                raise InvalidPlistException("Offset size is greater than maximum integer size.")
271
272            if self.trailer.objectRefSize == 0:
273                raise InvalidPlistException("Object reference size is zero.")
274
275            if offset >= len(self.contents) - 32:
276                raise InvalidPlistException("Offset table offset is too large.")
277
278            if offset < len("bplist00x"):
279                raise InvalidPlistException("Offset table offset is too small.")
280
281            if self.trailer.topLevelObjectNumber >= self.trailer.offsetCount:
282                raise InvalidPlistException("Top level object number is larger than the number of objects.")
283
284            offset_contents = self.contents[offset:offset+offset_size]
285            offset_i = 0
286            offset_table_length = len(offset_contents)
287
288            while offset_i < self.trailer.offsetCount:
289                begin = self.trailer.offsetSize*offset_i
290                end = begin+self.trailer.offsetSize
291                if end > offset_table_length:
292                    raise InvalidPlistException("End of object is at invalid offset %d in offset table of length %d" % (end, offset_table_length))
293                tmp_contents = offset_contents[begin:end]
294                tmp_sized = self.getSizedInteger(tmp_contents, self.trailer.offsetSize)
295                self.offsets.append(tmp_sized)
296                offset_i += 1
297            self.setCurrentOffsetToObjectNumber(self.trailer.topLevelObjectNumber)
298            result = self.readObject()
299        except TypeError as e:
300            raise InvalidPlistException(e)
301        return result
302
303    def setCurrentOffsetToObjectNumber(self, objectNumber):
304        if objectNumber > len(self.offsets) - 1:
305            raise InvalidPlistException("Invalid offset number: %d" % objectNumber)
306        self.currentOffset = self.offsets[objectNumber]
307        if self.currentOffset in self.offsetsStack:
308            raise InvalidPlistException("Recursive data structure detected in object: %d" % objectNumber)
309
310    def beginOffsetProtection(self):
311        self.offsetsStack.append(self.currentOffset)
312        return self.currentOffset
313
314    def endOffsetProtection(self, offset):
315        try:
316            index = self.offsetsStack.index(offset)
317            self.offsetsStack = self.offsetsStack[:index]
318        except ValueError as e:
319            pass
320
321    def readObject(self):
322        protection = self.beginOffsetProtection()
323        result = None
324        tmp_byte = self.contents[self.currentOffset:self.currentOffset+1]
325        if len(tmp_byte) != 1:
326            raise InvalidPlistException("No object found at offset: %d" % self.currentOffset)
327        marker_byte = unpack("!B", tmp_byte)[0]
328        format = (marker_byte >> 4) & 0x0f
329        extra = marker_byte & 0x0f
330        self.currentOffset += 1
331
332        def proc_extra(extra):
333            if extra == 0b1111:
334                extra = self.readObject()
335            return extra
336
337        # bool, null, or fill byte
338        if format == 0b0000:
339            if extra == 0b0000:
340                result = None
341            elif extra == 0b1000:
342                result = False
343            elif extra == 0b1001:
344                result = True
345            elif extra == 0b1111:
346                pass # fill byte
347            else:
348                raise InvalidPlistException("Invalid object found at offset: %d" % (self.currentOffset - 1))
349        # int
350        elif format == 0b0001:
351            result = self.readInteger(pow(2, extra))
352        # real
353        elif format == 0b0010:
354            result = self.readReal(extra)
355        # date
356        elif format == 0b0011 and extra == 0b0011:
357            result = self.readDate()
358        # data
359        elif format == 0b0100:
360            extra = proc_extra(extra)
361            result = self.readData(extra)
362        # ascii string
363        elif format == 0b0101:
364            extra = proc_extra(extra)
365            result = self.readAsciiString(extra)
366        # Unicode string
367        elif format == 0b0110:
368            extra = proc_extra(extra)
369            result = self.readUnicode(extra)
370        # uid
371        elif format == 0b1000:
372            result = self.readUid(extra)
373        # array
374        elif format == 0b1010:
375            extra = proc_extra(extra)
376            result = self.readArray(extra)
377        # set
378        elif format == 0b1100:
379            extra = proc_extra(extra)
380            result = set(self.readArray(extra))
381        # dict
382        elif format == 0b1101:
383            extra = proc_extra(extra)
384            result = self.readDict(extra)
385        else:
386            raise InvalidPlistException("Invalid object found: {format: %s, extra: %s}" % (bin(format), bin(extra)))
387        self.endOffsetProtection(protection)
388        return result
389
390    def readContents(self, length, description="Object contents"):
391        end = self.currentOffset + length
392        if end >= len(self.contents) - 32:
393            raise InvalidPlistException("%s extends into trailer" % description)
394        elif length < 0:
395            raise InvalidPlistException("%s length is less than zero" % length)
396        data = self.contents[self.currentOffset:end]
397        return data
398
399    def readInteger(self, byteSize):
400        data = self.readContents(byteSize, "Integer")
401        self.currentOffset = self.currentOffset + byteSize
402        return self.getSizedInteger(data, byteSize, as_number=True)
403
404    def readReal(self, length):
405        to_read = pow(2, length)
406        data = self.readContents(to_read, "Real")
407        if length == 2: # 4 bytes
408            result = unpack('>f', data)[0]
409        elif length == 3: # 8 bytes
410            result = unpack('>d', data)[0]
411        else:
412            raise InvalidPlistException("Unknown Real of length %d bytes" % to_read)
413        return result
414
415    def readRefs(self, count):
416        refs = []
417        i = 0
418        while i < count:
419            fragment = self.readContents(self.trailer.objectRefSize, "Object reference")
420            ref = self.getSizedInteger(fragment, len(fragment))
421            refs.append(ref)
422            self.currentOffset += self.trailer.objectRefSize
423            i += 1
424        return refs
425
426    def readArray(self, count):
427        if not isinstance(count, (int, long)):
428            raise InvalidPlistException("Count of entries in dict isn't of integer type.")
429        result = []
430        values = self.readRefs(count)
431        i = 0
432        while i < len(values):
433            self.setCurrentOffsetToObjectNumber(values[i])
434            value = self.readObject()
435            result.append(value)
436            i += 1
437        return result
438
439    def readDict(self, count):
440        if not isinstance(count, (int, long)):
441            raise InvalidPlistException("Count of keys/values in dict isn't of integer type.")
442        result = {}
443        keys = self.readRefs(count)
444        values = self.readRefs(count)
445        i = 0
446        while i < len(keys):
447            self.setCurrentOffsetToObjectNumber(keys[i])
448            key = self.readObject()
449            self.setCurrentOffsetToObjectNumber(values[i])
450            value = self.readObject()
451            result[key] = value
452            i += 1
453        return result
454
455    def readAsciiString(self, length):
456        if not isinstance(length, (int, long)):
457            raise InvalidPlistException("Length of ASCII string isn't of integer type.")
458        data = self.readContents(length, "ASCII string")
459        result = unpack("!%ds" % length, data)[0]
460        self.currentOffset += length
461        return str(result.decode('ascii'))
462
463    def readUnicode(self, length):
464        if not isinstance(length, (int, long)):
465            raise InvalidPlistException("Length of Unicode string isn't of integer type.")
466        actual_length = length*2
467        data = self.readContents(actual_length, "Unicode string")
468        self.currentOffset += actual_length
469        return data.decode('utf_16_be')
470
471    def readDate(self):
472        data = self.readContents(8, "Date")
473        x = unpack(">d", data)[0]
474        if math.isnan(x):
475            raise InvalidPlistException("Date is NaN")
476        # Use timedelta to workaround time_t size limitation on 32-bit python.
477        try:
478            result = datetime.timedelta(seconds=x) + apple_reference_date
479        except OverflowError:
480            if x > 0:
481                result = datetime.datetime.max
482            else:
483                result = datetime.datetime.min
484        self.currentOffset += 8
485        return result
486
487    def readData(self, length):
488        if not isinstance(length, (int, long)):
489            raise InvalidPlistException("Length of data isn't of integer type.")
490        result = self.readContents(length, "Data")
491        self.currentOffset += length
492        return Data(result)
493
494    def readUid(self, length):
495        if not isinstance(length, (int, long)):
496            raise InvalidPlistException("Uid length isn't of integer type.")
497        return Uid(self.readInteger(length+1))
498
499    def getSizedInteger(self, data, byteSize, as_number=False):
500        """Numbers of 8 bytes are signed integers when they refer to numbers, but unsigned otherwise."""
501        result = 0
502        if byteSize == 0:
503            raise InvalidPlistException("Encountered integer with byte size of 0.")
504        # 1, 2, and 4 byte integers are unsigned
505        elif byteSize == 1:
506            result = unpack('>B', data)[0]
507        elif byteSize == 2:
508            result = unpack('>H', data)[0]
509        elif byteSize == 4:
510            result = unpack('>L', data)[0]
511        elif byteSize == 8:
512            if as_number:
513                result = unpack('>q', data)[0]
514            else:
515                result = unpack('>Q', data)[0]
516        elif byteSize <= 16:
517            # Handle odd-sized or integers larger than 8 bytes
518            # Don't naively go over 16 bytes, in order to prevent infinite loops.
519            result = 0
520            if hasattr(int, 'from_bytes'):
521                result = int.from_bytes(data, 'big')
522            else:
523                for byte in data:
524                    if not isinstance(byte, int): # Python3.0-3.1.x return ints, 2.x return str
525                        byte = unpack_from('>B', byte)[0]
526                    result = (result << 8) | byte
527        else:
528            raise InvalidPlistException("Encountered integer longer than 16 bytes.")
529        return result
530
531class HashableWrapper(object):
532    def __init__(self, value):
533        self.value = value
534    def __repr__(self):
535        return "<HashableWrapper: %s>" % [self.value]
536
537class BoolWrapper(object):
538    def __init__(self, value):
539        self.value = value
540    def __repr__(self):
541        return "<BoolWrapper: %s>" % self.value
542
543class FloatWrapper(object):
544    _instances = {}
545    def __new__(klass, value):
546        # Ensure FloatWrapper(x) for a given float x is always the same object
547        wrapper = klass._instances.get(value)
548        if wrapper is None:
549            wrapper = object.__new__(klass)
550            wrapper.value = value
551            klass._instances[value] = wrapper
552        return wrapper
553    def __repr__(self):
554        return "<FloatWrapper: %s>" % self.value
555
556class StringWrapper(object):
557    __instances = {}
558
559    encodedValue = None
560    encoding = None
561
562    def __new__(cls, value):
563        '''Ensure we only have a only one instance for any string,
564         and that we encode ascii as 1-byte-per character when possible'''
565
566        encodedValue = None
567
568        for encoding in ('ascii', 'utf_16_be'):
569            try:
570               encodedValue = value.encode(encoding)
571            except: pass
572            if encodedValue is not None:
573                if encodedValue not in cls.__instances:
574                    cls.__instances[encodedValue] = super(StringWrapper, cls).__new__(cls)
575                    cls.__instances[encodedValue].encodedValue = encodedValue
576                    cls.__instances[encodedValue].encoding = encoding
577                return cls.__instances[encodedValue]
578
579        raise ValueError('Unable to get ascii or utf_16_be encoding for %s' % repr(value))
580
581    def __len__(self):
582        '''Return roughly the number of characters in this string (half the byte length)'''
583        if self.encoding == 'ascii':
584            return len(self.encodedValue)
585        else:
586            return len(self.encodedValue)//2
587
588    def __lt__(self, other):
589        return self.encodedValue < other.encodedValue
590
591    @property
592    def encodingMarker(self):
593        if self.encoding == 'ascii':
594            return 0b0101
595        else:
596            return 0b0110
597
598    def __repr__(self):
599        return '<StringWrapper (%s): %s>' % (self.encoding, self.encodedValue)
600
601
602# Library code ends here, program starts
603#
604# Written by Dhiru Kholia <dhiru at openwall.com> in September of 2012.
605#
606# My code is under "Simplified BSD License".
607#
608# Redistribution and use in source and binary forms, with or without
609# modification, are permitted.
610
611import sys
612import base64
613import binascii
614try:
615    from StringIO import StringIO
616except ImportError:
617    from io import BytesIO as StringIO
618
619PY3 = sys.version_info[0] == 3
620
621
622def process_file(filename):
623    try:
624        p1 = readPlist(filename)
625    except IOError as e:
626        print("%s : %s" % (filename, str(e)))
627        return False
628    except (InvalidPlistException, NotBinaryPlistException):
629        print("%s is not a plist file!" % filename)
630        return False
631
632    s = p1.get('ShadowHashData', [None])[0]
633    if not s:
634        # Process the "preprocessed" output XMLs generated by "plutil" command.
635        #
636        # Example: sudo defaults read /var/db/dslocal/nodes/Default/users/<username>.plist ShadowHashData | tr -dc 0-9a-f | xxd -r -p | plutil -convert xml1 - -o -
637        p2 = p1
638    else:
639        # Handle regular binary plist files and default XML output of "plutil
640        # -convert xml1 username.plist".
641        s = StringIO(s)
642        if not s:
643            print("%s: could not find ShadowHashData" % filename)
644            return -2
645        try:
646            p2 = readPlist(s)
647        except Exception:
648            e = sys.exc_info()[1]
649            sys.stderr.write("%s : %s\n" % (filename, str(e)))
650            return -3
651
652    d = p2.get('SALTED-SHA512-PBKDF2', None)
653    if not d:
654        sys.stderr.write("%s does not contain SALTED-SHA512-PBKDF2 hashes\n" % filename)
655        return -4
656
657    salt = d.get('salt')
658    entropy = d.get('entropy')
659    iterations = d.get('iterations')
660
661    salth = binascii.hexlify(salt)
662    entropyh = binascii.hexlify(entropy)
663
664    if PY3:
665        salth = salth.decode("ascii")
666        entropyh = entropyh.decode("ascii")
667
668    hints = ""
669    hl = p1.get('realname', []) + p1.get('hint', [])
670    hints += ",".join(hl)
671    uid = p1.get('uid', ["500"])[0]
672    gid = p1.get('gid', ["500"])[0]
673    shell = p1.get('shell', ["bash"])[0]
674    name = p1.get('name', ["user"])[0]
675
676    sys.stdout.write("%s:$pbkdf2-hmac-sha512$%d.%s.%s:%s:%s:%s:%s:%s\n" % \
677            (name, iterations, salth, entropyh[0:128], uid, gid, hints,
678             shell, filename))
679
680    # from passlib.hash import grub_pbkdf2_sha512
681    # hash = grub_pbkdf2_sha512.encrypt("password", rounds=iterations, salt=salt)
682    # print(hash)
683
684if __name__ == "__main__":
685    if len(sys.argv) < 2:
686        print("This program helps in extracting password hashes from OS X / macOS systems (>= Mountain Lion -> 10.8+).\n")
687        print("Run this program against .plist file(s) obtained from /var/db/dslocal/nodes/Default/users/<username>.plist location.\n")
688        print("Usage: %s <OS X / macOS .plist files>" % sys.argv[0])
689        sys.exit(-1)
690
691    for i in range(1, len(sys.argv)):
692        process_file(sys.argv[i])
693