sqlmap/tests/test_hash_crack.py
Miroslav Štampar 4c869817d4
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
Fixing CI/CD pipeline
2026-06-28 19:03:21 +02:00

221 lines
7.9 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Dictionary-attack machinery in lib/utils/hash.py (the cracking loop, hash-file
parsing, result storage and table/cache post-processing) - the part NOT covered
by tests/test_hash.py, which only exercises the pure hash-format functions.
These run the single-process cracking path (conf.disableMulti=True) against a
TINY temp wordlist that contains the known plaintext, so a known hash is cracked
deterministically in milliseconds without interactive prompts, multiprocessing
pools, network, or the real default dictionary. conf.hashDB is forced to None so
hashDBRetrieve/hashDBWrite become no-ops (no session DB side effects).
"""
import glob
import hashlib
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils import hash as H
from lib.core.data import conf, kb
from lib.core.enums import MKSTEMP_PREFIX
import atexit
import shutil
SCRATCH = tempfile.mkdtemp(prefix="sqlmap_test_hashcrack_")
atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True))
# known plaintext / hashes shared across tests
PW = "testpass"
MD5_HASH = hashlib.md5(PW.encode("utf-8")).hexdigest()
class _CrackBase(unittest.TestCase):
"""Sets up a tiny wordlist and non-interactive, no-DB, single-process state."""
@classmethod
def setUpClass(cls):
cls._tmpfiles = []
# tiny wordlist containing the known plaintext (plus decoys)
cls.wordlist = os.path.join(SCRATCH, "test_hash_crack_wl.txt")
with open(cls.wordlist, "w") as f:
f.write("foo\nbar\n%s\nbaz\n" % PW)
cls._tmpfiles.append(cls.wordlist)
@classmethod
def tearDownClass(cls):
for path in cls._tmpfiles:
try:
os.remove(path)
except OSError:
pass
def setUp(self):
# snapshot global state we mutate
self._saved = {
"disableMulti": conf.disableMulti,
"hashDB": conf.hashDB,
"hashFile": conf.hashFile,
"wordlists": kb.wordlists,
"cachedUsersPasswords": kb.data.cachedUsersPasswords if "cachedUsersPasswords" in kb.data else None,
"storeHashes": kb.choices.storeHashes if "storeHashes" in kb.choices else None,
}
# deterministic, fast, side-effect-free cracking
conf.disableMulti = True
conf.hashDB = None
kb.wordlists = [self.wordlist]
def tearDown(self):
conf.disableMulti = self._saved["disableMulti"]
conf.hashDB = self._saved["hashDB"]
conf.hashFile = self._saved["hashFile"]
kb.wordlists = self._saved["wordlists"]
kb.data.cachedUsersPasswords = self._saved["cachedUsersPasswords"]
kb.choices.storeHashes = self._saved["storeHashes"]
class TestDictionaryAttack(_CrackBase):
def test_crack_md5_generic_variant_a(self):
# generic (no-salt) algorithms go through _bruteProcessVariantA
results = H.dictionaryAttack({"admin": [MD5_HASH]})
self.assertEqual(results, [("admin", MD5_HASH, PW)])
def test_crack_postgres_variant_b(self):
# username-dependent algorithm goes through _bruteProcessVariantB
h = H.postgres_passwd(PW, "testuser", uppercase=False)
results = H.dictionaryAttack({"testuser": [h]})
self.assertEqual(results, [("testuser", h, PW)])
def test_crack_django_md5_salted_variant_b(self):
# salted algorithm: salt is parsed out of the stored hash by dictionaryAttack
h = H.django_md5_passwd(PW, "salt")
results = H.dictionaryAttack({"u2": [h]})
self.assertEqual(results, [("u2", h, PW)])
def test_no_password_found_returns_empty(self):
# plaintext not in wordlist -> nothing cracked
h = hashlib.md5(b"not-in-wordlist-xyz").hexdigest()
results = H.dictionaryAttack({"admin": [h]})
self.assertEqual(results, [])
def test_unknown_hash_format_ignored(self):
# a value that hashRecognition rejects produces no hash_regexes and no results
results = H.dictionaryAttack({"admin": ["not_a_hash"]})
self.assertEqual(results, [])
def test_empty_attack_dict(self):
self.assertEqual(H.dictionaryAttack({}), [])
class TestCrackHashFile(_CrackBase):
def setUp(self):
super(TestCrackHashFile, self).setUp()
# capture the parsed attack_dict that crackHashFile feeds to dictionaryAttack
self._captured = {}
self._real_attack = H.dictionaryAttack
def _capture(attack_dict):
self._captured.clear()
self._captured.update(attack_dict)
return []
H.dictionaryAttack = _capture
def tearDown(self):
H.dictionaryAttack = self._real_attack
super(TestCrackHashFile, self).tearDown()
def test_user_colon_hash_file(self):
path = os.path.join(SCRATCH, "test_hash_crack_hashes.txt")
with open(path, "w") as f:
f.write("admin:%s\n" % MD5_HASH)
self._tmpfiles.append(path)
conf.hashFile = path
self.assertIsNone(H.crackHashFile(path))
# the "user:hash" line is parsed into {username: [hash]}
self.assertEqual(self._captured, {"admin": [MD5_HASH]})
def test_bare_hash_file(self):
# no "user:hash" structure -> a dummy user is synthesised per line
path = os.path.join(SCRATCH, "test_hash_crack_bare.txt")
with open(path, "w") as f:
f.write("%s\n" % MD5_HASH)
self._tmpfiles.append(path)
conf.hashFile = path
self.assertIsNone(H.crackHashFile(path))
from lib.core.settings import DUMMY_USER_PREFIX
self.assertEqual(len(self._captured), 1)
(key, value), = self._captured.items()
# the synthesised key uses the dummy-user prefix and maps to the bare hash
self.assertTrue(key.startswith(DUMMY_USER_PREFIX),
msg="bare line was not assigned a dummy user: %r" % key)
self.assertEqual(value, [MD5_HASH])
class TestAttackCachedUsersPasswords(_CrackBase):
def test_annotates_cleartext(self):
kb.data.cachedUsersPasswords = {"admin": [MD5_HASH]}
H.attackCachedUsersPasswords()
# the original value is augmented in place with the recovered clear-text
self.assertIn("clear-text password: %s" % PW, kb.data.cachedUsersPasswords["admin"][0])
def test_no_cached_data_is_noop(self):
kb.data.cachedUsersPasswords = {}
# must simply return without touching anything
self.assertIsNone(H.attackCachedUsersPasswords())
class TestStoreHashesToFile(_CrackBase):
def _hash_tempfiles(self):
pattern = os.path.join(tempfile.gettempdir(), MKSTEMP_PREFIX.HASHES + "*")
return set(glob.glob(pattern))
def test_store_disabled_writes_nothing(self):
kb.choices.storeHashes = False
before = self._hash_tempfiles()
H.storeHashesToFile({"admin": [MD5_HASH]})
self.assertEqual(self._hash_tempfiles(), before)
def test_store_enabled_writes_recognised_hash(self):
kb.choices.storeHashes = True
before = self._hash_tempfiles()
try:
H.storeHashesToFile({"admin": [MD5_HASH]})
new = self._hash_tempfiles() - before
self.assertEqual(len(new), 1)
with open(next(iter(new))) as fh:
written = fh.read()
self.assertIn(MD5_HASH, written)
self.assertIn("admin", written)
finally:
for path in self._hash_tempfiles() - before:
try:
os.remove(path)
except OSError:
pass
def test_empty_attack_dict_is_noop(self):
kb.choices.storeHashes = True
before = self._hash_tempfiles()
H.storeHashesToFile({})
self.assertEqual(self._hash_tempfiles(), before)
if __name__ == "__main__":
unittest.main(verbosity=2)