├── LICENSE ├── README.md └── R3D-SSH-Hunter.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TLP-R3D 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # R3D-SSH-Hunter 2 | R3D SSH Hunter: The Ultimate SSH Key and Bad Guy Tracker! 3 | 4 | SSH Hunter is a tool for cyber threat intelligence professionals to automate the search for adversary SSH keys across the internet using Shodan. With asynchronous scanning and persistent tracking of known threat groups, SSH Hunter helps streamline the identification and analysis of potentially malicious SSH keys and configurations. 5 | 6 | 1. Introduction 7 | SSH Hunter is a tool designed for threat intelligence experts to aid in the discovery of adversary SSH keys. Utilizing the Shodan API, this tool allows asynchronous scanning to detect open SSH configurations, identify threat group affiliations, and filter out known benign entries, assisting in proactive threat detection. 8 | 9 | 2. Key Features 10 | Asynchronous Scanning: Efficiently scan multiple IPs simultaneously with asyncio and aiohttp to maximize Shodan queries. 11 | Threat Group Mapping: Tracks and maps IP addresses associated with known threat groups, stored in a JSON file (threat_groups.json) for persistent tracking. 12 | Junk Hash Filtering: Filters out junk or benign hashes from search results based on a predefined list (junk_hashes.json). 13 | Colored Output for Quick Analysis: Provides colored terminal outputs using Colorama for easier interpretation of results. 14 | Data Persistence: Saves known threat groups and junk hashes for continuity across sessions. 15 | 16 | 3. Setup and Installation 17 | 18 | Prerequisites: Python 3.7+, Shodan API Key, dependencies in requirements.txt. 19 | Install required packages: 20 | 21 | pip install -r requirements.txt 22 | 23 | Configuration: Replace 'YOUR_SHODAN_API_KEY' in the script with your Shodan API key. 24 | 25 | 4. Usage 26 | 27 | Run the script with: 28 | 29 | python ssh_hunter-share.py 30 | 31 | Function Descriptions: 32 | 33 | load_threat_groups(): Loads threat group mappings from threat_groups.json. 34 | load_junk_hashes(): Loads junk hashes from junk_hashes.json to filter out irrelevant results. 35 | 36 | 5. Contributing 37 | Contributions are welcome, especially in expanding threat group mappings and enhancing filtering logic. 38 | 39 | 6. License 40 | This project is open-source and available under the MIT License. 41 | -------------------------------------------------------------------------------- /R3D-SSH-Hunter.py: -------------------------------------------------------------------------------- 1 | import shodan 2 | import asyncio 3 | import aiohttp 4 | import ssl 5 | import certifi 6 | import json 7 | from collections import defaultdict 8 | import os 9 | import pandas as pd 10 | from colorama import Fore, Style, init 11 | 12 | # Initialize colorama for colored outputs 13 | init(autoreset=True) 14 | 15 | # Replace 'YOUR_SHODAN_API_KEY' with your actual Shodan API key 16 | API_KEY = 'API HERE' 17 | 18 | # Initialize the Shodan API client 19 | api = shodan.Shodan(API_KEY) 20 | 21 | # File paths for persistence 22 | THREAT_GROUP_FILE = 'threat_groups.json' 23 | JUNK_HASH_FILE = 'junk_hashes.json' 24 | 25 | # Load the threat group mappings from the file 26 | def load_threat_groups(): 27 | if os.path.exists(THREAT_GROUP_FILE): 28 | with open(THREAT_GROUP_FILE, 'r') as f: 29 | return json.load(f) 30 | return {} 31 | 32 | # Load the junk hashes from the file 33 | def load_junk_hashes(): 34 | if os.path.exists(JUNK_HASH_FILE): 35 | with open(JUNK_HASH_FILE, 'r') as f: 36 | return set(json.load(f)) # Convert list back to set 37 | return set() 38 | 39 | # Initialize threat groups and junk hashes 40 | hash_threat_groups = load_threat_groups() 41 | junk_hashes = load_junk_hashes() 42 | 43 | # Dictionary to store banner hashes and their corresponding IPs (use a set to avoid duplicate IPs) 44 | banner_hashes = defaultdict(set) 45 | 46 | # Save the threat groups to a file 47 | def save_threat_groups(): 48 | with open(THREAT_GROUP_FILE, 'w') as f: 49 | json.dump(hash_threat_groups, f) 50 | print(f"{Fore.CYAN}Threat groups saved to {THREAT_GROUP_FILE}") 51 | 52 | # Save the junk hashes to a file 53 | def save_junk_hashes(): 54 | with open(JUNK_HASH_FILE, 'w') as f: 55 | json.dump(list(junk_hashes), f) # Convert set to list for JSON serialization 56 | print(f"{Fore.CYAN}Junk hashes saved to {JUNK_HASH_FILE}") 57 | 58 | # ASCII Art for a cool banner 59 | from colorama import Fore, Style 60 | 61 | from colorama import Fore, Style 62 | 63 | def show_banner(): 64 | print(f"{Fore.LIGHTRED_EX}{Style.BRIGHT}") 65 | print("TTTTTTTTTTTTTTT LLLLL PPPPPPPPPPP") 66 | print(" TTT LLLLL PPPPP PPP") 67 | print(" TTT LLLLL PPPPP PPP") 68 | print(" TTT LLLLL PPPPPPPPPPP ") 69 | print(" TTT LLLLL PPPP ") 70 | print(" TTT LLLLLLLLLLLL PPPP ") 71 | print(" TTT LLLLLLLLLLLL PPPP ") 72 | print("") 73 | print("RRRRRRRRRRR 33333333333333 DDDDDDDDDDDD") 74 | print("RRRR RRRRR 33333333333333 DDDDD DDDD") 75 | print("RRRR RRRRR 3333 DDDD DDD") 76 | print("RRRRRRRRRRR 3333333333 DDDD DDD") 77 | print("RRRR RRRRR 333333333 DDDD DDD") 78 | print("RRRR RRRRR 3333 DDDDD DDDD") 79 | print("RRRR RRRRR 33333333333333 DDDDDDDDDDDD") 80 | print("RRRR RRRRR 33333333333333 DDDDDDDDDDDD") 81 | print("") 82 | print(" --- R3D SSH Hunter: The Ultimate SSH Key and Bad Guy Tracker ---") 83 | print(f"{Style.RESET_ALL}") 84 | 85 | # Function to gather SSH banner hash for the IP from the Shodan search results 86 | async def check_port_22_and_get_banner_hash(session, ip): 87 | print(f"{Fore.YELLOW}Checking IP: {ip} for port 22...{Style.RESET_ALL}") 88 | url = f"https://api.shodan.io/shodan/host/{ip}?key={API_KEY}" 89 | 90 | async with session.get(url) as response: 91 | if response.status == 200: 92 | result = await response.json() 93 | 94 | # Try to retrieve SSH banner hash from the result if port 22 is open 95 | for service in result.get('data', []): 96 | if service['port'] == 22: 97 | # Look for the hash field 98 | banner_hash = service.get('hash') 99 | 100 | if banner_hash: 101 | # Ignore junk hashes 102 | if banner_hash in junk_hashes: 103 | print(f"{Fore.RED}Ignoring junk hash for IP {ip}: {banner_hash}{Style.RESET_ALL}") 104 | return None, None 105 | print(f"{Fore.GREEN}Found SSH banner hash for IP {ip}: {banner_hash}{Style.RESET_ALL}") 106 | return banner_hash, ip 107 | print(f"{Fore.RED}No SSH banner found for IP {ip} or port 22 is closed.{Style.RESET_ALL}") 108 | return None, None 109 | 110 | # Function to run a search query and then check for SSH on port 22 111 | async def query_shodan(search_query): 112 | try: 113 | ssl_context = ssl.create_default_context(cafile=certifi.where()) # Use certifi's CA certificates 114 | 115 | print(f"{Fore.CYAN}Running search: {search_query}{Style.RESET_ALL}") 116 | 117 | results = api.search(search_query) 118 | print(f"{Fore.CYAN}Found {results['total']} devices using the query: {search_query}{Style.RESET_ALL}") 119 | 120 | async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context)) as session: 121 | tasks = [] 122 | for result in results['matches']: 123 | ip = result['ip_str'] 124 | # Create a task for each IP to check if port 22 is open 125 | tasks.append(check_port_22_and_get_banner_hash(session, ip)) 126 | responses = await asyncio.gather(*tasks) 127 | 128 | for banner_hash, ip in responses: 129 | if banner_hash: 130 | banner_hashes[banner_hash].add(ip) 131 | 132 | except shodan.APIError as e: 133 | print(f"{Fore.RED}Error: {e}{Style.RESET_ALL}") 134 | 135 | # Function to display the results in a table 136 | def display_results(): 137 | print(f"\n{Fore.BLUE}{Style.BRIGHT}Displaying results...\n{Style.RESET_ALL}") 138 | known_threats = [] 139 | unknown_duplicates = [] 140 | 141 | for banner_hash, ips in banner_hashes.items(): 142 | threat_group = hash_threat_groups.get(str(banner_hash)) # Ensure string conversion for comparison 143 | if threat_group: 144 | known_threats.append({"Banner Hash": banner_hash, "Threat Group": threat_group, "IPs": list(ips)}) 145 | elif len(ips) > 1: 146 | unknown_duplicates.append({"Banner Hash": banner_hash, "IPs": list(ips)}) 147 | 148 | # Display known threats 149 | if known_threats: 150 | print(f"{Fore.GREEN}{Style.BRIGHT}Known Threat Actor Hashes:{Style.RESET_ALL}") 151 | df_threats = pd.DataFrame(known_threats) 152 | print(df_threats) 153 | 154 | # Display unknown duplicates 155 | if unknown_duplicates: 156 | print(f"\n{Fore.YELLOW}{Style.BRIGHT}Duplicate Unknown Hashes:{Style.RESET_ALL}") 157 | df_unknown = pd.DataFrame(unknown_duplicates) 158 | print(df_unknown) 159 | 160 | # Function to add a new hash and threat group to the dictionary 161 | def add_new_hash(): 162 | try: 163 | # Prompt user for the SSH banner hash (integer only, no "hash:" prefix) 164 | new_hash = int(input(f"{Fore.YELLOW}Enter the new SSH banner hash (integer only, without 'hash:' prefix): {Style.RESET_ALL}")) 165 | threat_group = input(f"{Fore.YELLOW}Enter the associated threat group: {Style.RESET_ALL}") # Associated threat group name 166 | 167 | # Check if the hash already exists 168 | if str(new_hash) in hash_threat_groups: 169 | print(f"{Fore.RED}Hash {new_hash} is already associated with {hash_threat_groups[str(new_hash)]}.{Style.RESET_ALL}") 170 | else: 171 | # Add new hash and associated threat group to the dictionary 172 | hash_threat_groups[str(new_hash)] = threat_group 173 | print(f"{Fore.GREEN}Added hash {new_hash} with threat group {threat_group}.{Style.RESET_ALL}") 174 | save_threat_groups() # Save the updated threat group 175 | except ValueError: 176 | # Handle invalid input for hash 177 | print(f"{Fore.RED}Invalid hash value. Please enter a valid integer.{Style.RESET_ALL}") 178 | 179 | # Function to add a "junk" hash to be ignored 180 | def add_junk_hash(): 181 | try: 182 | # Prompt user for the junk SSH banner hash (integer only) 183 | junk_hash = int(input(f"{Fore.YELLOW}Enter the SSH banner hash to mark as junk: {Style.RESET_ALL}")) 184 | 185 | # Check if the hash is already marked as junk 186 | if junk_hash in junk_hashes: 187 | print(f"{Fore.RED}Hash {junk_hash} is already marked as junk.{Style.RESET_ALL}") 188 | else: 189 | junk_hashes.add(junk_hash) 190 | print(f"{Fore.GREEN}Added hash {junk_hash} to the junk list.{Style.RESET_ALL}") 191 | save_junk_hashes() # Save the updated junk hashes 192 | except ValueError: 193 | print(f"{Fore.RED}Invalid hash value. Please enter a valid integer.{Style.RESET_ALL}") 194 | 195 | # Function to search by threat group in the library with a list of options 196 | async def search_threat_group(): 197 | if not hash_threat_groups: 198 | print(f"{Fore.RED}No threat groups available.{Style.RESET_ALL}") 199 | return 200 | 201 | # Display available threat groups 202 | print(f"{Fore.CYAN}{Style.BRIGHT}Available Threat Groups:{Style.RESET_ALL}") 203 | threat_group_list = list(set(hash_threat_groups.values())) # Unique threat groups 204 | for idx, group in enumerate(threat_group_list): 205 | print(f"{Fore.GREEN}{idx + 1}. {group}{Style.RESET_ALL}") 206 | 207 | try: 208 | # Prompt the user to choose a threat group by number 209 | choice = int(input(f"{Fore.YELLOW}Select a threat group by number (1-{len(threat_group_list)}): {Style.RESET_ALL}")) 210 | if 1 <= choice <= len(threat_group_list): 211 | selected_group = threat_group_list[choice - 1] 212 | print(f"{Fore.GREEN}Selected threat group: {selected_group}{Style.RESET_ALL}") 213 | else: 214 | print(f"{Fore.RED}Invalid choice. Please try again.{Style.RESET_ALL}") 215 | return 216 | except ValueError: 217 | print(f"{Fore.RED}Invalid input. Please enter a number.{Style.RESET_ALL}") 218 | return 219 | 220 | # Search for hashes associated with the selected threat group 221 | matched_hashes = [hash_ for hash_, group in hash_threat_groups.items() if group == selected_group] 222 | 223 | if not matched_hashes: 224 | print(f"{Fore.RED}No hashes found for the threat group: {selected_group}{Style.RESET_ALL}") 225 | return 226 | 227 | print(f"{Fore.CYAN}Found hashes for {selected_group}: {matched_hashes}{Style.RESET_ALL}") 228 | 229 | for hash_ in matched_hashes: 230 | # Search for each hash 231 | search_query = f"hash:{hash_}" # Correctly use "hash:" instead of "ssh.hash:" 232 | await query_shodan(search_query) 233 | 234 | # Main menu with fancy art and color 235 | def main_menu(): 236 | show_banner() # Show cool ASCII art banner 237 | print(f"{Fore.BLUE}{Style.BRIGHT}\nMenu:{Style.RESET_ALL}") 238 | print(f"{Fore.CYAN}1. Enter custom search query (e.g., product:cobalt, org:fly, etc.)") 239 | print(f"{Fore.CYAN}2. Search by specific SSH banner hash (e.g., hash:-12345678)") 240 | print(f"{Fore.CYAN}3. Add a new SSH banner hash and associated threat group") 241 | print(f"{Fore.CYAN}4. Search for IPs by threat group name") 242 | print(f"{Fore.CYAN}5. Mark a hash as junk (to be ignored in future results){Style.RESET_ALL}") 243 | 244 | choice = input(f"{Fore.YELLOW}Enter your choice (1, 2, 3, 4, or 5): {Style.RESET_ALL}") 245 | 246 | if choice == '1': 247 | search_query = input(f"{Fore.YELLOW}Enter your Shodan search query: {Style.RESET_ALL}") 248 | return search_query, False 249 | elif choice == '2': 250 | specific_hash = input(f"{Fore.YELLOW}Enter the SSH banner hash: {Style.RESET_ALL}") 251 | search_query = f"hash:{specific_hash}" # Correctly use "hash:" for querying hashes 252 | return search_query, False 253 | elif choice == '3': 254 | add_new_hash() 255 | return None, None 256 | elif choice == '4': 257 | asyncio.run(search_threat_group()) 258 | return None, None 259 | elif choice == '5': 260 | add_junk_hash() 261 | return None, None 262 | else: 263 | print(f"{Fore.RED}Invalid choice. Please try again.{Style.RESET_ALL}") 264 | return None, None 265 | 266 | # Main entry point 267 | if __name__ == "__main__": 268 | search_query, _ = main_menu() 269 | 270 | if search_query: 271 | asyncio.run(query_shodan(search_query)) 272 | display_results() 273 | --------------------------------------------------------------------------------