Skip to content

Commit da07c87

Browse files
committed
Minor improvement for slow hash cracking algos
1 parent 71afbf0 commit da07c87

2 files changed

Lines changed: 227 additions & 11 deletions

File tree

lib/core/settings.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.104"
23+
VERSION = "1.10.7.105"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@@ -519,6 +519,10 @@
519519
# Reference: http://www.the-interweb.com/serendipity/index.php?/archives/94-A-brief-analysis-of-40,000-leaked-MySpace-passwords.html
520520
COMMON_PASSWORD_SUFFIXES += ("!", ".", "*", "!!", "?", ";", "..", "!!!", ",", "@")
521521

522+
# Most common passwords (frequency-ordered) tried for very slow, per-hash-salted algorithms (bcrypt) where a
523+
# full dictionary is impractical; kept small on purpose so a candidate-major attack stays within a time budget
524+
COMMON_PASSWORDS = ("123456", "123456789", "12345678", "password", "qwerty", "12345", "123123", "111111", "1234567890", "1234567", "qwerty123", "000000", "1q2w3e", "abc123", "password1", "1234", "qwertyuiop", "123321", "password123", "1q2w3e4r5t", "iloveyou", "654321", "666666", "987654321", "1q2w3e4r", "7777777", "dragon", "1qaz2wsx", "123qwe", "monkey", "123456a", "112233", "qwe123", "159753", "letmein", "11111111", "222222", "123abc", "qazwsx", "555555", "princess", "admin", "121212", "1234qwer", "sunshine", "football", "aaaaaa", "123123123", "computer", "michael", "superman", "welcome", "zxcvbnm", "asdfghjkl", "1111", "shadow", "master", "999999", "88888888", "secret", "qwerty1", "12341234", "101010", "1111111", "asdfgh", "147258369", "qwertyui", "123654", "google", "123456789a", "ashley", "jesus", "ninja", "mustang", "baseball", "jennifer", "hunter", "soccer", "batman", "andrew", "tigger", "charlie", "robert", "thomas", "hockey", "ranger", "daniel", "hannah", "maggie", "696969", "harley", "1234abcd", "trustno1", "buster", "starwars", "freedom", "whatever", "qazwsxedc", "passw0rd")
525+
522526
# Splitter used between requests in WebScarab log files
523527
WEBSCARAB_SPLITTER = "### Conversation"
524528

@@ -882,6 +886,11 @@
882886
# Give up on hash recognition if nothing was found in first given number of rows
883887
HASH_RECOGNITION_QUIT_THRESHOLD = 1000
884888

889+
# Wall-clock budget (in seconds) for the pure-Python cracking of very slow, per-hash-salted algorithms
890+
# (bcrypt); candidate-major so the most common passwords are tried against every hash first, then it stops
891+
# and the remainder is left for a dedicated tool (e.g. 'hashcat'). Overridable via 'SQLMAP_HASH_ATTACK_TIME_LIMIT'
892+
HASH_ATTACK_TIME_LIMIT = 300
893+
885894
# Regular expression used for automatic hex conversion and hash cracking of (RAW) binary column values
886895
HASH_BINARY_COLUMNS_REGEX = r"(?i)pass|psw|hash"
887896

lib/utils/hash.py

Lines changed: 217 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@
8080
from lib.core.exception import SqlmapDataException
8181
from lib.core.exception import SqlmapUserQuitException
8282
from lib.core.settings import COMMON_PASSWORD_SUFFIXES
83+
from lib.core.settings import COMMON_PASSWORDS
8384
from lib.core.settings import COMMON_USER_COLUMNS
8485
from lib.core.settings import DEV_EMAIL_ADDRESS
8586
from lib.core.settings import DUMMY_USER_PREFIX
87+
from lib.core.settings import HASH_ATTACK_TIME_LIMIT
8688
from lib.core.settings import HASH_BINARY_COLUMNS_REGEX
8789
from lib.core.settings import HASH_EMPTY_PASSWORD_MARKER
8890
from lib.core.settings import HASH_MOD_ITEM_DISPLAY
@@ -807,9 +809,40 @@ def _encode64(input_, count):
807809
if _scrypt is not None:
808810
__functions__[HASH.WERKZEUG_SCRYPT] = werkzeug_scrypt_passwd
809811

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,
813846
}
814847

815848
def _finalize(retVal, results, processes, attack_info=None):
@@ -852,12 +885,18 @@ def storeHashesToFile(attack_dict):
852885
return
853886

854887
items = OrderedSet()
888+
regexes = set()
855889

856890
for user, hashes in attack_dict.items():
857891
for hash_ in hashes:
858892
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+
861900
if user and not user.startswith(DUMMY_USER_PREFIX):
862901
item = "%s:%s\n" % (user, hash_)
863902
else:
@@ -886,6 +925,12 @@ def storeHashesToFile(attack_dict):
886925
except (UnicodeError, TypeError):
887926
pass
888927

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+
889934
def attackCachedUsersPasswords():
890935
if kb.data.cachedUsersPasswords:
891936
results = dictionaryAttack(kb.data.cachedUsersPasswords)
@@ -1204,6 +1249,90 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found
12041249
with proc_count.get_lock():
12051250
proc_count.value -= 1
12061251

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+
12071336
def dictionaryAttack(attack_dict):
12081337
global _multiprocessing
12091338

@@ -1251,8 +1380,9 @@ def dictionaryAttack(attack_dict):
12511380
infoMsg = "using hash method '%s'" % __functions__[regex].__name__
12521381
logger.info(infoMsg)
12531382
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]
12561386
singleTimeWarnMessage(warnMsg)
12571387

12581388
for hash_regex in hash_regexes:
@@ -1350,11 +1480,21 @@ def dictionaryAttack(attack_dict):
13501480
if not attack_info:
13511481
continue
13521482

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:
13541494
while not kb.wordlists:
13551495

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):
13581498
dictPaths = [paths.SMALL_DICT]
13591499
else:
13601500
dictPaths = [paths.WORDLIST]
@@ -1469,6 +1609,73 @@ def dictionaryAttack(attack_dict):
14691609

14701610
clearConsoleLine()
14711611

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+
14721679
else:
14731680
for ((user, hash_), kwargs) in attack_info:
14741681
if processException:

0 commit comments

Comments
 (0)