Skip to content

Commit

Permalink
Merge pull request #55 from madhav6ram/main
Browse files Browse the repository at this point in the history
Added Namecheap Nameserver integration
  • Loading branch information
Prateek-Thakare authored Nov 25, 2024
2 parents 2fd6536 + cbc29bf commit 43c2b1d
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 4 deletions.
4 changes: 2 additions & 2 deletions configs/local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ workflow:
scanNewOnly: true
workflowConfig:
- moduleName : dns
tools: ['Route53', 'Cloudflare']
tools: ['Route53', 'Cloudflare', 'Namecheap']
order: 1
- moduleName : discovery
tools: ['Subfinder', 'SSLMate']
Expand Down Expand Up @@ -89,7 +89,7 @@ workflow:
cmd: []
workflowConfig:
- moduleName : dns
tools: ['Cloudflare']
tools: ['Namecheap']
order: 1

- workflowName: 'testreport'
Expand Down
164 changes: 164 additions & 0 deletions mantis/modules/dns/Namecheap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import logging
import requests
import json
from lxml import etree
from mantis.constants import ASSET_TYPE_TLD, ASSET_TYPE_SUBDOMAIN
from mantis.models.args_model import ArgsModel
from mantis.utils.asset_type import AssetType
from mantis.utils.crud_utils import CrudUtils
from mantis.utils.base_request import BaseRequestExecutor
from mantis.utils.tool_utils import get_assets_grouped_by_type
from mantis.utils.list_assets import ListAssets
from mantis.tool_base_classes.baseScanner import BaseScanner


logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

namecheap_api_url = 'https://api.namecheap.com/xml.response'

api_user = None
user_name = None
api_key = None
client_ip = None

class Namecheap(BaseScanner):

async def init(self, args: ArgsModel):
self.args = args
self.db_assets = await get_assets_grouped_by_type(self, args, ASSET_TYPE_TLD)
return [(self, "Namecheap")]

async def execute(self, tooltuple):
logging.info(f"Reading zone files from Namecheap nameservers")
return await self.main()

def namecheap_dns_request(self, sld, tld):
data = {
'ApiUser': api_user,
'UserName': user_name,
'ApiKey': api_key,
'ClientIP': client_ip,
'Command': 'namecheap.domains.dns.getHosts',
'SLD': sld,
'TLD': tld
}

url = f"{namecheap_api_url}?{requests.compat.urlencode(data)}"
api_tuple = (url, None, None, {"sld": sld, "tld": tld})
_, response = BaseRequestExecutor.sendRequest("GET", api_tuple)
response_xml = etree.XML(response.content)

if response_xml.get('Status') != 'OK':
logging.info("Error in response from Namecheap")
raise Exception(response.content)

return response_xml

def namecheap_domain_request(self):
data = {
'ApiUser': api_user,
'UserName': user_name,
'ApiKey': api_key,
'ClientIP': client_ip,
'Command': 'namecheap.domains.getList',
'Page': 1,
'PageSize': 100
}

url = f"{namecheap_api_url}?{requests.compat.urlencode(data)}"
api_tuple = (url, None, None, None)
_, response = BaseRequestExecutor.sendRequest("GET", api_tuple)
response_xml = etree.XML(response.content)

if response_xml.get('Status') != 'OK':
logging.info("Error in response from Namecheap")
raise Exception(response.content)

return response_xml

def get_records(self, sld, tld):
response = self.namecheap_dns_request(sld, tld)
host_elements = response.xpath(
'/x:ApiResponse/x:CommandResponse/x:DomainDNSGetHostsResult/x:host',
namespaces={'x': 'http://api.namecheap.com/xml.response'}
)

records = [dict(h.attrib) for h in host_elements]
for record in records:
record.pop('AssociatedAppTitle', None)
record.pop('FriendlyName', None)
record.pop('HostId', None)
record['HostName'] = record.pop('Name') + '.' + sld + '.' + tld
record.pop('IsActive', None)
record.pop('IsDDNSEnabled', None)
if record['Type'] != 'MX':
record.pop('MXPref', None)
record['RecordType'] = record.pop('Type')
if record['TTL'] == '1800':
record.pop('TTL')
return records

async def main(self):
results = {}
output_dict_list = []
results["success"] = 0
results["failure"] = 0

# Fetch domains from Namecheap
request = self.namecheap_domain_request()
domains = request.xpath(
'//x:DomainGetListResult/x:Domain/@Name',
namespaces={'x': 'http://api.namecheap.com/xml.response'}
)

logging.info(f"Current Domains in scope added to org: {self.db_assets}")
logging.info(f"Domains registered in Namecheap: {domains}")

for domain in domains:
(sld, tld) = domain.split('.', 1)

# If using the --in_scope/-is arguments, list only domains from nameserver that are in scope
if self.args.in_scope == True and domain not in self.db_assets:
logging.info(f"Asset {domain} not in scope. Skipping...")
continue

logging.info(f'Enumerating domain: {domain}')

try:
records = self.get_records(sld, tld)
for record in records:
domain_dict = {}
domain_dict['_id'] = record['HostName']
domain_dict['asset'] = record['HostName']

if AssetType.check_tld(record['HostName']):
domain_dict['asset_type'] = ASSET_TYPE_TLD
else:
domain_dict['asset_type'] = ASSET_TYPE_SUBDOMAIN

domain_dict['org'] = self.args.org
output_dict_list.append(domain_dict)
results["success"] += 1

except Exception as e:
logging.info(f"Enumeration for {domain} failed: {e}")
results["failure"] += 1
results['exception'] = str(e)

err = e.args[0].decode("utf-8").encode()
logging.info(err)
API_exception = etree.fromstring(err)
error_code = API_exception.find('.//{http://api.namecheap.com/xml.response}Error').get('Number')

logging.info(error_code)
# Continue with scanning other domains if DNS is misconfigured.
# Terminate if any other error in API response.
# Error Code: 2030288 - Cannot complete this command as this domain is not using proper DNS servers
# Read more: https://www.namecheap.com/support/api/methods/domains-dns/get-hosts/
if error_code != 2030288:
raise Exception(f"Error in response from Namecheap API: {e}")
return results

await CrudUtils.insert_assets(output_dict_list, source='internal')
logging.info("Inserted into the database.")
return results
10 changes: 8 additions & 2 deletions mantis/utils/args_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,11 @@ def args_parse() -> ArgsModel:
action = 'store_true'
)

onboard_parser.add_argument('-tc', '--thread_count',
onboard_parser.add_argument('-tc', '--thread_count',
type = int,
dest = 'thread_count',
help = 'thread count, default 3',
default = 3
)

onboard_parser.add_argument('-r', '--use_ray',
Expand Down Expand Up @@ -200,9 +202,11 @@ def args_parse() -> ArgsModel:
action = 'store_true'
)

scan_parser.add_argument('-tc', '--thread_count',
scan_parser.add_argument('-tc', '--thread_count',
type = int,
dest = 'thread_count',
help = 'thread count, default 3',
default = 3
)

scan_parser.add_argument('-r', '--use_ray',
Expand Down Expand Up @@ -322,6 +326,8 @@ def args_parse() -> ArgsModel:
if args.subcommand == "scan":
if args.subdomain:
parsed_args["subdomain"] = args.subdomain
if args.in_scope:
parsed_args["in_scope"] = True

if args.subcommand == "onboard":
if args.subdomain:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ slack-sdk==3.21.3
rocketry==2.5.1
boto3==1.28.20
tqdm==4.66.5
lxml==5.3.0
cloudflare

0 comments on commit 43c2b1d

Please sign in to comment.