|
80 | 80 | from lib.core.exception import SqlmapDataException |
81 | 81 | from lib.core.exception import SqlmapUserQuitException |
82 | 82 | from lib.core.settings import COMMON_PASSWORD_SUFFIXES |
| 83 | +from lib.core.settings import COMMON_PASSWORDS |
83 | 84 | from lib.core.settings import COMMON_USER_COLUMNS |
84 | 85 | from lib.core.settings import DEV_EMAIL_ADDRESS |
85 | 86 | from lib.core.settings import DUMMY_USER_PREFIX |
| 87 | +from lib.core.settings import HASH_ATTACK_TIME_LIMIT |
86 | 88 | from lib.core.settings import HASH_BINARY_COLUMNS_REGEX |
87 | 89 | from lib.core.settings import HASH_EMPTY_PASSWORD_MARKER |
88 | 90 | from lib.core.settings import HASH_MOD_ITEM_DISPLAY |
@@ -807,9 +809,40 @@ def _encode64(input_, count): |
807 | 809 | if _scrypt is not None: |
808 | 810 | __functions__[HASH.WERKZEUG_SCRYPT] = werkzeug_scrypt_passwd |
809 | 811 |
|
810 | | -# Recognized-only formats with no pure-Python/stdlib crack path; identified and pointed to dedicated tools |
811 | | -HASH_TOOL_HINTS = { |
812 | | - HASH.ARGON2: "an Argon2 hash (e.g. 'hashcat -m 34000' or 'john --format=argon2')", |
| 812 | +# hashcat '-m' mode per recognized hash format (Reference: https://hashcat.net/wiki/doku.php?id=example_hashes); |
| 813 | +# used to point the user at the right command when hashes are stored or cannot be cracked in pure Python |
| 814 | +# (e.g. Argon2). Only well-established modes are listed - a format left out just gets the generic hint |
| 815 | +HASHCAT_MODES = { |
| 816 | + HASH.ARGON2: 34000, |
| 817 | + HASH.MYSQL_OLD: 200, |
| 818 | + HASH.MYSQL: 300, |
| 819 | + HASH.POSTGRES: 12, |
| 820 | + HASH.MSSQL_OLD: 131, |
| 821 | + HASH.MSSQL: 132, |
| 822 | + HASH.MSSQL_NEW: 1731, |
| 823 | + HASH.ORACLE_OLD: 3100, |
| 824 | + HASH.ORACLE: 112, |
| 825 | + HASH.ORACLE_12C: 12300, |
| 826 | + HASH.MD5_GENERIC: 0, |
| 827 | + HASH.SHA1_GENERIC: 100, |
| 828 | + HASH.SHA224_GENERIC: 1300, |
| 829 | + HASH.SHA256_GENERIC: 1400, |
| 830 | + HASH.SHA384_GENERIC: 10800, |
| 831 | + HASH.SHA512_GENERIC: 1700, |
| 832 | + HASH.CRYPT_GENERIC: 1500, |
| 833 | + HASH.APACHE_SHA1: 101, |
| 834 | + HASH.SSHA: 111, |
| 835 | + HASH.SSHA256: 1411, |
| 836 | + HASH.SSHA512: 1711, |
| 837 | + HASH.UNIX_MD5_CRYPT: 500, |
| 838 | + HASH.APACHE_MD5_CRYPT: 1600, |
| 839 | + HASH.SHA256_UNIX_CRYPT: 7400, |
| 840 | + HASH.SHA512_UNIX_CRYPT: 1800, |
| 841 | + HASH.BCRYPT: 3200, |
| 842 | + HASH.PHPASS: 400, |
| 843 | + HASH.VBULLETIN: 2611, |
| 844 | + HASH.DJANGO_SHA1: 124, |
| 845 | + HASH.DJANGO_PBKDF2_SHA256: 10000, |
813 | 846 | } |
814 | 847 |
|
815 | 848 | def _finalize(retVal, results, processes, attack_info=None): |
@@ -852,12 +885,18 @@ def storeHashesToFile(attack_dict): |
852 | 885 | return |
853 | 886 |
|
854 | 887 | items = OrderedSet() |
| 888 | + regexes = set() |
855 | 889 |
|
856 | 890 | for user, hashes in attack_dict.items(): |
857 | 891 | for hash_ in hashes: |
858 | 892 | hash_ = hash_.split()[0] if hash_ and hash_.strip() else hash_ |
859 | | - if hash_ and hash_ != NULL and hashRecognition(hash_): |
860 | | - item = None |
| 893 | + if hash_ and hash_ != NULL: |
| 894 | + regex = hashRecognition(hash_) |
| 895 | + if not regex: |
| 896 | + continue |
| 897 | + |
| 898 | + regexes.add(regex) |
| 899 | + |
861 | 900 | if user and not user.startswith(DUMMY_USER_PREFIX): |
862 | 901 | item = "%s:%s\n" % (user, hash_) |
863 | 902 | else: |
@@ -886,6 +925,12 @@ def storeHashesToFile(attack_dict): |
886 | 925 | except (UnicodeError, TypeError): |
887 | 926 | pass |
888 | 927 |
|
| 928 | + modes = sorted(set(HASHCAT_MODES[_] for _ in regexes if _ in HASHCAT_MODES)) |
| 929 | + if modes: |
| 930 | + infoMsg = "the stored hashes can be cracked with a dedicated tool " |
| 931 | + infoMsg += "(e.g. %s)" % ", ".join("'hashcat -m %d'" % _ for _ in modes) |
| 932 | + logger.info(infoMsg) |
| 933 | + |
889 | 934 | def attackCachedUsersPasswords(): |
890 | 935 | if kb.data.cachedUsersPasswords: |
891 | 936 | results = dictionaryAttack(kb.data.cachedUsersPasswords) |
@@ -1204,6 +1249,90 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found |
1204 | 1249 | with proc_count.get_lock(): |
1205 | 1250 | proc_count.value -= 1 |
1206 | 1251 |
|
| 1252 | +def _bruteProcessVariantSalted(attack_info, hash_regex, suffix, retVal, proc_id, proc_count, wordlists, custom_wordlist, api, deadline): |
| 1253 | + # Candidate-major crack for the very slow, per-hash-salted algorithms (bcrypt): the OUTER loop is the |
| 1254 | + # candidate word (partitioned across processes) and the INNER loop is every still-unsolved hash, each |
| 1255 | + # verified with its own salt. Trying the most common passwords against ALL hashes first means a weak |
| 1256 | + # account anywhere in a dumped table is found in the first rounds - a hash-major loop would instead grind |
| 1257 | + # the whole wordlist on row 1 before ever testing row 2. Bounded by `deadline` so hundreds of separately |
| 1258 | + # salted hashes can't become an hours-long run; whatever is left is meant for a dedicated tool. |
| 1259 | + if IS_WIN: |
| 1260 | + coloramainit() |
| 1261 | + |
| 1262 | + count = 0 |
| 1263 | + rotator = 0 |
| 1264 | + remaining = attack_info[:] # per-process (post-fork) copy; shrinks as this process solves hashes |
| 1265 | + |
| 1266 | + wordlist = Wordlist(wordlists, proc_id, getattr(proc_count, "value", 0), custom_wordlist) |
| 1267 | + |
| 1268 | + try: |
| 1269 | + for word in wordlist: |
| 1270 | + if not remaining or time.time() > deadline: |
| 1271 | + break |
| 1272 | + |
| 1273 | + count += 1 |
| 1274 | + |
| 1275 | + if isinstance(word, six.binary_type): |
| 1276 | + word = getUnicode(word) |
| 1277 | + elif not isinstance(word, six.string_types): |
| 1278 | + continue |
| 1279 | + |
| 1280 | + if suffix: |
| 1281 | + word = word + suffix |
| 1282 | + |
| 1283 | + for item in remaining[:]: |
| 1284 | + ((user, hash_), kwargs) = item |
| 1285 | + |
| 1286 | + try: |
| 1287 | + current = __functions__[hash_regex](password=word, uppercase=False, **kwargs) |
| 1288 | + |
| 1289 | + if hash_ == current: |
| 1290 | + retVal.put((user, hash_, word)) |
| 1291 | + |
| 1292 | + clearConsoleLine() |
| 1293 | + |
| 1294 | + infoMsg = "\r[%s] [INFO] cracked password '%s'" % (time.strftime("%X"), word) |
| 1295 | + |
| 1296 | + if user and not user.startswith(DUMMY_USER_PREFIX): |
| 1297 | + infoMsg += " for user '%s'\n" % user |
| 1298 | + else: |
| 1299 | + infoMsg += " for hash '%s'\n" % hash_ |
| 1300 | + |
| 1301 | + dataToStdout(infoMsg, True) |
| 1302 | + |
| 1303 | + remaining.remove(item) |
| 1304 | + |
| 1305 | + except KeyboardInterrupt: |
| 1306 | + raise |
| 1307 | + |
| 1308 | + except (UnicodeEncodeError, UnicodeDecodeError): |
| 1309 | + pass # ignore possible encoding problems caused by some words in custom dictionaries |
| 1310 | + |
| 1311 | + except Exception as ex: |
| 1312 | + warnMsg = "there was a problem while hashing entry: %s ('%s'). " % (repr(word), getSafeExString(ex)) |
| 1313 | + warnMsg += "Please report by e-mail to '%s'" % DEV_EMAIL_ADDRESS |
| 1314 | + logger.critical(warnMsg) |
| 1315 | + |
| 1316 | + if (proc_id == 0 or getattr(proc_count, "value", 0) == 1) and count % HASH_MOD_ITEM_DISPLAY == 0: |
| 1317 | + rotator += 1 |
| 1318 | + |
| 1319 | + if rotator >= len(ROTATING_CHARS): |
| 1320 | + rotator = 0 |
| 1321 | + |
| 1322 | + status = "current status: %s... %s" % (word.ljust(5)[:5], ROTATING_CHARS[rotator]) |
| 1323 | + |
| 1324 | + if not api: |
| 1325 | + dataToStdout("\r[%s] [INFO] %s" % (time.strftime("%X"), status)) |
| 1326 | + |
| 1327 | + except KeyboardInterrupt: |
| 1328 | + pass |
| 1329 | + |
| 1330 | + finally: |
| 1331 | + wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file) |
| 1332 | + if hasattr(proc_count, "value"): |
| 1333 | + with proc_count.get_lock(): |
| 1334 | + proc_count.value -= 1 |
| 1335 | + |
1207 | 1336 | def dictionaryAttack(attack_dict): |
1208 | 1337 | global _multiprocessing |
1209 | 1338 |
|
@@ -1251,8 +1380,9 @@ def dictionaryAttack(attack_dict): |
1251 | 1380 | infoMsg = "using hash method '%s'" % __functions__[regex].__name__ |
1252 | 1381 | logger.info(infoMsg) |
1253 | 1382 | else: |
1254 | | - warnMsg = "sqlmap identified %s that cannot be cracked with the " % HASH_TOOL_HINTS.get(regex, "a hash") |
1255 | | - warnMsg += "built-in dictionary attack" |
| 1383 | + warnMsg = "sqlmap identified a hash that cannot be cracked with the built-in dictionary attack" |
| 1384 | + if regex in HASHCAT_MODES: |
| 1385 | + warnMsg += " (use e.g. 'hashcat -m %d')" % HASHCAT_MODES[regex] |
1256 | 1386 | singleTimeWarnMessage(warnMsg) |
1257 | 1387 |
|
1258 | 1388 | for hash_regex in hash_regexes: |
@@ -1350,11 +1480,21 @@ def dictionaryAttack(attack_dict): |
1350 | 1480 | if not attack_info: |
1351 | 1481 | continue |
1352 | 1482 |
|
1353 | | - if not kb.wordlists: |
| 1483 | + # the pure-Python bcrypt is so slow (seconds per candidate) that even the small dictionary would take |
| 1484 | + # many hours; it uses the built-in COMMON_PASSWORDS list (no file), tried candidate-major under a time |
| 1485 | + # budget below, and points at a dedicated tool for anything beyond it |
| 1486 | + if hash_regex in (HASH.BCRYPT, HASH.WORDPRESS_BCRYPT): |
| 1487 | + warnMsg = "bcrypt hashing is very slow in pure Python; trying only the most common passwords. " |
| 1488 | + warnMsg += "For an exhaustive attack use a dedicated tool" |
| 1489 | + if hash_regex in HASHCAT_MODES: |
| 1490 | + warnMsg += " (e.g. 'hashcat -m %d')" % HASHCAT_MODES[hash_regex] |
| 1491 | + singleTimeWarnMessage(warnMsg) |
| 1492 | + |
| 1493 | + elif not kb.wordlists: |
1354 | 1494 | while not kb.wordlists: |
1355 | 1495 |
|
1356 | | - # the slowest of all methods hence smaller default dict |
1357 | | - if hash_regex in (HASH.ORACLE_OLD, HASH.ORACLE_12C, HASH.PHPASS, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.WERKZEUG_SCRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.MYSQL_SHA2): |
| 1496 | + # the slowest of the remaining methods hence smaller default dict |
| 1497 | + if hash_regex in (HASH.ORACLE_OLD, HASH.ORACLE_12C, HASH.PHPASS, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.WERKZEUG_SCRYPT, HASH.MYSQL_SHA2): |
1358 | 1498 | dictPaths = [paths.SMALL_DICT] |
1359 | 1499 | else: |
1360 | 1500 | dictPaths = [paths.WORDLIST] |
@@ -1469,6 +1609,73 @@ def dictionaryAttack(attack_dict): |
1469 | 1609 |
|
1470 | 1610 | clearConsoleLine() |
1471 | 1611 |
|
| 1612 | + # bcrypt is minutes-per-candidate in pure Python and every hash is separately salted (no shared |
| 1613 | + # work), so a whole table's worth would run for hours; crack candidate-major (most common passwords |
| 1614 | + # against all hashes first) under a wall-clock budget, then leave the rest for a dedicated tool |
| 1615 | + elif hash_regex in (HASH.BCRYPT, HASH.WORDPRESS_BCRYPT): |
| 1616 | + deadline = time.time() + HASH_ATTACK_TIME_LIMIT |
| 1617 | + bcryptWordlist = list(COMMON_PASSWORDS) + custom_wordlist # built-in list (+usernames), no file |
| 1618 | + |
| 1619 | + for suffix in suffix_list: |
| 1620 | + if not attack_info or processException or time.time() > deadline: |
| 1621 | + break |
| 1622 | + |
| 1623 | + if suffix: |
| 1624 | + clearConsoleLine() |
| 1625 | + infoMsg = "using suffix '%s'" % suffix |
| 1626 | + logger.info(infoMsg) |
| 1627 | + |
| 1628 | + retVal = None |
| 1629 | + processes = [] |
| 1630 | + |
| 1631 | + try: |
| 1632 | + if _multiprocessing: |
| 1633 | + if _multiprocessing.cpu_count() > 1: |
| 1634 | + infoMsg = "starting %d processes " % _multiprocessing.cpu_count() |
| 1635 | + singleTimeLogMessage(infoMsg) |
| 1636 | + |
| 1637 | + gc.disable() |
| 1638 | + |
| 1639 | + retVal = _multiprocessing.Queue() |
| 1640 | + count = _multiprocessing.Value('i', _multiprocessing.cpu_count()) |
| 1641 | + |
| 1642 | + for i in xrange(_multiprocessing.cpu_count()): |
| 1643 | + process = _multiprocessing.Process(target=_bruteProcessVariantSalted, args=(attack_info, hash_regex, suffix, retVal, i, count, [], bcryptWordlist, conf.api, deadline)) |
| 1644 | + processes.append(process) |
| 1645 | + |
| 1646 | + for process in processes: |
| 1647 | + process.daemon = True |
| 1648 | + process.start() |
| 1649 | + |
| 1650 | + while count.value > 0: |
| 1651 | + time.sleep(0.5) |
| 1652 | + |
| 1653 | + else: |
| 1654 | + warnMsg = "multiprocessing hash cracking is currently " |
| 1655 | + warnMsg += "%s on this platform" % ("not supported" if not conf.disableMulti else "disabled") |
| 1656 | + singleTimeWarnMessage(warnMsg) |
| 1657 | + |
| 1658 | + retVal = _queue.Queue() |
| 1659 | + _bruteProcessVariantSalted(attack_info, hash_regex, suffix, retVal, 0, 1, [], bcryptWordlist, conf.api, deadline) |
| 1660 | + |
| 1661 | + except KeyboardInterrupt: |
| 1662 | + print() |
| 1663 | + processException = True |
| 1664 | + warnMsg = "user aborted during dictionary-based attack phase (Ctrl+C was pressed)" |
| 1665 | + logger.warning(warnMsg) |
| 1666 | + |
| 1667 | + finally: |
| 1668 | + _finalize(retVal, results, processes, attack_info) |
| 1669 | + |
| 1670 | + clearConsoleLine() |
| 1671 | + |
| 1672 | + if attack_info and not processException: # _finalize() drops solved hashes, so any left are uncracked |
| 1673 | + warnMsg = "%d bcrypt hash(es) not cracked with common passwords; " % len(attack_info) |
| 1674 | + warnMsg += "use a dedicated tool for an exhaustive attack" |
| 1675 | + if hash_regex in HASHCAT_MODES: |
| 1676 | + warnMsg += " (e.g. 'hashcat -m %d')" % HASHCAT_MODES[hash_regex] |
| 1677 | + logger.warning(warnMsg) |
| 1678 | + |
1472 | 1679 | else: |
1473 | 1680 | for ((user, hash_), kwargs) in attack_info: |
1474 | 1681 | if processException: |
|
0 commit comments