444 lines
14 KiB
Python
444 lines
14 KiB
Python
|
|
"""
|
||
|
|
AUTARCH GEO IP/Domain Lookup Module
|
||
|
|
Get geolocation info for IPs, domains, and URLs
|
||
|
|
Based on Snoop Project's GEO_IP/domain plugin
|
||
|
|
"""
|
||
|
|
|
||
|
|
import ipaddress
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import socket
|
||
|
|
import sys
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
from urllib.parse import urlparse
|
||
|
|
|
||
|
|
# Add parent directory to path for imports
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
|
|
|
||
|
|
from core.banner import Colors
|
||
|
|
|
||
|
|
# Module metadata
|
||
|
|
NAME = "GEO IP Lookup"
|
||
|
|
DESCRIPTION = "Get geolocation for IPs, domains, and URLs"
|
||
|
|
AUTHOR = "darkHal Security Group"
|
||
|
|
VERSION = "1.0"
|
||
|
|
CATEGORY = "osint"
|
||
|
|
|
||
|
|
# Try to import requests
|
||
|
|
try:
|
||
|
|
import requests
|
||
|
|
except ImportError:
|
||
|
|
requests = None
|
||
|
|
|
||
|
|
|
||
|
|
class GeoIPLookup:
|
||
|
|
"""GEO IP/Domain lookup utility."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.session = None
|
||
|
|
self.timeout = 10
|
||
|
|
self.user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
|
||
|
|
self._init_session()
|
||
|
|
|
||
|
|
def _init_session(self):
|
||
|
|
"""Initialize requests session."""
|
||
|
|
if requests is None:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.session = requests.Session()
|
||
|
|
adapter = requests.adapters.HTTPAdapter(max_retries=2)
|
||
|
|
self.session.mount('https://', adapter)
|
||
|
|
self.session.mount('http://', adapter)
|
||
|
|
self.session.headers.update({'User-Agent': self.user_agent})
|
||
|
|
|
||
|
|
def _resolve_domain(self, target: str, timeout: int = 4) -> dict:
|
||
|
|
"""Resolve domain to IP addresses.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
target: Domain name or IP address.
|
||
|
|
timeout: Socket timeout in seconds.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict with resolved IPs and domain info.
|
||
|
|
"""
|
||
|
|
result = {
|
||
|
|
'domain': None,
|
||
|
|
'ipv4': None,
|
||
|
|
'ipv6': None,
|
||
|
|
}
|
||
|
|
|
||
|
|
def get_fqdn():
|
||
|
|
try:
|
||
|
|
result['domain'] = socket.getfqdn(target)
|
||
|
|
except Exception:
|
||
|
|
result['domain'] = target
|
||
|
|
|
||
|
|
def get_ips():
|
||
|
|
try:
|
||
|
|
addr_info = socket.getaddrinfo(target, 443)
|
||
|
|
for info in addr_info:
|
||
|
|
ip = info[4][0]
|
||
|
|
try:
|
||
|
|
if ipaddress.IPv4Address(ip):
|
||
|
|
result['ipv4'] = ip
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
try:
|
||
|
|
if ipaddress.IPv6Address(ip):
|
||
|
|
result['ipv6'] = ip
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Run in threads with timeout
|
||
|
|
t1 = threading.Thread(target=get_fqdn)
|
||
|
|
t2 = threading.Thread(target=get_ips)
|
||
|
|
t1.start()
|
||
|
|
t2.start()
|
||
|
|
t1.join(timeout)
|
||
|
|
t2.join(timeout)
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
def _parse_target(self, target: str) -> str:
|
||
|
|
"""Parse and clean target input.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
target: User input (IP, domain, or URL).
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Cleaned target string.
|
||
|
|
"""
|
||
|
|
target = target.strip()
|
||
|
|
|
||
|
|
# Check if it's a URL
|
||
|
|
if '://' in target:
|
||
|
|
parsed = urlparse(target)
|
||
|
|
if parsed.hostname:
|
||
|
|
target = parsed.hostname.replace('www.', '')
|
||
|
|
elif '/' in target:
|
||
|
|
target = target.split('/')[0]
|
||
|
|
|
||
|
|
return target
|
||
|
|
|
||
|
|
def _is_ip(self, target: str) -> bool:
|
||
|
|
"""Check if target is an IP address."""
|
||
|
|
try:
|
||
|
|
ipaddress.ip_address(target)
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def lookup(self, target: str) -> dict:
|
||
|
|
"""Perform GEO IP lookup.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
target: IP address, domain, or URL.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict with geolocation information.
|
||
|
|
"""
|
||
|
|
if self.session is None:
|
||
|
|
return {'error': 'requests library not available'}
|
||
|
|
|
||
|
|
target = self._parse_target(target)
|
||
|
|
|
||
|
|
# Validate input
|
||
|
|
if not target or len(target) < 4:
|
||
|
|
return {'error': 'Invalid target'}
|
||
|
|
|
||
|
|
if '..' in target:
|
||
|
|
return {'error': 'Invalid target format'}
|
||
|
|
|
||
|
|
result = {
|
||
|
|
'target': target,
|
||
|
|
'country_code': None,
|
||
|
|
'country': None,
|
||
|
|
'region': None,
|
||
|
|
'city': None,
|
||
|
|
'latitude': None,
|
||
|
|
'longitude': None,
|
||
|
|
'isp': None,
|
||
|
|
'org': None,
|
||
|
|
'ipv4': None,
|
||
|
|
'ipv6': None,
|
||
|
|
'domain': None,
|
||
|
|
'map_osm': None,
|
||
|
|
'map_google': None,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Resolve domain/IP
|
||
|
|
print(f"{Colors.CYAN}[*] Resolving target...{Colors.RESET}")
|
||
|
|
resolved = self._resolve_domain(target)
|
||
|
|
result['domain'] = resolved.get('domain')
|
||
|
|
result['ipv4'] = resolved.get('ipv4')
|
||
|
|
result['ipv6'] = resolved.get('ipv6')
|
||
|
|
|
||
|
|
# If target is IP, use it directly
|
||
|
|
if self._is_ip(target):
|
||
|
|
try:
|
||
|
|
if ipaddress.IPv4Address(target):
|
||
|
|
result['ipv4'] = target
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
try:
|
||
|
|
if ipaddress.IPv6Address(target):
|
||
|
|
result['ipv6'] = target
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Determine IP to lookup
|
||
|
|
lookup_ip = result['ipv4'] or target
|
||
|
|
|
||
|
|
# Try ipwho.is first
|
||
|
|
print(f"{Colors.CYAN}[*] Querying geolocation APIs...{Colors.RESET}")
|
||
|
|
geo_data = self._query_ipwhois(lookup_ip)
|
||
|
|
|
||
|
|
if not geo_data or geo_data.get('success') is False:
|
||
|
|
# Fallback to ipinfo.io
|
||
|
|
geo_data = self._query_ipinfo(lookup_ip)
|
||
|
|
|
||
|
|
if geo_data:
|
||
|
|
result['country_code'] = geo_data.get('country_code') or geo_data.get('country')
|
||
|
|
result['country'] = geo_data.get('country_name') or geo_data.get('country')
|
||
|
|
result['region'] = geo_data.get('region')
|
||
|
|
result['city'] = geo_data.get('city')
|
||
|
|
result['latitude'] = geo_data.get('latitude') or geo_data.get('lat')
|
||
|
|
result['longitude'] = geo_data.get('longitude') or geo_data.get('lon')
|
||
|
|
result['isp'] = geo_data.get('isp') or geo_data.get('org')
|
||
|
|
result['org'] = geo_data.get('org')
|
||
|
|
|
||
|
|
if not result['ipv4']:
|
||
|
|
result['ipv4'] = geo_data.get('ip')
|
||
|
|
|
||
|
|
# Generate map links
|
||
|
|
if result['latitude'] and result['longitude']:
|
||
|
|
lat, lon = result['latitude'], result['longitude']
|
||
|
|
result['map_osm'] = f"https://www.openstreetmap.org/#map=13/{lat}/{lon}"
|
||
|
|
result['map_google'] = f"https://www.google.com/maps/@{lat},{lon},12z"
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
def _query_ipwhois(self, ip: str) -> dict:
|
||
|
|
"""Query ipwho.is API.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
ip: IP address to lookup.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict with GEO data or None.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
url = f"https://ipwho.is/{ip}" if ip else "https://ipwho.is/"
|
||
|
|
response = self.session.get(url, timeout=self.timeout)
|
||
|
|
data = response.json()
|
||
|
|
|
||
|
|
if data.get('success') is False:
|
||
|
|
return None
|
||
|
|
|
||
|
|
return {
|
||
|
|
'ip': data.get('ip'),
|
||
|
|
'country_code': data.get('country_code'),
|
||
|
|
'country_name': data.get('country'),
|
||
|
|
'region': data.get('region'),
|
||
|
|
'city': data.get('city'),
|
||
|
|
'latitude': data.get('latitude'),
|
||
|
|
'longitude': data.get('longitude'),
|
||
|
|
'isp': data.get('connection', {}).get('isp'),
|
||
|
|
'org': data.get('connection', {}).get('org'),
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
print(f"{Colors.DIM} ipwho.is error: {e}{Colors.RESET}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def _query_ipinfo(self, ip: str) -> dict:
|
||
|
|
"""Query ipinfo.io API.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
ip: IP address to lookup.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict with GEO data or None.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
url = f"https://ipinfo.io/{ip}/json" if ip else "https://ipinfo.io/json"
|
||
|
|
response = self.session.get(url, timeout=self.timeout)
|
||
|
|
data = response.json()
|
||
|
|
|
||
|
|
loc = data.get('loc', ',').split(',')
|
||
|
|
lat = float(loc[0]) if len(loc) > 0 and loc[0] else None
|
||
|
|
lon = float(loc[1]) if len(loc) > 1 and loc[1] else None
|
||
|
|
|
||
|
|
return {
|
||
|
|
'ip': data.get('ip'),
|
||
|
|
'country_code': data.get('country'),
|
||
|
|
'country_name': data.get('country'),
|
||
|
|
'region': data.get('region'),
|
||
|
|
'city': data.get('city'),
|
||
|
|
'latitude': lat,
|
||
|
|
'longitude': lon,
|
||
|
|
'isp': data.get('org'),
|
||
|
|
'org': data.get('org'),
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
print(f"{Colors.DIM} ipinfo.io error: {e}{Colors.RESET}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def lookup_self(self) -> dict:
|
||
|
|
"""Lookup your own public IP.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict with geolocation information.
|
||
|
|
"""
|
||
|
|
print(f"{Colors.CYAN}[*] Looking up your public IP...{Colors.RESET}")
|
||
|
|
return self.lookup('')
|
||
|
|
|
||
|
|
def bulk_lookup(self, targets: list) -> list:
|
||
|
|
"""Perform bulk GEO lookups.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
targets: List of IPs/domains to lookup.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of result dicts.
|
||
|
|
"""
|
||
|
|
results = []
|
||
|
|
for i, target in enumerate(targets):
|
||
|
|
print(f"\n{Colors.CYAN}[{i+1}/{len(targets)}] Looking up: {target}{Colors.RESET}")
|
||
|
|
result = self.lookup(target)
|
||
|
|
results.append(result)
|
||
|
|
time.sleep(0.5) # Rate limiting
|
||
|
|
return results
|
||
|
|
|
||
|
|
|
||
|
|
def display_result(result: dict):
|
||
|
|
"""Display lookup result nicely."""
|
||
|
|
if 'error' in result:
|
||
|
|
print(f"{Colors.RED}[X] Error: {result['error']}{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n{Colors.CYAN}{'=' * 50}{Colors.RESET}")
|
||
|
|
print(f"{Colors.GREEN}{Colors.BOLD}Target:{Colors.RESET} {result['target']}")
|
||
|
|
print(f"{Colors.CYAN}{'=' * 50}{Colors.RESET}")
|
||
|
|
|
||
|
|
if result['ipv4']:
|
||
|
|
print(f" {Colors.GREEN}IPv4:{Colors.RESET} {result['ipv4']}")
|
||
|
|
if result['ipv6']:
|
||
|
|
print(f" {Colors.GREEN}IPv6:{Colors.RESET} {result['ipv6']}")
|
||
|
|
if result['domain'] and result['domain'] != result['target']:
|
||
|
|
print(f" {Colors.GREEN}Domain:{Colors.RESET} {result['domain']}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
|
||
|
|
if result['country_code']:
|
||
|
|
country_str = f"{result['country_code']}"
|
||
|
|
if result['country'] and result['country'] != result['country_code']:
|
||
|
|
country_str += f" ({result['country']})"
|
||
|
|
print(f" {Colors.GREEN}Country:{Colors.RESET} {country_str}")
|
||
|
|
|
||
|
|
if result['region']:
|
||
|
|
print(f" {Colors.GREEN}Region:{Colors.RESET} {result['region']}")
|
||
|
|
if result['city']:
|
||
|
|
print(f" {Colors.GREEN}City:{Colors.RESET} {result['city']}")
|
||
|
|
if result['isp']:
|
||
|
|
print(f" {Colors.GREEN}ISP:{Colors.RESET} {result['isp']}")
|
||
|
|
|
||
|
|
if result['latitude'] and result['longitude']:
|
||
|
|
print(f"\n {Colors.GREEN}Coordinates:{Colors.RESET} {result['latitude']}, {result['longitude']}")
|
||
|
|
|
||
|
|
if result['map_osm']:
|
||
|
|
print(f"\n {Colors.DIM}OpenStreetMap: {result['map_osm']}{Colors.RESET}")
|
||
|
|
if result['map_google']:
|
||
|
|
print(f" {Colors.DIM}Google Maps: {result['map_google']}{Colors.RESET}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
|
||
|
|
|
||
|
|
def display_menu():
|
||
|
|
"""Display the GEO IP module menu."""
|
||
|
|
print(f"""
|
||
|
|
{Colors.CYAN} GEO IP/Domain Lookup{Colors.RESET}
|
||
|
|
{Colors.DIM} Get geolocation for IPs, domains, and URLs{Colors.RESET}
|
||
|
|
{Colors.DIM}{'─' * 50}{Colors.RESET}
|
||
|
|
|
||
|
|
{Colors.GREEN}[1]{Colors.RESET} Lookup IP/Domain/URL
|
||
|
|
{Colors.GREEN}[2]{Colors.RESET} Lookup My IP
|
||
|
|
{Colors.GREEN}[3]{Colors.RESET} Bulk Lookup from File
|
||
|
|
|
||
|
|
{Colors.RED}[0]{Colors.RESET} Back to OSINT Menu
|
||
|
|
""")
|
||
|
|
|
||
|
|
|
||
|
|
def run():
|
||
|
|
"""Main entry point for the module."""
|
||
|
|
if requests is None:
|
||
|
|
print(f"{Colors.RED}[X] This module requires 'requests' library{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} Install with: pip install requests{Colors.RESET}")
|
||
|
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
lookup = GeoIPLookup()
|
||
|
|
|
||
|
|
while True:
|
||
|
|
display_menu()
|
||
|
|
choice = input(f"{Colors.GREEN}Select option: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if choice == '0':
|
||
|
|
break
|
||
|
|
|
||
|
|
elif choice == '1':
|
||
|
|
print(f"\n{Colors.CYAN}Enter IP, domain, or URL:{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}Examples: 8.8.8.8, google.com, https://example.com/path{Colors.RESET}")
|
||
|
|
target = input(f"\n{Colors.GREEN}Target: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if not target:
|
||
|
|
continue
|
||
|
|
|
||
|
|
result = lookup.lookup(target)
|
||
|
|
display_result(result)
|
||
|
|
input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
elif choice == '2':
|
||
|
|
result = lookup.lookup_self()
|
||
|
|
display_result(result)
|
||
|
|
input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
elif choice == '3':
|
||
|
|
print(f"\n{Colors.CYAN}Enter path to file with targets (one per line):{Colors.RESET}")
|
||
|
|
filepath = input(f"\n{Colors.GREEN}File path: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if not filepath or not os.path.exists(filepath):
|
||
|
|
print(f"{Colors.RED}[X] File not found{Colors.RESET}")
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
with open(filepath, 'r') as f:
|
||
|
|
targets = [line.strip() for line in f if line.strip()]
|
||
|
|
|
||
|
|
if not targets:
|
||
|
|
print(f"{Colors.RED}[X] No targets found in file{Colors.RESET}")
|
||
|
|
continue
|
||
|
|
|
||
|
|
print(f"{Colors.GREEN}[+] Found {len(targets)} targets{Colors.RESET}")
|
||
|
|
confirm = input(f"\n{Colors.YELLOW}Proceed with lookup? (y/n): {Colors.RESET}").strip().lower()
|
||
|
|
|
||
|
|
if confirm == 'y':
|
||
|
|
results = lookup.bulk_lookup(targets)
|
||
|
|
for result in results:
|
||
|
|
display_result(result)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"{Colors.RED}[X] Error reading file: {e}{Colors.RESET}")
|
||
|
|
|
||
|
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
else:
|
||
|
|
print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
run()
|