├── .gitignore ├── LICENSE ├── RDPEntry.png ├── README.md ├── TableOfContents.png ├── extract ├── extract.py └── requirements.txt └── squeegee ├── filters ├── en.filter.txt └── ru.filter.txt ├── modules ├── __init__.py └── objects.py ├── requirements.txt ├── signatures ├── en.domain.txt ├── en.missingupdates.txt ├── en.os.json ├── ru.domain.txt ├── ru.missingupdates.txt └── ru.os.json └── squeegee.py /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Offensive Operations against Foreign Adversaries 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /RDPEntry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OOAFA/squeegee/6610ea58ab9f8ece9207e984d467a92f4692f8e5/RDPEntry.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squeegee 2 | 3 | A collection of tools using OCR to extract potential usernames from RDP screenshots. 4 | 5 | ## Installing within a virtual environment ( no root needed ) 6 | ``` 7 | $ python3 -m virtualenv venv 8 | $ source venv/bin/activate 9 | $ cd venv 10 | $ git clone https://github.com/OOAFA/squeegee 11 | $ cd squeegee 12 | $ pip install -r requirements.txt 13 | ``` 14 | 15 | ## Extract.py 16 | This tool is used to copy screen captures out of Shodan search result logs stored as base64 image content. 17 | 18 | ## squeegee.py 19 | 20 | This tool is used to extract useful strings from RDP screen captures and identify the OS running on the host, whether missing patches have been identifed, the domain (if any) the host is joined to, and usernames displayed on the logon screen. squeegee.py uses the easyocr library, this library is likely to be more accurate on a system with a supported GPU. Depending on the resolution of the target image, string detection may be inaccurate. Any unwanted strings should be added to the filter.txt file to suppress display in reporting output. 21 | 22 | 23 | ### Getting Started: 24 | 25 | Example Command: `python3 squeegee.py -f /image/folder` 26 | 27 | Example Command: `python3 squeegee.py -C --nohtml` 28 | 29 | Example Command: `python3 squeegee.py -L --nohtml` 30 | 31 | Example Command: `python3 squeegee.py -c 0.5 -f /image/folder` 32 | 33 | Example Command: `python3 squeegee.py -l 'en' 'ru' -f /image/folder` 34 | 35 | Example Command: `python3 extract.py -f shodan.json.gz -D /image/folder` 36 | 37 | ### Sample HTML Report Output: 38 | 39 | The following image shows the table of contents. Report pages are created for each detected operating system version and a count of each type is included. At the bottom of the table of contents is a link to a list of unique useranames discovered in the group. 40 | 41 | ![Example RDP Entry](https://github.com/OOAFA/squeegee/blob/main/TableOfContents.png?raw=true) 42 | 43 | The following image shows an individual entry in the HTML report. String content on the image is interpreted to identify the operating system, whether patches are missing, and the domain the system belongs to. Meaningless strings are filtered from the output and usernames are extracted, listed on the page, and included in a text-based users file. 44 | 45 | ![Example RDP Table of Contents](https://github.com/OOAFA/squeegee/blob/main/RDPEntry.png?raw=true) 46 | 47 | ### Filtering and Signatures: 48 | 49 | RDP screen captures often contain information that is not useful in the context of username extraction and host characteristic identification. Filtered strings are included in files found in the filters directory of this project that are line terminated. The filter files are are prefixed with the targeted language code (example: English filter file = en.filter.txt). 50 | 51 | Signatures are used to identify useful characteristics in a RDP screen capture like Active Directory domain, operating system version, and whether the host is missing updates. All signature files are found in the signatures directory of this project. Signatures to identify Active Directory domain and missing updates are line terminated. Signatures to identify operating systems are stored in JSON format to minimize duplication. All signature files are prefixed with the targeted language code (example: English variants = en.domain.txt, en.missingupdates.txt, en.os.json). 52 | 53 | The easiest method to generate new filter and signature files is to run the tool with no support for the targeted language, then transcribe unnecessary strings into the appropriate file. 54 | 55 | ### Credits: 56 | David Fletcher 57 | github.com/aut0m8r 58 | -------------------------------------------------------------------------------- /TableOfContents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OOAFA/squeegee/6610ea58ab9f8ece9207e984d467a92f4692f8e5/TableOfContents.png -------------------------------------------------------------------------------- /extract/extract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import base64 4 | import shodan 5 | import shodan.helpers as helpers 6 | 7 | import os 8 | import sys 9 | import argparse 10 | 11 | def create_cli_parser(): 12 | parser = argparse.ArgumentParser( 13 | add_help=False, description="Extract is a tool used to extract screen captures from Shodan\ 14 | log output (.json.gz file).") 15 | parser.add_argument('-h', '-?', '--h', '-help', '--help', action="store_true", help=argparse.SUPPRESS) 16 | 17 | input_options = parser.add_argument_group('Input Options') 18 | input_options.add_argument('-f', help='Shodan log (.json.gz) input file containing RDP screen captures. Search\ 19 | criteria should be scoped to appropriate networks and services to ensure that only RDP screen captures\ 20 | are included in the results.', metavar='shodan_log.json.gz') 21 | 22 | output_options = parser.add_argument_group('Output Options') 23 | output_options.add_argument('-D', help='Output directory where extracted images should be saved.', metavar='/temp/shodan_images/') 24 | 25 | args = parser.parse_args() 26 | 27 | if args.h: 28 | parser.print_help() 29 | sys.exit() 30 | 31 | if args.f is None: 32 | print("[*] Error: You didn't specify an input file! I need a Shodan log to parse!") 33 | parser.print_help() 34 | sys.exit() 35 | 36 | if args.D is None: 37 | print("[*] Error: You didn't specify an output folder to save screen captures!") 38 | parser.print_help() 39 | sys.exit() 40 | 41 | return args 42 | 43 | 44 | def main(): 45 | 46 | args = create_cli_parser() 47 | 48 | shodan_log = args.f 49 | output_dir = args.D 50 | 51 | if not os.path.exists(output_dir): 52 | os.mkdir(output_dir) 53 | 54 | screen_count = 0 55 | 56 | print("[-] Processing screen captures in {}...".format(shodan_log)) 57 | 58 | 59 | for banner in helpers.iterate_files(shodan_log): 60 | screenshot_instance = helpers.get_screenshot(banner) 61 | 62 | if screenshot_instance is not None: 63 | screenshot_image = open('{}/{}.jpg'.format(output_dir, banner['ip_str']), 'wb') 64 | screenshot_image.write(base64.b64decode(screenshot_instance['data'])) 65 | screen_count = screen_count+1 66 | print("[-] Successfully extracted {} screen captures from {} into {}.".format(screen_count,shodan_log,output_dir)) 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /extract/requirements.txt: -------------------------------------------------------------------------------- 1 | shodan 2 | -------------------------------------------------------------------------------- /squeegee/filters/en.filter.txt: -------------------------------------------------------------------------------- 1 | 2 | Cance 3 | Cancel 4 | Capvright ~ 2015 5 | Capyrshl i 1g0 5 6 | Datacenter 7 | Deploy 8 | ENG 9 | ENG 10 | Enterprise 11 | Enterprise Editian 12 | Enterprise x64 Edition 13 | Fassword 14 | How do 15 | How do [ sign in to another domain? 16 | Hser name 17 | Important updates are available: Go to PC settings to 18 | install them 19 | [Jser name 20 | |Jser name 21 | Logged cn 22 | Logged on 23 | Optians < < 24 | Other user 25 | Passward; 26 | Password 27 | Password: 28 | PPassword 29 | ser name 30 | Server 31 | Signed in 32 | Signed in 33 | Signed in remotely from 34 | Sign-in options 35 | Sign in to: 36 | sign in to another domain? 37 | Standard 38 | Standard Ediion 39 | Standard Edition 40 | switch 41 | Switch Usel 42 | switch userszsign-in 43 | Tablet ₽C Edlition 44 | ther user 45 | UJser name 46 | Unaulhorized access 47 | Unautharized Use is an Uffense 48 | User 49 | Web Edition 50 | Windows 7 51 | Windows Server 2003 52 | WindowsServer2003 53 | Windows Server 2003 R2 54 | Windows Server 2008 55 | Windows Server 2008 R2 56 | Windows Server 2012 57 | Windows Server 2012R2 58 | Windows Small Business Server 2011 59 | Windows Update 60 | Windows xp 61 | -------------------------------------------------------------------------------- /squeegee/filters/ru.filter.txt: -------------------------------------------------------------------------------- 1 | Kаk я могу вОйти 2 | Windows 7 Максимальная 3 | Windows 7 Профессиональная 4 | автоматически до конца дня: 5 | Вхад 6 | Вхад 7 | Вход 8 | Вход в: 9 | Входв: 10 | Вход выполнен 11 | Вход выполнен 12 | Вход выпслнен 13 | Вхсд выполнен 14 | выполнен 15 | Выполнен удаленный ВхОД 16 | Доступны важные обновления: Чтобы установить 17 | другой домен? 18 | Другой пользова .. 19 | Другой пользователь 20 | Если у вашей группы нет этого права или оно было удалено для 21 | Завершенне работы ' 22 | Завершенне работы ' ' 23 | имеют члены группы "Пользователи удаленного рабочего стола 24 | Имя пользователя 25 | их перейдите к параметрам компьютер 26 | их, перейдите к параметрам компьютера 27 | Каk вайти в другай дсмен? 28 | Как 29 | МАмя пользователя 30 | могу войти 31 | МОГу войти 32 | могу войти в другой домен? 33 | Отмена 34 | Отнена 35 | Параметры 36 | Параметры < < 37 | Параметры входа 38 | Пароль 39 | Пароль; 40 | Перезагрузите компьютер, или это произойдет 41 | Пользователь 42 | Пользователь; 43 | Псльзсватель 44 | РУС 45 | токен: . 46 | Удаленный вход 47 | Центр обновления Windows 48 | -------------------------------------------------------------------------------- /squeegee/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OOAFA/squeegee/6610ea58ab9f8ece9207e984d467a92f4692f8e5/squeegee/modules/__init__.py -------------------------------------------------------------------------------- /squeegee/modules/objects.py: -------------------------------------------------------------------------------- 1 | class RDPObject: 2 | 3 | def __init__(self): 4 | self._fileName = None 5 | self._operatingSystem = "None found" 6 | self._domain = "None found" 7 | self._isPatched = True 8 | self._usernames = [] 9 | 10 | @property 11 | def fileName(self): 12 | return self._fileName 13 | 14 | @fileName.setter 15 | def fileName(self, fileName): 16 | self._fileName = fileName 17 | 18 | @property 19 | def operatingSystem(self): 20 | return self._operatingSystem 21 | 22 | @operatingSystem.setter 23 | def operatingSystem(self, operatingSystem): 24 | self._operatingSystem = operatingSystem 25 | 26 | @property 27 | def domain(self): 28 | return self._domain 29 | 30 | @domain.setter 31 | def domain(self, domain): 32 | self._domain = domain 33 | 34 | @property 35 | def isPatched(self): 36 | return self._isPatched 37 | 38 | @isPatched.setter 39 | def isPatched(self, isPatched): 40 | self._isPatched = isPatched 41 | 42 | @property 43 | def usernames(self): 44 | return self._usernames 45 | 46 | @usernames.setter 47 | def usernames(self, usernames): 48 | self._usernames = usernames 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /squeegee/requirements.txt: -------------------------------------------------------------------------------- 1 | pillow~=10.3.0 2 | easyocr 3 | -------------------------------------------------------------------------------- /squeegee/signatures/en.domain.txt: -------------------------------------------------------------------------------- 1 | Sign in to 2 | -------------------------------------------------------------------------------- /squeegee/signatures/en.missingupdates.txt: -------------------------------------------------------------------------------- 1 | Important updates are available: Go to PC settings to 2 | -------------------------------------------------------------------------------- /squeegee/signatures/en.os.json: -------------------------------------------------------------------------------- 1 | { 2 | "Windows Server 2012":"Windows Server 2012", 3 | "Windows Server 2012R2":"Windows Server 2012 R2", 4 | "Windows Server 2008 R2":"Windows Server 2008 R2", 5 | "Windows Server 2008R2":"Windows Server 2008 R2", 6 | "Windows Server 2008":"Windows Server 2008", 7 | "WindowsServer2003":"Windows Server 2003", 8 | "Windows Server 2003 R2":"Windows Server 2003 R2", 9 | "Windows 7":"Windows 7" 10 | } 11 | 12 | -------------------------------------------------------------------------------- /squeegee/signatures/ru.domain.txt: -------------------------------------------------------------------------------- 1 | Вход в 2 | -------------------------------------------------------------------------------- /squeegee/signatures/ru.missingupdates.txt: -------------------------------------------------------------------------------- 1 | Доступны важные обновления: Чтобы установить 2 | -------------------------------------------------------------------------------- /squeegee/signatures/ru.os.json: -------------------------------------------------------------------------------- 1 | { 2 | "Windows 7 Профессиональная":"Windows 7", 3 | "Windows 7 Максимальная":"Windows 7" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /squeegee/squeegee.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import json 6 | import argparse 7 | import time 8 | import easyocr 9 | 10 | from modules import objects 11 | 12 | def create_cli_parser(): 13 | parser = argparse.ArgumentParser( 14 | add_help=False, description="Squeegee is a tool used to scrape text from RDP screen\ 15 | captures contained in a single folder.") 16 | parser.add_argument('-h', '-?', '--h', '-help', '--help', action="store_true", help=argparse.SUPPRESS) 17 | 18 | input_options = parser.add_argument_group('Input Options') 19 | input_options.add_argument('-l', help='List of two-character ISO 639 language codes to process.\ 20 | Current dictionaries support en and ru languages. Supported languages is dependent on support\ 21 | by the easyocr parser. Defaults to [\'en\',\'ru\'].',metavar='\'en\' \'ru\'',default=['en','ru'], nargs='+') 22 | input_options.add_argument('-f', help='Input folder containing RDP screen captures in JPG format.\ 23 | Output log and HTML report will be written to this folder.',metavar='/tmp/rdp/images') 24 | input_options.add_argument('-c', type=float, help='Baseline confidence interval for text\ 25 | detection. Value must be between 0 and 1. Lower value indicates lower confidece for correct\ 26 | string match. Strings with a confidence interval below this value will be suppressed. Defaults to 0.6',metavar='0.6',default=0.6) 27 | 28 | filter_options = parser.add_argument_group('Filtering and Signature Matching') 29 | filter_options.add_argument('--nofilter', help='Disable string filtering. Can be useful for troubleshooting\ 30 | ',default=False, action='store_true') 31 | filter_options.add_argument('--nosig', help='Disable signature matching.',default=False, action='store_true') 32 | 33 | output_options = parser.add_argument_group('Output Options') 34 | output_options.add_argument('-H','--nohtml', help='Disable HTML report output (Default on). Will be written to input folder as RDPScrape.html.', default=False, action='store_true') 35 | output_options.add_argument('-C','--console', help='Enable Console output.',default=False, action='store_true') 36 | output_options.add_argument('-L','--log', help='Enable Log File output. Will be written to input folder as RDPScrape.log',default=False, action='store_true') 37 | 38 | args = parser.parse_args() 39 | args.date = time.strftime('%Y/%m/%d') 40 | args.time = time.strftime('%H:%M:%S') 41 | 42 | if args.h: 43 | parser.print_help() 44 | sys.exit() 45 | 46 | if args.f is None: 47 | print("[*] Error: You didn't specify a folder! I need a folder containing RDP screen captures!") 48 | parser.print_help() 49 | sys.exit() 50 | 51 | if ((args.f is not None) and not os.path.isdir(args.f)): 52 | print("[*] Error: You didn't specify the correct path to a folder. Try again!\n") 53 | parser.print_help() 54 | sys.exit() 55 | 56 | if (args.nohtml == True) and (args.console == False) and (args.log == False): 57 | print("[*] Error: You didn't specify an output type. At least one should be enabled.") 58 | parser.print_help() 59 | sys.exit() 60 | 61 | return args 62 | 63 | def generate_html_report(screens, pages, outfolder): 64 | 65 | files_dict = {} 66 | os_count = {} 67 | 68 | for page in pages: 69 | targetFile = outfolder + '/' + page + ".html" 70 | if os.path.exists(targetFile): 71 | os.remove(targetFile) 72 | file = open(targetFile,'x') 73 | file.write(get_html_header(page)) 74 | files_dict[page] = file 75 | os_count[page] = 0 76 | 77 | for screen in screens: 78 | files_dict[screen.operatingSystem].write(get_html_row(screen,outfolder)) 79 | os_count[screen.operatingSystem] = os_count[screen.operatingSystem] + 1 80 | 81 | tocFile = outfolder + "/Report.html" 82 | if os.path.isfile(tocFile): 83 | os.remove(tocFile) 84 | 85 | toc = open(tocFile,'x') 86 | toc.write(get_toc_header()) 87 | total = 0 88 | for operatingSystem in os_count: 89 | toc.write(get_toc_row(operatingSystem, os_count[operatingSystem], outfolder)) 90 | total = total + os_count[operatingSystem] 91 | toc.write(get_total_row(total)) 92 | toc.write(get_toc_footer()) 93 | 94 | for file in files_dict: 95 | files_dict[file].write(get_html_footer()) 96 | files_dict[file].close() 97 | 98 | print("[+] Processing complete. Output recorded in " + toc.name + ".") 99 | toc.close() 100 | 101 | def get_html_header(title): 102 | page_content = """ 103 | 104 | 105 | Squeegee Report - {} 106 | 107 | 108 |

Squeegee Report - {}

109 | 110 | 111 | 112 | 113 | 114 | """.format(title,title) 115 | return page_content 116 | 117 | def get_html_footer(): 118 | page_content = """ 119 |
Capture TextRDP Screen Capture
120 | 121 | 122 | """ 123 | return page_content 124 | 125 | def get_html_row(screen, outfolder): 126 | if screen.isPatched == True: 127 | patched = "
" 128 | else: 129 | patched = "
Missing Patches
" 130 | 131 | if len(screen.usernames) > 0: 132 | userFile = screen.fileName + ".txt" 133 | userLink = "
Users File:" + userFile + "" 134 | else: 135 | userLink = "" 136 | 137 | page_content = """ 138 | 139 | 140 | Filename: {} {} 141 | Domain: {}
142 | Usernames:
143 |     {} 144 | {} 145 | 146 | 147 | 148 | 149 | """.format(screen.fileName,patched,screen.domain,'
    '.join(screen.usernames),userLink,screen.fileName) 150 | return page_content 151 | 152 | def get_toc_header(): 153 | page_content = """ 154 | 155 | 156 | Squeegee Report - Table of Contents 157 | 158 | 159 |

Squeegee Report - Table of Contents

160 |
Report Generated on {} at {} 161 |

Table of Contents

162 | 163 | """.format(time.strftime('%Y/%m/%d'),time.strftime('%H:%M:%S')) 164 | return page_content 165 | 166 | def get_toc_footer(): 167 | page_content = """ 168 |
169 | All Discovered Usernames 170 |
171 | 172 | 173 | """ 174 | return page_content 175 | 176 | def get_toc_row(operatingSystem, count, outfolder): 177 | page_content = """ 178 | 179 | {} 180 | {} 181 | 182 | """.format(outfolder,operatingSystem,operatingSystem,count) 183 | return page_content 184 | 185 | def get_total_row(count): 186 | page_content = """ 187 | 188 | Total 189 | {} 190 | 191 | """.format(count) 192 | return page_content 193 | 194 | def get_banner(): 195 | banner_content = """ 196 | --------------------------------------------------------------------------------- 197 | | [ ]. Remote Desktop Connection (Squeegee) -- [ ] X | 198 | --------------------------------------------------------------------------------- 199 | | .---------. ___ _ ___ _ _ | 200 | | ||```````|| | _ \___ _ __ ___| |_ ___ | \ ___ __| |_| |_ ___ _ __ | 201 | | || \ \ || | / -_) ' \/ _ \ _/ -_) | |) / -_|_-< / / _/ _ \ '_ \ | 202 | | || \ \ || |_|_\___|_|_|_\___/\__\___|_|___/\___/__/_\_\\\\__\___/ .__/ | 203 | | ||.......|| / __|___ _ _ _ _ ___ __| |_(_)___ _ _ |_| | 204 | | )---( | (__/ _ \ ' \| ' \/ -_) _| _| / _ \ ' \ | 205 | | /_______(><)\ \___\___/_||_|_||_\___\__|\__|_\___/_||_| | 206 | | | 207 | --------------------------------------------------------------------------------- 208 | | | 209 | | Computer: [ Example: computer.blackhillsinfosec.com ^] | 210 | | | 211 | | User name: David.Fletcher | 212 | | | 213 | | The computer name field is blank. Enter a full remote computer | 214 | | name. | 215 | | | 216 | | (^) Show Options [ Connect ] [ Help] | 217 | | | 218 | --------------------------------------------------------------------------------- """ 219 | return banner_content 220 | 221 | def main(): 222 | print(get_banner()) 223 | cli_parsed = create_cli_parser() 224 | 225 | if cli_parsed.f.endswith('/'): 226 | folder = cli_parsed.f[:-1] 227 | else: 228 | folder = cli_parsed.f 229 | language_list = cli_parsed.l 230 | 231 | reader = easyocr.Reader(language_list) 232 | 233 | filtered = [] 234 | missingupdates = [] 235 | domainsig = [] 236 | os_dict = {} 237 | 238 | if cli_parsed.nofilter == False: 239 | for language in language_list: 240 | filterFile = "./filters/" + language + ".filter.txt" 241 | if os.path.exists(filterFile): 242 | with open(filterFile) as filter_list: 243 | filtered.extend(filter_list.read().splitlines()) 244 | else: 245 | print("[!] No filter file exists for language: " + language) 246 | 247 | if cli_parsed.nosig == False: 248 | for language in language_list: 249 | updateFile = "./signatures/" + language + ".missingupdates.txt" 250 | if os.path.exists(updateFile): 251 | with open(updateFile) as missingupdates_list: 252 | missingupdates.extend(missingupdates_list.read().splitlines()) 253 | else: 254 | print("[!] No missing updates signature file exists for language: " + language) 255 | 256 | domainFile = "./signatures/" + language + ".domain.txt" 257 | if os.path.exists(domainFile): 258 | with open(domainFile) as domain_list: 259 | domainsig.extend(domain_list.read().splitlines()) 260 | else: 261 | print("[!] No domain signature file exists for language: " + language) 262 | 263 | osFile = "./signatures/" + language + ".os.json" 264 | if os.path.exists(osFile): 265 | with open("./signatures/" + language + ".os.json") as os_dictionary: 266 | data = os_dictionary.read() 267 | new_os = json.loads(data) 268 | os_dict.update(new_os) 269 | else: 270 | print("[!] No OS signature file exists for language: " + language) 271 | 272 | if os.path.exists(folder): 273 | if cli_parsed.log == True: 274 | logfile = folder + '/Squeegee.log' 275 | if os.path.exists(logfile): 276 | os.remove(logfile) 277 | log_file = open(folder + '/Squeegee.log','x') 278 | files = os.listdir(folder) 279 | index = 0 280 | screens = [] 281 | pages = [] 282 | allusers = [] 283 | 284 | print("[+] Processing files in " + folder + " please wait...") 285 | 286 | while index < len(files): 287 | filename = files[index] 288 | if filename.endswith('.jpg') or filename.endswith('.jpeg'): 289 | result = reader.readtext(os.path.join(folder,filename)) 290 | screen = objects.RDPObject() 291 | screen.fileName = filename 292 | usernames = [] 293 | for x in range(len(result)): 294 | if result[x][2] > cli_parsed.c: 295 | domain_match = result[x][1].split(":") 296 | if (domain_match[0] in domainsig): 297 | screen.domain = domain_match[1] 298 | else: 299 | if (result[x][1] in filtered): 300 | if (result[x][1] in missingupdates): 301 | screen.isPatched = False 302 | if (result[x][1] in os_dict): 303 | screen.operatingSystem = os_dict[result[x][1]] 304 | else: 305 | if (result[x][1] in missingupdates): 306 | screen.isPatched = False 307 | if (result[x][1] in os_dict): 308 | screen.operatingSystem = os_dict[result[x][1]] 309 | usernames.append(result[x][1]) 310 | if (result[x][1] not in allusers): 311 | allusers.append(result[x][1]) 312 | 313 | screen.usernames = usernames 314 | 315 | if len(usernames) > 0: 316 | screenUsersFile = os.path.join(folder,filename + ".txt") 317 | if os.path.exists(screenUsersFile): 318 | os.remove(screenUsersFile) 319 | 320 | with open(screenUsersFile, mode='x') as userlog: 321 | userlog.write('\n'.join(usernames)) 322 | 323 | if cli_parsed.nohtml == False: 324 | if screen.operatingSystem not in pages: 325 | pages.append(screen.operatingSystem) 326 | 327 | screens.append(screen) 328 | 329 | if cli_parsed.console == True: 330 | print("[+] - Filename: " + screen.fileName) 331 | print(" [-] OS: " + screen.operatingSystem) 332 | if (screen.isPatched == False): 333 | print(" [!] Missing Updates") 334 | print(" [-] Domain: " + screen.domain) 335 | print(" [-] Usernames: ") 336 | for x in range(len(screen.usernames)): 337 | print(" " + screen.usernames[x]) 338 | if cli_parsed.log == True: 339 | log_file.write("[+] - Filename: " + screen.fileName + "\n") 340 | log_file.write(" [-] OS: " + screen.operatingSystem + "\n") 341 | if (screen.isPatched == False): 342 | log_file.write(" [!] Missing Updates\n") 343 | log_file.write(" [-] Domain: " + screen.domain + "\n") 344 | log_file.write(" [-] Usernames: " + "\n") 345 | for x in range(len(screen.usernames)): 346 | log_file.write(" " + screen.usernames[x] + "\n") 347 | 348 | index += 1 349 | if cli_parsed.log == True: 350 | print("[+] Processing complete. Output recorded in " + log_file.name + ".") 351 | log_file.close() 352 | if cli_parsed.nohtml == False: 353 | generate_html_report(screens, pages, folder) 354 | 355 | if len(allusers) > 0: 356 | allUsersFile = os.path.join(folder,"allusers.txt") 357 | if os.path.exists(allUsersFile): 358 | os.remove(allUsersFile) 359 | 360 | with open(allUsersFile, mode='x') as alluserlog: 361 | allusers.sort() 362 | alluserlog.write('\n'.join(allusers)) 363 | 364 | else: 365 | print("Screen capture folder does not exist") 366 | 367 | if __name__ == "__main__": 368 | main() 369 | --------------------------------------------------------------------------------