├── README.md └── servicelens.py /README.md: -------------------------------------------------------------------------------- 1 | # Servicelens 2 | 3 | ## Description 4 | 5 | Servicelens is a Python script that enumerates Microsoft 365 domains for a given domain and analyzes the services associated with it. It checks DNS records (TXT, DMARC, SPF) to identify various services used by the domain and its subdomains, categorizing them into different service types such as Email Services, Cloud Platforms, Analytics, and more. 6 | 7 | ## Features 8 | 9 | - Enumerates Microsoft 365 domains associated with the input domain 10 | - Validates subdomains and extracts services from DNS records 11 | - Categorizes services into predefined categories 12 | - Provides a detailed, categorized summary of services found 13 | - Verbose mode for more detailed output 14 | 15 | ## Requirements 16 | 17 | - Python 3.6+ 18 | - dnspython library 19 | 20 | ## Installation 21 | 22 | 1. Clone this repository: 23 | ``` 24 | git clone https://github.com/nullenc0de/servicelens.git 25 | cd servicelens 26 | ``` 27 | 28 | 2. Install the required dependencies: 29 | ``` 30 | pip install dnspython 31 | ``` 32 | 33 | ## Usage 34 | 35 | Run the script with a domain name: 36 | 37 | ``` 38 | python servicelens.py -d example.com 39 | ``` 40 | 41 | For verbose output, add the `-v` flag: 42 | 43 | ``` 44 | python servicelens.py -d example.com -v 45 | ``` 46 | 47 | ## Output 48 | 49 | The script provides a categorized summary of services, including: 50 | 51 | - Email Services 52 | - Cloud Platforms 53 | - Analytics and Surveys 54 | - Customer Support 55 | - Security and Training 56 | - Marketing and CRM 57 | - Productivity and Collaboration 58 | - Development and Version Control 59 | - Content Delivery and Hosting 60 | - Payment and Financial Services 61 | - Uncategorized Services 62 | 63 | For each service, it shows which domain or subdomain it was associated with. 64 | 65 | Example Output: 66 | 67 | ![image](https://github.com/user-attachments/assets/3b8d165b-da12-46d8-8cab-f25f1db7a76e) 68 | 69 | 70 | ## Contributing 71 | 72 | Contributions are welcome! Please feel free to submit a Pull Request. 73 | 74 | ## License 75 | 76 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 77 | 78 | ## Disclaimer 79 | 80 | This tool is for educational and informational purposes only. Ensure you have permission to scan domains that you do not own. The authors are not responsible for any misuse or damage caused by this program. 81 | -------------------------------------------------------------------------------- /servicelens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import dns.resolver 5 | import xml.etree.ElementTree as ET 6 | from urllib.request import urlopen, Request 7 | from collections import defaultdict 8 | import re 9 | 10 | def get_m365_domains(domain): 11 | body = f""" 12 | 17 | 18 | Exchange2010 19 | urn:uuid:6389558d-9e05-465e-ade9-aae14c4bcd10 20 | http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation 21 | https://autodiscover.byfcxu-dom.extest.microsoft.com/autodiscover/autodiscover.svc 22 | 23 | http://www.w3.org/2005/08/addressing/anonymous 24 | 25 | 26 | 27 | 28 | 29 | {domain} 30 | 31 | 32 | 33 | """ 34 | 35 | headers = { 36 | "Content-type": "text/xml; charset=utf-8", 37 | "User-agent": "AutodiscoverClient" 38 | } 39 | 40 | try: 41 | httprequest = Request( 42 | "https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc", headers=headers, data=body.encode()) 43 | 44 | with urlopen(httprequest) as response: 45 | response = response.read().decode() 46 | except Exception as e: 47 | print(f"[-] Unable to execute request: {e}") 48 | return [] 49 | 50 | domains = [] 51 | tree = ET.fromstring(response) 52 | for elem in tree.iter(): 53 | if elem.tag == "{http://schemas.microsoft.com/exchange/2010/Autodiscover}Domain": 54 | domains.append(elem.text) 55 | 56 | return domains 57 | 58 | def check_txt_records(domain): 59 | try: 60 | txt_records = dns.resolver.resolve(domain, 'TXT') 61 | return [str(record) for record in txt_records] 62 | except Exception as e: 63 | return [] 64 | 65 | def check_dmarc(domain): 66 | try: 67 | dmarc_records = dns.resolver.resolve(f"_dmarc.{domain}", 'TXT') 68 | return [str(record) for record in dmarc_records if "v=DMARC1" in str(record)] 69 | except Exception as e: 70 | return [] 71 | 72 | def check_spf(domain): 73 | try: 74 | spf_records = dns.resolver.resolve(domain, 'TXT') 75 | return [str(record) for record in spf_records if "v=spf1" in str(record)] 76 | except Exception as e: 77 | return [] 78 | 79 | def extract_services(records): 80 | services = set() 81 | domain_pattern = re.compile(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}') 82 | 83 | for record in records: 84 | domains = domain_pattern.findall(record) 85 | services.update(domains) 86 | 87 | if "include:" in record: 88 | services.update(domain.strip() for domain in record.split("include:")[1:]) 89 | 90 | return services 91 | 92 | def validate_subdomain(subdomain, verbose=False): 93 | if verbose: 94 | print(f"\nValidating subdomain: {subdomain}") 95 | txt_records = check_txt_records(subdomain) 96 | dmarc_records = check_dmarc(subdomain) 97 | spf_records = check_spf(subdomain) 98 | 99 | if verbose: 100 | print(f"TXT Records: {txt_records}") 101 | print(f"DMARC Records: {dmarc_records}") 102 | print(f"SPF Records: {spf_records}") 103 | 104 | services = extract_services(txt_records + dmarc_records + spf_records) 105 | 106 | if verbose: 107 | print(f"Subdomain appears to be {'valid' if services else 'invalid'}") 108 | 109 | return services 110 | 111 | def categorize_services(all_services): 112 | service_categories = { 113 | 'Email Services': defaultdict(set), 114 | 'Cloud Platforms': defaultdict(set), 115 | 'Analytics and Surveys': defaultdict(set), 116 | 'Customer Support': defaultdict(set), 117 | 'Security and Training': defaultdict(set), 118 | 'Marketing and CRM': defaultdict(set), 119 | 'Productivity and Collaboration': defaultdict(set), 120 | 'Development and Version Control': defaultdict(set), 121 | 'Content Delivery and Hosting': defaultdict(set), 122 | 'Payment and Financial Services': defaultdict(set), 123 | 'Uncategorized Services': defaultdict(set) 124 | } 125 | 126 | known_services = { 127 | 'Email Services': ['outlook', 'gmail', 'yahoo', 'sendgrid', 'mailchimp', 'amazonses', 'zoho', 'postmark', 'mandrill', 'mailgun', 'sparkpost', 'protonmail', 'fastmail', 'sendpulse', 'sendinblue', 'mailjet', 'constantcontact', 'campaignmonitor', 'icontact', 'getresponse', 'aweber', 'activecampaign', 'drip', 'convertkit'], 128 | 'Cloud Platforms': ['aws', 'azure', 'googlecloud', 'digitalocean', 'heroku', 'linode', 'vultr', 'rackspace', 'ibmcloud', 'oracle', 'onmicrosoft', 'protection.outlook', 'cloudflare', 'fastly', 'akamai', 'cdn77', 'maxcdn', 'stackpath', 'cloudflarenet', 'salesforce', 'force.com'], 129 | 'Analytics and Surveys': ['google-analytics', 'googletagmanager', 'hotjar', 'mixpanel', 'kissmetrics', 'qualtrics', 'surveymonkey', 'typeform', 'segment', 'amplitude', 'heap', 'pendo', 'fullstory', 'optimizely', 'crazyegg', 'clicktale', 'mouseflow', 'luckyorange'], 130 | 'Customer Support': ['zendesk', 'freshdesk', 'helpscout', 'intercom', 'drift', 'tawk', 'livechat', 'olark', 'uservoice', 'desk.com', 'gorgias', 'kustomer', 'gladly', 'frontapp', 'helpwise', 'kayako', 'zopim', 'snapengage'], 131 | 'Security and Training': ['cloudflare', 'akamai', 'imperva', 'zscaler', 'proofpoint', 'mimecast', 'knowbe4', 'dmarcian', 'barracuda', 'symantec', 'mcafee', 'trend', 'sophos', 'kaspersky', 'bitdefender', 'malwarebytes', 'crowdstrike', 'carbonblack', 'cylance', 'sentinelone', 'fireeye', 'paloaltonetworks', 'fortinet'], 132 | 'Marketing and CRM': ['hubspot', 'salesforce', 'marketo', 'eloqua', 'pardot', 'mailchimp', 'exacttarget', 'constantcontact', 'campaignmonitor', 'getresponse', 'activecampaign', 'drip', 'pipedrive', 'zoho', 'freshsales', 'insightly', 'nutshell', 'agilecrm', 'sugarcrm'], 133 | 'Productivity and Collaboration': ['office365', 'gsuite', 'google.workspace', 'zoom', 'slack', 'dropbox', 'box', 'atlassian', 'asana', 'trello', 'basecamp', 'monday', 'notion', 'evernote', 'miro', 'airtable', 'clickup', 'wrike', 'teamwork', 'podio'], 134 | 'Development and Version Control': ['github', 'gitlab', 'bitbucket', 'jira', 'confluence', 'circleci', 'travis-ci', 'jenkins', 'teamcity', 'azure.devops', 'dockerhub', 'npmjs', 'rubygems', 'pypi', 'sonarqube', 'sentry'], 135 | 'Content Delivery and Hosting': ['cloudflare', 'akamai', 'fastly', 'cdn77', 'maxcdn', 'stackpath', 'aws.cloudfront', 'azure.cdn', 'google.cloud.cdn', 'netlify', 'vercel', 'heroku', 'wpengine', 'pantheon', 'acquia', 'godaddy', 'bluehost', 'hostgator'], 136 | 'Payment and Financial Services': ['paypal', 'stripe', 'square', 'adyen', 'worldpay', 'cybersource', 'authorize.net', 'braintree', '2checkout', 'wepay', 'quickbooks', 'xero', 'freshbooks', 'wave', 'zuora', 'chargify', 'recurly', 'chargebee'] 137 | } 138 | 139 | for domain, services in all_services.items(): 140 | for service in services: 141 | categorized = False 142 | for category, known_list in known_services.items(): 143 | if any(known in service.lower() for known in known_list): 144 | service_categories[category][service].add(domain) 145 | categorized = True 146 | break 147 | if not categorized: 148 | service_categories['Uncategorized Services'][service].add(domain) 149 | 150 | return service_categories 151 | 152 | def print_categorized_summary(categorized_services): 153 | print("\nCategorized Summary of Services:") 154 | print("================================") 155 | 156 | for category, services in categorized_services.items(): 157 | if services: 158 | print(f"\n{category}") 159 | print("-" * len(category)) 160 | for service, domains in sorted(services.items()): 161 | print(f" • {service}") 162 | if len(domains) > 1: 163 | print(" Seen in:") 164 | for domain in sorted(domains): 165 | print(f" - {domain}") 166 | else: 167 | print(f" Seen in: {next(iter(domains))}") 168 | print() # Add an extra newline for readability 169 | 170 | def main(): 171 | parser = argparse.ArgumentParser(description="Enumerates and validates Microsoft 365 domains") 172 | parser.add_argument("-d", "--domain", help="input domain name, example format: example.com", required=True) 173 | parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output") 174 | args = parser.parse_args() 175 | 176 | print(f"Enumerating Microsoft 365 domains for {args.domain}...") 177 | m365_domains = get_m365_domains(args.domain) 178 | 179 | all_services = defaultdict(set) 180 | 181 | if m365_domains: 182 | print("Found the following Microsoft 365 domains:") 183 | for domain in m365_domains: 184 | print(domain) 185 | services = validate_subdomain(domain, args.verbose) 186 | all_services[domain].update(services) 187 | else: 188 | print("No Microsoft 365 domains found.") 189 | 190 | # Also validate the original domain 191 | services = validate_subdomain(args.domain, args.verbose) 192 | all_services[args.domain].update(services) 193 | 194 | if args.verbose: 195 | print("\nDetailed Summary of services used:") 196 | for domain, services in all_services.items(): 197 | print(f"\n{domain}:") 198 | for service in sorted(services): 199 | print(f" - {service}") 200 | 201 | categorized_services = categorize_services(all_services) 202 | print_categorized_summary(categorized_services) 203 | 204 | if __name__ == "__main__": 205 | main() 206 | --------------------------------------------------------------------------------