├── logo.png ├── ntrl.png ├── Pipfile ├── LICENSE ├── proxy_handler.py ├── setup.sh ├── config.py ├── .gitignore ├── red_team_tool_jarms.json ├── main.py ├── Pipfile.lock ├── README.md └── setup.py /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netskopeoss/jarm_randomizer/HEAD/logo.png -------------------------------------------------------------------------------- /ntrl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netskopeoss/jarm_randomizer/HEAD/ntrl.png -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | pyjarm = "*" 9 | urllib3 = "*" 10 | shodan = "*" 11 | pybinaryedge = "*" 12 | 13 | [dev-packages] 14 | 15 | [requires] 16 | python_version = "3.9" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Netskope OSS 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /proxy_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Netskope, Inc. 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 13 | products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 21 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | Written by Dagmawi Mulugeta 24 | """ 25 | 26 | import urllib.request 27 | import urllib 28 | 29 | from http.server import SimpleHTTPRequestHandler 30 | 31 | 32 | class ProxyHandler(SimpleHTTPRequestHandler): 33 | def do_GET(self): 34 | """ 35 | Handler for a GET request 36 | """ 37 | # Update this based on the format the proxy is supposed to use 38 | url = self.path[1:] 39 | self.send_response(200) 40 | self.end_headers() 41 | with urllib.request.urlopen(url) as url_content: 42 | self.copyfile(url_content, self.wfile) 43 | 44 | 45 | def serve_forever(httpd): 46 | with httpd: 47 | httpd.serve_forever() 48 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #Copyright 2021 Netskope, Inc. 4 | #Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | #following conditions are met: 6 | # 7 | #1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | #disclaimer. 9 | # 10 | #2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | #disclaimer in the documentation and/or other materials provided with the distribution. 12 | # 13 | #3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 14 | #products derived from this software without specific prior written permission. 15 | # 16 | #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 17 | #INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | #DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | #SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | #SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | #WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | #OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | # 24 | #Written by Dagmawi Mulugeta 25 | 26 | set -e 27 | 28 | # Generate a certificate 29 | if [ ! -f cert.pem ]; then 30 | echo "[x] Generate a certificate and private key file" 31 | openssl req -newkey rsa:4096 -nodes -keyout key.pem -x509 -days 365 -out cert.pem 32 | fi 33 | 34 | # Get a list of ciphers 35 | echo "[x] Grabbing the list of ciphers that are supported on this system" 36 | openssl ciphers -v | cut -d" " -f1 | sort | uniq > ./ciphers 37 | 38 | # Check which server configurations would work 39 | echo "[x] Running setup.py to grab the valid JARMs" 40 | pipenv run python3 setup.py 41 | rm ciphers 42 | 43 | # Ready to use 44 | echo "[x] Proxy is ready to use" 45 | echo "[x] Run pipenv run python3 ./main.py to start the proxy server" -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Netskope, Inc. 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 13 | products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 21 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | Written by Dagmawi Mulugeta 24 | """ 25 | 26 | import ssl 27 | 28 | # List of TLS versions supported 29 | ssl_versions = [ssl.PROTOCOL_TLS, 30 | ssl.PROTOCOL_TLSv1_2] 31 | 32 | # API keys for grabbing JARM metrics 33 | SHODAN_KEY = '' 34 | BINARY_EDGE_KEY = '' 35 | 36 | # File path settings 37 | paths = { 38 | 'possible_jarms': './possible_jarms.json', 39 | 'valid_configs': './valid_configs.json', 40 | 'invalid_configs': './invalid_configs.json', 41 | 'raw_jarm_stats': './raw_jarm_stats.json', 42 | 'red_team_tool_jarms': './red_team_tool_jarms.json', 43 | } 44 | 45 | # Configurations for the proxy 46 | ip = '127.0.0.1' 47 | port = 8443 48 | 49 | # Path to the TLS private key and cert files 50 | keyfile = 'key.pem' 51 | certfile = "cert.pem" 52 | 53 | # If there is a specific config that is required based on the stats/preference. 54 | # For now, you only have the option to set both or none...will come back and change this 55 | force_ssl_version = None 56 | force_cipher = None 57 | 58 | # Cycle JARM configs at certain interval. Avoid setting this and the force_ssl_version and 59 | # force_cipher above. That would not work too well 60 | cycle_jarms = False 61 | cycle_interval_secs = 5 62 | -------------------------------------------------------------------------------- /.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 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | previous_run 141 | -------------------------------------------------------------------------------- /red_team_tool_jarms.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tool": "Mythic", 4 | "framework": "python 3 w/aiohttp 3", 5 | "jarm": "2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb", 6 | "link": "https://github.com/its-a-feature/Mythic" 7 | }, 8 | { 9 | "tool": "Metasploit ssl listener", 10 | "framework": "ruby 2.7.0p0", 11 | "jarm": "07d14d16d21d21d00042d43d000000aa99ce74e2c6d013c745aa52b5cc042d", 12 | "link": "https://github.com/rapid7/metasploit-framework" 13 | }, 14 | { 15 | "tool": "Metasploit ssl listener", 16 | "framework": "ruby", 17 | "jarm": "07d14d16d21d21d07c42d43d000000f50d155305214cf247147c43c0f1a823", 18 | "link": "https://github.com/rapid7/metasploit-framework" 19 | }, 20 | { 21 | "tool": "Cobalt Strike", 22 | "framework": "Java 11", 23 | "jarm": "07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1", 24 | "link": "https://www.cobaltstrike.com/" 25 | }, 26 | { 27 | "tool": "Merlin", 28 | "framework": "go 1.15.2 linux/amd64", 29 | "jarm": "29d21b20d29d29d21c41d21b21b41d494e0df9532e75299f15ba73156cee38", 30 | "link": "https://github.com/Ne0nd0g/merlin" 31 | }, 32 | { 33 | "tool": "Deimos", 34 | "framework": "go 1.15.2 linux/amd64 with github.com/gorilla/websocket package", 35 | "jarm": "00000000000000000041d00000041d9535d5979f591ae8e547c5e5743e5b64", 36 | "link": "https://github.com/DeimosC2/DeimosC2" 37 | }, 38 | { 39 | "tool": "MacC2", 40 | "framework": "python 3.8.6 w/aiohttp 3", 41 | "jarm": "2ad2ad0002ad2ad22c42d42d000000faabb8fd156aa8b4d8a37853e1063261", 42 | "link": "https://github.com/cedowens/MacC2" 43 | }, 44 | { 45 | "tool": "MacC2", 46 | "framework": "python 3.8.2 w/aiohttp 3", 47 | "jarm": "2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb", 48 | "link": "https://github.com/cedowens/MacC2" 49 | }, 50 | { 51 | "tool": "MacShellSwift", 52 | "framework": "python 3.8.6 socket", 53 | "jarm": "2ad000000000000000000000000000eeebf944d0b023a00f510f06a29b4f46", 54 | "link": "https://github.com/cedowens/MacShellSwift" 55 | }, 56 | { 57 | "tool": "MacShell", 58 | "framework": "python 3.8.6 socket", 59 | "jarm": "2ad000000000000000000000000000eeebf944d0b023a00f510f06a29b4f46", 60 | "link": "https://github.com/cedowens/MacShellSwift" 61 | }, 62 | { 63 | "tool": "Sliver", 64 | "framework": "go 1.15.2 linux/amd64", 65 | "jarm": "2ad2ad0002ad2ad00041d2ad2ad41da5207249a18099be84ef3c8811adc883", 66 | "link": "https://github.com/BishopFox/sliver" 67 | }, 68 | { 69 | "tool": "EvilGinx2", 70 | "framework": "go 1.10.4 linux/amd64", 71 | "jarm": "20d14d20d21d20d20c20d14d20d20daddf8a68a1444c74b6dbe09910a511e6", 72 | "link": "https://github.com/kgretzky/evilginx2" 73 | }, 74 | { 75 | "tool": "Shad0w", 76 | "framework": "python 3.8 flask", 77 | "jarm": "2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb", 78 | "link": "https://github.com/bats3c/shad0w" 79 | }, 80 | { 81 | "tool": "Get2", 82 | "framework": "N/A", 83 | "jarm": "07d19d12d21d21d07c07d19d07d21da5a8ab90bcc6bf8bbc6fbec4bcaa8219", 84 | "link": "" 85 | }, 86 | { 87 | "tool": "GRAT2 C2", 88 | "framework": "python3 http.server", 89 | "jarm": "2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb", 90 | "link": "https://github.com/r3nhat/GRAT2" 91 | }, 92 | { 93 | "tool": "Covenant", 94 | "framework": "ASP.net core", 95 | "jarm": "21d14d00000000021c21d14d21d21d1ee8ae98bf3ef941e91529a93ac62b8b", 96 | "link": "https://github.com/cobbr/Covenant" 97 | }, 98 | { 99 | "tool": "SILENTRINITY", 100 | "framework": "ironpython", 101 | "jarm": "2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb", 102 | "link": "https://github.com/byt3bl33d3r/SILENTTRINITY" 103 | }, 104 | { 105 | "tool": "PoshC2", 106 | "framework": "python3 http.server", 107 | "jarm": "2ad2ad0002ad2ad22c42d42d000000faabb8fd156aa8b4d8a37853e1063261", 108 | "link": "https://github.com/nettitude/PoshC2" 109 | } 110 | ] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Copyright 2021 Netskope, Inc. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 14 | products derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 17 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | Written by Dagmawi Mulugeta 25 | """ 26 | 27 | import ssl 28 | import random 29 | import json 30 | import time 31 | import config 32 | import proxy_handler 33 | 34 | from http.server import HTTPServer 35 | from threading import Thread 36 | 37 | 38 | def get_jarm_from_local(tls_version, cipher): 39 | """ 40 | Grab the JARM fingerprint from the local 'possible_jarms' store. 41 | This should be ran after setup.sh to output the possible configurations. 42 | """ 43 | try: 44 | with open(config.paths['possible_jarms']) as _file: 45 | jarms = json.load(_file) 46 | for j in jarms: 47 | if any(c['tls_version'] == tls_version and c['cipher'] == cipher for c in jarms[j]['configs']): 48 | return j 49 | except: 50 | return '' 51 | 52 | 53 | def grab_valid_configs(return_all=False): 54 | """ 55 | Grab all the valid configurations for this system 56 | """ 57 | tls_version = config.force_ssl_version if config.force_ssl_version else None 58 | cipher = config.force_cipher if config.force_ssl_version else None 59 | if tls_version and cipher: 60 | return tls_version, cipher, get_jarm_from_local(tls_version, cipher) 61 | else: 62 | with open(config.paths['valid_configs']) as _file: 63 | valid_configs = json.load(_file) 64 | if return_all: 65 | new_valid_configs = list({e['jarm']: e for e in valid_configs}.values()) 66 | return new_valid_configs 67 | else: 68 | choice = random.choice(valid_configs) 69 | return int(choice['tls_version']), choice['cipher'], choice['jarm'] 70 | 71 | 72 | def start_server(version, cipher, jarm): 73 | """ 74 | Start the Proxy Server on the desired network and port. 75 | """ 76 | httpd = HTTPServer((config.ip, config.port), proxy_handler.ProxyHandler) 77 | httpd.socket = ssl.wrap_socket( 78 | sock=httpd.socket, 79 | keyfile=config.keyfile, 80 | certfile=config.certfile, 81 | server_side=True, 82 | ssl_version=version, 83 | ciphers=cipher 84 | ) 85 | print(f"\n[x] Selected configs: TLS -> {version}, Cipher -> {cipher}, JARM -> {jarm}") 86 | if config.cycle_jarms: 87 | print(f"[x] Cycle mode selected: server running on https://{config.ip}:{config.port} for {config.cycle_interval_secs} secs") 88 | httpd.server_activate() 89 | thread = Thread(target=proxy_handler.serve_forever, args=(httpd,)) 90 | thread.setDaemon(True) 91 | thread.start() 92 | return httpd 93 | else: 94 | print(f"[x] Server running on https://{config.ip}:{config.port} forever...") 95 | httpd.serve_forever() 96 | 97 | 98 | def main(): 99 | """ 100 | Check the supplied configurations and start the Proxy Server. 101 | """ 102 | if config.cycle_jarms: 103 | while True: 104 | all_configs = grab_valid_configs(return_all=True) 105 | random.shuffle(all_configs) 106 | for ssl_config in all_configs: 107 | httpd = start_server(ssl_config['tls_version'], ssl_config['cipher'], ssl_config['jarm']) 108 | time.sleep(config.cycle_interval_secs) 109 | httpd.shutdown() 110 | time.sleep(3) 111 | else: 112 | tls_version, cipher, jarm = grab_valid_configs() 113 | start_server(tls_version, cipher, jarm) 114 | 115 | 116 | if __name__ == '__main__': 117 | main() 118 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "a710f2da56ceb0319e1f48d04396e72b8ee4d65a1c8426fd4e50d97cd9a2bcec" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", 22 | "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==2022.6.15" 26 | }, 27 | "chardet": { 28 | "hashes": [ 29 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 30 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 31 | ], 32 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 33 | "version": "==4.0.0" 34 | }, 35 | "click": { 36 | "hashes": [ 37 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 38 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 39 | ], 40 | "markers": "python_version >= '3.7'", 41 | "version": "==8.1.3" 42 | }, 43 | "click-plugins": { 44 | "hashes": [ 45 | "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", 46 | "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" 47 | ], 48 | "version": "==1.1.1" 49 | }, 50 | "colorama": { 51 | "hashes": [ 52 | "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", 53 | "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" 54 | ], 55 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 56 | "version": "==0.4.5" 57 | }, 58 | "configparser": { 59 | "hashes": [ 60 | "sha256:1b35798fdf1713f1c3139016cfcbc461f09edbf099d1fb658d4b7479fcaa3daa", 61 | "sha256:e8b39238fb6f0153a069aa253d349467c3c4737934f253ef6abac5fe0eca1e5d" 62 | ], 63 | "markers": "python_version >= '3.6'", 64 | "version": "==5.2.0" 65 | }, 66 | "idna": { 67 | "hashes": [ 68 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 69 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 70 | ], 71 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 72 | "version": "==2.10" 73 | }, 74 | "pybinaryedge": { 75 | "hashes": [ 76 | "sha256:00cf2f253aa44c7d6589a56d70c5b820a5060c3a0a2aee018f0b4ed732fe7632" 77 | ], 78 | "index": "pypi", 79 | "version": "==0.5" 80 | }, 81 | "pyjarm": { 82 | "hashes": [ 83 | "sha256:73110c42e65ce21944d18ebf0639d1ade3d36ef36b416d4577cf83574fce6541", 84 | "sha256:c955396901e59710d61b1cb7e442735fe9840ed6d8dd87ffba8cef159cc083c4" 85 | ], 86 | "index": "pypi", 87 | "version": "==0.0.5" 88 | }, 89 | "requests": { 90 | "hashes": [ 91 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 92 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 93 | ], 94 | "index": "pypi", 95 | "version": "==2.25.1" 96 | }, 97 | "shodan": { 98 | "hashes": [ 99 | "sha256:7e2bddbc1b60bf620042d0010f4b762a80b43111dbea9c041d72d4325e260c23" 100 | ], 101 | "index": "pypi", 102 | "version": "==1.25.0" 103 | }, 104 | "urllib3": { 105 | "hashes": [ 106 | "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", 107 | "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" 108 | ], 109 | "index": "pypi", 110 | "version": "==1.26.5" 111 | }, 112 | "xlsxwriter": { 113 | "hashes": [ 114 | "sha256:df0aefe5137478d206847eccf9f114715e42aaea077e6a48d0e8a2152e983010", 115 | "sha256:e89f4a1d2fa2c9ea15cde77de95cd3fd8b0345d0efb3964623f395c8c4988b7f" 116 | ], 117 | "markers": "python_version >= '3.4'", 118 | "version": "==3.0.3" 119 | } 120 | }, 121 | "develop": {} 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Netskope Threat Labs logo 2 | 3 | # JARM Randomizer 4 | 5 | JARM Randomizer 6 | 7 | ## Introduction 8 | 9 | JARM Randomizer is a Python3 tool that iterates over supported server side TLS version and Cipher suites to defeat JARM based fingerprinting. 10 | This tool was open sourced as part of [JARM Randomizer: Evading JARM Fingerprinting](https://conference.hitb.org/hitbsecconf2021ams/sessions/commsec-jarm-randomizer-evading-jarm-fingerprinting/ "JARM Randomizer: Evading JARM Fingerprinting") for HiTB Amsterdam 2021. 11 | 12 | ## Setup 13 | 14 | ### Dependencies 15 | 16 | This tool relies on the following to be installed on the system: 17 | 18 | - [pipenv](https://pypi.org/project/pipenv/) 19 | - [Python 3.9](https://docs.python.org/3/whatsnew/3.9.html) 20 | - [openssl](https://www.openssl.org/) 21 | 22 | Once these dependencies has been installed, run `pipenv install` in the root directory to setup the virtual environment and install the required dependencies. 23 | 24 | ## Usage 25 | 26 | ### Configuration file 27 | 28 | The `config.py` file present in the root directory can be modified to match the desired configurations. 29 | 30 | To set what network/port you would like the proxy to serve, change these configurations: 31 | 32 | ```python 33 | # Configurations for the proxy 34 | ip = '0.0.0.0' 35 | port = 8443 36 | ``` 37 | 38 | To set what private key and SSL/TLS certificate path, change these configurations: 39 | 40 | ```python 41 | # Path to the TLS private key and cert files 42 | keyfile = 'key.pem' 43 | certfile = "cert.pem" 44 | ``` 45 | 46 | During the setup process, the proxy will read in and output certain required files. 47 | To tweak these file paths (Possibly to output them all in an `output` directory), change these configurations: 48 | 49 | ```python 50 | # File path settings 51 | paths = { 52 | 'possible_jarms': './possible_jarms.json', # The output file that has the possible JARMs, calid TLS - Cipher pairs, and general stats of occurence on the internet. 53 | 'valid_configs': './valid_configs.json', # A lighter version of the possible_jarms.json that just has the TLS - Cipher pairs. 54 | 'invalid_configs': './invalid_configs.json', # A list of TLS - Cipher pairs that the proxy can not support. 55 | 'raw_jarm_stats': './raw_jarm_stats.json', # A raw dump of everything the proxy found during setup. Helpful for debugging and research. 56 | 'red_team_tool_jarms': './red_team_tool_jarms.json', # An input file contianing JARMs for red team tools that was mapped from https://github.com/cedowens/C2-JARM. 57 | } 58 | ``` 59 | 60 | If these API keys are provided, the setup process will also check the possible JARMs for occurence on the internet. 61 | 62 | ```python 63 | # API keys for grabbing JARM metrics 64 | SHODAN_KEY = '' 65 | BINARY_EDGE_KEY = '' 66 | ``` 67 | 68 | If there is a specific TLS - Cipher pair that is desired, change these configurations to match. 69 | 70 | ```python 71 | # If there is a specific config that is required based on the stats/preference. 72 | # For now, you only have the option to set both or none...will come back and change this 73 | force_ssl_version = None # E.g., 2 74 | force_cipher = None # E.g., 'ECDHE-RSA-CHACHA20-POLY1305' 75 | ``` 76 | 77 | If you would like to cycle through the TLS - Cipher pairs that are supported on a system, change these configurations to match. 78 | 79 | ```python 80 | # Cycle JARM configs at certain interval. Avoid setting this and the force_ssl_version and 81 | # force_cipher above. That would not work too well 82 | cycle_jarms = True 83 | cycle_interval_secs = 5 84 | ``` 85 | 86 | ### Grabbing valid configurations 87 | 88 | Once the `config.py` has been tweaked to match the desired configurations, run the following command to setup the proxy 89 | 90 | ```bash 91 | ubuntu@jarm:~/jarm_randomizer$ chmod u+x ./setup.sh && ./setup.sh 92 | 93 | [x] Grabbing the list of ciphers that are supported on this system 94 | [x] Running setup.py to grab the valid JARMs 95 | [X] Finding all the possible JARMs 96 | [x] Validating tls 2 and cipher AES128-GCM-SHA256 97 | 127.0.0.1 - - [11/May/2021 17:32:25] "GET /http://google.com HTTP/1.1" 200 - 98 | [x] Validating tls 2 and cipher AES128-SHA 99 | ... 100 | [X] There are 27 possible JARMS across 70 TLS - Cipher pairs 101 | [X] Grabbing the metrics for the JARMs...might take a while for long list of JARMs 102 | [X] Writing the results to disk 103 | [x] Proxy is ready to use 104 | [x] Run python3 ./main.py to start the proxy server 105 | ``` 106 | 107 | If there is no private key and certificate at the specified file paths, this script will generate a self signed certificate for testing purposes. 108 | 109 | ### Running Proxy 110 | 111 | Once the `config.py` has been updated to match the desire configurations, and `setup.sh` has been run, the proxy is ready to use. 112 | 113 | Run the following command to start running the proxy 114 | 115 | ```bash 116 | ubuntu@jarm:~/jarm_randomizer$ pipenv run python3 ./main.py 117 | 118 | [x] Selected configs: TLS -> 5, Cipher -> ECDHE-RSA-CHACHA20-POLY1305, JARM -> 3fd3fd0003fd3fd0003fd3fd3fd3fd02098c5f1b1aef82f7daaf9fed36c4e8 119 | [x] Server running on https://0.0.0.0:8443 forever... 120 | ``` 121 | 122 | ### Testing the proxy 123 | 124 | If we run the following command, we should recieve a valid proxied response 125 | 126 | ```bash 127 | ubuntu@jarm:~$ curl -k https://127.0.0.1:8443/http://google.com 128 | ``` 129 | 130 | In the above example add `-k` if the certificate was self signed. 131 | 132 | The `proxy_handler.py` file contains logic to handle the specific proxy request. 133 | If other proxy specific changes are required, change this script to match. 134 | 135 | ## Future Improvements 136 | 137 | We have ongoing research to identify areas of improvment around JARM randomizer including: 138 | 139 | - Scale to generate larger list of signatures 140 | - Ability to mimic a targeted server’s JARM 141 | - Dig deeper into the extensions 142 | - Stick sessions to not rotate configuration based on IP address 143 | 144 | For the latest research around JARM Randomizer, check out our [Blog](https://www.netskope.com/blog/category/netskope-threat-labs "Netskope Threat Labs Blog") 145 | 146 | ## Feedback 147 | 148 | Any and all feedback around JARM Randomizer are welcome: 149 | 150 | - [Twitter](https://twitter.com/dagmulu) 151 | - [Linkedin](https://www.linkedin.com/in/dmulugeta) 152 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Copyright 2021 Netskope, Inc. 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | products derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | Written by Dagmawi Mulugeta 26 | """ 27 | 28 | import ssl 29 | import time 30 | import json 31 | import random 32 | import requests 33 | import urllib3 34 | import shodan 35 | 36 | import config 37 | import proxy_handler 38 | 39 | from pybinaryedge import BinaryEdge 40 | from jarm.scanner.scanner import Scanner 41 | from http.server import HTTPServer 42 | from threading import Thread 43 | 44 | urllib3.disable_warnings() 45 | 46 | 47 | def start_test_server(version, cipher): 48 | """ 49 | Start a test server on a random port to verify the TLS handshake 50 | """ 51 | test_port = random.randint(8000, 9999) 52 | httpd = HTTPServer((config.ip, test_port), proxy_handler.ProxyHandler) 53 | httpd.socket = ssl.wrap_socket( 54 | sock=httpd.socket, 55 | keyfile=config.keyfile, 56 | certfile=config.certfile, 57 | server_side=True, 58 | ssl_version=version, 59 | ciphers=cipher 60 | ) 61 | httpd.server_activate() 62 | thread = Thread(target=proxy_handler.serve_forever, args=(httpd,)) 63 | thread.setDaemon(True) 64 | thread.start() 65 | return test_port, httpd 66 | 67 | 68 | def grab_unvalidated_ciphers(): 69 | """ 70 | Load the cipher list 71 | """ 72 | with open('./ciphers', 'r') as _file: 73 | return sorted([line.strip() for line in _file]) 74 | 75 | 76 | def validate_working_configs(): 77 | """ 78 | Load the cipher list after openssl has ran. 79 | Then test the configurations the proxy can support. 80 | """ 81 | blocklist = {} 82 | jarms = [] 83 | for version in config.ssl_versions: 84 | c_list = grab_unvalidated_ciphers() 85 | for cipher in c_list: 86 | print(f"[x] Validating tls {version} and cipher {cipher}") 87 | try: 88 | test_port, httpd = start_test_server(version, cipher) 89 | x = requests.get(f'https://{config.ip}:{test_port}/http://google.com', verify=False) 90 | 91 | jarms.append({ 92 | 'tls_version': version, 93 | 'cipher': cipher, 94 | 'jarm': Scanner.scan(config.ip, test_port)[0], 95 | }) 96 | time.sleep(1) 97 | httpd.shutdown() 98 | except Exception as e: 99 | blocklist[f'{version}-{cipher}'] = str(e) 100 | if httpd: 101 | httpd.shutdown() 102 | return jarms, blocklist 103 | 104 | 105 | def get_key(obj, key, default=None): 106 | """ 107 | Utility method to grab nested key 108 | """ 109 | try: 110 | result = obj[key.split('.')[0]] 111 | for k in key.split('.')[1:]: 112 | result = result[k] 113 | except Exception as e: 114 | result = default 115 | return result 116 | 117 | 118 | def query_shodan(jarm): 119 | """ 120 | Query Shodan and parse the results 121 | """ 122 | raw_result = shodan.Shodan(config.SHODAN_KEY).search(f'ssl.jarm:{jarm}') 123 | cert_orgs = set() 124 | isps = set() 125 | cipher_versions = set() 126 | cipher_names = set() 127 | hostnames = set() 128 | domains = set() 129 | orgs = set() 130 | cloud_providers = set() 131 | servers = set() 132 | 133 | for r in raw_result['matches']: 134 | cert_orgs.add(get_key(r, 'ssl.cert.subject.O', default='')) 135 | cipher_versions.add(get_key(r, 'ssl.cipher.version', default='')) 136 | cipher_names.add(get_key(r, 'ssl.cipher.name', default='')) 137 | orgs.add(get_key(r, 'org', default='')) 138 | isps.add(get_key(r, 'isp', default='')) 139 | cloud_providers.add(get_key(r, 'cloud.provider', default='')) 140 | hostnames.update(get_key(r, 'hostnames', default=[])) 141 | domains.update(get_key(r, 'domains', default=[])) 142 | 143 | if 'data' in r and len(r['data'].split('Server: ')) > 1: 144 | servers.add(r['data'].split('Server: ')[1].split('\r\n')[0]) 145 | 146 | parsed_results = { 147 | 'total': raw_result['total'], 148 | 'cert_orgs': list(cert_orgs), 149 | 'cipher_versions': list(cipher_versions), 150 | 'cipher_names': list(cipher_names), 151 | 'hostnames': list(hostnames), 152 | 'domains': list(domains), 153 | 'orgs': list(orgs), 154 | 'isps': list(isps), 155 | 'cloud_providers': list(cloud_providers), 156 | 'servers': sorted(list(servers)) 157 | } 158 | return raw_result, parsed_results 159 | 160 | 161 | def query_binary_edge(jarm): 162 | """ 163 | Query Binary Edge and parse the results 164 | """ 165 | raw_result = BinaryEdge(config.BINARY_EDGE_KEY).host_search(f'jarm.jarm_hash:"{jarm}"') 166 | ips = set() 167 | ports = set() 168 | protocols = set() 169 | 170 | for r in raw_result['events']: 171 | ips.add(get_key(r, 'target.ip', default='')) 172 | ports.add(get_key(r, 'target.port', default='')) 173 | protocols.add(get_key(r, 'target.protocol', default='')) 174 | 175 | parsed_results = { 176 | 'total': raw_result['total'], 177 | 'ips': list(ips), 178 | 'ports': list(ports), 179 | 'protocols': list(protocols), 180 | } 181 | return raw_result, parsed_results 182 | 183 | 184 | def check_red_team_usage(jarm): 185 | """ 186 | Checks the JARM against a list of C2 tools here: https://github.com/cedowens/C2-JARM 187 | """ 188 | with open(config.paths['red_team_tool_jarms']) as _file: 189 | tools = json.load(_file) 190 | usage = [] 191 | for tool in tools: 192 | if jarm == tool['jarm']: 193 | usage.append(tool) 194 | return usage 195 | 196 | 197 | def grab_stats_for_jarms(jarms): 198 | """ 199 | Grab the occurence of JARM on the internet and red team tool usage 200 | """ 201 | stats = {} 202 | for jarm in jarms: 203 | red_team_usage = check_red_team_usage(jarm['jarm']) 204 | stats[jarm['jarm']] = { 205 | 'raw': { 206 | 'binary_edge': {}, 207 | 'shodan': {} 208 | }, 209 | 'red_team_usage': red_team_usage, 210 | 'parsed_be_results': {}, 211 | 'parsed_shodan_results': {} 212 | } 213 | if config.BINARY_EDGE_KEY: 214 | raw_be_result, parsed_be_results = query_binary_edge(jarm['jarm']) 215 | stats[jarm['jarm']]['raw']['binary_edge'] = raw_be_result 216 | stats[jarm['jarm']]['parsed_be_results'] = parsed_be_results 217 | 218 | if config.SHODAN_KEY: 219 | raw_shodan_result, parsed_shodan_results = query_shodan(jarm=jarm['jarm']) 220 | stats[jarm['jarm']]['raw']['shodan'] = raw_shodan_result 221 | stats[jarm['jarm']]['parsed_shodan_results'] = parsed_shodan_results 222 | return stats 223 | 224 | 225 | def reformat_possible_configs(jarms, stats): 226 | """ 227 | Reformat the data into useful stats and configs 228 | """ 229 | possible_jarms = {} 230 | valid_configs = [] 231 | for entry in jarms: 232 | jarm = entry['jarm'] 233 | tls_version = entry['tls_version'] 234 | cipher = entry['cipher'] 235 | valid_configs.append({ 236 | 'jarm': jarm, 237 | 'tls_version': tls_version, 238 | 'cipher': cipher, 239 | }) 240 | if jarm in possible_jarms: 241 | possible_jarms[jarm]['configs'].append({ 242 | 'tls_version': tls_version, 243 | 'cipher': cipher 244 | }) 245 | else: 246 | possible_jarms[jarm] = { 247 | 'configs': [ 248 | { 249 | 'tls_version': tls_version, 250 | 'cipher': cipher 251 | } 252 | ], 253 | 'binary_edge': stats[jarm]['parsed_be_results'], 254 | 'shodan': stats[jarm]['parsed_shodan_results'], 255 | 'red_team_usage': stats[jarm]['red_team_usage'], 256 | } 257 | return valid_configs, possible_jarms 258 | 259 | 260 | def write_results_and_configs(jarms, blocklist, stats): 261 | """ 262 | Write the configurations and stats to disk 263 | """ 264 | with open(config.paths['invalid_configs'], 'w') as _file: 265 | json.dump(blocklist, _file, indent=2) 266 | 267 | with open(config.paths['raw_jarm_stats'], 'w') as _file: 268 | json.dump(stats, _file, indent=2) 269 | 270 | valid_configs, possible_jarms = reformat_possible_configs(jarms, stats) 271 | 272 | with open(config.paths['possible_jarms'], 'w') as _file: 273 | json.dump(possible_jarms, _file, indent=2) 274 | 275 | with open(config.paths['valid_configs'], 'w') as _file: 276 | json.dump(valid_configs, _file, indent=2) 277 | 278 | 279 | def main(): 280 | """ 281 | Perform the required setup to run the proxy 282 | """ 283 | 284 | print("[X] Finding all the possible JARMs") 285 | jarms, blocklist = validate_working_configs() 286 | print(f"[X] There are {len(set(j['jarm'] for j in jarms))} possible JARMS across {len(jarms)} TLS - Cipher pairs") 287 | 288 | print("[X] Grabbing the metrics for the JARMs...might take a while for long list of JARMs") 289 | stats = grab_stats_for_jarms(jarms) 290 | 291 | print("[X] Writing the results to disk") 292 | write_results_and_configs(jarms, blocklist, stats) 293 | 294 | 295 | if __name__ == '__main__': 296 | main() 297 | --------------------------------------------------------------------------------