├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PyPi.rst ├── README.md ├── h8mail ├── __init__.py ├── __main__.py ├── requirements.txt └── utils │ ├── __init__.py │ ├── breachcompilation.py │ ├── chase.py │ ├── classes.py │ ├── colors.py │ ├── gen_config.py │ ├── helpers.py │ ├── intelx.py │ ├── intelx_helpers.py │ ├── localgzipsearch.py │ ├── localsearch.py │ ├── print_json.py │ ├── print_results.py │ ├── run.py │ ├── summary.py │ ├── url.py │ └── version.py ├── setup.py └── tests ├── __init__.py ├── test_email.txt └── test_h8mail.py /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [![platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux%20%7C%20OSX-success.svg)](https://pypi.org/project/h8mail/) [![PyPI version](https://badge.fury.io/py/h8mail.svg)](https://badge.fury.io/py/h8mail) 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/h8mail.svg)](https://pypi.org/project/h8mail/) [![Downloads](https://pepy.tech/badge/h8mail)](https://pepy.tech/project/h8mail) [![travis](https://img.shields.io/travis/khast3x/h8mail.svg)](https://travis-ci.org/khast3x/h8mail) 7 | [![Docker Pulls](https://img.shields.io/docker/pulls/kh4st3x00/h8mail.svg)](https://hub.docker.com/r/kh4st3x00/h8mail) 8 | **h8mail** is an email OSINT and breach hunting tool using [different breach and reconnaissance services](#apis), or local breaches such as Troy Hunt's "Collection1" and the infamous "Breach Compilation" torrent. 9 | 10 | ---- 11 | 12 | 13 |

14 | 15 |

16 | 17 | 18 | 19 | ---- 20 | 21 | 22 | ## :book: Table of Content 23 | 24 | - [Table of Content](#book-Table-of-Content) 25 | - [Features](#tangerine-Features) 26 | - [APIs](#APIs) 27 | - [Usage](#tangerine-Usage) 28 | - [Usage examples](#tangerine-Usage-examples) 29 | - [Thanks & Credits](#tangerine-Thanks--Credits) 30 | - [Related open source projects](#tangerine-Related-open-source-projects) 31 | 32 | 33 | ---- 34 | 35 | 36 | ## :tangerine: Features 37 | 38 | * :mag_right: Email pattern matching (reg exp), useful for reading from other tool outputs 39 | * :earth_africa: Pass URLs to directly find and target emails in pages 40 | * :dizzy: Loosey patterns for local searchs ("john.smith", "evilcorp") 41 | * :package: Painless install. Available through `pip`, only requires `requests` 42 | * :white_check_mark: Bulk file-reading for targeting 43 | * :memo: Output to CSV file or JSON 44 | * :muscle: Compatible with the "Breach Compilation" torrent scripts 45 | * :house: Search cleartext and compressed .gz files locally using multiprocessing 46 | * :cyclone: Compatible with "Collection#1" 47 | * :fire: Get related emails 48 | * :dragon_face: Chase related emails by adding them to the ongoing search 49 | * :crown: Supports premium lookup services for advanced users 50 | * :factory: Custom query premium APIs. Supports username, hash, ip, domain and password and more 51 | * :books: Regroup breach results for all targets and methods 52 | * :eyes: Includes option to hide passwords for demonstrations 53 | * :rainbow: Delicious colors 54 | 55 | --- 56 | 57 | ### :package: `pip3 install h8mail` 58 | 59 | ----- 60 | 61 | 62 | #### APIs 63 | | Service | Functions | Status | 64 | |-|-|-| 65 | | [HaveIBeenPwned(v3)](https://haveibeenpwned.com/) | Number of email breaches | :white_check_mark: :key: | 66 | | [HaveIBeenPwned Pastes(v3)](https://haveibeenpwned.com/Pastes) | URLs of text files mentioning targets | :white_check_mark: :key: | 67 | | [Hunter.io](https://hunter.io/) - Public | Number of related emails | :white_check_mark: | 68 | | [Hunter.io](https://hunter.io/) - Service (free tier) | Cleartext related emails, Chasing | :white_check_mark: :key: | 69 | | [Snusbase](https://api.snusbase.com/admin/purchase) - Service | Cleartext passwords, hashs and salts, usernames, IPs - Fast :zap: | :white_check_mark: :key: | 70 | | [Leak-Lookup](https://leak-lookup.com/) - Public | Number of search-able breach results | :white_check_mark: (:key:) | 71 | | [Leak-Lookup](https://leak-lookup.com/) - Service | Cleartext passwords, hashs and salts, usernames, IPs, domain | :white_check_mark: :key: | 72 | | [Emailrep.io](https://emailrep.io/) - Service (free) | Last seen in breaches, social media profiles | :white_check_mark: :key: | 73 | | [scylla.so](https://scylla.so/) - Service (free) | Cleartext passwords, hashs and salts, usernames, IPs, domain | :construction: | 74 | | [Dehashed.com](https://dehashed.com/) - Service | Cleartext passwords, hashs and salts, usernames, IPs, domain | :white_check_mark: :key: | 75 | | [IntelX.io](https://intelx.io/signup) - Service (free trial) | Cleartext passwords, hashs and salts, usernames, IPs, domain, Bitcoin Wallets, IBAN | :white_check_mark: :key: | 76 | | :new: [Breachdirectory.org](https://breachdirectory.org) - Service (free) | Cleartext passwords, hashs and salts, usernames, domain | :construction: :key: | 77 | 78 | *:key: - API key required* 79 | 80 | 81 | 82 | 83 | ----- 84 | 85 | ## :tangerine: Usage 86 | 87 | ```bash 88 | usage: h8mail [-h] [-t USER_TARGETS [USER_TARGETS ...]] 89 | [-u USER_URLS [USER_URLS ...]] [-q USER_QUERY] [--loose] 90 | [-c CONFIG_FILE [CONFIG_FILE ...]] [-o OUTPUT_FILE] 91 | [-j OUTPUT_JSON] [-bc BC_PATH] [-sk] 92 | [-k CLI_APIKEYS [CLI_APIKEYS ...]] 93 | [-lb LOCAL_BREACH_SRC [LOCAL_BREACH_SRC ...]] 94 | [-gz LOCAL_GZIP_SRC [LOCAL_GZIP_SRC ...]] [-sf] 95 | [-ch [CHASE_LIMIT]] [--power-chase] [--hide] [--debug] 96 | [--gen-config] 97 | 98 | Email information and password lookup tool 99 | 100 | optional arguments: 101 | -h, --help show this help message and exit 102 | -t USER_TARGETS [USER_TARGETS ...], --targets USER_TARGETS [USER_TARGETS ...] 103 | Either string inputs or files. Supports email pattern 104 | matching from input or file, filepath globing and 105 | multiple arguments 106 | -u USER_URLS [USER_URLS ...], --url USER_URLS [USER_URLS ...] 107 | Either string inputs or files. Supports URL pattern 108 | matching from input or file, filepath globing and 109 | multiple arguments. Parse URLs page for emails. 110 | Requires http:// or https:// in URL. 111 | -q USER_QUERY, --custom-query USER_QUERY 112 | Perform a custom query. Supports username, password, 113 | ip, hash, domain. Performs an implicit "loose" search 114 | when searching locally 115 | --loose Allow loose search by disabling email pattern 116 | recognition. Use spaces as pattern seperators 117 | -c CONFIG_FILE [CONFIG_FILE ...], --config CONFIG_FILE [CONFIG_FILE ...] 118 | Configuration file for API keys. Accepts keys from 119 | Snusbase, WeLeakInfo, Leak-Lookup, HaveIBeenPwned, 120 | Emailrep, Dehashed and hunterio 121 | -o OUTPUT_FILE, --output OUTPUT_FILE 122 | File to write CSV output 123 | -j OUTPUT_JSON, --json OUTPUT_JSON 124 | File to write JSON output 125 | -bc BC_PATH, --breachcomp BC_PATH 126 | Path to the breachcompilation torrent folder. Uses the 127 | query.sh script included in the torrent 128 | -sk, --skip-defaults Skips Scylla and HunterIO check. Ideal for local scans 129 | -k CLI_APIKEYS [CLI_APIKEYS ...], --apikey CLI_APIKEYS [CLI_APIKEYS ...] 130 | Pass config options. Supported format: "K=V,K=V" 131 | -lb LOCAL_BREACH_SRC [LOCAL_BREACH_SRC ...], --local-breach LOCAL_BREACH_SRC [LOCAL_BREACH_SRC ...] 132 | Local cleartext breaches to scan for targets. Uses 133 | multiprocesses, one separate process per file, on 134 | separate worker pool by arguments. Supports file or 135 | folder as input, and filepath globing 136 | -gz LOCAL_GZIP_SRC [LOCAL_GZIP_SRC ...], --gzip LOCAL_GZIP_SRC [LOCAL_GZIP_SRC ...] 137 | Local tar.gz (gzip) compressed breaches to scans for 138 | targets. Uses multiprocesses, one separate process per 139 | file. Supports file or folder as input, and filepath 140 | globing. Looks for 'gz' in filename 141 | -sf, --single-file If breach contains big cleartext or tar.gz files, set 142 | this flag to view the progress bar. Disables 143 | concurrent file searching for stability 144 | -ch [CHASE_LIMIT], --chase [CHASE_LIMIT] 145 | Add related emails from hunter.io to ongoing target 146 | list. Define number of emails per target to chase. 147 | Requires hunter.io private API key if used without 148 | power-chase 149 | --power-chase Add related emails from ALL API services to ongoing 150 | target list. Use with --chase 151 | --hide Only shows the first 4 characters of found passwords 152 | to output. Ideal for demonstrations 153 | --debug Print request debug information 154 | --gen-config, -g Generates a configuration file template in the current 155 | working directory & exits. Will overwrite existing 156 | h8mail_config.ini file 157 | 158 | ``` 159 | 160 | ----- 161 | 162 | ## :tangerine: Usage examples 163 | 164 | ###### Query for a single target 165 | 166 | ```bash 167 | $ h8mail -t target@example.com 168 | ``` 169 | 170 | ###### Query for list of targets, indicate config file for API keys, output to `pwned_targets.csv` 171 | ```bash 172 | $ h8mail -t targets.txt -c config.ini -o pwned_targets.csv 173 | ``` 174 | 175 | ###### Query a list of targets against local copy of the Breach Compilation, pass API key for [Snusbase](https://snusbase.com/) from the command line 176 | ```bash 177 | $ h8mail -t targets.txt -bc ../Downloads/BreachCompilation/ -k "snusbase_token=$snusbase_token" 178 | ``` 179 | 180 | ###### Query without making API calls against local copy of the Breach Compilation 181 | ```bash 182 | $ h8mail -t targets.txt -bc ../Downloads/BreachCompilation/ -sk 183 | ``` 184 | 185 | ###### Search every .gz file for targets found in targets.txt locally, skip default checks 186 | 187 | ```bash 188 | $ h8mail -t targets.txt -gz /tmp/Collection1/ -sk 189 | ``` 190 | 191 | ###### Check a cleartext dump for target. Add the next 10 related emails to targets to check. Read keys from CLI 192 | 193 | ```bash 194 | $ h8mail -t admin@evilcorp.com -lb /tmp/4k_Combo.txt -ch 10 -k "hunterio=ABCDE123" 195 | ``` 196 | ###### Query username. Read keys from CLI 197 | 198 | ```bash 199 | $ h8mail -t JSmith89 -q username -k "dehashed_email=user@email.com" "dehashed_key=ABCDE123" 200 | ``` 201 | 202 | ###### Query IP. Chase all related targets. Read keys from CLI 203 | 204 | 205 | ```bash 206 | $ h8mail -t 42.202.0.42 -q ip -c h8mail_config_priv.ini -ch 2 --power-chase 207 | ``` 208 | 209 | ###### Fetch URL content (CLI + file). Target all found emails 210 | 211 | 212 | ```bash 213 | $ h8mail -u "https://pastebin.com/raw/kQ6WNKqY" "list_of_urls.txt" 214 | ``` 215 | 216 | 217 | ----- 218 | 219 | ## :tangerine: Thanks & Credits 220 | 221 | * [Snusbase](https://snusbase.com/) for being developer friendly 222 | * [kodykinzie](https://twitter.com/kodykinzie) for making a nice [introduction and walkthrough article](https://null-byte.wonderhowto.com/how-to/exploit-recycled-credentials-with-h8mail-break-into-user-accounts-0188600/) and [video](https://www.youtube.com/watch?v=z8G_vBBHtfA) on installing and using h8mail 223 | * [Leak-Lookup](https://leak-lookup.com/) for being developer friendly 224 | * [Dehashed](https://dehashed.com/) for being developer friendly 225 | * h8mail's Pypi integration is strongly based on the work of audreyr's [CookieCutter PyPackage](https://github.com/audreyr/cookiecutter-pypackage) 226 | * Logo generated using Hatchful by Shopify 227 | * [Jake Creps](https://twitter.com/jakecreps) for his [h8mail v2 introduction](https://jakecreps.com/2019/06/21/h8mail/) 228 | * [Alejandro Caceres](https://twitter.com/_hyp3ri0n) for making scylla.so available. Be sure to [support](https://www.buymeacoffee.com/Eiw47ImnT) him if you can 229 | * [IntelX](https://intelx.io) for being developer friendly 230 | * [Breachdirectory.tk](https://breachdirectory.tk) for being developer friendly 231 | 232 | :purple_heart: **h8mail can be found in:** 233 | * [BlackArch Linux](https://blackarch.org/recon.html) 234 | * [Tsurugi DFIR VM](https://tsurugi-linux.org/) 235 | * [CSI Linux](https://csilinux.com) 236 | * [Trace Labs OSINT VM](https://www.tracelabs.org/trace-labs-osint-vm/) 237 | 238 | 239 | ----- 240 | 241 | ## :tangerine: Related open source projects 242 | * [WhatBreach](https://github.com/Ekultek/WhatBreach) by Ekultek 243 | * [HashBuster](https://github.com/s0md3v/Hash-Buster) by s0md3v 244 | * [BaseQuery](https://github.com/g666gle/BaseQuery) by g666gle 245 | * [LeakLooker](https://github.com/woj-ciech/LeakLooker) by woj-ciech 246 | * [buster](https://github.com/sham00n/buster) by sham00n 247 | * [Scavenger](https://github.com/rndinfosecguy/Scavenger) by ndinfosecguy 248 | * [pwndb](https://github.com/davidtavarez/pwndb) by davidtavarez 249 | 250 | 251 | ----- 252 | 253 | ## :tangerine: Notes 254 | 255 | * Service providers that wish being integrated can send me an email at `k at khast3x dot club` (PGP friendly) 256 | * h8mail is maintained on my free time. Feedback and war stories are welcomed. 257 | * Licence is BSD 3 clause 258 | * My code is [signed](https://help.github.com/en/articles/signing-commits) with my [Keybase](https://keybase.io/ktx) PGP key. You can get it using: 259 | ```bash 260 | # curl + gpg pro tip: import ktx's keys 261 | curl https://keybase.io/ktx/pgp_keys.asc | gpg --import 262 | 263 | # the Keybase app can push to gpg keychain, too 264 | keybase pgp pull ktx 265 | ``` 266 | ___ 267 | 268 | *If you wish to stay updated on this project:* 269 | 270 | 271 |

272 | 273 |

274 | 275 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /h8mail/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khast3x/h8mail/ee31c8ff6d148580047ae5048fea2234decb5932/h8mail/utils/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /h8mail/utils/classes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from .intelx import intelx as i 3 | from time import sleep 4 | from .colors import colors as c 5 | import requests 6 | import json 7 | import socket 8 | import sys 9 | import platform 10 | from .version import __version__ 11 | 12 | 13 | class local_breach_target: 14 | """ 15 | Class is called when performing local file search. 16 | This class is meant to store found data, to be later appended to the existing target objects. 17 | local_to_targets() tranforms to target object 18 | Used by both cleartext and gzip search 19 | """ 20 | 21 | def __init__(self, target_data, fp, ln, content): 22 | self.target = target_data 23 | self.filepath = fp 24 | self.line = ln 25 | self.content = content 26 | 27 | def dump(self): 28 | print(f"Email: {self.target}") 29 | print(f"Path: {self.filepath}") 30 | print(f"Line: {self.line}") 31 | print(f"Content: {self.content}") 32 | print() 33 | 34 | 35 | class target: 36 | """ 37 | Main class used to create and follow breach data. 38 | Found data is stored in self.data. Each method increments self.pwned when data is found. 39 | """ 40 | 41 | def __init__(self, target_data, debug=False): 42 | self.headers = { 43 | "User-Agent": "h8mail-v.{h8ver}-OSINT-and-Education-Tool (PythonVersion={pyver}; Platform={platfrm})".format( 44 | h8ver=__version__, 45 | pyver=sys.version.split(" ")[0], 46 | platfrm=platform.platform().split("-")[0], 47 | ) 48 | } 49 | self.target = target_data 50 | self.pwned = 0 51 | self.data = [()] 52 | self.debug = debug 53 | if debug: 54 | print( 55 | c.fg.red, 56 | f"DEBUG: Created target object for {self.target}", 57 | c.reset, 58 | ) 59 | 60 | def not_exists(self, pattern): 61 | for d in self.data: 62 | if len(d) >= 2: 63 | if d[1] == pattern: 64 | return False 65 | return True 66 | 67 | def make_request( 68 | self, 69 | url, 70 | meth="GET", 71 | timeout=20, 72 | redirs=True, 73 | data=None, 74 | params=None, 75 | verify=True, 76 | auth=None, 77 | ): 78 | try: 79 | response = requests.request( 80 | url=url, 81 | headers=self.headers, 82 | method=meth, 83 | timeout=timeout, 84 | allow_redirects=redirs, 85 | data=data, 86 | params=params, 87 | verify=verify, 88 | auth=auth, 89 | ) 90 | # response = requests.request(url="http://127.0.0.1:8000", headers=self.headers, method=meth, timeout=timeout, allow_redirects=redirs, data=data, params=params) 91 | if self.debug: 92 | c.debug_news("DEBUG: Sent the following---------------------") 93 | print(self.headers) 94 | print(url, meth, data, params) 95 | c.debug_news("DEBUG: Received the following---------------------") 96 | c.debug_news(response.url) 97 | c.debug_news("DEBUG: RESPONSE HEADER---------------------") 98 | print( 99 | "\n".join( 100 | f"{k}: {v}" for k, v in response.headers.items() 101 | ) 102 | ) 103 | c.debug_news("DEBUG: RESPONSE BODY---------------------") 104 | print(response.content) 105 | # print(response) 106 | except Exception as ex: 107 | c.bad_news("Request could not be made for " + self.target) 108 | print(url) 109 | print(ex) 110 | print(response) 111 | return response 112 | 113 | # New HIBP API 114 | def get_hibp3(self, api_key): 115 | try: 116 | c.info_news("[" + self.target + "]>[hibp]") 117 | sleep(1.3) 118 | url = f"https://haveibeenpwned.com/api/v3/breachedaccount/{self.target}" 119 | self.headers.update({"hibp-api-key": api_key}) 120 | response = self.make_request(url) 121 | if response.status_code not in [200, 404]: 122 | c.bad_news("Could not contact HIBP v3 for " + self.target) 123 | print(response.status_code) 124 | return 125 | 126 | if response.status_code == 200: 127 | data = response.json() 128 | for d in data: # Returned type is a dict of Name : Service 129 | for _, ser in d.items(): 130 | self.data.append(("HIBP3", ser)) 131 | self.pwned += 1 132 | 133 | c.good_news( 134 | "Found {num} breaches for {target} using HIBP v3".format( 135 | num=len(self.data) - 1, target=self.target 136 | ) 137 | ) 138 | self.get_hibp3_pastes() 139 | self.headers.popitem() 140 | elif response.status_code == 404: 141 | c.info_news( 142 | f"No breaches found for {self.target} using HIBP v3" 143 | ) 144 | 145 | else: 146 | c.bad_news( 147 | f"HIBP v3: got API response code {response.status_code} for {self.target}" 148 | ) 149 | 150 | except Exception as e: 151 | c.bad_news("haveibeenpwned v3: " + self.target) 152 | print(e) 153 | 154 | # New HIBP API 155 | def get_hibp3_pastes(self): 156 | try: 157 | c.info_news("[" + self.target + "]>[hibp-paste]") 158 | sleep(1.3) 159 | url = f"https://haveibeenpwned.com/api/v3/pasteaccount/{self.target}" 160 | 161 | response = self.make_request(url) 162 | if response.status_code not in [200, 404]: 163 | c.bad_news("Could not contact HIBP PASTE for " + self.target) 164 | print(response.status_code) 165 | print(response) 166 | return 167 | 168 | if response.status_code == 200: 169 | 170 | data = response.json() 171 | for d in data: # Returned type is a dict of Name : Service 172 | self.pwned += 1 173 | if "Pastebin" in d["Source"]: 174 | self.data.append( 175 | ("HIBP3_PASTE", "https://pastebin.com/" + d["Id"]) 176 | ) 177 | else: 178 | self.data.append(("HIBP3_PASTE", d["Id"])) 179 | 180 | c.good_news( 181 | "Found {num} pastes for {target} using HIBP v3 Pastes".format( 182 | num=len(data), target=self.target 183 | ) 184 | ) 185 | 186 | elif response.status_code == 404: 187 | c.info_news( 188 | f"No pastes found for {self.target} using HIBP v3 PASTE" 189 | ) 190 | else: 191 | c.bad_news( 192 | f"HIBP v3 PASTE: got API response code {response.status_code} for {self.target}" 193 | ) 194 | 195 | except Exception as ex: 196 | c.bad_news("HIBP v3 PASTE error: " + self.target) 197 | print(ex) 198 | 199 | def get_intelx(self, api_keys): 200 | try: 201 | intel_files = [] 202 | intelx = i(key=api_keys["intelx_key"], ua="h8mail-v.{h8ver}-OSINT-and-Education-Tool (PythonVersion={pyver}; Platform={platfrm})".format( 203 | h8ver=__version__, 204 | pyver=sys.version.split(" ")[0], 205 | platfrm=platform.platform().split("-")[0], 206 | )) 207 | from .intelx_helpers import intelx_getsearch 208 | from .localsearch import local_search 209 | from os import remove, fspath 210 | 211 | maxfile = 10 212 | if api_keys["intelx_maxfile"]: 213 | maxfile = int(api_keys["intelx_maxfile"]) 214 | search = intelx_getsearch(self.target, intelx, maxfile) 215 | if self.debug: 216 | import json 217 | 218 | print(json.dumps(search, indent=4)) 219 | 220 | for record in search["records"]: 221 | filename = record["systemid"].strip() + ".txt" 222 | intel_files.append(filename) 223 | if record["media"] != 24: 224 | c.info_news( 225 | "Skipping {name}, not text ({type})".format( 226 | type=record["mediah"], name=record["name"] 227 | ) 228 | ) 229 | continue 230 | c.good_news( 231 | "[" 232 | + self.target 233 | + "]>[intelx.io] Fetching " 234 | + record["name"] 235 | + " as file " 236 | + filename 237 | + " (" 238 | + "{:,.0f}".format(record["size"] / float(1 << 20)) 239 | + " MB)" 240 | ) 241 | intelx.FILE_READ(record["systemid"], 0, record["bucket"], filename) 242 | found_list = local_search([filename], [self.target]) 243 | for f in found_list: 244 | self.pwned += 1 245 | self.data.append( 246 | ( 247 | "INTELX.IO", 248 | "{name} | Line: {line} - {content}".format( 249 | name=record["name"].strip(), 250 | line=f.line, 251 | content=" ".join(f.content.split()), 252 | ), 253 | ) 254 | ) 255 | # print(contents) # Contains search data 256 | for file in intel_files: 257 | try: 258 | if self.debug: 259 | c.info_news( 260 | "[" 261 | + self.target 262 | + f"]>[intelx.io] [DEBUG] Keeping {file}" 263 | ) 264 | else: 265 | c.info_news( 266 | "[" 267 | + self.target 268 | + f"]>[intelx.io] Removing {file}" 269 | ) 270 | remove(file) 271 | except Exception as ex: 272 | c.bad_news("intelx.io cleanup error: " + self.target) 273 | print(ex) 274 | 275 | except Exception as ex: 276 | c.bad_news("intelx.io error: " + self.target) 277 | print(ex) 278 | 279 | def get_emailrepio(self, api_key=""): 280 | try: 281 | sleep(0.5) 282 | if len(api_key) != 0: 283 | self.headers.update({"Key": api_key}) 284 | c.info_news("[" + self.target + "]>[emailrep.io+key]") 285 | else: 286 | c.info_news("[" + self.target + "]>[emailrep.io]") 287 | url = f"https://emailrep.io/{self.target}" 288 | response = self.make_request(url) 289 | if response.status_code not in [200, 404, 429]: 290 | c.bad_news("Could not contact emailrep for " + self.target) 291 | print(response.status_code) 292 | print(response) 293 | return 294 | 295 | if response.status_code == 429: 296 | c.info_news( 297 | "[warning] Is your emailrep key working? Get a free API key here: https://bit.ly/3b1e7Pw" 298 | ) 299 | elif response.status_code == 404: 300 | c.info_news( 301 | f"No data found for {self.target} using emailrep.io" 302 | ) 303 | elif response.status_code == 200: 304 | data = response.json() 305 | 306 | self.data.append( 307 | ( 308 | "EMAILREP_INFO", 309 | "Reputation: {rep} | Deliverable: {deli}".format( 310 | rep=data["reputation"].capitalize(), 311 | deli=data["details"]["deliverable"], 312 | ), 313 | ) 314 | ) 315 | 316 | if data["details"]["credentials_leaked"] is True: 317 | self.pwned += int(data["references"]) # or inc num references 318 | if data["references"] == 1: 319 | self.data.append( 320 | ( 321 | "EMAILREP_LEAKS", 322 | f"{data['references']} leaked credential", 323 | ) 324 | ) 325 | else: 326 | self.data.append( 327 | ( 328 | "EMAILREP_LEAKS", 329 | "{} leaked credentials".format(data["references"]), 330 | ) 331 | ) 332 | c.good_news( 333 | "Found {num} breaches for {target} using emailrep.io".format( 334 | num=data["references"], target=self.target 335 | ) 336 | ) 337 | if len(data["details"]["profiles"]) != 0: 338 | for profile in data["details"]["profiles"]: 339 | self.data.append(("EMAILREP_SOCIAL", profile.capitalize())) 340 | c.good_news( 341 | "Found {num} social profiles linked to {target} using emailrep.io".format( 342 | num=len(data["details"]["profiles"]), target=self.target 343 | ) 344 | ) 345 | if "never" in data["details"]["last_seen"]: 346 | return 347 | self.data.append(("EMAILREP_1ST_SN", data["details"]["first_seen"])) 348 | c.good_news( 349 | "{target} was first seen on the {data}".format( 350 | data=data["details"]["first_seen"], target=self.target 351 | ) 352 | ) 353 | self.data.append(("EMAILREP_LASTSN", data["details"]["last_seen"])) 354 | c.good_news( 355 | "{target} was last seen on the {data}".format( 356 | data=data["details"]["last_seen"], target=self.target 357 | ) 358 | ) 359 | else: 360 | c.bad_news( 361 | "emailrep.io: got API response code {code} for {target}".format( 362 | code=response.status_code, target=self.target 363 | ) 364 | ) 365 | if len(api_key) != 0: 366 | self.headers.popitem() 367 | except Exception as ex: 368 | c.bad_news("emailrep.io error: " + self.target) 369 | print(ex) 370 | 371 | def get_scylla(self, user_query="email"): 372 | try: 373 | c.info_news("[" + self.target + "]>[scylla.so]") 374 | sleep(0.5) 375 | self.headers.update({"Accept": "application/json"}) 376 | if user_query == "email": 377 | uri_scylla = 'email: "' + self.target + '"' 378 | elif user_query == "password": 379 | uri_scylla = 'password: "' + self.target + '"' 380 | elif user_query == "username": 381 | uri_scylla = 'name: "' + self.target + '"' 382 | elif user_query == "ip": 383 | uri_scylla = 'ip: "' + self.target + '"' 384 | elif user_query == "hash": 385 | uri_scylla = 'passhash: "' + self.target + '"' 386 | elif user_query == "domain": 387 | uri_scylla = 'email: "*@' + self.target + '"' 388 | url = "https://scylla.so/search?q={}".format( 389 | requests.utils.requote_uri(uri_scylla) 390 | ) 391 | 392 | # https://github.com/khast3x/h8mail/issues/64 393 | response = self.make_request( 394 | url, 395 | verify=False, 396 | # auth=requests.auth.HTTPBasicAuth("sammy", "BasicPassword!"), 397 | ) 398 | self.headers.popitem() 399 | 400 | if response.status_code not in [200, 404]: 401 | c.bad_news("Could not contact scylla.so for " + self.target) 402 | print(response.status_code) 403 | print(response) 404 | return 405 | data = response.json() 406 | total = 0 407 | for d in data: 408 | for field, k in d["fields"].items(): 409 | if k is not None: 410 | total += 1 411 | c.good_news( 412 | "Found {num} entries for {target} using scylla.so ".format( 413 | num=total, target=self.target 414 | ) 415 | ) 416 | for d in data: 417 | for field, k in d["fields"].items(): 418 | if "name" in field and k is not None: 419 | self.data.append(("SCYLLA_USERNAME", k)) 420 | self.pwned += 1 421 | elif ( 422 | "email" in field and k is not None and user_query != "email" 423 | ): 424 | self.data.append(("SCYLLA_EMAIL", k)) 425 | self.pwned += 1 426 | elif "password" in field and k is not None: 427 | self.data.append(("SCYLLA_PASSWORD", k)) 428 | self.pwned += 1 429 | elif "passhash" in field and k is not None: 430 | self.data.append(("SCYLLA_HASH", k)) 431 | self.pwned += 1 432 | elif "passsalt" in field and k is not None: 433 | self.data.append(("SCYLLA_HASHSALT", k)) 434 | self.pwned += 1 435 | elif "ip" in field and k is not None: 436 | self.data.append(("SCYLLA_LASTIP", k)) 437 | self.pwned += 1 438 | if "domain" in field and k is not None: 439 | self.data.append(("SCYLLA_SOURCE", k)) 440 | self.pwned += 1 441 | else: 442 | self.data.append(("SCYLLA_SOURCE", "N/A")) 443 | except Exception as ex: 444 | c.bad_news("scylla.so error: " + self.target) 445 | print(ex) 446 | 447 | def get_hunterio_public(self): 448 | try: 449 | c.info_news("[" + self.target + "]>[hunter.io public]") 450 | target_domain = self.target.split("@")[1] 451 | url = f"https://api.hunter.io/v2/email-count?domain={target_domain}" 452 | req = self.make_request(url) 453 | response = req.json() 454 | if response["data"]["total"] != 0: 455 | self.data.append(("HUNTER_PUB", response["data"]["total"])) 456 | c.good_news( 457 | "Found {num} related emails for {target} using hunter.io (public)".format( 458 | num=response["data"]["total"], target=self.target 459 | ) 460 | ) 461 | except Exception as ex: 462 | c.bad_news("hunter.io (public API) error: " + self.target) 463 | print(ex) 464 | 465 | def get_hunterio_private(self, api_key): 466 | try: 467 | c.info_news("[" + self.target + "]>[hunter.io private]") 468 | target_domain = self.target.split("@")[1] 469 | url = f"https://api.hunter.io/v2/domain-search?domain={target_domain}&api_key={api_key}" 470 | req = self.make_request(url) 471 | response = req.json() 472 | b_counter = 0 473 | for e in response["data"]["emails"]: 474 | self.data.append(("HUNTER_RELATED", e["value"])) 475 | b_counter += 1 476 | if self.pwned != 0: 477 | self.pwned += 1 478 | c.good_news( 479 | "Found {num} related emails for {target} using hunter.io (private)".format( 480 | num=b_counter, target=self.target 481 | ) 482 | ) 483 | except Exception as ex: 484 | c.bad_news( 485 | f"hunter.io (private API) error for {self.target}:" 486 | ) 487 | print(ex) 488 | 489 | def get_snusbase(self, api_url, api_key, user_query): 490 | try: 491 | if user_query == "ip": 492 | user_query = "lastip" 493 | if user_query == "domain": 494 | payload = {"type": "email", "term": "%@" + self.target, "wildcard": "true"} 495 | # elif user_query == "hash": If we want hash to search for password instead of reverse searching emails from the hash 496 | # payload = {"hash": self.target} 497 | # api_url = "https://api.snusbase.com/v3/hash" 498 | else: 499 | payload = {"type": user_query, "term": self.target} 500 | c.info_news("[" + self.target + "]>[snusbase]") 501 | url = api_url 502 | self.headers.update({"authorization": api_key}) 503 | # payload = {"type": user_query, "term": self.target} 504 | req = self.make_request(url, meth="POST", data=payload) 505 | self.headers.popitem() 506 | response = req.json() 507 | if "error" in response: 508 | c.bad_news("[snusbase]> " + response["error"]) 509 | c.bad_news("[snusbase]> " + response["reason"]) 510 | return 1 511 | if "size" in response: 512 | c.good_news( 513 | "Found {num} entries for {target} using Snusbase".format( 514 | num=response["size"], target=self.target 515 | ) 516 | ) 517 | for result in response["results"]: 518 | if "email" in result and self.not_exists(result["email"]): 519 | self.data.append(("SNUS_RELATED", result["email"].strip())) 520 | if "username" in result: 521 | self.data.append(("SNUS_USERNAME", result["username"])) 522 | self.pwned += 1 523 | if "password" in result: 524 | self.data.append(("SNUS_PASSWORD", result["password"])) 525 | self.pwned += 1 526 | if "hash" in result: 527 | if "salt" in result: 528 | self.data.append( 529 | ( 530 | "SNUS_HASH_SALT", 531 | result["hash"].strip() + " : " + result["salt"].strip(), 532 | ) 533 | ) 534 | self.pwned += 1 535 | else: 536 | self.data.append(("SNUS_HASH", result["hash"])) 537 | self.pwned += 1 538 | if "lastip" in result: 539 | self.data.append(("SNUS_LASTIP", result["lastip"])) 540 | self.pwned += 1 541 | if "name" in result: 542 | self.data.append(("SNUS_NAME", result["name"])) 543 | self.pwned += 1 544 | if "db" in result and self.not_exists(result["db"]): 545 | self.data.append(("SNUS_SOURCE", result["db"])) 546 | else: 547 | self.data.append(("SNUS_SOURCE", "N/A")) 548 | except Exception as ex: 549 | c.bad_news(f"Snusbase error with {self.target}") 550 | print(ex) 551 | 552 | def get_leaklookup_pub(self, api_key): 553 | try: 554 | c.info_news("[" + self.target + "]>[leaklookup public]") 555 | url = "https://leak-lookup.com/api/search" 556 | payload = {"key": api_key, "type": "email_address", "query": self.target} 557 | req = self.make_request(url, meth="POST", data=payload, timeout=20) 558 | response = req.json() 559 | if "false" in response["error"] and len(response["message"]) != 0: 560 | c.good_news( 561 | "Found {num} entries for {target} using LeakLookup (public)".format( 562 | num=len(response["message"]), target=self.target 563 | ) 564 | ) 565 | for result in response["message"]: 566 | self.pwned += 1 567 | self.data.append(("LEAKLOOKUP_PUB", result)) 568 | if "false" in response["error"] and len(response["message"]) == 0: 569 | c.info_news( 570 | f"No breaches found for {self.target} using Leak-lookup (public)" 571 | ) 572 | 573 | except Exception as ex: 574 | c.bad_news( 575 | f"Leak-lookup error with {self.target} (public)" 576 | ) 577 | print(ex) 578 | 579 | def get_leaklookup_priv(self, api_key, user_query): 580 | try: 581 | if user_query == "ip": 582 | user_query = "ipadress" 583 | if user_query in ["hash"]: 584 | c.bad_news( 585 | f"Leaklookup does not support {user_query} search (yet)" 586 | ) 587 | return 588 | c.info_news("[" + self.target + "]>[leaklookup private]") 589 | url = "https://leak-lookup.com/api/search" 590 | payload = {"key": api_key, "type": user_query, "query": self.target} 591 | req = self.make_request(url, meth="POST", data=payload, timeout=60) 592 | response = req.json() 593 | if "false" in response["error"] and len(response["message"]) != 0: 594 | b_counter = 0 595 | for db, data in response["message"].items(): 596 | for d in data: 597 | if "username" in d.keys(): 598 | self.pwned += 1 599 | self.data.append(("LKLP_USERNAME", d["username"])) 600 | if "email_address" in d.keys() and self.not_exists( 601 | d["email_address"] 602 | ): 603 | self.data.append( 604 | ("LKLP_RELATED", d["email_address"].strip()) 605 | ) 606 | if "password" in d.keys(): 607 | self.pwned += 1 608 | self.data.append(("LKLP_PASSWORD", d["password"])) 609 | b_counter += 1 610 | if "hash" in d.keys(): 611 | self.pwned += 1 612 | self.data.append(("LKLP_HASH", d["password"])) 613 | b_counter += 1 614 | if "ipaddress" in d.keys(): 615 | self.pwned += 1 616 | self.data.append(("LKLP_LASTIP", d["ipaddress"])) 617 | for tag in [ 618 | "address", 619 | "address1", 620 | "address2", 621 | "country", 622 | "zip", 623 | "zipcode", 624 | "postcode", 625 | "state", 626 | ]: 627 | if tag in d.keys(): 628 | self.pwned += 1 629 | self.data.append( 630 | ("LKLP_GEO", d[tag] + " (type: " + tag + ")") 631 | ) 632 | for tag in [ 633 | "firstname", 634 | "middlename", 635 | "lastname", 636 | "mobile", 637 | "number", 638 | "userid", 639 | ]: 640 | if tag in d.keys(): 641 | self.pwned += 1 642 | self.data.append( 643 | ("LKLP_ID", d[tag] + " (type: " + tag + ")") 644 | ) 645 | if self.not_exists(db): 646 | self.data.append(("LKLP_SOURCE", db)) 647 | 648 | c.good_news( 649 | "Found {num} entries for {target} using LeakLookup (private)".format( 650 | num=b_counter, target=self.target 651 | ) 652 | ) 653 | 654 | if "false" in response["error"] and len(response["message"]) == 0: 655 | c.info_news( 656 | f"No breaches found for {self.target} using Leak-lookup (private)" 657 | ) 658 | except Exception as ex: 659 | c.bad_news( 660 | f"Leak-lookup error with {self.target} (private)" 661 | ) 662 | print(ex) 663 | 664 | def get_weleakinfo_priv(self, api_key, user_query): 665 | try: 666 | c.info_news("[" + self.target + "]>[weleakinfo priv]") 667 | sleep(0.4) 668 | url = "https://api.weleakinfo.com/v3/search" 669 | self.headers.update({"Authorization": "Bearer " + api_key}) 670 | self.headers.update({"Content-Type": "application/x-www-form-urlencoded"}) 671 | 672 | payload = {"type": user_query, "query": self.target} 673 | req = self.make_request(url, meth="POST", data=payload, timeout=30) 674 | self.headers.popitem() 675 | self.headers.popitem() 676 | response = req.json() 677 | if req.status_code == 400: 678 | c.bad_news( 679 | f"Got WLI API response code {req.status_code}: Invalid search type provided" 680 | ) 681 | return 682 | elif req.status_code != 200: 683 | c.bad_news(f"Got WLI API response code {req.status_code} (private)") 684 | return 685 | if req.status_code == 200: 686 | if response["Success"] is False: 687 | c.bad_news( 688 | f"WeLeakInfo (private) error response {response['Message']}" 689 | ) 690 | return 691 | c.good_news( 692 | "Found {num} entries for {target} using WeLeakInfo (private)".format( 693 | num=response["Total"], target=self.target 694 | ) 695 | ) 696 | self.data.append(("WLI_TOTAL", response["Total"])) 697 | if response["Total"] == 0: 698 | return 699 | for result in response["Data"]: 700 | if "Username" in result: 701 | self.data.append(("WLI_USERNAME", result["Username"])) 702 | if "Email" in result and self.not_exists(result["Email"]): 703 | self.data.append(("WLI_RELATED", result["Email"].strip())) 704 | if "Password" in result: 705 | self.data.append(("WLI_PASSWORD", result["Password"])) 706 | self.pwned += 1 707 | if "Hash" in result: 708 | self.data.append(("WLI_HASH", result["Hash"])) 709 | self.pwned += 1 710 | if "Database" in result and self.not_exists(result["Database"]): 711 | self.data.append(("WLI_SOURCE", result["Database"])) 712 | else: 713 | self.data.append(("WLI_SOURCE", "N/A")) 714 | except Exception as ex: 715 | c.bad_news( 716 | f"WeLeakInfo error with {self.target} (private)" 717 | ) 718 | print(ex) 719 | 720 | def get_weleakinfo_pub(self, api_key): 721 | try: 722 | c.info_news("[" + self.target + "]>[weleakinfo public]") 723 | url = "https://api.weleakinfo.com/v3/public/email/{query}".format( 724 | query=self.target 725 | ) 726 | self.headers.update({"Authorization": "Bearer " + api_key}) 727 | req = self.make_request(url, timeout=30) 728 | self.headers.popitem() 729 | response = req.json() 730 | if req.status_code != 200: 731 | c.bad_news(f"Got WLI API response code {req.status_code} (public)") 732 | return 733 | else: 734 | c.good_news( 735 | "Found {num} entries for {target} using WeLeakInfo (public)".format( 736 | num=response["Total"], target=self.target 737 | ) 738 | ) 739 | if response["Success"] is False: 740 | c.bad_news(response["Message"]) 741 | return 742 | self.data.append(("WLI_PUB_TOTAL", response["Total"])) 743 | if response["Total"] == 0: 744 | return 745 | for name, data in response["Data"].items(): 746 | self.data.append(("WLI_PUB_SRC", name + " (" + str(data) + ")")) 747 | except Exception as ex: 748 | c.bad_news( 749 | f"WeLeakInfo error with {self.target} (public)" 750 | ) 751 | print(ex) 752 | 753 | def get_dehashed(self, api_email, api_key, user_query): 754 | try: 755 | if user_query == "hash": 756 | user_query == "hashed_password" 757 | if user_query == "ip": 758 | user_query == "ip_address" 759 | 760 | c.info_news("[" + self.target + "]>[dehashed]") 761 | url = "https://api.dehashed.com/search?query=" 762 | if user_query == "domain": 763 | search_query = "email" + ":" + '"*@' + self.target + '"' 764 | else: 765 | search_query = user_query + ":" + '"' + self.target + '"' 766 | self.headers.update({"Accept": "application/json"}) 767 | req = self.make_request( 768 | url + search_query, meth="GET", timeout=60, auth=(api_email, api_key) 769 | ) 770 | if req.status_code == 200: 771 | response = req.json() 772 | if response["total"] is not None: 773 | c.good_news( 774 | "Found {num} entries for {target} using Dehashed.com".format( 775 | num=str(response["total"]), target=self.target 776 | ) 777 | ) 778 | 779 | for result in response["entries"]: 780 | if ( 781 | "username" in result 782 | and result["username"] is not None 783 | and len(result["username"].strip()) > 0 784 | ): 785 | self.data.append(("DHASHD_USERNAME", result["username"])) 786 | if ( 787 | "email" in result 788 | and self.not_exists(result["email"]) 789 | and result["email"] is not None 790 | and len(result["email"].strip()) > 0 791 | ): 792 | self.data.append(("DHASHD_RELATED", result["email"].strip())) 793 | if ( 794 | "password" in result 795 | and result["password"] is not None 796 | and len(result["password"].strip()) > 0 797 | ): 798 | self.data.append(("DHASHD_PASSWORD", result["password"])) 799 | self.pwned += 1 800 | if ( 801 | "hashed_password" in result 802 | and result["hashed_password"] is not None 803 | and len(result["hashed_password"].strip()) > 0 804 | ): 805 | self.data.append(("DHASHD_HASH", result["hashed_password"])) 806 | self.pwned += 1 807 | for tag in ["name", "vin", "address", "phone"]: 808 | if ( 809 | tag in result 810 | and result[tag] is not None 811 | and len(result[tag].strip()) > 0 812 | ): 813 | self.data.append( 814 | ("DHASHD_ID", result[tag] + " (type: " + tag + ")") 815 | ) 816 | self.pwned += 1 817 | # Documentation and JSON are not synced, using both source keys 818 | if "obtained_from" in result and self.not_exists( 819 | result["obtained_from"] 820 | ): 821 | self.data.append(("DHASHD_SOURCE", result["obtained_from"])) 822 | elif "database_name" in result and self.not_exists( 823 | result["database_name"] 824 | ): 825 | self.data.append(("DHASHD_SOURCE", result["database_name"])) 826 | else: 827 | self.data.append(("DHASHD_SOURCE", "N/A")) 828 | if response["balance"] is not None: 829 | self.data.append( 830 | ( 831 | "DHASHD_CREDITS", 832 | str(response["balance"]) + " DEHASHED CREDITS REMAINING", 833 | ) 834 | ) 835 | else: 836 | c.bad_news("Dehashed error: status code " + str(req.status_code)) 837 | self.headers.popitem() 838 | except Exception as ex: 839 | c.bad_news(f"Dehashed error with {self.target}") 840 | print(ex) 841 | 842 | def get_breachdirectory(self, user, passw, user_query): 843 | # Todo: implement password source search when email has answer 844 | c.info_news("[" + self.target + "]>[breachdirectory.org]") 845 | if user_query not in ["email", "username", "password", "domain"]: 846 | c.bad_news("Breachdirectory does not support this option") 847 | exit(1) 848 | mode = "pastes" 849 | url = "https://breachdirectory.org/api/index?username={user}&password={passw}&func={mode}&term={target}".format(user=user, passw=passw, mode=mode, target=self.target) 850 | try: 851 | req = self.make_request( 852 | url, timeout=60 853 | ) 854 | if req.status_code == 200: 855 | response = req.json() 856 | if response["data"] is not None: 857 | for result in response["data"]: 858 | if "email" in result and "email" not in user_query: 859 | self.data.append(("BREACHDR_EMAIL", result["email"])) 860 | if "password" in result: 861 | self.data.append(("BREACHDR_PASS", result["password"])) 862 | if "hash" in result: 863 | self.data.append(("BREACHDR_HASH", result["hash"])) 864 | if "source" in result: 865 | self.data.append(("BREACHDR_SOURCE", result["source"])) 866 | self.pwned += 1 867 | else: 868 | self.data.append(("BREACHDR_SOURCE", "N/A")) 869 | # Follow up with an aggregated leak sources query 870 | url_src = "https://breachdirectory.org/api/index?username={user}&password={passw}&func={mode}&term={target}".format(user=user, passw=passw, mode="sources", target=self.target) 871 | req = self.make_request( 872 | url_src, timeout=60 873 | ) 874 | if req.status_code == 200: 875 | response = req.json() 876 | if response["sources"] is not None: 877 | for result in response["sources"]: 878 | self.data.append(("BREACHDR_EXTSRC", result)) 879 | ## If using the 'auto' mode instead of pastes 880 | # c.good_news( 881 | # "Found {num} entries for {target} using breachdirectory.org".format( 882 | # num=str(response["found"]), target=self.target 883 | # ) 884 | # ) 885 | 886 | # for result in response["result"]: 887 | # if result["has_password"] is True: 888 | # self.data.append(("BREACHDR_PASS", result["password"])) 889 | # self.data.append(("BREACHDR_MD5", result["md5"])) 890 | # if result["sources"] == "Unverified": 891 | # source = result["sources"] 892 | # elif len(result["sources"]) > 1: 893 | # source = ", ".join(result["sources"]) 894 | # else: 895 | # source = result["sources"][0] 896 | # self.data.append(("BREACHDR_SOURCE", source)) 897 | # self.pwned += 1 898 | except Exception as ex: 899 | c.bad_news(f"Breachdirectory error with {self.target}") 900 | print(ex) 901 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/intelx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | import time 5 | import json 6 | import sys 7 | import re 8 | 9 | class intelx: 10 | 11 | # API_ROOT = 'https://public.intelx.io' 12 | API_ROOT = '' 13 | API_KEY = '' 14 | USER_AGENT = '' 15 | 16 | # If an API key isn't supplied, it will use the free API key (limited functionality) 17 | def __init__(self, key="01a61412-7629-4288-b18a-b287266f2798", ua='IX-Python/0.5'): 18 | """ 19 | Initialize API by setting the API key. 20 | """ 21 | if key == "01a61412-7629-4288-b18a-b287266f2798" or key == "ac572eea-3902-4e9a-972d-f5996d76174c": 22 | self.API_ROOT = "https://public.intelx.io" 23 | else: 24 | self.API_ROOT = "https://2.intelx.io" 25 | 26 | self.API_KEY = key 27 | self.USER_AGENT = ua 28 | 29 | def get_error(self, code): 30 | """ 31 | Get error string by respective HTTP response code. 32 | """ 33 | if code == 200: 34 | return "200 | Success" 35 | if code == 204: 36 | return "204 | No Content" 37 | if code == 400: 38 | return "400 | Bad Request" 39 | if code == 401: 40 | return "401 | Unauthorized" 41 | if code == 402: 42 | return "402 | Payment required." 43 | if code == 404: 44 | return "404 | Not Found" 45 | 46 | def cleanup_treeview(self, treeview): 47 | """ 48 | Cleans up treeview output from the API. 49 | """ 50 | lines = [] 51 | for line in treeview.split("\r\n"): 52 | if '[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 | -------------------------------------------------------------------------------- /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/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/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/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Most imports are after python2/3 check further down 3 | import configparser 4 | import argparse 5 | import os 6 | import re 7 | import time 8 | import sys 9 | 10 | from .breachcompilation import breachcomp_check 11 | from .classes import target 12 | from .colors import colors as c 13 | from .helpers import ( 14 | fetch_emails, 15 | find_files, 16 | get_config_from_file, 17 | get_emails_from_file, 18 | print_banner, 19 | save_results_csv, 20 | check_latest_version, 21 | check_scylla_online, 22 | ) 23 | from .print_json import save_results_json 24 | from .localsearch import local_search, local_search_single, local_to_targets 25 | from .localgzipsearch import local_gzip_search, local_search_single_gzip 26 | from .summary import print_summary 27 | from .chase import chase 28 | from .print_results import print_results 29 | from .gen_config import gen_config_file 30 | from .url import target_urls 31 | 32 | 33 | def target_factory(targets, user_args): 34 | """ 35 | Receives list of emails and user args. Fetchs API keys from config file using user_args path and cli keys. 36 | For each target, launch target.methods() associated to found config artifacts. 37 | Handles chase logic with counters from enumerate() 38 | """ 39 | # Removing duplicates here to avoid dups from chasing 40 | targets = list(set(targets)) 41 | 42 | finished = [] 43 | if user_args.config_file is not None or user_args.cli_apikeys is not None: 44 | api_keys = get_config_from_file(user_args) 45 | else: 46 | api_keys = None 47 | init_targets_len = len(targets) 48 | 49 | query = "email" 50 | skip_default_queries = False 51 | if user_args.user_query is not None: 52 | query = user_args.user_query 53 | skip_default_queries = True # custom query skips default query automatically 54 | 55 | scylla_up = False 56 | if user_args.skip_defaults is False: 57 | scylla_up = check_scylla_online() 58 | 59 | for counter, t in enumerate(targets): 60 | c.info_news("Target factory started for {target}".format(target=t)) 61 | if user_args.debug: 62 | current_target = target(t, debug=True) 63 | else: 64 | current_target = target(t) 65 | if not skip_default_queries: 66 | if not user_args.skip_defaults: 67 | current_target.get_hunterio_public() 68 | ## emailrep seems to insta-block h8mail user agent without a key 69 | # if api_keys is None or "emailrep" not in api_keys: 70 | # current_target.get_emailrepio() 71 | # elif ( 72 | # api_keys is not None and "emailrep" in api_keys and query == "email" 73 | # ): 74 | # current_target.get_emailrepio(api_keys["emailrep"]) 75 | 76 | if api_keys is not None: 77 | if ( 78 | "breachdirectory_user" in api_keys 79 | and "breachdirectory_pass" in api_keys 80 | ): 81 | current_target.get_breachdirectory( 82 | api_keys["breachdirectory_user"], api_keys["breachdirectory_pass"], query 83 | ) 84 | if "hibp" in api_keys and query == "email": 85 | current_target.get_hibp3(api_keys["hibp"]) 86 | if "emailrep" in api_keys and query == "email": 87 | current_target.get_emailrepio(api_keys["emailrep"]) 88 | if "hunterio" in api_keys and query == "email": 89 | current_target.get_hunterio_private(api_keys["hunterio"]) 90 | if "intelx_key" in api_keys: 91 | current_target.get_intelx(api_keys) 92 | if "snusbase_token" in api_keys: 93 | if "snusbase_url" in api_keys: 94 | snusbase_url = api_keys["snusbase_url"] 95 | else: 96 | snusbase_url = "http://api.snusbase.com/v3/search" 97 | current_target.get_snusbase( 98 | snusbase_url, api_keys["snusbase_token"], query 99 | ) 100 | if "leak-lookup_priv" in api_keys: 101 | current_target.get_leaklookup_priv(api_keys["leak-lookup_priv"], query) 102 | if "leak-lookup_pub" in api_keys and query == "email": 103 | current_target.get_leaklookup_pub(api_keys["leak-lookup_pub"]) 104 | if "weleakinfo_pub" in api_keys and query == "email": 105 | current_target.get_weleakinfo_pub(api_keys["weleakinfo_pub"]) 106 | if "weleakinfo_priv" in api_keys: 107 | current_target.get_weleakinfo_priv(api_keys["weleakinfo_priv"], query) 108 | if "dehashed_key" in api_keys: 109 | if "dehashed_email" in api_keys: 110 | current_target.get_dehashed( 111 | api_keys["dehashed_email"], api_keys["dehashed_key"], query 112 | ) 113 | else: 114 | c.bad_news("Missing Dehashed email") 115 | if scylla_up: 116 | current_target.get_scylla(query) 117 | 118 | # Chasing 119 | if user_args.chase_limit and counter <= init_targets_len: 120 | user_args_force_email = user_args 121 | user_args_force_email.user_query = "email" 122 | user_args_force_email.chase_limit -= 1 123 | finished_chased = target_factory( 124 | chase(current_target, user_args), user_args_force_email 125 | ) 126 | finished.extend((finished_chased)) 127 | finished.append(current_target) 128 | return finished 129 | 130 | 131 | def h8mail(user_args): 132 | """ 133 | Handles most user arg logic. Creates a list() of targets from user input. 134 | Starts the target object factory loop; starts local searches after factory if in user inputs 135 | Prints results, saves to csv or JSON if in user inputs 136 | """ 137 | 138 | if user_args.user_targets and user_args.user_urls: 139 | c.bad_news("Cannot use --url with --target. Use one or the other.") 140 | exit(1) 141 | 142 | if not user_args.user_targets and not user_args.user_urls: 143 | c.bad_news("Missing Target or URL") 144 | exit(1) 145 | 146 | start_time = time.time() 147 | 148 | import warnings 149 | 150 | warnings.filterwarnings("ignore", message="Unverified HTTPS request") 151 | 152 | targets = [] 153 | if user_args.user_urls: 154 | targets = target_urls(user_args) 155 | if len(targets) == 0: 156 | c.bad_news("No targets found in URLs. Quitting") 157 | exit(0) 158 | 159 | # If we found emails from URLs, `targets` array already has stuff 160 | if len(targets) != 0: 161 | if user_args.user_targets is None: 162 | user_args.user_targets = [] 163 | user_args.user_targets.extend(targets) 164 | 165 | else: # Find targets in user input or file 166 | if user_args.user_targets is not None: 167 | for arg in user_args.user_targets: 168 | user_stdin_target = fetch_emails(arg, user_args) 169 | if os.path.isfile(arg): 170 | c.info_news("Reading from file " + arg) 171 | targets.extend(get_emails_from_file(arg, user_args)) 172 | elif user_stdin_target: 173 | targets.extend(user_stdin_target) 174 | else: 175 | c.bad_news("No targets found in user input. Quitting") 176 | exit(0) 177 | 178 | c.info_news("Removing duplicates") 179 | targets = list(set(targets)) 180 | 181 | c.good_news("Targets:") 182 | for t in targets: 183 | c.good_news(t) 184 | 185 | # Launch 186 | breached_targets = target_factory(targets, user_args) 187 | 188 | # These are not done inside the factory as the factory iterates over each target individually 189 | # The following functions perform line by line checks of all targets per line 190 | 191 | if user_args.bc_path: 192 | breached_targets = breachcomp_check(breached_targets, user_args.bc_path) 193 | 194 | local_found = None 195 | # Handle cleartext search 196 | if user_args.local_breach_src: 197 | for arg in user_args.local_breach_src: 198 | res = find_files(arg) 199 | if user_args.single_file: 200 | local_found = local_search_single(res, targets) 201 | else: 202 | local_found = local_search(res, targets) 203 | if local_found is not None: 204 | breached_targets = local_to_targets( 205 | breached_targets, local_found, user_args 206 | ) 207 | # Handle gzip search 208 | if user_args.local_gzip_src: 209 | for arg in user_args.local_gzip_src: 210 | res = find_files(arg, "gz") 211 | if user_args.single_file: 212 | local_found = local_search_single_gzip(res, targets) 213 | else: 214 | local_found = local_gzip_search(res, targets) 215 | if local_found is not None: 216 | breached_targets = local_to_targets( 217 | breached_targets, local_found, user_args 218 | ) 219 | 220 | print_results(breached_targets, user_args.hide) 221 | 222 | print_summary(start_time, breached_targets) 223 | if user_args.output_file: 224 | save_results_csv(user_args.output_file, breached_targets) 225 | if user_args.output_json: 226 | save_results_json(user_args.output_json, breached_targets) 227 | 228 | def parse_args(args): 229 | """ 230 | Seperate functions to make it easier to run tests 231 | Pass args as an array 232 | """ 233 | parser = argparse.ArgumentParser( 234 | description="Email information and password lookup tool", prog="h8mail" 235 | ) 236 | 237 | parser.add_argument( 238 | "-t", 239 | "--targets", 240 | dest="user_targets", 241 | help="Either string inputs or files. Supports email pattern matching from input or file, filepath globing and multiple arguments", 242 | nargs="+", 243 | ) 244 | parser.add_argument( 245 | "-u", 246 | "--url", 247 | dest="user_urls", 248 | help="Either string inputs or files. Supports URL pattern matching from input or file, filepath globing and multiple arguments. Parse URLs page for emails. Requires http:// or https:// in URL.", 249 | nargs="+", 250 | ) 251 | parser.add_argument( 252 | "-q", 253 | "--custom-query", 254 | dest="user_query", 255 | help='Perform a custom query. Supports username, password, ip, hash, domain. Performs an implicit "loose" search when searching locally', 256 | ) 257 | parser.add_argument( 258 | "--loose", 259 | dest="loose", 260 | help="Allow loose search by disabling email pattern recognition. Use spaces as pattern seperators", 261 | action="store_true", 262 | default=False, 263 | ) 264 | parser.add_argument( 265 | "-c", 266 | "--config", 267 | dest="config_file", 268 | help="Configuration file for API keys. Accepts keys from Snusbase, WeLeakInfo, Leak-Lookup, HaveIBeenPwned, Emailrep, Dehashed and hunterio", 269 | nargs="+", 270 | ) 271 | parser.add_argument( 272 | "-o", "--output", dest="output_file", help="File to write CSV output" 273 | ) 274 | parser.add_argument( 275 | "-j", "--json", dest="output_json", help="File to write JSON output" 276 | ) 277 | parser.add_argument( 278 | "-bc", 279 | "--breachcomp", 280 | dest="bc_path", 281 | help="Path to the breachcompilation torrent folder. Uses the query.sh script included in the torrent", 282 | ) 283 | parser.add_argument( 284 | "-sk", 285 | "--skip-defaults", 286 | dest="skip_defaults", 287 | help="Skips Scylla and HunterIO check. Ideal for local scans", 288 | action="store_true", 289 | default=False, 290 | ) 291 | parser.add_argument( 292 | "-k", 293 | "--apikey", 294 | dest="cli_apikeys", 295 | help='Pass config options. Supported format: "K=V,K=V"', 296 | nargs="+", 297 | ) 298 | parser.add_argument( 299 | "-lb", 300 | "--local-breach", 301 | dest="local_breach_src", 302 | help="Local cleartext breaches to scan for targets. Uses multiprocesses, one separate process per file, on separate worker pool by arguments. Supports file or folder as input, and filepath globing", 303 | nargs="+", 304 | ) 305 | parser.add_argument( 306 | "-gz", 307 | "--gzip", 308 | dest="local_gzip_src", 309 | help="Local tar.gz (gzip) compressed breaches to scans for targets. Uses multiprocesses, one separate process per file. Supports file or folder as input, and filepath globing. Looks for 'gz' in filename", 310 | nargs="+", 311 | ) 312 | parser.add_argument( 313 | "-sf", 314 | "--single-file", 315 | dest="single_file", 316 | help="If breach contains big cleartext or tar.gz files, set this flag to view the progress bar. Disables concurrent file searching for stability", 317 | action="store_true", 318 | default=False, 319 | ), 320 | parser.add_argument( 321 | "-ch", 322 | "--chase", 323 | dest="chase_limit", 324 | help="Add related emails from hunter.io to ongoing target list. Define number of emails per target to chase. Requires hunter.io private API key if used without power-chase", 325 | type=int, 326 | nargs="?", 327 | ), 328 | parser.add_argument( 329 | "--power-chase", 330 | dest="power_chase", 331 | help="Add related emails from ALL API services to ongoing target list. Use with --chase", 332 | action="store_true", 333 | default=False, 334 | ), 335 | parser.add_argument( 336 | "--hide", 337 | dest="hide", 338 | help="Only shows the first 4 characters of found passwords to output. Ideal for demonstrations", 339 | action="store_true", 340 | default=False, 341 | ), 342 | parser.add_argument( 343 | "--debug", 344 | dest="debug", 345 | help="Print request debug information", 346 | action="store_true", 347 | default=False, 348 | ), 349 | parser.add_argument( 350 | "--gen-config", 351 | "-g", 352 | dest="gen_config", 353 | help="Generates a configuration file template in the current working directory & exits. Will overwrite existing h8mail_config.ini file", 354 | action="store_true", 355 | default=False, 356 | ), 357 | 358 | return parser.parse_args(args) 359 | 360 | 361 | def main(): 362 | 363 | print_banner("warn") 364 | print_banner("version") 365 | print_banner() 366 | check_latest_version() 367 | user_args = parse_args(sys.argv[1:]) 368 | if user_args.gen_config: 369 | gen_config_file() 370 | exit(0) 371 | h8mail(user_args) 372 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.5.6" 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for h8mail.""" 4 | -------------------------------------------------------------------------------- /tests/test_email.txt: -------------------------------------------------------------------------------- 1 | john.smith@gmail.com 2 | test@example.com 3 | test@gmail.com 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------