├── h8mail ├── utils │ ├── __init__.py │ ├── version.py │ ├── gen_config.py │ ├── print_json.py │ ├── summary.py │ ├── intelx_helpers.py │ ├── chase.py │ ├── print_results.py │ ├── url.py │ ├── breachcompilation.py │ ├── localgzipsearch.py │ ├── localsearch.py │ ├── helpers.py │ ├── colors.py │ ├── run.py │ ├── intelx.py │ └── classes.py ├── requirements.txt ├── __init__.py └── __main__.py ├── tests ├── test_email.txt ├── __init__.py └── test_h8mail.py ├── Dockerfile ├── .editorconfig ├── .travis.yml ├── .github └── ISSUE_TEMPLATE.md ├── LICENSE ├── PyPi.rst ├── CONTRIBUTING.md ├── setup.py ├── .gitignore ├── Makefile └── README.md /h8mail/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /h8mail/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /h8mail/utils/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.5.6" 2 | -------------------------------------------------------------------------------- /tests/test_email.txt: -------------------------------------------------------------------------------- 1 | john.smith@gmail.com 2 | test@example.com 3 | test@gmail.com 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for h8mail.""" 4 | -------------------------------------------------------------------------------- /h8mail/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for h8mail.""" 4 | 5 | from h8mail.utils.version import __version__ 6 | 7 | __author__ = """khast3x""" 8 | __email__ = "k@kast3x.club" 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | RUN apk add --update --no-cache git bash 4 | WORKDIR h8mail 5 | RUN pip3 install requests 6 | COPY . . 7 | RUN ["python", "setup.py", "install"] 8 | ENTRYPOINT ["h8mail"] 9 | CMD ["-h"] 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | # This file will be regenerated if you run travis_pypi_setup.py 3 | 4 | language: python 5 | python: 6 | - 3.6 7 | 8 | 9 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 10 | # install: pip install -r requirements.txt 11 | 12 | install: pip install -e . 13 | 14 | # Command to run tests, e.g. python setup.py test 15 | script: python setup.py test -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issues not respecting the issue template will be closed without being read, thank you.* 2 | 3 | ### Checkbox 4 | 5 | - [ ] I have thoroughly checked the answer is not in the [wiki](https://github.com/khast3x/h8mail/wiki). 6 | 7 | ### Env 8 | 9 | * h8mail version: 10 | * Python version: 11 | * Operating System: 12 | 13 | ### Description 14 | 15 | Describe what you were trying to get done. 16 | Tell us what happened, what went wrong, and what you expected to happen. 17 | 18 | ### What I Did 19 | 20 | ``` 21 | Paste the command(s) you ran and the output. 22 | If there was a crash, please include the traceback here. 23 | ``` 24 | -------------------------------------------------------------------------------- /h8mail/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .utils.run import main 3 | 4 | if __name__ == "__main__": 5 | # Check major and minor python version 6 | if sys.version_info[0] < 3: 7 | sys.stdout.write( 8 | "\n/!\\ h8mail requires Python 3.6+ /!\\\nTry running h8mail with python3 if on older systems\n\neg: python --version\neg: python3 h8mail v --help\n\n" 9 | ) 10 | sys.exit(1) 11 | if sys.version_info[1] < 6: 12 | sys.stdout.write( 13 | "\n/!\\ h8mail requires Python 3.6+ /!\\\nTry running h8mail with python3 if on older systems\n\neg: python --version\neg: python3 h8mail --help\n\n" 14 | ) 15 | sys.exit(1) 16 | main() 17 | print("Done") 18 | -------------------------------------------------------------------------------- /h8mail/utils/gen_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .colors import colors as c 3 | import os 4 | 5 | 6 | def gen_config_file(): 7 | with open( 8 | os.path.join(os.getcwd(), "h8mail_config.ini"), "w", newline="" 9 | ) as dest_config: 10 | config = """[h8mail] 11 | ; h8mail will automatically detect present keys & launch services accordingly 12 | ; Uncomment to activate 13 | ;hunterio = 14 | ;snusbase_token = 15 | ;;weleakinfo_priv = 16 | ;;weleakinfo_pub = 17 | ;hibp = 18 | ;leak-lookup_pub = 1bf94ff907f68d511de9a610a6ff9263 19 | ;leak-lookup_priv = 20 | ;emailrep = 21 | ;dehashed_email = 22 | ;dehashed_key = 23 | ;intelx_key = 24 | ;intelx_maxfile = 10 25 | ;breachdirectory_user = 26 | ;breachdirectory_pass = 27 | """ 28 | dest_config.write(config) 29 | c.good_news( 30 | "Generated h8mail template configuration file ({})".format( 31 | os.path.join(os.getcwd(), "h8mail_config.ini") 32 | ) 33 | ) 34 | -------------------------------------------------------------------------------- /h8mail/utils/print_json.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .colors import colors as c 3 | import json 4 | 5 | def generate_source_arrays(pwned_data): 6 | data_array = [] 7 | no_src = 0 # To check for data with no explicit source 8 | temp_array = [] 9 | for i in range(len(pwned_data)): 10 | if len(pwned_data[i]) == 2: 11 | temp_array.append(pwned_data[i][0] + ":" + str(pwned_data[i][1])) 12 | no_src += 1 13 | if "SOURCE" in pwned_data[i][0]: 14 | data_array.append(temp_array) 15 | temp_array = [] 16 | no_src = 0 17 | if no_src > 0: 18 | data_array.append(temp_array) 19 | return data_array 20 | 21 | 22 | 23 | def save_results_json(dest_json, target_obj_list): 24 | data = {} 25 | data['targets'] = [] 26 | for t in target_obj_list: 27 | current_target = {} 28 | current_target["target"] = t.target 29 | current_target["pwn_num"] = t.pwned 30 | current_target["data"] = generate_source_arrays(t.data) 31 | data['targets'].append(current_target) 32 | 33 | with open(dest_json, 'w') as outfile: 34 | json.dump(data, outfile) -------------------------------------------------------------------------------- /h8mail/utils/summary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | from .colors import colors as c 5 | 6 | 7 | def print_summary(start_time, breached_targets): 8 | """ 9 | Prints a padded table where each line is a target, and associated value is simplified to breached/not breached. 10 | If breached, shows len(t.data). Shown elements and total may differ as some elements of t.data are not outputted to stdout. 11 | """ 12 | print("{:_^90}".format("")) 13 | print( 14 | "\n\n\n{:^32}".format(""), 15 | c.bold, 16 | c.underline, 17 | "Session Recap:", 18 | c.reset, 19 | "\n\n", 20 | ) 21 | print("{:^40} | ".format("Target"), "{:^40}".format("Status"), c.reset) 22 | print("{:_^90}\n".format("")) 23 | for t in breached_targets: 24 | if t.pwned != 0: 25 | print( 26 | f"{t.target:^40} | ", 27 | c.fg.green, 28 | "{:^40}".format("Breach Found (" + str(t.pwned) + " elements)"), 29 | c.reset, 30 | ) 31 | else: 32 | print( 33 | f"{t.target:^40} | ", 34 | c.fg.lightgrey, 35 | "{:^40}".format("Not Compromised"), 36 | c.reset, 37 | ) 38 | print("{:_^90}\n".format("")) 39 | total_time = time.time() - start_time 40 | print("Execution time (seconds): ", c.fg.lightcyan, f"{total_time}", c.reset, "\n") 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | BSD License 4 | 5 | Copyright (c) 2019, khast3x 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, this 15 | list of conditions and the following disclaimer in the documentation and/or 16 | other materials provided with the distribution. 17 | 18 | * Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from this 20 | software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 26 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 29 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 30 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 31 | OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | -------------------------------------------------------------------------------- /PyPi.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | h8mail 3 | ====== 4 | 5 | .. image:: https://i.postimg.cc/LXR6Jq8Y/logo-transparent.png 6 | ::target: https://github.com/khast3x/h8mail 7 | 8 | .. image:: https://img.shields.io/pypi/v/h8mail.svg 9 | :target: https://pypi.python.org/pypi/h8mail 10 | 11 | .. image:: https://img.shields.io/travis/khast3x/h8mail.svg 12 | :target: https://travis-ci.org/khast3x/h8mail 13 | 14 | 15 | 16 | 17 | 18 | 19 | Email OSINT and password breach hunting. Use h8mail to find passwords through different breach and reconnaissance services, or the infamous Breached Compilation torrent 20 | 21 | 22 | * Free software: BSD license 23 | 24 | 25 | Features 26 | -------- 27 | 28 | * Email pattern matching (reg exp), useful for reading from other tool outputs 29 | * Loosey patterns for local searchs ("john.smith", "evilcorp") 30 | * Painless install. Available through `pip`, only requires `requests` 31 | * Small and fast Alpine Dockerfile available 32 | * CLI or Bulk file-reading for targeting 33 | * Output to CSV file 34 | * Compatible with the "Breach Compilation" torrent scripts 35 | * Search .txt and .gz files locally using multiprocessing 36 | * Compatible with "Collection#1" 37 | * Get related emails 38 | * Chase and target related emails in ongoing search 39 | * Supports premium lookup services for advanced users 40 | * Regroup breach results for all targets and methods 41 | * Delicious colors 42 | 43 | Credits 44 | ------- 45 | 46 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 47 | 48 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 49 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 50 | 51 | -------------------------------------------------------------------------------- /h8mail/utils/intelx_helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from .intelx import intelx as i 4 | from time import sleep 5 | from .colors import colors as c 6 | 7 | 8 | def intelx_getsearch(target, intelx, maxfile): 9 | 10 | c.info_news("[" + target + "]>[intelx.io] Starting search") 11 | cap = intelx.GET_CAPABILITIES() 12 | # print(cap["buckets"]) 13 | # import json 14 | # print(json.dumps(cap, indent=4)) 15 | c.info_news( 16 | "[" 17 | + target 18 | + "]>[intelx.io] Search credits remaining : {creds}".format( 19 | creds=cap["paths"]["/intelligent/search"]["Credit"] 20 | ) 21 | ) 22 | available_buckets = [] 23 | desired_buckets=["leaks.public", "leaks.private", "pastes", "darknet"], 24 | for b in cap["buckets"]: 25 | if any(b in d for d in desired_buckets): 26 | available_buckets.append(b) 27 | c.info_news("[" + target + "]>[intelx.io] Available buckets for h8mail:") 28 | print(available_buckets) 29 | c.info_news("[" + target + "]>[intelx.io] Search in progress (max results : "+ str(maxfile) + ")") 30 | search = intelx.search( 31 | target, 32 | buckets=available_buckets, 33 | maxresults=maxfile, 34 | media=24, 35 | ) 36 | c.good_news("[" + target + "]>[intelx.io] Search returned the following files :") 37 | for record in search["records"]: 38 | c.good_news("Name: " + record["name"]) 39 | c.good_news("Bucket: " + record["bucket"]) 40 | c.good_news( 41 | "Size: " + "{:,.0f}".format(record["size"] / float(1 << 20)) + " MB" 42 | ) 43 | c.info_news("Storage ID: " + record["storageid"]) 44 | print("----------") 45 | return search 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to h8mail 2 | 3 | *First of all, thank you for considering contributing :purple_heart:* 4 | *h8mail is very much a script turned into a tool. As such, the project is still evolving in regards to contribution guidelines.* 5 | 6 | *If this is your first contribution, I highly suggest you check [this repo](https://github.com/firstcontributions/first-contributions) first.* 7 | 8 | ---- 9 | 10 | 11 | ### The basics 12 | 13 | * h8mail should only depend on the `requests` package 14 | 15 | * If adding a new feature or refactoring, consider opening an issue or sending me an email beforehand to ensure things go smoothly 16 | 17 | * Comment your functions as docstrings 18 | 19 | * Pull requests should be merged into the latest development branch, not in the main branch 20 | * If there is no on-going development branch, there should still be the current version's dev branch 21 | * If unsure, open an issue and you'll be pointed to the right branch, or a seperate branch can be created for you 22 | 23 | * Make sure your code passes the included tests. You can run them by using `python3 -m unittest` in h8mail's top level directory 24 | 25 | * Code should be formatted using [Python Black](https://github.com/psf/black). Most IDE's can run `black` directly. You can also launch it [from CLI](https://github.com/psf/black#installation-and-usage) 26 | 27 | ---- 28 | 29 | ### Ways to contribute 30 | 31 | * Bug reports using issues 32 | * Bug fixes *(please open issues for typos)* 33 | * Code refactoring *(please open issue beforehand if > 2 lines)* 34 | * Code optimisation 35 | * New features 36 | * New API integrations 37 | 38 | ---- 39 | 40 | ### Code of conduct 41 | 42 | h8mail follows [Django's code of conduct](https://www.djangoproject.com/conduct/). 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | from setuptools import setup, find_packages, Extension 6 | from distutils.util import convert_path 7 | import os 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | exec(open(os.path.join(here, "h8mail/utils/version.py")).read()) 11 | 12 | with open("PyPi.rst") as readme_file: 13 | readme = readme_file.read() 14 | 15 | # with open("HISTORY.rst") as history_file: 16 | # history = history_file.read() 17 | 18 | requirements = ["requests"] 19 | 20 | setup_requirements = ["requests"] 21 | 22 | test_requirements = ["requests"] 23 | 24 | setup( 25 | author="khast3x", 26 | author_email="k@khast3x.club", 27 | classifiers=[ 28 | "Development Status :: 5 - Production/Stable", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: BSD License", 31 | "Natural Language :: English", 32 | "Programming Language :: Python :: 3.6", 33 | "Programming Language :: Python :: 3.7", 34 | "Environment :: Console", 35 | ], 36 | description="Email OSINT and password breach hunting. Use h8mail to find passwords through different breach and reconnaissance services, or using your local data", 37 | install_requires=requirements, 38 | license="BSD license", 39 | # long_description_content_type="text/markdown", 40 | long_description=readme + "\n\n", 41 | # long_description=readme + "\n\n" + history, 42 | include_package_data=True, 43 | keywords="h8mail", 44 | name="h8mail", 45 | packages=find_packages(), 46 | entry_points={"console_scripts": ["h8mail = h8mail.__main__:main"]}, 47 | setup_requires=setup_requirements, 48 | url="https://github.com/khast3x/h8mail", 49 | version=__version__, 50 | zip_safe=False, 51 | ) 52 | -------------------------------------------------------------------------------- /h8mail/utils/chase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .colors import colors as c 5 | import re 6 | 7 | def chase(target, user_args): 8 | """ 9 | Takes the current target & returns adequate chase list 10 | to add to the current target list 11 | """ 12 | new_targets = [] 13 | chase_counter = 0 14 | if user_args.debug: 15 | print(c.fg.red, "\nCHASING DEBUG-----------") 16 | print(f"Hunting targets from {target.target}") 17 | print(f"Recursive Chase Stop is at {user_args.chase_limit}" + c.reset) 18 | if user_args.chase_limit > 0: 19 | for d in target.data: 20 | if len(d) != 2: 21 | continue 22 | 23 | if user_args.power_chase: 24 | if "RELATED" in d[0] or "EMAIL" in d[0]: 25 | c.good_news( 26 | "Chasing {new_target} as new target".format(new_target=d[1]) 27 | ) 28 | new_targets.append(d[1]) 29 | else: # in case there is an email as a username 30 | e = re.findall(r"[\w\.-]+@[\w\.-]+", d[1]) 31 | if e: 32 | for email in e: 33 | c.good_news( 34 | "Chasing {new_target} as new target (found as pattern)".format( 35 | new_target=d[1] 36 | ) 37 | ) 38 | new_targets.append(d[1]) 39 | 40 | else: 41 | if "HUNTER_RELATED" in d[0]: 42 | c.good_news( 43 | "Chasing {new_target} as new target".format(new_target=d[1]) 44 | ) 45 | new_targets.append(d[1]) 46 | return new_targets 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # h8mail 10 | 11 | my_config* 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | .vscode/settings.json 108 | h8mail_config.ini 109 | h8mail_keys.ini 110 | h8mail_config.ini 111 | keysbackup.ini 112 | .vscode/launch.json 113 | file1.bin 114 | *.txt 115 | test.json 116 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := 3 -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | black h8mail tests 55 | 56 | test: ## run tests quickly with the default Python 57 | python3 setup.py test 58 | 59 | 60 | coverage: ## check code coverage quickly with the default Python 61 | coverage run --source h8mail setup.py test 62 | coverage report -m 63 | coverage html 64 | $(BROWSER) htmlcov/index.html 65 | 66 | release: dist ## package and upload a release 67 | twine upload dist/* 68 | 69 | dist: clean ## builds source and wheel package 70 | python3 setup.py sdist 71 | python3 setup.py bdist_wheel 72 | ls -l dist 73 | 74 | install: clean ## install the package to the active Python's site-packages 75 | python3 setup.py install 76 | -------------------------------------------------------------------------------- /h8mail/utils/print_results.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .colors import colors as c 3 | from time import sleep 4 | 5 | def print_results(results, hide=False): 6 | 7 | for t in results: 8 | print() 9 | c.print_res_header(t.target) 10 | for i in range(len(t.data)): 11 | # sleep(0.001) 12 | if len(t.data) == 1: 13 | print() 14 | c.info_news("No results founds") 15 | continue 16 | if len(t.data[i]) >= 2: # Contains header + body data 17 | if hide: 18 | if "PASS" in t.data[i][0]: 19 | c.print_result( 20 | t.target, t.data[i][1][:4] + "********", t.data[i][0] 21 | ) 22 | continue 23 | if "LOCAL" in t.data[i][0]: 24 | c.print_result( 25 | t.target, t.data[i][1][:-5] + "********", t.data[i][0] 26 | ) 27 | continue 28 | if "HIBP" in t.data[i][0]: 29 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 30 | if "HUNTER_PUB" in t.data[i][0]: 31 | c.print_result( 32 | t.target, str(t.data[i][1]) + " RELATED EMAILS", "HUNTER_PUB" 33 | ) 34 | if "HUNTER_RELATED" in t.data[i][0]: 35 | c.print_result(t.target, t.data[i][1], "HUNTER_RELATED") 36 | if "EMAILREP" in t.data[i][0]: 37 | c.print_result( 38 | t.target, str(t.data[i][1]), t.data[i][0] 39 | ) 40 | if "SNUS" in t.data[i][0]: 41 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 42 | if "LOCAL" in t.data[i][0]: 43 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 44 | if "BC_PASS" in t.data[i][0]: 45 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 46 | if "LEAKLOOKUP" in t.data[i][0]: 47 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 48 | if "LKLP" in t.data[i][0]: 49 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 50 | if "WLI" in t.data[i][0]: 51 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 52 | if "SCYLLA" in t.data[i][0]: 53 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 54 | if "DHASHD" in t.data[i][0]: 55 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 56 | if "INTELX" in t.data[i][0]: 57 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 58 | if "BREACHDR" in t.data[i][0]: 59 | c.print_result(t.target, t.data[i][1], t.data[i][0]) 60 | 61 | -------------------------------------------------------------------------------- /h8mail/utils/url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | import requests 7 | 8 | from .colors import colors as c 9 | from .version import __version__ 10 | from .helpers import get_emails_from_file 11 | from .helpers import fetch_emails 12 | 13 | def fetch_urls(target): 14 | """ 15 | Returns a list of URLs found in 'target'. 16 | """ 17 | # https://www.geeksforgeeks.org/python-check-url-string/ 18 | e = re.findall( 19 | "http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\), ]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", 20 | target, 21 | ) 22 | if e: 23 | # print(", ".join(e), c.reset) 24 | return e 25 | return None 26 | 27 | 28 | def get_urls_from_file(targets_file): 29 | """ 30 | For each line in file, check for URLs using fetch_urls(). 31 | Returns list of URLs. 32 | """ 33 | email_obj_list = [] 34 | c.info_news("Parsing urls from \t" + targets_file) 35 | try: 36 | target_fd = open(targets_file).readlines() 37 | for line in target_fd: 38 | e = fetch_urls(line) 39 | if e is None: 40 | continue 41 | else: 42 | email_obj_list.extend(e) 43 | return email_obj_list 44 | except Exception as ex: 45 | c.bad_news("Problems occurred while trying to get URLs from file") 46 | print(ex) 47 | 48 | 49 | 50 | def worker_url(url): 51 | """ 52 | Fetches the URL without the h8mail UA 53 | """ 54 | paramsUA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"} 55 | try: 56 | c.info_news("Worker fetching " + url) 57 | r = requests.get(url, params = paramsUA, allow_redirects=False) 58 | c.info_news("Worker done fetch url") 59 | print(f"Status code: {r.status_code}") 60 | 61 | e = re.findall(r"[\w\.-]+@[\w\.-]+", r.text) 62 | print(e) 63 | if e: 64 | print(", ".join(e), c.reset) 65 | return e 66 | return None 67 | except Exception as ex: 68 | c.bad_news("URL fetch worker error:") 69 | print(ex) 70 | 71 | 72 | def target_urls(user_args): 73 | """ 74 | For each user input with --url, check if its a file. 75 | If yes open and parse each line with regexp, else parse the input with regexp directly. 76 | Parse html pages from URLs for email patterns. 77 | Returns list of email targets 78 | """ 79 | try: 80 | c.info_news("Starting URL fetch") 81 | urls = [] 82 | emails = [] 83 | for arg in user_args.user_urls: 84 | if os.path.isfile(arg): 85 | e = get_urls_from_file(arg) 86 | else: 87 | e = fetch_urls(arg) 88 | if e is None: 89 | continue 90 | else: 91 | urls.extend(e) 92 | 93 | for url in urls: 94 | e = worker_url(url) 95 | # e = get_emails_from_file(tmpfile, user_args) 96 | if e is None: 97 | continue 98 | else: 99 | emails.extend(e) 100 | 101 | return emails 102 | except Exception as ex: 103 | c.bad_news("URL fetch error:") 104 | print(ex) 105 | 106 | -------------------------------------------------------------------------------- /h8mail/utils/breachcompilation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .colors import colors as c 3 | import os 4 | import re 5 | import stat 6 | from string import printable 7 | from .localsearch import local_search 8 | 9 | def check_shell(): 10 | shell = os.environ['SHELL'] 11 | print("SHELL IS " + shell) 12 | if "bash" not in shell: 13 | c.info_news("If you're having issues or not results, be sure to launch h8mail using bash for this operation.") 14 | c.info_news("OSX users should read this https://khast3x.club/posts/2021-02-17-h8mail-with-COMB/#targeting-emails\n") 15 | 16 | 17 | def clean_targets(targets): 18 | """ 19 | This function is necessary since local_search performs a loose search. 20 | We'll double check results to ensure ONLY target email is present is results 21 | """ 22 | for t in targets: 23 | cleaned_data = [] 24 | for d in t.data: 25 | if d: 26 | found_email = re.split("[;:]",d[1])[0] 27 | if found_email == t.target: 28 | # We rebuild the expected data array ["BC_PASS", "password"] 29 | new_data = [d[0], re.split("[;:]",d[1])[-1]] 30 | cleaned_data.append(new_data) 31 | else: 32 | c.info_news("Removing " + d[1] + " (cleaning function)") 33 | t.data = cleaned_data 34 | 35 | return targets 36 | 37 | def breachcomp_check(targets, breachcomp_path): 38 | breachcomp_path = os.path.join(breachcomp_path, "data") 39 | for t in targets: 40 | if len(t.target): 41 | for i in range(len(t.target)): 42 | if t.target[i].isalnum(): 43 | next_dir_to_test = os.path.join(breachcomp_path, t.target[i]) 44 | else: 45 | next_dir_to_test = os.path.join(breachcomp_path, "symbols") 46 | if os.path.isdir(next_dir_to_test): 47 | breachcomp_path = next_dir_to_test 48 | else: 49 | if os.path.isfile(next_dir_to_test): 50 | found_list = local_search([next_dir_to_test], [t.target]) 51 | for f in found_list: 52 | t.pwned += 1 53 | t.data.append(("BC_PASS", f.content.strip())) 54 | else: 55 | c.bad_news(next_dir_to_test + " is neither a file or directory") 56 | 57 | break 58 | 59 | targets = clean_targets(targets) 60 | return targets 61 | 62 | def old_breachcomp_check(targets, breachcomp_path): 63 | # https://gist.github.com/scottlinux/9a3b11257ac575e4f71de811322ce6b3 64 | try: 65 | import subprocess 66 | 67 | check_shell() 68 | query_bin = os.path.join(breachcomp_path, "query.sh") 69 | st = os.stat(query_bin) 70 | os.chmod(query_bin, st.st_mode | stat.S_IEXEC) 71 | for t in targets: 72 | c.info_news(f"Looking up {t.target} in BreachCompilation") 73 | procfd = subprocess.run([query_bin, t.target], stdout=subprocess.PIPE) 74 | try: 75 | output = procfd.stdout.decode("cp437") 76 | except Exception as e: 77 | c.bad_news(f"Could not decode bytes for {t.target} results") 78 | output = procfd.stdout 79 | # print(output[:85], "[...]") 80 | print(output) 81 | continue 82 | if len(output) != 0: 83 | split_output = output.split("\n") 84 | for line in split_output: 85 | if ":" in line: 86 | t.pwned += 1 87 | t.data.append(("BC_PASS", re.split("[;:]",line)[-1])) 88 | c.good_news( 89 | f"Found BreachedCompilation entry {line}" 90 | ) 91 | return targets 92 | except Exception as e: 93 | c.bad_news("Breach compilation") 94 | print(e) 95 | return targets 96 | -------------------------------------------------------------------------------- /tests/test_h8mail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `h8mail` package.""" 5 | 6 | 7 | import unittest 8 | import sys 9 | import time 10 | import tempfile 11 | import shutil 12 | import contextlib 13 | import os 14 | import tarfile 15 | import gzip 16 | import argparse 17 | from h8mail.utils import run 18 | from h8mail.utils import classes 19 | from h8mail.utils import helpers 20 | from h8mail.utils import localsearch 21 | from h8mail.utils import localgzipsearch 22 | 23 | def print_test_banner(testname): 24 | print("========================") 25 | print("========================") 26 | print("\tTESTING: "+testname) 27 | print("========================") 28 | print("========================") 29 | 30 | 31 | def make_temp_directory(): 32 | emails = """ 33 | john.smith@gmail.com 34 | john.smith@gmail.com 35 | fijsdhkfnhqsdkf 36 | fdqfqsdff 37 | test@evilcorp.com 38 | notfound@email.com 39 | """ 40 | creds = """ 41 | john.smith@gmail.com:SecretPASS 42 | bloblfd 43 | fjsdkf,ds 44 | test@evilcorp.com:An0therSECRETpassw0rd 45 | ddqsdqs 46 | """ 47 | temp_dir = tempfile.mkdtemp() 48 | try: 49 | fd_emails = open(os.path.join(temp_dir, "test-emails.txt"), "w") 50 | fd_emails.writelines(emails) 51 | fd_emails.close() 52 | fd_creds = open(os.path.join(temp_dir, "test-creds.txt"), "w") 53 | fd_creds.writelines(creds) 54 | fd_creds.close() 55 | tar = tarfile.open(os.path.join(temp_dir, "test-creds.tar.gz"), "w:gz") 56 | tar.add(os.path.join(temp_dir, "test-creds.txt")) 57 | tar.close() 58 | 59 | return temp_dir 60 | except Exception as e: 61 | print(e) 62 | 63 | 64 | class TestH8mail(unittest.TestCase): 65 | """Tests for `h8mail` package.""" 66 | 67 | def setUp(self): 68 | """Generating local files""" 69 | self.temp_dir = make_temp_directory() 70 | print("Created Temp Dir: " + self.temp_dir) 71 | print(os.listdir(self.temp_dir)) 72 | self.filetargets = os.path.join(self.temp_dir, "test-emails.txt") 73 | self.filetxt = os.path.join(self.temp_dir, "test-creds.txt") 74 | self.filegz = os.path.join(self.temp_dir, "test-creds.tar.gz") 75 | print("Test files generated in : " + self.temp_dir) 76 | 77 | # a = open(self.filetxt, "r") 78 | # print(a.readlines()) 79 | 80 | def tearDown(self): 81 | """Cleaning temp files""" 82 | print("Removing dir + content: " + self.temp_dir) 83 | shutil.rmtree(self.temp_dir) 84 | 85 | def test_000_simple(self): 86 | """Simple test""" 87 | run.print_banner() 88 | print_test_banner("VANILLA") 89 | 90 | user_args = run.parse_args(["-t", "test@example.com"]) 91 | run.h8mail(user_args) 92 | 93 | def test_002_local_files_txt_gz(self): 94 | """Local file search Test""" 95 | run.print_banner() 96 | print_test_banner("TXT LOCAL") 97 | user_args_lb = run.parse_args(["-t", self.filetargets, "-lb", self.filetxt, "-sk"]) 98 | run.h8mail(user_args_lb) 99 | print_test_banner("TXT LOCAL-SINGLFILE") 100 | user_args_lb = run.parse_args(["-t", self.filetargets, "-lb", self.filetxt, "-sk", "-sf"]) 101 | run.h8mail(user_args_lb) 102 | 103 | run.print_banner() 104 | print_test_banner("GZ LOCAL") 105 | user_args_gz = run.parse_args(["-t", self.filetargets, "-gz", self.filegz, "-sk"]) 106 | run.h8mail(user_args_gz) 107 | print_test_banner("GZ LOCAL-SINGLEFILE") 108 | user_args_gz = run.parse_args(["-t", self.filetargets, "-gz", self.filegz, "-sk", "-sf"]) 109 | run.h8mail(user_args_gz) 110 | 111 | def test_003_url(self): 112 | run.print_banner() 113 | print_test_banner("URL-RAW") 114 | user_args_lb = run.parse_args(["-u", "https://raw.githubusercontent.com/khast3x/h8mail/master/tests/test_email.txt"]) 115 | run.h8mail(user_args_lb) 116 | run.print_banner() 117 | print_test_banner("URL-MESSY") 118 | user_args_lb = run.parse_args(["-u", "https://raw.githubusercontent.com/khast3x/h8mail/master/tests/test_email.txt"]) 119 | run.h8mail(user_args_lb) 120 | -------------------------------------------------------------------------------- /h8mail/utils/localgzipsearch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from multiprocessing import Pool 4 | from .classes import local_breach_target 5 | from .colors import colors as c 6 | from .localsearch import raw_in_count, progress 7 | import gzip 8 | import os 9 | import sys 10 | import signal 11 | 12 | 13 | def progress_gzip(count): 14 | """ 15 | Prints count without rewriting to stdout 16 | """ 17 | sys.stdout.write("Lines checked:%i\r" % (count)) 18 | sys.stdout.write("\033[K") 19 | 20 | 21 | def gzip_worker(filepath, target_list): 22 | """ 23 | Searches for every email from target_list in every line of filepath. 24 | Uses python native gzip lib to decompress file line by line. 25 | Archives with multiple files are read as long single files. 26 | Attempts to decode line using cp437. If it fails, catch and use raw data. 27 | """ 28 | try: 29 | found_list = [] 30 | size = os.stat(filepath).st_size 31 | with gzip.open(filepath, "r") as gzipfile: 32 | c.info_news( 33 | "Worker [{PID}] is searching for targets in {filepath} ({size:,.0f} MB)".format( 34 | PID=os.getpid(), filepath=filepath, size=size / float(1 << 20) 35 | ) 36 | ) 37 | for cnt, line in enumerate(gzipfile): 38 | for t in target_list: 39 | if t in str(line): 40 | try: 41 | decoded = str(line, "cp437") 42 | found_list.append( 43 | local_breach_target(t, filepath, cnt, decoded) 44 | ) 45 | c.good_news( 46 | f"Found occurrence [{filepath}] Line {cnt}: {decoded}" 47 | ) 48 | except Exception as e: 49 | c.bad_news( 50 | f"Got a decoding error line {cnt} - file: {filepath}" 51 | ) 52 | c.good_news( 53 | f"Found occurrence [{filepath}] Line {cnt}: {line}" 54 | ) 55 | found_list.append( 56 | local_breach_target(t, filepath, cnt, str(line)) 57 | ) 58 | return found_list 59 | except Exception as e: 60 | c.bad_news("Something went wrong with gzip worker") 61 | print(e) 62 | 63 | 64 | def local_gzip_search(files_to_parse, target_list): 65 | """ 66 | Receives list of all files to check for target_list. 67 | Starts a worker pool, one worker per file. 68 | Return list of local_breach_targets objects to be tranformed in target objects. 69 | """ 70 | original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) 71 | pool = Pool() 72 | found_list = [] 73 | signal.signal(signal.SIGINT, original_sigint_handler) 74 | # Launch 75 | try: 76 | async_results = [ 77 | pool.apply_async(gzip_worker, args=(f, target_list)) 78 | for i, f in enumerate(files_to_parse) 79 | ] 80 | for r in async_results: 81 | if r.get() is not None: 82 | found_list.extend(r.get(60)) 83 | except KeyboardInterrupt: 84 | c.bad_news("Caught KeyboardInterrupt, terminating workers") 85 | pool.terminate() 86 | else: 87 | c.info_news("Terminating worker pool") 88 | pool.close() 89 | pool.join() 90 | return found_list 91 | 92 | 93 | def local_search_single_gzip(files_to_parse, target_list): 94 | """ 95 | Single process searching of every target_list emails, in every files_to_parse list. 96 | To be used when stability for big files is a priority 97 | Return list of local_breach_target objects to be tranformed in target objects 98 | """ 99 | found_list = [] 100 | for file_to_parse in files_to_parse: 101 | with gzip.open(file_to_parse, "r") as fp: 102 | size = os.stat(file_to_parse).st_size 103 | c.info_news( 104 | f"Searching for targets in {file_to_parse} ({size} bytes)" 105 | ) 106 | for cnt, line in enumerate(fp): 107 | progress_gzip(cnt) 108 | for t in target_list: 109 | if t in str(line): 110 | try: 111 | decoded = str(line, "cp437") 112 | found_list.append( 113 | local_breach_target(t, file_to_parse, cnt, decoded) 114 | ) 115 | c.good_news( 116 | f"Found occurrence [{file_to_parse}] Line {cnt}: {decoded}" 117 | ) 118 | except: 119 | c.bad_news( 120 | f"Got a decoding error line {cnt} - file: {file_to_parse}" 121 | ) 122 | c.good_news( 123 | f"Found occurrence [{file_to_parse}] Line {cnt}: {line}" 124 | ) 125 | found_list.append( 126 | local_breach_target(t, file_to_parse, cnt, str(line)) 127 | ) 128 | return found_list 129 | -------------------------------------------------------------------------------- /h8mail/utils/localsearch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import signal 7 | 8 | from multiprocessing import Pool 9 | from itertools import takewhile, repeat 10 | from .classes import local_breach_target 11 | from .colors import colors as c 12 | 13 | 14 | def local_to_targets(targets, local_results, user_args): 15 | """ 16 | Appends data from local_breach_target objects using existing list of targets. 17 | Finds corresponding email in dest object list, and adds data to the t.data object variable. 18 | Full output line is stored in t.data[1] and original found data in t.data[2] 19 | 20 | """ 21 | for t in targets: 22 | for l in local_results: 23 | if l.target == t.target: 24 | t.data.append( 25 | ( 26 | "LOCALSEARCH", 27 | f"[{os.path.basename(l.filepath)}] Line {l.line}: {l.content}".strip(), 28 | l.content.strip(), 29 | ) 30 | ) 31 | t.pwned += 1 32 | if user_args.debug: 33 | c.debug_news(f"DEBUG: Found following content matching {t.target.target}") 34 | l.target.dump() 35 | return targets 36 | 37 | 38 | def raw_in_count(filename): 39 | """ 40 | StackOverflow trick to rapidly count lines in big files. 41 | Returns total line number. 42 | """ 43 | c.info_news("Identifying total line number...") 44 | f = open(filename, "rb") 45 | bufgen = takewhile(lambda x: x, (f.raw.read(1024 * 1024) for _ in repeat(None))) 46 | return sum(buf.count(b"\n") for buf in bufgen) 47 | 48 | 49 | def worker(filepath, target_list): 50 | """ 51 | Searches for every email from target_list in every line of filepath. 52 | Attempts to decode line using cp437. If it fails, catch and use raw data 53 | """ 54 | try: 55 | with open(filepath, "rb") as fp: 56 | found_list = [] 57 | size = os.stat(filepath).st_size 58 | c.info_news( 59 | "Worker [{PID}] is searching for targets in {filepath} ({size:,.0f} MB)".format( 60 | PID=os.getpid(), filepath=filepath, size=size / float(1 << 20)) 61 | ) 62 | for cnt, line in enumerate(fp): 63 | for t in target_list: 64 | if t in str(line, "cp437"): 65 | try: 66 | decoded = str(line, "cp437") 67 | found_list.append( 68 | local_breach_target(t, filepath, cnt, decoded) 69 | ) 70 | c.good_news( 71 | f"Found occurrence [{filepath}] Line {cnt}: {decoded}" 72 | ) 73 | except Exception as e: 74 | c.bad_news( 75 | f"Got a decoding error line {cnt} - file: {filepath}" 76 | ) 77 | c.good_news( 78 | f"Found occurrence [{filepath}] Line {cnt}: {line}" 79 | ) 80 | found_list.append( 81 | local_breach_target(t, filepath, cnt, str(line)) 82 | ) 83 | return found_list 84 | except Exception as e: 85 | c.bad_news("Something went wrong with worker") 86 | print(e) 87 | 88 | 89 | def local_search(files_to_parse, target_list): 90 | original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) 91 | pool = Pool() 92 | found_list = [] 93 | signal.signal(signal.SIGINT, original_sigint_handler) 94 | try: 95 | async_results = [ 96 | pool.apply_async(worker, args=(f, target_list)) 97 | for i, f in enumerate(files_to_parse) 98 | ] 99 | for r in async_results: 100 | if r.get() is not None: 101 | found_list.extend(r.get(60)) 102 | except KeyboardInterrupt: 103 | c.bad_news("Caught KeyboardInterrupt, terminating workers") 104 | pool.terminate() 105 | else: 106 | c.info_news("Terminating worker pool") 107 | pool.close() 108 | pool.join() 109 | return found_list 110 | 111 | 112 | def progress(count, total, status=""): 113 | bar_len = 60 114 | filled_len = int(round(bar_len * count / float(total))) 115 | 116 | percents = round(100.0 * count / float(total), 1) 117 | bar = "=" * filled_len + "-" * (bar_len - filled_len) 118 | 119 | sys.stdout.write("[%s] %s%s ...%s\r" % (bar, percents, "%", status)) 120 | sys.stdout.write("\033[K") 121 | 122 | 123 | sys.stdout.flush() 124 | 125 | 126 | def local_search_single(files_to_parse, target_list): 127 | found_list = [] 128 | for file_to_parse in files_to_parse: 129 | with open(file_to_parse, "rb") as fp: 130 | size = os.stat(file_to_parse).st_size 131 | lines_no = raw_in_count(file_to_parse) 132 | c.info_news( 133 | f"Searching for targets in {file_to_parse} ({size} bytes, {lines_no} lines)" 134 | ) 135 | for cnt, line in enumerate(fp): 136 | lines_left = lines_no - cnt 137 | progress( 138 | cnt, lines_no, f"{cnt} lines checked - {lines_left} lines left" 139 | ) 140 | for t in target_list: 141 | if t in str(line): 142 | try: 143 | decoded = str(line, "cp437") 144 | found_list.append( 145 | local_breach_target(t, file_to_parse, cnt, decoded) 146 | ) 147 | c.good_news( 148 | f"Found occurrence [{file_to_parse}] Line {cnt}: {decoded}" 149 | ) 150 | except: 151 | c.bad_news( 152 | f"Got a decoding error line {cnt} - file: {file_to_parse}" 153 | ) 154 | c.good_news( 155 | f"Found occurrence [{file_to_parse}] Line {cnt}: {line}" 156 | ) 157 | found_list.append( 158 | local_breach_target(t, file_to_parse, cnt, str(line)) 159 | ) 160 | return found_list 161 | -------------------------------------------------------------------------------- /h8mail/utils/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | from .colors import colors as c 6 | import configparser 7 | import csv 8 | import os 9 | import glob 10 | from .version import __version__ 11 | import requests 12 | import json 13 | 14 | 15 | def find_files(to_parse, pattern=""): 16 | """ 17 | Returns list of files from t_parse filepath. 18 | Supports using globing (*) in filepaths. 19 | Can check for patterns such as 'gz'. 20 | """ 21 | allfiles = [] 22 | if "*" in to_parse: 23 | glob_result = glob.glob(to_parse) 24 | for g in glob_result: 25 | allfiles.append(g) 26 | c.info_news(f"Using file {g}") 27 | if os.path.isfile(to_parse): 28 | if pattern in to_parse: 29 | c.info_news(f"Using file {to_parse}") 30 | allfiles.append(to_parse) 31 | elif os.path.isdir(to_parse): 32 | for root, _, filenames in os.walk(to_parse): 33 | for filename in filenames: 34 | if pattern in filename: 35 | c.info_news("Using file {}".format(os.path.join(root, filename))) 36 | allfiles.append(os.path.join(root, filename)) 37 | return allfiles 38 | 39 | 40 | def print_banner(b_type="intro"): 41 | if "intro" in b_type: 42 | banner = """ 43 | ._____. ._____. ;____________; 44 | | ._. | | ._. | ; h8mail ; 45 | | !_| |_|_|_! | ;------------; 46 | !___| |_______! Heartfelt Email OSINT 47 | .___|_|_| |___. Use responsibly 48 | | ._____| |_. | ;____________________; 49 | | !_! | | !_! | ; github.com/khast3x ; 50 | !_____! !_____! ;--------------------; 51 | """ 52 | # print(c.bold, c.fg.pink, banner, c.reset) 53 | banner_tab = banner.splitlines() 54 | code = 33 55 | keep = True 56 | for b in banner_tab: 57 | clr = "\u001b[38;5;" + str(code) + "m " 58 | print(c.bold + clr + b + c.reset) 59 | if keep: 60 | code += 36 61 | keep = False 62 | else: 63 | keep = True 64 | elif "warn" in b_type: 65 | print( 66 | c.fg.lightgrey, 67 | "\t\t Official h8mail posts: \n\t\t https://khast3x.club/tags/h8mail/\n\n", 68 | c.reset, 69 | ) 70 | elif "version" in b_type: 71 | print( 72 | "\t", 73 | c.fg.cyan, 74 | "Version " + __version__ + ' - "ROCKSMASSON.6" ', 75 | c.reset, 76 | ) 77 | 78 | 79 | def fetch_emails(target, user_args): 80 | """ 81 | Returns a list of emails found in 'target'. 82 | Can be loosy to skip email pattern search. 83 | """ 84 | if user_args.loose or user_args.user_query is not None: 85 | t = target.split(" ") 86 | # print(t) 87 | return t 88 | e = re.findall(r"[\w\.-]+@[\w\.-]+", target) 89 | if e: 90 | # print(", ".join(e), c.reset) 91 | return e 92 | return None 93 | 94 | 95 | def get_emails_from_file(targets_file, user_args): 96 | """ 97 | For each line in file, check for emails using fetch_emails(). 98 | Returns list of emails. 99 | """ 100 | email_obj_list = [] 101 | try: 102 | target_fd = open(targets_file).readlines() 103 | c.info_news("Parsing emails from" + targets_file) 104 | for line in target_fd: 105 | e = fetch_emails(line.strip(), user_args) 106 | if e is None: 107 | continue 108 | else: 109 | email_obj_list.extend(e) 110 | return email_obj_list 111 | except Exception as ex: 112 | c.bad_news("Problems occurred while trying to get emails from file") 113 | print(ex) 114 | 115 | 116 | def get_config_from_file(user_args): 117 | """ 118 | Read config in file. If keys are passed using CLI, add them to the configparser object. 119 | Returns a configparser object already set to "DEFAULT" section. 120 | """ 121 | 122 | try: 123 | config = configparser.ConfigParser() 124 | # Config file 125 | if user_args.config_file: 126 | for counter, config_file in enumerate(user_args.config_file): 127 | config_file = user_args.config_file[counter] 128 | config.read(config_file) 129 | # Use -k option 130 | if user_args.cli_apikeys: 131 | if config.has_section("h8mail") is False: 132 | config.add_section("h8mail") 133 | for counter, user_key in enumerate(user_args.cli_apikeys): 134 | user_cli_keys = user_args.cli_apikeys[counter].split(",") 135 | for user_key in user_cli_keys: 136 | if user_key and "=" in user_key: 137 | config.set( 138 | "h8mail", 139 | user_key.split("=", maxsplit=1)[0].strip(), 140 | user_key.split("=", maxsplit=1)[1].strip(), 141 | ) 142 | for k in config["h8mail"]: 143 | if len((config["h8mail"][k])) != 0: 144 | c.good_news(f"Found {k} configuration key") 145 | return config["h8mail"] 146 | except Exception as ex: 147 | c.bad_news("Problems occurred while trying to get configuration file") 148 | print(ex) 149 | 150 | 151 | def save_results_csv(dest_csv, target_obj_list): 152 | """ 153 | Outputs CSV from target object list. 154 | Dumps the target.data object variable into CSV file. 155 | """ 156 | with open(dest_csv, "w", newline="") as csvfile: 157 | try: 158 | writer = csv.writer(csvfile) 159 | 160 | writer.writerow(["Target", "Type", "Data"]) 161 | c.good_news("Writing to CSV") 162 | for t in target_obj_list: 163 | for i in range(len(t.data)): 164 | if len(t.data[i]) == 2: # Contains data header + body 165 | writer.writerow([t.target, t.data[i][0], t.data[i][1]]) 166 | except Exception as ex: 167 | c.bad_news("Error writing to csv") 168 | print(ex) 169 | 170 | 171 | def check_latest_version(): 172 | """ 173 | Fetches local version and compares it to github api tag version 174 | """ 175 | try: 176 | response = requests.request( 177 | url="https://api.github.com/repos/khast3x/h8mail/releases/latest", method="GET" 178 | ) 179 | data = response.json() 180 | latest = data["tag_name"] 181 | if __version__ == data["tag_name"]: 182 | c.good_news("h8mail is up to date") 183 | else: 184 | c.bad_news( 185 | "Not running latest h8mail version. [Current: {current} | Latest: {latest}]".format( 186 | current=__version__, latest=latest 187 | ) 188 | ) 189 | except Exception: 190 | c.bad_news("Could not check for updates. Is Github blocking requests?") 191 | def check_scylla_online(): 192 | """ 193 | Checks if scylla.so is online 194 | """ 195 | # Supress SSL Warning on UI 196 | # https://github.com/khast3x/h8mail/issues/64 197 | try: 198 | re = requests.head( 199 | url="https://scylla.so", verify=False, auth=requests.auth.HTTPBasicAuth("sammy", "BasicPassword!"), timeout=10, 200 | ) 201 | if re.status_code == 200: 202 | c.good_news("scylla.so is up") 203 | return True 204 | else: 205 | c.info_news("scylla.so is down, skipping") 206 | return False 207 | except Exception: 208 | c.info_news("scylla.so is down, skipping") 209 | -------------------------------------------------------------------------------- /h8mail/utils/colors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class colors: 5 | """Colors class: 6 | reset all colors with colors.reset 7 | two subclasses fg for foreground and bg for background. 8 | use as colors.subclass.colorname. 9 | i.e. colors.fg.red or colors.bg.green 10 | also, the generic bold, disable, underline, reverse, strikethrough, 11 | and invisible work with the main class 12 | i.e. colors.bold 13 | """ 14 | 15 | reset = "\033[0m" 16 | bold = "\033[01m" 17 | disable = "\033[02m" 18 | underline = "\033[04m" 19 | reverse = "\033[07m" 20 | strikethrough = "\033[09m" 21 | invisible = "\033[08m" 22 | 23 | class fg: 24 | black = "\033[30m" 25 | red = "\033[31m" 26 | green = "\033[32m" 27 | orange = "\033[33m" 28 | blue = "\033[34m" 29 | purple = "\033[35m" 30 | cyan = "\033[36m" 31 | lightgrey = "\033[37m" 32 | darkgrey = "\033[90m" 33 | lightred = "\033[91m" 34 | lightgreen = "\033[92m" 35 | yellow = "\033[93m" 36 | lightblue = "\033[94m" 37 | pink = "\033[95m" 38 | lightcyan = "\033[96m" 39 | 40 | class bg: 41 | black = "\033[40m" 42 | red = "\033[41m" 43 | green = "\033[42m" 44 | orange = "\033[43m" 45 | blue = "\033[44m" 46 | purple = "\033[45m" 47 | cyan = "\033[46m" 48 | lightgrey = "\033[47m" 49 | 50 | @staticmethod 51 | def good_news(news): 52 | """ 53 | Print a Success 54 | """ 55 | print(colors.bold + colors.fg.green + "[>] " + colors.reset + news.strip()) 56 | 57 | @staticmethod 58 | def debug_news(news): 59 | """ 60 | Print a Debug 61 | """ 62 | print() 63 | print(colors.bold + colors.fg.lightred + "[@] " + news + colors.reset) 64 | 65 | @staticmethod 66 | def bad_news(news): 67 | """ 68 | Print a Failure, error 69 | """ 70 | print(colors.bold + colors.fg.red + "[!] " + colors.reset + news.strip()) 71 | 72 | @staticmethod 73 | def info_news(news): 74 | """ 75 | Print an information with grey text 76 | """ 77 | print( 78 | colors.bold 79 | + colors.fg.lightblue 80 | + "[~] " 81 | + colors.reset 82 | + colors.fg.lightgrey 83 | + news.strip() 84 | + colors.reset 85 | ) 86 | 87 | @staticmethod 88 | def question_news(news): 89 | """ 90 | Print an information with yellow text 91 | """ 92 | print( 93 | colors.bold 94 | + colors.fg.blue 95 | + "[?] " 96 | + colors.reset 97 | + colors.fg.yellow 98 | + news.strip() 99 | + colors.reset 100 | ) 101 | 102 | @staticmethod 103 | def print_result(target, data, source): 104 | """ 105 | Print Breach results 106 | """ 107 | if "PASS" in source: 108 | print( 109 | "{}{}{:15}{}|{}{:>25.25}{} > {}{}{}{}".format( 110 | colors.fg.lightblue, 111 | colors.bold, 112 | source, 113 | colors.fg.lightgrey, 114 | colors.fg.pink, 115 | target, 116 | colors.fg.lightgrey, 117 | colors.bold, 118 | colors.fg.green, 119 | data, 120 | colors.reset, 121 | ) 122 | ) 123 | elif "LOCALSEARCH" in source: 124 | if len(data) > 140: 125 | print( 126 | "{}{}{:15}{}|{}{:>25.25}{} > {}{}{}{}".format( 127 | colors.fg.lightblue, 128 | colors.bold, 129 | source, 130 | colors.fg.lightgrey, 131 | colors.fg.pink, 132 | target, 133 | colors.fg.lightgrey, 134 | colors.bold, 135 | colors.fg.green, 136 | "[...]" + data[-135:], 137 | colors.reset, 138 | ) 139 | ) 140 | else: 141 | print( 142 | "{}{}{:15}{}|{}{:>25.25}{} > {}{}{}{}".format( 143 | colors.fg.lightblue, 144 | colors.bold, 145 | source, 146 | colors.fg.lightgrey, 147 | colors.fg.pink, 148 | target, 149 | colors.fg.lightgrey, 150 | colors.bold, 151 | colors.fg.green, 152 | data, 153 | colors.reset, 154 | ) 155 | ) 156 | # Underscore to avoid coloring like a HASH 157 | elif "_HASH" in source: 158 | print( 159 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format( 160 | colors.fg.lightblue, 161 | source, 162 | colors.fg.lightgrey, 163 | colors.fg.pink, 164 | target, 165 | colors.fg.lightgrey, 166 | colors.fg.red, 167 | data, 168 | colors.reset, 169 | ) 170 | ) 171 | # Underscore to avoid coloring service with "email" in name 172 | elif "_EMAIL" in source: 173 | print( 174 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format( 175 | colors.fg.lightblue, 176 | source, 177 | colors.fg.lightgrey, 178 | colors.fg.pink, 179 | target, 180 | colors.fg.lightgrey, 181 | colors.fg.lightgrey, 182 | data, 183 | colors.reset, 184 | ) 185 | ) 186 | elif "USER" in source: 187 | print( 188 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format( 189 | colors.fg.lightblue, 190 | source, 191 | colors.fg.lightgrey, 192 | colors.fg.pink, 193 | target, 194 | colors.fg.lightgrey, 195 | colors.fg.lightcyan, 196 | data, 197 | colors.reset, 198 | ) 199 | ) 200 | elif "SOURCE" in source: 201 | print( 202 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}\n".format( 203 | colors.fg.lightblue, 204 | source, 205 | colors.fg.lightgrey, 206 | colors.fg.pink, 207 | target, 208 | colors.fg.lightgrey, 209 | colors.reset, 210 | data, 211 | colors.reset, 212 | ) 213 | ) 214 | elif "IP" in source: 215 | print( 216 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format( 217 | colors.fg.lightblue, 218 | source, 219 | colors.fg.lightgrey, 220 | colors.fg.pink, 221 | target, 222 | colors.fg.lightgrey, 223 | colors.fg.red, 224 | data, 225 | colors.reset, 226 | ) 227 | ) 228 | else: 229 | print( 230 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format( 231 | colors.fg.lightblue, 232 | source, 233 | colors.fg.lightgrey, 234 | colors.fg.pink, 235 | target, 236 | colors.fg.lightgrey, 237 | colors.fg.lightgrey, 238 | data, 239 | colors.reset, 240 | ) 241 | ) 242 | 243 | @staticmethod 244 | def print_res_header(target): 245 | """ 246 | Print Breach result header 247 | """ 248 | print(colors.bold, "{:_^90}\n".format(""), colors.reset) 249 | print( 250 | colors.bold 251 | + colors.fg.green 252 | + "[>] " 253 | + colors.reset 254 | + "Showing results for " 255 | + target 256 | + colors.reset 257 | ) 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
3 |
15 |
273 |