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