├── .gitignore ├── Pipfile ├── README.md ├── alert.py ├── browser.py ├── host.py ├── jast-report-sample.png ├── jast.py ├── report.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # misc ignore 107 | .idea/ 108 | .DS_Store 109 | geckodriver.log 110 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JAST v 0.4 2 | ============ 3 | Jast (Just Another Screenshot Tool) is a tool that can be used to automate the 4 | process for screenshots of webhosts. Jast uses the Selenium webdriver and the 5 | Firefox browser for image capture and writes all results to the specified output 6 | folder. 7 | 8 | Requirements 9 | ------------ 10 | Jast requires Python>=3.4 in order to operate along with geckodriver>=0.23.0 11 | and Firefox>=64.0.2. 12 | 13 | Python-specific requirements can be found in the requirements.txt file. 14 | 15 | Installation 16 | ------------ 17 | To install Jast, it's recommended to setup a Pipenv shell in order to prevent 18 | the module installation from affecting yout other Python tools. This can be done 19 | via the follwing: 20 | `$ pipenv shell` 21 | 22 | To install any requirements, once in the Pipenv shell run: 23 | `$ pip install -r requirements.txt` 24 | 25 | Usage 26 | ----- 27 | Running Jast is relatively straightforward. Jast requires either a single URL or 28 | a list of hosts to screenshot along with a directory to write the report to. 29 | Hosts should be in the format http(s)://:. Hosts not in this format 30 | will generate an error message and will be skipped. 31 | 32 | **Single URL Example** 33 | `$ jast.py -u https://www.example.com -o /path/for/report` 34 | 35 | **Multiple Host File** 36 | `$ jast.py -f /path/to/hosts -o /path/for/report` 37 | 38 | Additionally, Jast supports a variety of flags to deal with common scenarios 39 | that may arise with webhosts, such as redirects. If you want to have Jast follow 40 | redirects before taking a screenshot supply the `--follow-redirects` flag. 41 | 42 | **Following Redirects** 43 | `$ jast.py -u http://www.example.com -o /path/for/report --follow-redirects` 44 | 45 | If you want Jast to also document any headers that are sent from the webhost, 46 | use the `--headers` flag, and Jast will write all received headers below the 47 | captured screenshot. 48 | 49 | **Capturing Headers** 50 | `$ jast.py -u http://www.example.com -o /path/for/report --headers` 51 | 52 | Finally, you can control the image size by passing a `--size` parameter to 53 | specify the size of the browser window. By default, Jast will use a window size 54 | of 800x600. 55 | 56 | **Changing Window Size** 57 | `$ jast.py -u http://www.exmaple/com -o /path/for/report --size=1024x768` 58 | 59 | Output 60 | ------ 61 | Jast writes an HTML report in the specified output folder containing a 62 | *report.html* file and some associated style documents. All screenshots are 63 | saved in the */screenshots/* subfolder. The image below shows an example report 64 | that is created when scanning a multiple hosts using Jast and including the 65 | `--headers` flag: 66 | 67 | ![Example Report](./jast-report-sample.png?raw=true) 68 | 69 | -------------------------------------------------------------------------------- /alert.py: -------------------------------------------------------------------------------- 1 | SUCCESS = "\033[0;32m[+]\033[0m " 2 | FAIL = "\033[0;31m[!]\033[0m " 3 | WARN = "\033[0;33m[!]\033[0m " 4 | -------------------------------------------------------------------------------- /browser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from selenium.webdriver import Firefox 3 | from selenium.webdriver.firefox.options import Options 4 | 5 | from alert import FAIL 6 | 7 | class Browser: 8 | def __init__(self, size="800x600"): 9 | try: 10 | self._width, self._height = size.split('x') 11 | 12 | except: 13 | print(FAIL + "Error parsing browser window size, defaulting to 800x600") 14 | self._width = 800 15 | self._height = 600 16 | 17 | self._browser = self.start_browser(self._width, self._height) 18 | 19 | def get_url(self, url): 20 | self._browser.get(url) 21 | 22 | def get_image(self): 23 | return self._browser.get_screenshot_as_base64() 24 | 25 | @staticmethod 26 | def start_browser(width, height): 27 | options = Options() 28 | options.add_argument('--headless') 29 | b = Firefox(executable_path='geckodriver', options=options) 30 | b.set_window_size(width, height) 31 | b.set_page_load_timeout(30) 32 | return b 33 | 34 | def close(self): 35 | self._browser.quit() -------------------------------------------------------------------------------- /host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import requests 3 | import hashlib 4 | import urllib3 5 | 6 | from alert import SUCCESS, FAIL 7 | 8 | class Host: 9 | def __init__(self, url='', status_code=0, ss_file='', content_hash='', follow_redirects=False, store_headers=False): 10 | self._url = url 11 | self._status_code = status_code 12 | self._b64image = '' 13 | self._content_hash = content_hash 14 | self._follow_redirects = follow_redirects 15 | self._store_headers = store_headers 16 | self._headers = {} 17 | self.error = False 18 | self.error_msg = '' 19 | 20 | def set_url(self, url): 21 | self._url = url 22 | 23 | def get_url(self): 24 | return self._url 25 | 26 | def set_status_code(self, status_code): 27 | self._status_code = status_code 28 | 29 | def get_status_code(self): 30 | return self._status_code 31 | 32 | def set_hash(self, content): 33 | m = hashlib.sha256() 34 | m.update(content.text.encode('utf-8')) 35 | self._content_hash = m.hexdigest() 36 | 37 | def get_hash(self): 38 | return self._content_hash 39 | 40 | def add_header(self, headers): 41 | self._headers = headers 42 | 43 | def get_headers(self): 44 | return self._headers 45 | 46 | def store_headers(self): 47 | return self._store_headers 48 | 49 | def set_image(self, image): 50 | self._b64image = image 51 | 52 | def get_image(self): 53 | return self._b64image 54 | 55 | def check_host(self): 56 | try: 57 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 58 | request = requests.get(self._url, allow_redirects=False, verify=False, timeout=10) 59 | self.set_status_code(request.status_code) 60 | 61 | if (request.status_code == 302 or request.status_code == 301) and self._follow_redirects: 62 | self.set_url(request.headers['Location']) 63 | request = requests.get(self._url, allow_redirects=True, verify=False, timeout=10) 64 | 65 | if request.raise_for_status() is None or request.status_code == 404: 66 | try: 67 | if self._store_headers: 68 | self.add_header(request.headers) 69 | except KeyError: 70 | pass 71 | 72 | self.set_hash(request) 73 | 74 | return True 75 | else: 76 | print(FAIL + "Bad HTTP response code from host: {0}. Skipping.".format(str(request.status_code))) 77 | return False 78 | 79 | except (requests.ConnectionError, requests.HTTPError) as error: 80 | print(FAIL + "Error taking screenshot for host. See report for details. Skipping.") 81 | self.error_msg = error 82 | return False 83 | 84 | except (urllib3.exceptions.ReadTimeoutError, requests.exceptions.ReadTimeout) as error: 85 | print(FAIL + "Timeout taking screenshot for host. See report for details. Skipping.") 86 | self.error_msg = error 87 | return False -------------------------------------------------------------------------------- /jast-report-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehacksthings/jast/361ecde663600d6f64953c920e1b3a6d61575297/jast-report-sample.png -------------------------------------------------------------------------------- /jast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Name: JAST - Just Another Screenshot Tool 3 | # Description: JAST is a tool to capture web server screenshots 4 | # and server information using headless Firefox/Selenium. 5 | # Version: 0.4 6 | # Author: Mike Lisi (mike@mikehacksthings.com) 7 | 8 | """ 9 | JAST 10 | 11 | Usage: 12 | jast.py [options] (-u URL | -f FILE) -o DIR 13 | jast.py (-h | --help) 14 | jast.py (-v | --version) 15 | 16 | Arguments: 17 | -u Single URL to screenshot. 18 | -f File containing hosts to screenshot. 19 | -o Output directory. 20 | 21 | Screenshot Options: 22 | -s --size SIZE Screenshot window size [default: 800x600]. 23 | --headers Include HTTP Headers in report. 24 | --follow-redirects Follow redirects before taking screenshot. 25 | 26 | Options: 27 | -h --help Show this screen. 28 | -v --version Show version. 29 | """ 30 | 31 | import os 32 | import sys 33 | from docopt import docopt 34 | 35 | from host import Host 36 | from browser import Browser 37 | from report import Report 38 | from alert import SUCCESS, FAIL, WARN 39 | 40 | header = "\n\ 41 | ██╗ █████╗ ███████╗████████╗\n\ 42 | ██║██╔══██╗██╔════╝╚══██╔══╝\n\ 43 | ██║███████║███████╗ ██║ \n\ 44 | ██ ██║██╔══██║╚════██║ ██║ \n\ 45 | ╚█████╔╝██║ ██║███████║ ██║ \n\ 46 | ╚════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ \n\ 47 | \n\ 48 | Just Another Screenshot Tool v 0.4\n\ 49 | \n\ 50 | Author: Mike Lisi (@MikeHacksThings)" 51 | 52 | def process_hosts(data, args): 53 | hosts = [] 54 | for item in data: 55 | u = item.rstrip('\n') 56 | if 'http' not in u: 57 | print(FAIL + "No protocol supplied for host: {0}. Use format http(s):// Skipping...".format(u)) 58 | continue 59 | try: 60 | host = Host(store_headers=args['--headers'], follow_redirects=args['--follow-redirects']) 61 | host.set_url(u) 62 | hosts.append(host) 63 | print(SUCCESS + "Adding host to list: {0}".format(host.get_url())) 64 | 65 | except IndexError: # URL doesn't begin with a protocol 66 | print(FAIL + "No protocol supplied for host: {0}. Use format http(s)://. Skipping...".format(u)) 67 | continue 68 | 69 | return hosts 70 | 71 | 72 | def take_screenshot(h, b, args): 73 | print(SUCCESS + "Taking screenshot for URL: {0}".format(h.get_url())) 74 | if host.check_host(): 75 | try: 76 | b.get_url(h.get_url()) 77 | host.set_image(b.get_image()) 78 | 79 | except: 80 | print(FAIL + "Error taking screenshot for host: {0}. Skipping.".format(h.get_url())) 81 | host.error = True 82 | else: 83 | host.error = True 84 | 85 | 86 | if __name__ == '__main__': 87 | args = docopt(__doc__, version='JAST - Just Another Screenshot Tool v0.4') 88 | 89 | data = [] 90 | hosts = [] 91 | size = "800x600" 92 | output_file = args['-o'] + '.html' 93 | 94 | # Check for output file and prompt for overwrite if it already exists 95 | if os.path.exists(output_file) and os.path.isfile(output_file): 96 | overwrite = input(WARN + "Output file exists (" + output_file + "), overwrite? (Y/n): ") or 'y' 97 | 98 | if 'n' in overwrite.lower(): 99 | print(FAIL + "Report not being overwritten, exiting.") 100 | sys.exit(-1) 101 | elif 'y' not in overwrite.lower(): 102 | print(FAIL + "Unknown response, exiting.") 103 | sys.exit(-1) 104 | 105 | print(header) 106 | 107 | # Parse host file/host 108 | print(SUCCESS + "Processing host(s)...") 109 | if args['-f']: 110 | if os.path.exists(args['-f']) and os.path.isfile(args['-f']): 111 | f = open(args['-f'], 'r') 112 | data = f.readlines() 113 | f.close() 114 | else: # File doesn't exist 115 | print(FAIL + "Host file not found! Exiting.") 116 | sys.exit(-1) 117 | 118 | elif args['-u']: 119 | data = [args['-u']] 120 | 121 | hosts = process_hosts(data, args) 122 | 123 | if len(hosts) == 0: 124 | print(FAIL + "No hosts processed, exiting...") 125 | sys.exit(-1) 126 | 127 | print(SUCCESS + "All hosts processed.") 128 | 129 | print(SUCCESS + "Starting browser.") 130 | browser = Browser(size=args['--size']) 131 | for host in hosts: 132 | take_screenshot(host, browser, args) 133 | 134 | print(SUCCESS + "Stopping browser.") 135 | browser.close() 136 | 137 | print(SUCCESS + "Creating report.") 138 | report = Report(args) 139 | report.start() 140 | for host in hosts: 141 | report.write_host(host) 142 | report.finish() 143 | 144 | print(SUCCESS + "Report written to {0}".format(output_file)) 145 | -------------------------------------------------------------------------------- /report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | from alert import FAIL 6 | 7 | class Report: 8 | def __init__(self, args={}): 9 | self._args = args 10 | self._report = args['-o'] 11 | self._fd = '' 12 | self._header = """ 13 | 14 | 15 | 16 | 17 | JAST Report 18 | 19 | 20 | 21 | 30 | 31 | 32 |
33 | 36 | """ 37 | 38 | self.footer = """ 39 | 40 | """ 41 | 42 | self.style = """ 43 | 44 | """ 45 | 46 | def start(self): 47 | self._report = self._report + ".html" 48 | try: 49 | self._fd = open(self._report, 'w') 50 | self._fd.write(self._header) 51 | return 52 | except OSError: 53 | print(FAIL + "Failed to create output file: {0}.".format(self._report)) 54 | sys.exit(-1) 55 | 56 | def write_host(self, host): 57 | host_data = "
\n" 58 | 59 | if host.error: 60 | host_data += "\t\t
\n\t\t

{0}

\n".format(host.get_url()) 61 | host_data += "\t\t
Error taking screenshot:

\n\t\t\t

{0}

\n\t\t
\n\t
\n".format(host.error_msg) 62 | self._fd.write(host_data) 63 | else: 64 | host_data += "\t\n".format(host.get_image()) 65 | host_data += "\t\t
\n\t\t

" 66 | host_data += "

{1}

\n\t
\n".format(host.get_url(), host.get_url()) 67 | 68 | self._fd.write(host_data) 69 | 70 | if host.store_headers() is True: 71 | self._fd.write("
\n") 72 | for key, value in host.get_headers().items(): 73 | self._fd.write("\t\n\t\t\n\t\t\n\t\n") 74 | self._fd.write("
" + key + "" + value +"
\n") 75 | 76 | self._fd.write("\n") 77 | 78 | def finish(self): 79 | self._fd.write(self.footer) 80 | self._fd.close() 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docopt==0.6.2 2 | selenium 3 | requests 4 | urllib3 --------------------------------------------------------------------------------