├── .gitignore ├── LICENSE ├── README.md └── origin_recon.py /.gitignore: -------------------------------------------------------------------------------- 1 | .qodo 2 | 3 | # Python 4 | __pycache__/ 5 | *.py[cod] 6 | *.so 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # Virtual Environment 25 | venv/ 26 | ENV/ 27 | env/ 28 | 29 | # IDE & Editors 30 | .vscode/ 31 | .idea/ 32 | *.swp 33 | *.swo 34 | *.sublime-workspace 35 | *.sublime-project 36 | 37 | # Logs and databases 38 | *.log 39 | *.sqlite 40 | *.db 41 | 42 | # Local Overrides 43 | .env 44 | .env.local 45 | .env.development 46 | .env.test 47 | .env.production 48 | 49 | # Testing 50 | .coverage 51 | htmlcov/ 52 | .pytest_cache/ 53 | test-results/ 54 | 55 | # OS-Specific 56 | .DS_Store 57 | Thumbs.db 58 | 59 | # Output Files 60 | *.csv 61 | *.json 62 | *.txt 63 | *.xlsx 64 | 65 | # Custom Outputs (if generated) 66 | results/ 67 | scans/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025/4/21] [Nazanin Nazari] 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # N0aziXss Origin Recon v3.1 🍓 2 | 3 | ## 🌟 Introduction 4 | **N0aziXss Origin Recon** Advanced reconnaissance tool for subdomain enumeration, IP analysis, and origin server detection with multi-layer security checks. 5 | 6 | ## Features ✨ 7 | - Subdomain extraction via Certificate Transparency (CRT.sh) 8 | - DNS resolution with SSRF protection 9 | - IP geolocation and ASN analysis 10 | - Common port scanning (80, 443, 22, etc.) 11 | - Critical origin IP detection (non-CDN) 12 | - SSL/TLS certificate analysis (optional) 13 | - Error logging and reporting system 14 | - JSON output support 15 | 16 | ## Requirements ⚙️ 17 | - Python 3.8+ 18 | - Required libraries: `pip install -r requirements.txt` 19 | 20 | ## Installation 📦 21 | ```bash 22 | git clone https://github.com/NazaninNazari/Origin_Recon.git 23 | cd Origin_Recon-tools 24 | 25 | # install dependencies 26 | pip install -r requirements.txt 27 | 28 | #Usage 29 | python origin_recon.py -d example.com [--ssl] [--output results.json] 30 | 31 | ## Options 32 | -d & --domain --> Target domain (required) 33 | --ssl --> Enable SSL/TLS scanning (optional) 34 | --output --> Save results to JSON file (optional) 35 | 36 | # Sample Output 37 | 🔥 Scan Results for example.com 🔥 38 | ┌─────────────────────────┬──────────────────────┬───────────────────────┬─────────────────┐ 39 | │ Subdomain │ IP Addresses │ ASN │ Open Ports │ 40 | ├─────────────────────────┼──────────────────────┼───────────────────────┼─────────────────┤ 41 | │ mail.example.com │ 192.0.2.1 │ AS15169 (Google LLC) │ 80, 443 │ 42 | │ │ │ [dim]US/California[/] │ │ 43 | ├─────────────────────────┼──────────────────────┼───────────────────────┼─────────────────┤ 44 | │ api.example.com │ 203.0.113.5 │ AS13335 (Cloudflare) │ 443, 8080 │ 45 | │ │ │ [dim]DE/Frankfurt[/] │ │ 46 | └─────────────────────────┴──────────────────────┴───────────────────────┴─────────────────┘ 47 | 48 | 🔥 Critical Origin IPs 🔥 49 | ┌─────────────────┬───────────────────────────────┬─────────────────────────┐ 50 | │ IP │ Detection Reasons │ Risk Level │ 51 | ├─────────────────┼───────────────────────────────┼─────────────────────────┤ 52 | │ 198.51.100.22 │ High TTL (3600s) │ [bold]High Risk[/] │ 53 | │ │ Server: Apache/2.4.29 │ │ 54 | └─────────────────┴───────────────────────────────┴─────────────────────────┘ 55 | 56 | ⚠ Error Summary ⚠ 57 | ┌──────────┬──────────────────────┬─────────────────────────────┐ 58 | │ Time │ Type │ Message │ 59 | ├──────────┼──────────────────────┼─────────────────────────────┤ 60 | │ 12:34:56 │ CRT.sh Connection │ Server took too long... │ 61 | └──────────┴──────────────────────┴─────────────────────────────┘ 62 | 63 | ✓ Scan completed in 0:01:23 64 | Total Subdomains: 15 65 | Unique IPs Found: 9 66 | Critical Findings: 1 67 | Errors Occurred: 1 68 | ✓ Results saved to results.json -------------------------------------------------------------------------------- /origin_recon.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import re 4 | import aiohttp 5 | import ssl 6 | from socket import socket 7 | from colorama import Fore, Style 8 | from rich.console import Console 9 | from rich.table import Table 10 | from rich.style import Style 11 | from rich.progress import Progress 12 | from rich.box import DOUBLE_EDGE 13 | from dns.asyncresolver import Resolver 14 | from datetime import datetime 15 | from pyfiglet import Figlet 16 | 17 | # Initial settings 18 | try: 19 | import tldextract 20 | HAVE_TLDEXTRACT = True 21 | except ImportError: 22 | HAVE_TLDEXTRACT = False 23 | 24 | console = Console() 25 | 26 | # Banner_One 27 | PURPLE = '\033[0;35m' 28 | END = "\033[0m" 29 | 30 | banner = f""" 31 | {END} 32 | ███╗ ██╗ █████╗ ███████╗██╗██╗ ██╗███████╗███████╗ 33 | ████╗ ██║██╔══██╗╚══███╔╝██║╚██╗██╔╝██╔════╝██╔════╝ 34 | ██╔██╗ ██║███████║ ███╔╝ ██║ ╚███╔╝ ███████╗███████╗ 35 | ██║╚██╗██║██╔══██║ ███╔╝ ██║ ██╔██╗ ╚════██║╚════██║ 36 | ██║ ╚████║██║ ██║███████╗██║██╔╝ ██╗███████║███████║ 37 | ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ 38 | 39 | ██████╗ ██████╗ ██╗ ██████╗ ██╗███╗ ██╗ 40 | ██╔═══██╗██╔══██╗██║██╔════╝ ██║████╗ ██║ 41 | ██║ ██║██████╔╝██║██║ ███╗██║██╔██╗ ██║ 42 | ██║ ██║██╔══██╗██║██║ ██║██║██║╚██╗██║ 43 | ╚██████╔╝██║ ██║██║╚██████╔╝██║██║ ╚████║ 44 | ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ 45 | 46 | ██████╗ ███████╗ ██████╗ ██████╗ ███╗ ██╗ 47 | ██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ██║ 48 | ██████╔╝█████╗ ██║ ██║ ██║██╔██╗ ██║ 49 | ██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╗██║ 50 | ██║ ██║███████╗╚██████╗╚██████╔╝██║ ╚████║ 51 | ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ 52 | {Fore.YELLOW} 53 | ╔══════════════════════════════════════════════════════╗ 54 | ║ N0aziXss Origin Recon v3.0 ║ 55 | ╚══════════════════════════════════════════════════════╝ 56 | {Fore.RESET}""" 57 | 58 | print(banner) 59 | 60 | class N0aziXssScanner: 61 | def __init__(self, domain, enable_ssl=False, output_file=None): 62 | self.domain = domain 63 | self.resolver = Resolver() 64 | self.resolver.nameservers = ['8.8.8.8', '1.1.1.1'] 65 | self.session = None 66 | self.progress = Progress() 67 | self.found_ips = set() 68 | self.enable_ssl = enable_ssl 69 | self.output_file = output_file 70 | self.error_messages = [] 71 | self.critical_ips_count = 0 72 | 73 | async def log_error(self, message): 74 | """Save errors in the same format""" 75 | timestamp = datetime.now().strftime('%H:%M:%S') 76 | error_msg = f"[dim]{timestamp}[/dim] [bright_red]✗[/bright_red] [bright_white]{message}[/bright_white]" 77 | self.error_messages.append(message) 78 | console.print(error_msg) 79 | 80 | async def SafeDnsResolve(self, target, record_type='A'): 81 | try: 82 | answers = await self.resolver.resolve(target, record_type) 83 | return [str(r) for r in answers] 84 | except Exception as e: 85 | await self.log_error(f"DNS Resolution Failed: {target} ({str(e)})") 86 | return [] 87 | 88 | async def FetchLogs(self): 89 | url = f"https://crt.sh/?q=%.{self.domain}&output=json" 90 | try: 91 | async with self.session.get(url, timeout=15) as resp: 92 | if resp.status == 200: 93 | try: 94 | data = await resp.json(content_type=None) 95 | return list({entry['name_value'].lower().strip() for entry in data}) 96 | except Exception as e: 97 | await self.log_error(f"CRT.sh Data Parse Error: {str(e)}") 98 | return [] 99 | await self.log_error(f"CRT.sh HTTP Error: Status {resp.status}") 100 | return [] 101 | except asyncio.TimeoutError: 102 | await self.log_error("CRT.sh Request Timeout") 103 | return [] 104 | except Exception as e: 105 | await self.log_error(f"CRT.sh Connection Error: {str(e)}") 106 | return [] 107 | 108 | async def GetAsn(self, ip): 109 | try: 110 | reversed_ip = '.'.join(reversed(ip.split('.'))) 111 | result = await self.SafeDnsResolve(f"{reversed_ip}.origin.asn.cymru.com", 'TXT') 112 | return result[0].split('|')[0].strip() if result else "Unknown" 113 | except Exception as e: 114 | await self.log_error(f"ASN Lookup Failed: {ip} ({str(e)})") 115 | return "Unknown" 116 | 117 | async def GetGeo(self, ip): 118 | try: 119 | async with self.session.get(f"http://ip-api.com/json/{ip}", timeout=5) as resp: 120 | if resp.status == 200: 121 | data = await resp.json() 122 | if data.get('status') == 'success': 123 | return f"{data.get('country', '')}/{data.get('city', '')}" 124 | return "Unknown" 125 | await self.log_error(f"GeoIP API Error: Status {resp.status}") 126 | return "Unknown" 127 | except Exception as e: 128 | await self.log_error(f"GeoIP Lookup Failed: {ip} ({str(e)})") 129 | return "Unknown" 130 | 131 | async def DetectOrigin(self, ip): 132 | reasons = [] 133 | try: 134 | answers = await self.resolver.resolve(self.domain, 'A') 135 | ttl = answers.rrset.ttl 136 | if ttl > 300: 137 | reasons.append(f"High TTL ({ttl}s)") 138 | 139 | async with self.session.get(f"http://{ip}", timeout=5, ssl=False) as resp: 140 | headers = resp.headers 141 | if 'Server' in headers and 'cloudflare' not in headers['Server'].lower(): 142 | reasons.append(f"Server: {headers['Server']}") 143 | if 'X-Powered-By' in headers: 144 | reasons.append(f"Tech: {headers['X-Powered-By']}") 145 | return reasons if reasons else ["Potential CDN"] 146 | except Exception as e: 147 | await self.log_error(f"Origin Detection Failed: {ip} ({str(e)})") 148 | return ["Detection Failed"] 149 | 150 | async def ScanPorts(self, ip): 151 | common_ports = [80, 443, 22, 21, 8080] 152 | open_ports = [] 153 | for port in common_ports: 154 | try: 155 | reader, writer = await asyncio.wait_for( 156 | asyncio.open_connection(ip, port), 157 | timeout=1.5 158 | ) 159 | open_ports.append(str(port)) 160 | writer.close() 161 | except Exception: 162 | pass 163 | return open_ports if open_ports else ["None"] 164 | 165 | async def CheckSSL(self, domain): 166 | try: 167 | context = ssl.create_default_context() 168 | with socket() as sock: 169 | with context.wrap_socket(sock, server_hostname=domain) as ssock: 170 | ssock.connect((domain, 443)) 171 | cert = ssock.getpeercert() 172 | return f"Issuer: {cert['issuer']}, Expiry: {cert['notAfter']}" 173 | except Exception as e: 174 | return f"SSL Error: {str(e)}" 175 | 176 | async def RunScan(self): 177 | start_time = datetime.now() 178 | origin_table = None 179 | 180 | async with aiohttp.ClientSession( 181 | connector=aiohttp.TCPConnector(ssl=False), 182 | timeout=aiohttp.ClientTimeout(total=30) 183 | ) as self.session: 184 | with self.progress: 185 | # Collecting subdomains 186 | task = self.progress.add_task("[cyan]Collecting subdomains...", total=1) 187 | subdomains = await self.FetchLogs() 188 | self.progress.update(task, completed=1) 189 | 190 | # Create tables 191 | main_table = Table( 192 | title=f"[blink]🔥 Scan Results for {self.domain} 🔥[/]", 193 | box=DOUBLE_EDGE, 194 | style="bright_white", 195 | title_style=Style(blink=True, bold=True) 196 | ) 197 | 198 | # Main table columns 199 | columns = [ 200 | ("Subdomain", "bright_green", 25), 201 | ("IP Addresses", "cyan", 20), 202 | ("ASN", "bold yellow", 15), 203 | ("Open Ports", "bright_red", 15) 204 | ] 205 | 206 | if self.enable_ssl: 207 | columns.append(("SSL Info", "green", 30)) 208 | 209 | for col in columns: 210 | main_table.add_column(col[0], style=col[1], width=col[2]) 211 | 212 | # Critical IPs table 213 | origin_table = Table( 214 | title="[blink]🔥 Critical Origin IPs 🔥[/]", 215 | style="white", 216 | box=DOUBLE_EDGE, 217 | title_style=Style(blink=True, bold=True) 218 | ) 219 | origin_table.add_column("IP", style="bold yellow") 220 | origin_table.add_column("Detection Reasons", style="green") 221 | origin_table.add_column("Risk Level", style="red") 222 | 223 | task = self.progress.add_task("[yellow]Analyzing IPs...", total=len(subdomains)) 224 | for sub in subdomains: 225 | ips = await self.SafeDnsResolve(sub) 226 | 227 | # Subdomain colors 228 | sub_display = f"[bright_green]{sub}[/bright_green]" if ips else f"[dim]{sub}[/dim]" 229 | 230 | # Preparing row data 231 | row_data = [ 232 | sub_display, 233 | "\n".join(ips) if ips else "[dim]No IPs[/dim]", 234 | await self.GetAsn(ips[0]) if ips else "[dim]N/A[/dim]", 235 | ", ".join(await self.ScanPorts(ips[0])) if ips else "[dim]N/A[/dim]" 236 | ] 237 | 238 | if self.enable_ssl: 239 | row_data.append(await self.CheckSSL(sub) if ips else "[dim]N/A[/dim]") 240 | 241 | main_table.add_row(*row_data) 242 | 243 | # Checking critical IPs 244 | if ips: 245 | for ip in ips: 246 | if ip in self.found_ips: 247 | continue 248 | self.found_ips.add(ip) 249 | reasons = await self.DetectOrigin(ip) 250 | if "CDN" not in reasons[0]: 251 | origin_table.add_row( 252 | f"[bright_yellow]{ip}[/bright_yellow]", 253 | "\n".join(reasons), 254 | "[bold bright_red]High Risk[/bold bright_red]" if "Failed" not in reasons[0] else "[dim]Unknown[/dim]" 255 | ) 256 | self.critical_ips_count += 1 257 | 258 | self.progress.update(task, advance=1) 259 | 260 | # Show results 261 | console.print(main_table) 262 | 263 | # Show critical IPs 264 | if self.critical_ips_count > 0: 265 | console.print(origin_table) 266 | else: 267 | console.print("[bold yellow]No critical origin IPs found![/]") 268 | 269 | # Show errors if any 270 | if self.error_messages: 271 | error_table = Table( 272 | title="[blink]⚠ Error Summary ⚠[/]", 273 | box=DOUBLE_EDGE, 274 | style="bright_red", 275 | title_style=Style(bold=True, blink=True) 276 | ) 277 | error_table.add_column("Time", style="dim", width=8) 278 | error_table.add_column("Type", style="bright_white", width=20) 279 | error_table.add_column("Message", style="bright_red", width=50) 280 | 281 | for error in self.error_messages: 282 | parts = error.split(":", 1) 283 | error_type = parts[0] if len(parts) > 1 else "General Error" 284 | message = parts[1] if len(parts) > 1 else error 285 | 286 | error_table.add_row( 287 | f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim]", 288 | f"[bright_white]{error_type}[/bright_white]", 289 | f"[bright_red]{message}[/bright_red]" 290 | ) 291 | 292 | console.print("\n") 293 | console.print(error_table) 294 | 295 | # Summary of results 296 | console.print( 297 | f"\n[bold cyan]✓ Scan completed in {datetime.now() - start_time}[/]\n" 298 | f"Total Subdomains: [bright_green]{len(subdomains)}[/bright_green]\n" 299 | f"Unique IPs Found: [bright_cyan]{len(self.found_ips)}[/bright_cyan]\n" 300 | f"Critical Findings: [bright_red]{self.critical_ips_count}[/bright_red]\n" 301 | f"Errors Occurred: [bright_red]{len(self.error_messages)}[/bright_red]" 302 | ) 303 | 304 | # Save the results 305 | if self.output_file: 306 | import json 307 | with open(self.output_file, 'w') as f: 308 | json.dump({ 309 | "domain": self.domain, 310 | "subdomains": subdomains, 311 | "ips": list(self.found_ips), 312 | "critical_ips": self.critical_ips_count, 313 | "errors": self.error_messages, 314 | "timestamp": str(datetime.now()) 315 | }, f, indent=4) 316 | console.print(f"[bright_green]✓ Results saved to {self.output_file}[/bright_green]") 317 | 318 | if __name__ == "__main__": 319 | parser = argparse.ArgumentParser(description="N0aziXss Recon - Professional Reconnaissance Tool") 320 | parser.add_argument("-d", "--domain", required=True, help="Target domain name") 321 | parser.add_argument("--ssl", action="store_true", help="Enable SSL/TLS scanning") 322 | parser.add_argument("--output", type=str, help="Save results to a JSON file") 323 | args = parser.parse_args() 324 | 325 | # Domain validation 326 | if HAVE_TLDEXTRACT: 327 | ext = tldextract.extract(args.domain) 328 | if not ext.suffix: 329 | console.print("[bold bright_red]✗ Error: Invalid domain![/bold bright_red]") 330 | exit(1) 331 | else: 332 | if not re.match(r"^([a-z0-9\-]+\.)+[a-z]{2,10}$", args.domain): 333 | console.print("[bold bright_red]✗ Error: Invalid domain format![/bold bright_red]") 334 | exit(1) 335 | 336 | try: 337 | scanner = N0aziXssScanner(args.domain, enable_ssl=args.ssl, output_file=args.output) 338 | asyncio.run(scanner.RunScan()) 339 | except KeyboardInterrupt: 340 | console.print("\n[bold bright_yellow]⚠ Scan interrupted by user![/bold bright_yellow]") 341 | exit(0) --------------------------------------------------------------------------------