From 7ee21bbaf9198c51ecba5529f854f740a411afdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 13 Jul 2026 03:19:34 +0200 Subject: [PATCH 1/3] Fixes CI/CD errors --- lib/core/settings.py | 2 +- plugins/generic/users.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index b1a69bbd82b..f8a12caa448 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.102" +VERSION = "1.10.7.103" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/plugins/generic/users.py b/plugins/generic/users.py index 1f298ac8d28..ae1983ff891 100644 --- a/plugins/generic/users.py +++ b/plugins/generic/users.py @@ -204,8 +204,8 @@ def getPasswordHashes(self): condition = rootQuery.inband.condition if conf.user: - query += " WHERE " - query += " OR ".join("%s = '%s'" % (condition, user) for user in sorted(users)) + userCondition = " OR ".join("%s = '%s'" % (condition, user) for user in sorted(users)) + query += " %s (%s)" % ("AND" if re.search(r"(?i)\bWHERE\b", query) else "WHERE", userCondition) if Backend.isDbms(DBMS.SYBASE): getCurrentThreadData().disableStdOut = True @@ -417,12 +417,12 @@ def getPrivileges(self, query2=False): condition = rootQuery.inband.condition if conf.user: - query += " WHERE " - if Backend.isDbms(DBMS.MYSQL) and kb.data.has_information_schema: - query += " OR ".join("%s LIKE '%%%s%%'" % (condition, user) for user in sorted(users)) + userCondition = " OR ".join("%s LIKE '%%%s%%'" % (condition, user) for user in sorted(users)) else: - query += " OR ".join("%s = '%s'" % (condition, user) for user in sorted(users)) + userCondition = " OR ".join("%s = '%s'" % (condition, user) for user in sorted(users)) + + query += " %s (%s)" % ("AND" if re.search(r"(?i)\bWHERE\b", query) else "WHERE", userCondition) values = inject.getValue(query, blind=False, time=False) From 71afbf071142d92e2f5a282edbe4fdbb693fa309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 13 Jul 2026 03:20:07 +0200 Subject: [PATCH 2/3] Fixes #6083 --- lib/core/settings.py | 2 +- lib/request/http2.py | 23 ++++++++++++---- lib/request/timeless.py | 60 ++++++++++++++++++++++++++++++----------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index f8a12caa448..7924ef16563 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.103" +VERSION = "1.10.7.104" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/request/http2.py b/lib/request/http2.py index a8aa6287f77..7af598c4193 100644 --- a/lib/request/http2.py +++ b/lib/request/http2.py @@ -144,7 +144,7 @@ # HTTP/2 frame codec (RFC 7540 section 4.1) - the zero-table-risk brick. Pure stdlib, py2/py3, ASCII. # frame types (RFC 7540 s6) -DATA, HEADERS, RST_STREAM, SETTINGS, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x6, 0x7, 0x8, 0x9 +DATA, HEADERS, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9 # flags FLAG_END_STREAM = 0x1 FLAG_ACK = 0x1 @@ -374,6 +374,7 @@ def encode(self, headers): out += encode_string(value) return bytes(out) +SETTINGS_ENABLE_PUSH = 0x2 SETTINGS_INITIAL_WINDOW_SIZE = 0x4 BIG_WINDOW = (1 << 31) - 1 @@ -469,9 +470,12 @@ def __init__(self, host, port, proxy, timeout): if self.sock.selected_alpn_protocol() != "h2": raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol()) self.sock.settimeout(timeout) - # connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window + # connection preface + client SETTINGS (disable server push + advertise a large per-stream window) + # + bump conn window. ENABLE_PUSH=0 keeps servers from opening pushed streams whose HPACK header + # block we would otherwise have to decode to keep the dynamic table in sync (skipping it desyncs + # the decoder and corrupts every later header) - this client has no use for pushed responses. self.sock.sendall(CONNECTION_PREFACE) - self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW))) + self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HIHI", SETTINGS_ENABLE_PUSH, 0, SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW))) self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535))) except Exception: self.close() @@ -514,6 +518,9 @@ def exchange(self, method, path, authority, headers, body, timeout): elif ftype == PING: if not (flags & FLAG_ACK): self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + elif ftype == PUSH_PROMISE: + self.usable = False # we advertised ENABLE_PUSH=0; a push would desync HPACK + raise _UnprocessedStream("unexpected PUSH_PROMISE despite SETTINGS_ENABLE_PUSH=0") elif ftype == GOAWAY: self.usable = False # server won't accept new streams -> retire connection last_sid = (struct.unpack("!I", payload[4:8])[0] & 0x7fffffff) if len(payload) >= 8 else 0 @@ -601,12 +608,18 @@ def exchange_pair(self, requests, timeout): elif ftype == PING: if not (flags & FLAG_ACK): self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + elif ftype == PUSH_PROMISE: + self.usable = False # we advertised ENABLE_PUSH=0; a push would desync HPACK + raise _UnprocessedStream("unexpected PUSH_PROMISE despite SETTINGS_ENABLE_PUSH=0") elif ftype == GOAWAY: + # Routine on a long-lived connection (server retires it after its per-connection request cap). + # The pair did not complete cleanly, so it must be re-sent on a fresh connection; flag it + # retry-safe (idempotent boolean read) rather than crashing the extraction. self.usable = False - raise IOError("GOAWAY during timeless pair") + raise _UnprocessedStream("GOAWAY during timeless pair") elif ftype == RST_STREAM and fsid in state: self.usable = False - raise IOError("stream reset during timeless pair") + raise _UnprocessedStream("stream reset during timeless pair") elif ftype in (HEADERS, CONTINUATION) and fsid in state: p = payload if ftype == HEADERS: diff --git a/lib/request/timeless.py b/lib/request/timeless.py index 39f49615344..f0997594e54 100644 --- a/lib/request/timeless.py +++ b/lib/request/timeless.py @@ -17,6 +17,8 @@ # front-proxy makes order track arrival, not work. calibrate() detects that (and estimates the readable # delta) so the caller never applies the oracle blind - it falls back to classic time-based instead. +import socket +import ssl import threading from lib.core.enums import DBMS @@ -49,18 +51,32 @@ def buildConditionPair(condition, heavy, cheap="0"): return condExpr, negExpr -def _pairOrder(conn, reqA, reqB, timeout): +def _pairOrder(connSource, reqA, reqB, timeout, retries=2): """Send reqA and reqB as one coalesced pair; return the stream id that finished FIRST plus the two - stream ids in send order (reqA got the lower id).""" - order, _results = conn.exchange_pair([reqA, reqB], timeout) - loSid = conn.next_sid - 4 - hiSid = conn.next_sid - 2 - return order[0], loSid, hiSid + stream ids in send order (reqA got the lower id). + + `connSource` is either a live _H2Connection or a zero-arg callable returning one. The callable form + lets a dropped connection be replaced transparently and the pair re-sent: a long extraction routinely + outlives a single HTTP/2 connection (the server retires it with GOAWAY after its per-connection request + cap), and a coalesced boolean-read pair is idempotent, so re-sending it on a fresh connection is safe. + A raw connection (used by calibration and the self-test) is not retried - it simply raises.""" + attempt = 0 + while True: + conn = connSource() if callable(connSource) else connSource + try: + order, _results = conn.exchange_pair([reqA, reqB], timeout) + return order[0], conn.next_sid - 4, conn.next_sid - 2 + except (socket.error, ssl.SSLError, IOError): + conn.close() # retire; a callable source reopens on the next pass + attempt += 1 + if not callable(connSource) or attempt > retries: + raise -def readBit(conn, reqCond, reqNeg, votes=5, timeout=30): +def readBit(connSource, reqCond, reqNeg, votes=5, timeout=30): """Read one boolean by the cond-last FRACTION over symmetric pairs, ESCALATING when the fraction is - ambiguous (load-degraded). + ambiguous (load-degraded). `connSource` is a live connection or a factory (see _pairOrder) so a + connection dropped mid-vote (e.g. server GOAWAY) is replaced and that pair re-sent transparently. reqCond does the heavy work iff the guessed condition is TRUE; reqNeg does the SAME heavy work iff the condition is FALSE (it carries the negated condition). Exactly one runs heavy, so whichever finishes @@ -83,10 +99,10 @@ def readBit(conn, reqCond, reqNeg, votes=5, timeout=30): cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits while True: if i % 2 == 0: - first, loSid, _hiSid = _pairOrder(conn, reqCond, reqNeg, timeout) + first, loSid, _hiSid = _pairOrder(connSource, reqCond, reqNeg, timeout) condSid = loSid else: - first, _loSid, hiSid = _pairOrder(conn, reqNeg, reqCond, timeout) + first, _loSid, hiSid = _pairOrder(connSource, reqNeg, reqCond, timeout) condSid = hiSid if first != condSid: # reqCond finished last -> it ran heavy -> vote says TRUE condLast += 1 @@ -159,6 +175,16 @@ def __init__(self, host, port, refReq, proxy=None, asymVotes=12, votes=5, timeou def _conn(self): conn = getattr(self._local, "conn", None) if conn is None or not conn.usable: + if conn is not None: # retire the dead one so reconnects don't leak sockets + try: + conn.close() + except Exception: + pass + with self._lock: + try: + self._conns.remove(conn) + except ValueError: + pass conn = self._local.conn = connect(self.host, self.port, self.proxy, self.timeout) with self._lock: self._conns.append(conn) @@ -172,9 +198,11 @@ def readBitFromSpecs(self, condSpec, negSpec=None): be used for a heavy-base condition (the trivial-base reference is not comparable). Specs are the (url, method, headers, post) tuples from getPage(buildOnly=True).""" reqCond = _specToReq(condSpec, self.host) + # Pass the bound _conn factory (not a resolved connection) so a pair whose connection is retired + # mid-read (server GOAWAY on a long dump) is transparently re-sent on a fresh one. if negSpec is not None: - return readBit(self._conn(), reqCond, _specToReq(negSpec, self.host), votes=self.votes, timeout=self.timeout) - return readBitAsymmetric(self._conn(), reqCond, self.refReq, self.asymVotes, self.timeout) + return readBit(self._conn, reqCond, _specToReq(negSpec, self.host), votes=self.votes, timeout=self.timeout) + return readBitAsymmetric(self._conn, reqCond, self.refReq, self.asymVotes, self.timeout) def close(self): with self._lock: @@ -655,7 +683,7 @@ def readBitLive(conn, condition, vector=None, votes=1, timeout=30): return readBit(conn, reqCond, reqNeg, votes=votes, timeout=timeout) -def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30): +def readBitAsymmetric(connSource, reqCond, reqRef, votes=12, timeout=30): """Read one boolean WITHOUT the negated comparison - only the condition request and a FIXED always-heavy reference. When the condition is TRUE, reqCond runs the SAME heavy work as reqRef, so they race ~50/50 and reqCond finishes last about HALF the votes; when FALSE - or when the comparison is NULL / the DBMS @@ -668,10 +696,10 @@ def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30): condLast = 0 for i in range(votes): if i % 2 == 0: - order, _ = conn.exchange_pair([reqCond, reqRef], timeout); condSid = conn.next_sid - 4 + first, condSid, _hiSid = _pairOrder(connSource, reqCond, reqRef, timeout) else: - order, _ = conn.exchange_pair([reqRef, reqCond], timeout); condSid = conn.next_sid - 2 - if order[0] != condSid: # cond finished last -> it ran the heavy branch this vote + first, _loSid, condSid = _pairOrder(connSource, reqRef, reqCond, timeout) + if first != condSid: # cond finished last -> it ran the heavy branch this vote condLast += 1 return condLast * 4 >= votes # >= 25% cond-last -> TRUE (mid-way between ~50% and ~0%) From da07c8756d14c736cda90d16056fd8547fccfbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 13 Jul 2026 09:03:03 +0200 Subject: [PATCH 3/3] Minor improvement for slow hash cracking algos --- lib/core/settings.py | 11 ++- lib/utils/hash.py | 227 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 227 insertions(+), 11 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 7924ef16563..d8ecf9de5b4 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.104" +VERSION = "1.10.7.105" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} 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 @@ # Reference: http://www.the-interweb.com/serendipity/index.php?/archives/94-A-brief-analysis-of-40,000-leaked-MySpace-passwords.html COMMON_PASSWORD_SUFFIXES += ("!", ".", "*", "!!", "?", ";", "..", "!!!", ",", "@") +# Most common passwords (frequency-ordered) tried for very slow, per-hash-salted algorithms (bcrypt) where a +# full dictionary is impractical; kept small on purpose so a candidate-major attack stays within a time budget +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") + # Splitter used between requests in WebScarab log files WEBSCARAB_SPLITTER = "### Conversation" @@ -882,6 +886,11 @@ # Give up on hash recognition if nothing was found in first given number of rows HASH_RECOGNITION_QUIT_THRESHOLD = 1000 +# Wall-clock budget (in seconds) for the pure-Python cracking of very slow, per-hash-salted algorithms +# (bcrypt); candidate-major so the most common passwords are tried against every hash first, then it stops +# and the remainder is left for a dedicated tool (e.g. 'hashcat'). Overridable via 'SQLMAP_HASH_ATTACK_TIME_LIMIT' +HASH_ATTACK_TIME_LIMIT = 300 + # Regular expression used for automatic hex conversion and hash cracking of (RAW) binary column values HASH_BINARY_COLUMNS_REGEX = r"(?i)pass|psw|hash" diff --git a/lib/utils/hash.py b/lib/utils/hash.py index 537b396bbbd..9a7ef72e701 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -80,9 +80,11 @@ from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapUserQuitException from lib.core.settings import COMMON_PASSWORD_SUFFIXES +from lib.core.settings import COMMON_PASSWORDS from lib.core.settings import COMMON_USER_COLUMNS from lib.core.settings import DEV_EMAIL_ADDRESS from lib.core.settings import DUMMY_USER_PREFIX +from lib.core.settings import HASH_ATTACK_TIME_LIMIT from lib.core.settings import HASH_BINARY_COLUMNS_REGEX from lib.core.settings import HASH_EMPTY_PASSWORD_MARKER from lib.core.settings import HASH_MOD_ITEM_DISPLAY @@ -807,9 +809,40 @@ def _encode64(input_, count): if _scrypt is not None: __functions__[HASH.WERKZEUG_SCRYPT] = werkzeug_scrypt_passwd -# Recognized-only formats with no pure-Python/stdlib crack path; identified and pointed to dedicated tools -HASH_TOOL_HINTS = { - HASH.ARGON2: "an Argon2 hash (e.g. 'hashcat -m 34000' or 'john --format=argon2')", +# hashcat '-m' mode per recognized hash format (Reference: https://hashcat.net/wiki/doku.php?id=example_hashes); +# used to point the user at the right command when hashes are stored or cannot be cracked in pure Python +# (e.g. Argon2). Only well-established modes are listed - a format left out just gets the generic hint +HASHCAT_MODES = { + HASH.ARGON2: 34000, + HASH.MYSQL_OLD: 200, + HASH.MYSQL: 300, + HASH.POSTGRES: 12, + HASH.MSSQL_OLD: 131, + HASH.MSSQL: 132, + HASH.MSSQL_NEW: 1731, + HASH.ORACLE_OLD: 3100, + HASH.ORACLE: 112, + HASH.ORACLE_12C: 12300, + HASH.MD5_GENERIC: 0, + HASH.SHA1_GENERIC: 100, + HASH.SHA224_GENERIC: 1300, + HASH.SHA256_GENERIC: 1400, + HASH.SHA384_GENERIC: 10800, + HASH.SHA512_GENERIC: 1700, + HASH.CRYPT_GENERIC: 1500, + HASH.APACHE_SHA1: 101, + HASH.SSHA: 111, + HASH.SSHA256: 1411, + HASH.SSHA512: 1711, + HASH.UNIX_MD5_CRYPT: 500, + HASH.APACHE_MD5_CRYPT: 1600, + HASH.SHA256_UNIX_CRYPT: 7400, + HASH.SHA512_UNIX_CRYPT: 1800, + HASH.BCRYPT: 3200, + HASH.PHPASS: 400, + HASH.VBULLETIN: 2611, + HASH.DJANGO_SHA1: 124, + HASH.DJANGO_PBKDF2_SHA256: 10000, } def _finalize(retVal, results, processes, attack_info=None): @@ -852,12 +885,18 @@ def storeHashesToFile(attack_dict): return items = OrderedSet() + regexes = set() for user, hashes in attack_dict.items(): for hash_ in hashes: hash_ = hash_.split()[0] if hash_ and hash_.strip() else hash_ - if hash_ and hash_ != NULL and hashRecognition(hash_): - item = None + if hash_ and hash_ != NULL: + regex = hashRecognition(hash_) + if not regex: + continue + + regexes.add(regex) + if user and not user.startswith(DUMMY_USER_PREFIX): item = "%s:%s\n" % (user, hash_) else: @@ -886,6 +925,12 @@ def storeHashesToFile(attack_dict): except (UnicodeError, TypeError): pass + modes = sorted(set(HASHCAT_MODES[_] for _ in regexes if _ in HASHCAT_MODES)) + if modes: + infoMsg = "the stored hashes can be cracked with a dedicated tool " + infoMsg += "(e.g. %s)" % ", ".join("'hashcat -m %d'" % _ for _ in modes) + logger.info(infoMsg) + def attackCachedUsersPasswords(): if kb.data.cachedUsersPasswords: results = dictionaryAttack(kb.data.cachedUsersPasswords) @@ -1204,6 +1249,90 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found with proc_count.get_lock(): proc_count.value -= 1 +def _bruteProcessVariantSalted(attack_info, hash_regex, suffix, retVal, proc_id, proc_count, wordlists, custom_wordlist, api, deadline): + # Candidate-major crack for the very slow, per-hash-salted algorithms (bcrypt): the OUTER loop is the + # candidate word (partitioned across processes) and the INNER loop is every still-unsolved hash, each + # verified with its own salt. Trying the most common passwords against ALL hashes first means a weak + # account anywhere in a dumped table is found in the first rounds - a hash-major loop would instead grind + # the whole wordlist on row 1 before ever testing row 2. Bounded by `deadline` so hundreds of separately + # salted hashes can't become an hours-long run; whatever is left is meant for a dedicated tool. + if IS_WIN: + coloramainit() + + count = 0 + rotator = 0 + remaining = attack_info[:] # per-process (post-fork) copy; shrinks as this process solves hashes + + wordlist = Wordlist(wordlists, proc_id, getattr(proc_count, "value", 0), custom_wordlist) + + try: + for word in wordlist: + if not remaining or time.time() > deadline: + break + + count += 1 + + if isinstance(word, six.binary_type): + word = getUnicode(word) + elif not isinstance(word, six.string_types): + continue + + if suffix: + word = word + suffix + + for item in remaining[:]: + ((user, hash_), kwargs) = item + + try: + current = __functions__[hash_regex](password=word, uppercase=False, **kwargs) + + if hash_ == current: + retVal.put((user, hash_, word)) + + clearConsoleLine() + + infoMsg = "\r[%s] [INFO] cracked password '%s'" % (time.strftime("%X"), word) + + if user and not user.startswith(DUMMY_USER_PREFIX): + infoMsg += " for user '%s'\n" % user + else: + infoMsg += " for hash '%s'\n" % hash_ + + dataToStdout(infoMsg, True) + + remaining.remove(item) + + except KeyboardInterrupt: + raise + + except (UnicodeEncodeError, UnicodeDecodeError): + pass # ignore possible encoding problems caused by some words in custom dictionaries + + except Exception as ex: + warnMsg = "there was a problem while hashing entry: %s ('%s'). " % (repr(word), getSafeExString(ex)) + warnMsg += "Please report by e-mail to '%s'" % DEV_EMAIL_ADDRESS + logger.critical(warnMsg) + + if (proc_id == 0 or getattr(proc_count, "value", 0) == 1) and count % HASH_MOD_ITEM_DISPLAY == 0: + rotator += 1 + + if rotator >= len(ROTATING_CHARS): + rotator = 0 + + status = "current status: %s... %s" % (word.ljust(5)[:5], ROTATING_CHARS[rotator]) + + if not api: + dataToStdout("\r[%s] [INFO] %s" % (time.strftime("%X"), status)) + + except KeyboardInterrupt: + pass + + finally: + wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file) + if hasattr(proc_count, "value"): + with proc_count.get_lock(): + proc_count.value -= 1 + def dictionaryAttack(attack_dict): global _multiprocessing @@ -1251,8 +1380,9 @@ def dictionaryAttack(attack_dict): infoMsg = "using hash method '%s'" % __functions__[regex].__name__ logger.info(infoMsg) else: - warnMsg = "sqlmap identified %s that cannot be cracked with the " % HASH_TOOL_HINTS.get(regex, "a hash") - warnMsg += "built-in dictionary attack" + warnMsg = "sqlmap identified a hash that cannot be cracked with the built-in dictionary attack" + if regex in HASHCAT_MODES: + warnMsg += " (use e.g. 'hashcat -m %d')" % HASHCAT_MODES[regex] singleTimeWarnMessage(warnMsg) for hash_regex in hash_regexes: @@ -1350,11 +1480,21 @@ def dictionaryAttack(attack_dict): if not attack_info: continue - if not kb.wordlists: + # the pure-Python bcrypt is so slow (seconds per candidate) that even the small dictionary would take + # many hours; it uses the built-in COMMON_PASSWORDS list (no file), tried candidate-major under a time + # budget below, and points at a dedicated tool for anything beyond it + if hash_regex in (HASH.BCRYPT, HASH.WORDPRESS_BCRYPT): + warnMsg = "bcrypt hashing is very slow in pure Python; trying only the most common passwords. " + warnMsg += "For an exhaustive attack use a dedicated tool" + if hash_regex in HASHCAT_MODES: + warnMsg += " (e.g. 'hashcat -m %d')" % HASHCAT_MODES[hash_regex] + singleTimeWarnMessage(warnMsg) + + elif not kb.wordlists: while not kb.wordlists: - # the slowest of all methods hence smaller default dict - 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): + # the slowest of the remaining methods hence smaller default dict + 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): dictPaths = [paths.SMALL_DICT] else: dictPaths = [paths.WORDLIST] @@ -1469,6 +1609,73 @@ def dictionaryAttack(attack_dict): clearConsoleLine() + # bcrypt is minutes-per-candidate in pure Python and every hash is separately salted (no shared + # work), so a whole table's worth would run for hours; crack candidate-major (most common passwords + # against all hashes first) under a wall-clock budget, then leave the rest for a dedicated tool + elif hash_regex in (HASH.BCRYPT, HASH.WORDPRESS_BCRYPT): + deadline = time.time() + HASH_ATTACK_TIME_LIMIT + bcryptWordlist = list(COMMON_PASSWORDS) + custom_wordlist # built-in list (+usernames), no file + + for suffix in suffix_list: + if not attack_info or processException or time.time() > deadline: + break + + if suffix: + clearConsoleLine() + infoMsg = "using suffix '%s'" % suffix + logger.info(infoMsg) + + retVal = None + processes = [] + + try: + if _multiprocessing: + if _multiprocessing.cpu_count() > 1: + infoMsg = "starting %d processes " % _multiprocessing.cpu_count() + singleTimeLogMessage(infoMsg) + + gc.disable() + + retVal = _multiprocessing.Queue() + count = _multiprocessing.Value('i', _multiprocessing.cpu_count()) + + for i in xrange(_multiprocessing.cpu_count()): + process = _multiprocessing.Process(target=_bruteProcessVariantSalted, args=(attack_info, hash_regex, suffix, retVal, i, count, [], bcryptWordlist, conf.api, deadline)) + processes.append(process) + + for process in processes: + process.daemon = True + process.start() + + while count.value > 0: + time.sleep(0.5) + + else: + warnMsg = "multiprocessing hash cracking is currently " + warnMsg += "%s on this platform" % ("not supported" if not conf.disableMulti else "disabled") + singleTimeWarnMessage(warnMsg) + + retVal = _queue.Queue() + _bruteProcessVariantSalted(attack_info, hash_regex, suffix, retVal, 0, 1, [], bcryptWordlist, conf.api, deadline) + + except KeyboardInterrupt: + print() + processException = True + warnMsg = "user aborted during dictionary-based attack phase (Ctrl+C was pressed)" + logger.warning(warnMsg) + + finally: + _finalize(retVal, results, processes, attack_info) + + clearConsoleLine() + + if attack_info and not processException: # _finalize() drops solved hashes, so any left are uncracked + warnMsg = "%d bcrypt hash(es) not cracked with common passwords; " % len(attack_info) + warnMsg += "use a dedicated tool for an exhaustive attack" + if hash_regex in HASHCAT_MODES: + warnMsg += " (e.g. 'hashcat -m %d')" % HASHCAT_MODES[hash_regex] + logger.warning(warnMsg) + else: for ((user, hash_), kwargs) in attack_info: if processException: