Advertisement
Guest User

Untitled

a guest
Oct 14th, 2019
103
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.56 KB | None | 0 0
  1. import random
  2. import string
  3. import asyncio
  4. import functools
  5. import os
  6. import uvloop
  7. import aiodns
  8. import click
  9. import socket
  10. import sys
  11. from tqdm import tqdm
  12. from aiodnsbrute.logger import ConsoleLogger
  13.  
  14.  
  15. class aioDNSBrute(object):
  16. """aiodnsbrute implements fast domain name brute forcing using Python's asyncio module."""
  17.  
  18. def __init__(self, verbosity=0, max_tasks=512):
  19. """Constructor.
  20. Args:
  21. verbosity: set output verbosity: 0 (default) is none, 3 is debug
  22. max_tasks: the maximum number of tasks asyncio will queue (default 512)
  23. """
  24. self.tasks = []
  25. self.errors = []
  26. self.fqdn = []
  27. self.ignore_hosts = []
  28. asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
  29. self.loop = asyncio.get_event_loop()
  30. self.resolver = aiodns.DNSResolver(loop=self.loop, rotate=True)
  31. self.sem = asyncio.BoundedSemaphore(max_tasks)
  32. self.max_tasks = max_tasks
  33. self.verbosity = verbosity
  34. self.logger = ConsoleLogger(verbosity)
  35.  
  36. async def _dns_lookup(self, name):
  37. """Performs a DNS request using aiodns, self.lookup_type is set by the run function.
  38. A query for A record returns <ares_query_a_result> which does not return metadata about
  39. when a CNAME was resolved (just host and ttl attributes) however it should be faster.
  40. The <ares_host_result> returned by gethostbyname contains name, aliases, and addresses, if
  41. name is different in response we can surmise that the original domain was a CNAME entry.
  42. Args:
  43. name: the domain name to resolve
  44. Returns:
  45. object: <ares_query_a_result> if query, <ares_host_result> if gethostbyname
  46. """
  47. if self.lookup_type == "query":
  48. return await self.resolver.query(name, "A")
  49. elif self.lookup_type == "gethostbyname":
  50. return await self.resolver.gethostbyname(name, socket.AF_INET)
  51.  
  52. def _dns_result_callback(self, name, future):
  53. """Handles the pycares object passed by the _dns_lookup function. We expect an errror to
  54. be present in the returned object because most lookups will be for names that don't exist.
  55. c-ares errors are passed through directly, error types can be identified in ares_strerror.c
  56. Args:
  57. name: original lookup name (because the query_result object doesn't contain it)
  58. future: the completed future (pycares dns result)
  59. """
  60. # Record processed we can now release the lock
  61. self.sem.release()
  62. # Handle known exceptions, barf on other ones
  63. if future.exception() is not None:
  64. try:
  65. err_number = future.exception().args[0]
  66. err_text = future.exception().args[1]
  67. except IndexError:
  68. self.logger.error(f"Couldn't parse exception: {future.exception()}")
  69. # handle the DNS errors we expect to receive, show user unexpected errors
  70. if err_number == 4:
  71. # This is domain name not found, ignore it
  72. pass
  73. elif err_number == 12:
  74. # Timeout from DNS server
  75. self.logger.warn(f"Timeout for {name}")
  76. elif err_number == 1:
  77. # Server answered with no data
  78. pass
  79. else:
  80. self.logger.error(
  81. f"{name} generated an unexpected exception: {future.exception()}"
  82. )
  83. # for debugging/troubleshoooting keep a list of errors
  84. # self.errors.append({'hostname': name, 'error': err_text})
  85.  
  86. # parse and output and store results.
  87. else:
  88. if self.lookup_type == "query":
  89. ips = [ip.host for ip in future.result()]
  90. cname = False
  91. row = f"{name:<30}\t{ips}"
  92. elif self.lookup_type == "gethostbyname":
  93. r = future.result()
  94. ips = [ip for ip in r.addresses]
  95. if name == r.name:
  96. cname = False
  97. n = f"""{name:<30}\t{f"{'':<35}" if self.verbosity >= 2 else ""}"""
  98. else:
  99. cname = True
  100. # format the name based on verbosity - this is kluge
  101. short_cname = f"{r.name[:28]}.." if len(r.name) > 30 else r.name
  102. n = f'{name}{"**" if self.verbosity <= 1 else ""}'
  103. n = f'''{n:<30}\t{f"CNAME {short_cname:<30}" if self.verbosity >= 2 else ""}'''
  104. row = f"{n:<30}\t{ips}"
  105. # store the result
  106. if set(ips) != set(self.ignore_hosts):
  107. self.logger.success(row)
  108. dns_lookup_result = {"domain": name, "ip": ips}
  109. if self.lookup_type == "gethostbyname" and cname:
  110. dns_lookup_result["cname"] = r.name
  111. dns_lookup_result["aliases"] = r.aliases
  112. self.fqdn.append(dns_lookup_result)
  113. self.logger.debug(future.result())
  114. self.tasks.remove(future)
  115. if self.verbosity >= 1:
  116. self.pbar.update()
  117.  
  118. async def _queue_lookups(self, wordlist, domain):
  119. """Takes a list of words and adds them to the async loop also passing the original
  120. lookup domain name; then attaches the processing callback to deal with the result.
  121. Args:
  122. wordlist: a list of names to perform lookups for
  123. domain: the base domain to perform brute force against
  124. """
  125. for word in wordlist:
  126. # Wait on the semaphore before adding more tasks
  127. await self.sem.acquire()
  128. host = f"{word.strip()}.{domain}"
  129. task = asyncio.ensure_future(self._dns_lookup(host))
  130. task.add_done_callback(functools.partial(self._dns_result_callback, host))
  131. self.tasks.append(task)
  132. await asyncio.gather(*self.tasks, return_exceptions=True)
  133.  
  134. def run(
  135. self, wordlist, domain, resolvers=None, wildcard=True, verify=True, query=True
  136. ):
  137. """
  138. Sets up the bruteforce job, does domain verification, sets resolvers, checks for wildcard
  139. response to lookups, and sets the query type to be used. After all this, open the wordlist
  140. file and start the brute force - with ^C handling to cleanup nicely.
  141. Args:
  142. wordlist: a string containing a path to a filename to be used as a wordlist
  143. domain: the base domain name to be used for lookups
  144. resolvers: a list of DNS resolvers to be used (default None, uses system resolvers)
  145. wildcard: bool, do wildcard dns detection (default true)
  146. verify: bool, check if domain exists (default true)
  147. query: bool, use query to do lookups (default true), false means gethostbyname is used.
  148. Returns:
  149. dict containing result of lookups
  150. """
  151. self.logger.info(
  152. f"Brute forcing {domain} with a maximum of {self.max_tasks} concurrent tasks..."
  153. )
  154. if verify:
  155. self.logger.info(f"Using local resolver to verify {domain} exists.")
  156. try:
  157. socket.gethostbyname(domain)
  158. except socket.gaierror as err:
  159. self.logger.error(
  160. f"Couldn't resolve {domain}, use the --no-verify switch to ignore this error."
  161. )
  162. raise SystemExit(
  163. self.logger.error(f"Error from host lookup: {err}")
  164. )
  165. else:
  166. self.logger.warn("Skipping domain verification. YOLO!")
  167. if resolvers:
  168. self.resolver.nameservers = resolvers
  169. self.logger.info(
  170. f"Using recursive DNS with the following servers: {self.resolver.nameservers}"
  171. )
  172.  
  173. if wildcard:
  174. # 63 chars is the max allowed segment length, there is practically no chance that it will be a legit record
  175. random_sld = (
  176. lambda: f'{"".join(random.choice(string.ascii_lowercase + string.digits) for i in range(63))}'
  177. )
  178. try:
  179. self.lookup_type = "query"
  180. wc_check = self.loop.run_until_complete(
  181. self._dns_lookup(f"{random_sld()}.{domain}")
  182. )
  183. except aiodns.error.DNSError as err:
  184. # we expect that the record will not exist and error 4 will be thrown
  185. self.logger.info(
  186. f"No wildcard response was detected for this domain."
  187. )
  188. wc_check = None
  189. finally:
  190. if wc_check is not None:
  191. self.ignore_hosts = [host.host for host in wc_check]
  192. self.logger.warn(
  193. f"Wildcard response detected, ignoring answers containing {self.ignore_hosts}"
  194. )
  195. else:
  196. self.logger.warn("Wildcard detection is disabled")
  197.  
  198. if query:
  199. self.logger.info(
  200. "Using pycares `query` function to perform lookups, CNAMEs cannot be identified"
  201. )
  202. self.lookup_type = "query"
  203. else:
  204. self.logger.info(
  205. "Using pycares `gethostbyname` function to perform lookups, CNAME data will be appended to results (** denotes CNAME, show actual name with -vv)"
  206. )
  207. self.lookup_type = "gethostbyname"
  208.  
  209. with open(wordlist, encoding="utf-8", errors="ignore") as words:
  210. w = words.read().splitlines()
  211. self.logger.info(f"Wordlist loaded, proceeding with {len(w)} DNS requests")
  212. try:
  213. if self.verbosity >= 1:
  214. self.pbar = tqdm(
  215. total=len(w), unit="rec", maxinterval=0.1, mininterval=0
  216. )
  217. self.loop.run_until_complete(self._queue_lookups(w, domain))
  218. except KeyboardInterrupt:
  219. self.logger.warn("Caught keyboard interrupt, cleaning up...")
  220. asyncio.gather(*asyncio.Task.all_tasks()).cancel()
  221. self.loop.stop()
  222. finally:
  223. self.loop.close()
  224. if self.verbosity >= 1:
  225. self.pbar.close()
  226. self.logger.info(f"Completed, {len(self.fqdn)} subdomains found")
  227. return self.fqdn
  228.  
  229.  
  230. @click.command()
  231. @click.option(
  232. "--wordlist",
  233. "-w",
  234. help="Wordlist to use for brute force.",
  235. default=f"{os.path.dirname(os.path.realpath(__file__))}/wordlists/bitquark_20160227_subdomains_popular_1000",
  236. )
  237. @click.option(
  238. "--max-tasks",
  239. "-t",
  240. default=512,
  241. help="Maximum number of tasks to run asynchronosly.",
  242. )
  243. @click.option(
  244. "--resolver-file",
  245. "-r",
  246. type=click.File("r"),
  247. default=None,
  248. help="A text file containing a list of DNS resolvers to use, one per line, comments start with #. Default: use system resolvers",
  249. )
  250. @click.option(
  251. "--verbosity", "-v", count=True, default=1, help="Increase output verbosity"
  252. )
  253. @click.option(
  254. "--output",
  255. "-o",
  256. type=click.Choice(["csv", "json", "off"]),
  257. default="off",
  258. help="Output results to DOMAIN.csv/json (extension automatically appended when not using -f).",
  259. )
  260. @click.option(
  261. "--outfile",
  262. "-f",
  263. type=click.File("w"),
  264. help="Output filename. Use '-f -' to send file output to stdout overriding normal output.",
  265. )
  266. @click.option(
  267. "--query/--gethostbyname",
  268. default=True,
  269. help="DNS lookup type to use query (default) should be faster, but won't return CNAME information.",
  270. )
  271. @click.option(
  272. "--wildcard/--no-wildcard",
  273. default=True,
  274. help="Wildcard detection, enabled by default",
  275. )
  276. @click.option(
  277. "--verify/--no-verify",
  278. default=True,
  279. help="Verify domain name is sane before beginning, enabled by default",
  280. )
  281. @click.version_option("0.3.2")
  282. @click.argument("domain", required=True)
  283. def main(**kwargs):
  284. """aiodnsbrute is a command line tool for brute forcing domain names utilizing Python's asyncio module.
  285. credit: blark (@markbaseggio)
  286. """
  287. output = kwargs.get("output")
  288. verbosity = kwargs.get("verbosity")
  289. resolvers = kwargs.get("resolver_file")
  290. if output is not "off":
  291. outfile = kwargs.get("outfile")
  292. # turn off output if we want JSON/CSV to stdout, hacky
  293. if outfile.__class__.__name__ == "TextIOWrapper":
  294. verbosity = 0
  295. if outfile is None:
  296. # wasn't specified on command line
  297. outfile = open(f'{kwargs["domain"]}.{output}', "w")
  298. if resolvers:
  299. lines = resolvers.read().splitlines()
  300. resolvers = [x.strip() for x in lines if (x and not x.startswith("#"))]
  301.  
  302. bf = aioDNSBrute(verbosity=verbosity, max_tasks=kwargs.get("max_tasks"))
  303. results = bf.run(
  304. wordlist=kwargs.get("wordlist"),
  305. domain=kwargs.get("domain"),
  306. resolvers=resolvers,
  307. wildcard=kwargs.get("wildcard"),
  308. verify=kwargs.get("verify"),
  309. query=kwargs.get("query"),
  310. )
  311.  
  312. if output in ("json"):
  313. import json
  314. json.dump(results, outfile)
  315.  
  316. if output in ("csv"):
  317. import csv
  318. writer = csv.writer(outfile)
  319. writer.writerow(["Hostname", "IPs", "CNAME", "Aliases"])
  320. [
  321. writer.writerow(
  322. [
  323. r.get("domain"),
  324. r.get("ip", [""])[0],
  325. r.get("cname"),
  326. r.get("aliases", [""])[0],
  327. ]
  328. )
  329. for r in results
  330. ]
  331.  
  332.  
  333. if __name__ == "__main__":
  334. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement