199 lines
9.7 KiB
Python
199 lines
9.7 KiB
Python
"""
|
||
Hash identifier plugin – identifies 100+ hash types with confidence and tool modes.
|
||
Outputs a clean code block with emojis and perfectly aligned columns.
|
||
"""
|
||
|
||
import logging
|
||
import re
|
||
import simplematrixbotlib as botlib
|
||
from plugins.common import collapsible_summary, html_escape, code_block
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Hash identification logic (unchanged from original)
|
||
# ---------------------------------------------------------------------------
|
||
def identify_hash(hash_string):
|
||
hash_string = hash_string.strip()
|
||
hash_lower = hash_string.lower()
|
||
length = len(hash_string)
|
||
|
||
possible_types = []
|
||
|
||
# Unix crypt and modular crypt formats (most specific first)
|
||
if hash_string.startswith('$'):
|
||
if re.match(r'^\$y\$', hash_string):
|
||
possible_types.append(("yescrypt", None, "yescrypt", 95))
|
||
elif re.match(r'^\$7\$', hash_string):
|
||
possible_types.append(("scrypt", "8900", "scrypt", 95))
|
||
elif re.match(r'^\$argon2(id?|d)\$', hash_string):
|
||
if '$argon2i$' in hash_string:
|
||
possible_types.append(("Argon2i", "10900", "argon2", 95))
|
||
elif '$argon2d$' in hash_string:
|
||
possible_types.append(("Argon2d", None, "argon2", 95))
|
||
elif '$argon2id$' in hash_string:
|
||
possible_types.append(("Argon2id", "10900", "argon2", 95))
|
||
elif re.match(r'^\$(2[abxy]?)\$', hash_string):
|
||
bcrypt_type = re.match(r'^\$(2[abxy]?)\$', hash_string).group(1)
|
||
possible_types.append((f"bcrypt ({bcrypt_type})", "3200", "bcrypt", 95))
|
||
elif re.match(r'^\$6\$', hash_string):
|
||
possible_types.append(("SHA-512 Crypt (Unix)", "1800", "sha512crypt", 95))
|
||
elif re.match(r'^\$5\$', hash_string):
|
||
possible_types.append(("SHA-256 Crypt (Unix)", "7400", "sha256crypt", 95))
|
||
elif re.match(r'^\$1\$', hash_string):
|
||
possible_types.append(("MD5 Crypt (Unix)", "500", "md5crypt", 95))
|
||
elif re.match(r'^\$apr1\$', hash_string):
|
||
possible_types.append(("Apache MD5 (apr1)", "1600", "md5crypt", 95))
|
||
elif re.match(r'^\{smd5\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("AIX {smd5}", "6300", None, 90))
|
||
elif re.match(r'^\{ssha256\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("AIX {ssha256}", "6700", None, 90))
|
||
elif re.match(r'^\{ssha512\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("AIX {ssha512}", "6800", None, 90))
|
||
elif re.match(r'^\$H\$', hash_string):
|
||
possible_types.append(("phpBB3", "400", "phpass", 90))
|
||
elif re.match(r'^\$P\$', hash_string):
|
||
possible_types.append(("Wordpress", "400", "phpass", 90))
|
||
elif re.match(r'^\$S\$', hash_string):
|
||
possible_types.append(("Drupal 7+", "7900", "drupal7", 90))
|
||
elif re.match(r'^\$wbb3\$', hash_string):
|
||
possible_types.append(("WBB3 (Woltlab)", None, None, 85))
|
||
elif re.match(r'^\$pbkdf2-sha256\$', hash_string):
|
||
possible_types.append(("PBKDF2-HMAC-SHA256", "10900", "pbkdf2-hmac-sha256", 90))
|
||
elif re.match(r'^\$pbkdf2-sha512\$', hash_string):
|
||
possible_types.append(("PBKDF2-HMAC-SHA512", None, "pbkdf2-hmac-sha512", 90))
|
||
elif re.match(r'^pbkdf2_sha256\$', hash_string):
|
||
possible_types.append(("Django PBKDF2-SHA256", "10000", "django", 90))
|
||
else:
|
||
possible_types.append(("Unknown Modular Crypt Format", None, None, 30))
|
||
return possible_types
|
||
|
||
# LDAP formats
|
||
if hash_string.startswith('{'):
|
||
if re.match(r'^\{SHA\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("LDAP SHA-1", "101", "nsldap", 90))
|
||
elif re.match(r'^\{SSHA\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("LDAP SSHA (Salted SHA-1)", "111", "nsldaps", 90))
|
||
elif re.match(r'^\{MD5\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("LDAP MD5", "3210", None, 90))
|
||
elif re.match(r'^\{SMD5\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("LDAP SMD5 (Salted MD5)", "3211", None, 90))
|
||
elif re.match(r'^\{CRYPT\}', hash_string, re.IGNORECASE):
|
||
possible_types.append(("LDAP CRYPT", None, None, 85))
|
||
return possible_types
|
||
|
||
# Colon-separated formats
|
||
if ':' in hash_string:
|
||
parts = hash_string.split(':')
|
||
if len(parts) >= 5:
|
||
possible_types.append(("NetNTLMv2", "5600", "netntlmv2", 85))
|
||
possible_types.append(("NetNTLMv1", "5500", "netntlm", 75))
|
||
elif len(parts) == 2 and len(parts[0]) == 32 and len(parts[1]) == 32:
|
||
possible_types.append(("LM:NTLM", "1000", "nt", 90))
|
||
elif len(parts) == 2:
|
||
hash_part = parts[1]
|
||
if len(hash_part) == 32:
|
||
possible_types.append(("NTLM (with username)", "1000", "nt", 80))
|
||
elif len(hash_part) == 40:
|
||
possible_types.append(("SHA-1 (with salt/username)", "110", None, 70))
|
||
if hash_string.startswith('md5') and len(hash_string) == 35:
|
||
possible_types.append(("PostgreSQL MD5", "3100", "postgres", 90))
|
||
return possible_types if possible_types else None
|
||
|
||
# MySQL formats
|
||
if hash_string.startswith('*') and length == 41 and re.match(r'^\*[A-F0-9]{40}$', hash_string.upper()):
|
||
possible_types.append(("MySQL 4.1/5.x", "300", "mysql-sha1", 95))
|
||
return possible_types
|
||
|
||
# Oracle formats
|
||
if re.match(r'^[A-F0-9]{16}:[A-F0-9]{16}$', hash_string.upper()):
|
||
possible_types.append(("Oracle 11g", "112", "oracle11", 90))
|
||
return possible_types
|
||
if re.match(r'^S:[A-F0-9]{60}$', hash_string.upper()):
|
||
possible_types.append(("Oracle 12c/18c", "12300", "oracle12c", 90))
|
||
return possible_types
|
||
|
||
# MSSQL formats
|
||
if re.match(r'^0x0100[A-F0-9]{8}[A-F0-9]{40}$', hash_string.upper()):
|
||
possible_types.append(("MSSQL 2000", "131", "mssql", 90))
|
||
return possible_types
|
||
if re.match(r'^0x0200[A-F0-9]{8}[A-F0-9]{128}$', hash_string.upper()):
|
||
possible_types.append(("MSSQL 2012/2014", "1731", "mssql12", 90))
|
||
return possible_types
|
||
|
||
# Raw hash identification by length
|
||
is_hex = re.match(r'^[a-f0-9]+$', hash_lower)
|
||
if is_hex:
|
||
if length == 16:
|
||
possible_types.append(("MySQL < 4.1", "200", "mysql", 85))
|
||
possible_types.append(("Half MD5", None, None, 60))
|
||
elif length == 32:
|
||
possible_types.append(("MD5", "0", "raw-md5", 80))
|
||
possible_types.append(("MD4", "900", "raw-md4", 70))
|
||
possible_types.append(("NTLM", "1000", "nt", 75))
|
||
possible_types.append(("LM", "3000", "lm", 60))
|
||
elif length == 40:
|
||
possible_types.append(("SHA-1", "100", "raw-sha1", 85))
|
||
possible_types.append(("RIPEMD-160", "6000", "ripemd-160", 65))
|
||
elif length == 64:
|
||
possible_types.append(("SHA-256", "1400", "raw-sha256", 85))
|
||
possible_types.append(("SHA3-256", "17400", "raw-sha3", 70))
|
||
possible_types.append(("Keccak-256", "17800", "raw-keccak-256", 70))
|
||
elif length == 128:
|
||
possible_types.append(("SHA-512", "1700", "raw-sha512", 85))
|
||
possible_types.append(("Whirlpool", "6100", "whirlpool", 75))
|
||
|
||
return possible_types if possible_types else [("Unknown", None, None, 0)]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Output formatting
|
||
# ---------------------------------------------------------------------------
|
||
def _format_results(hash_input, results):
|
||
"""Build a code block with sections for each possible hash type."""
|
||
sections = []
|
||
for idx, (hash_type, hashcat_mode, john_format, confidence) in enumerate(results, 1):
|
||
emoji = "🟢" if confidence >= 90 else "🟡" if confidence >= 80 else "🟠" if confidence >= 60 else "🔴"
|
||
title = f"{emoji} Match #{idx}: {hash_type} ({confidence}%)"
|
||
rows = [
|
||
("", "Hash Type", hash_type),
|
||
("", "Confidence", f"{confidence}%"),
|
||
]
|
||
if hashcat_mode:
|
||
rows.append(("", "Hashcat Mode", f"-m {hashcat_mode}"))
|
||
if john_format:
|
||
rows.append(("", "John Format", f"--format={john_format}"))
|
||
sections.append({"title": title, "rows": rows})
|
||
|
||
block = code_block(f"🔐 Hash Identification: {hash_input[:30]}...", sections)
|
||
return collapsible_summary("🔐 Hash Identification Results", block)
|
||
|
||
|
||
async def handle_command(room, message, bot, prefix, config):
|
||
match = botlib.MessageMatch(room, message, bot, prefix)
|
||
if match.is_not_from_this_bot() and match.prefix() and match.command("hashid"):
|
||
args = match.args()
|
||
if len(args) < 1:
|
||
await bot.api.send_markdown_message(room.room_id, "<strong>Usage:</strong> <code>!hashid <hash></code>")
|
||
return
|
||
hash_input = ' '.join(args)
|
||
results = identify_hash(hash_input)
|
||
if not results or results[0][0] == "Unknown":
|
||
await bot.api.send_text_message(room.room_id, "Could not identify the hash type.")
|
||
return
|
||
# Sort by confidence descending
|
||
results.sort(key=lambda x: x[3], reverse=True)
|
||
output = _format_results(hash_input, results[:6]) # show top 6
|
||
await bot.api.send_markdown_message(room.room_id, output)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Plugin Metadata
|
||
# ---------------------------------------------------------------------------
|
||
__version__ = "1.1.0"
|
||
__author__ = "Funguy Bot"
|
||
__description__ = "Hash type identifier"
|
||
__help__ = """
|
||
<details>
|
||
<summary><strong>!hashid</strong> – Identify hash type</summary>
|
||
<p><code>!hashid <hash></code> – Recognises 100+ formats and displays tool modes in a clean table.</p>
|
||
</details>
|
||
"""
|