|
1 | | -import hashlib |
2 | | -import hmac |
3 | 1 | import html.parser |
4 | 2 | import http.cookiejar |
5 | 3 | import io |
6 | 4 | import os |
7 | 5 | import re |
8 | | -import socket |
9 | 6 | import sqlite3 |
10 | | -import struct |
11 | 7 | import sys |
12 | 8 | import textwrap |
13 | | -import time |
14 | | -import urllib.parse |
15 | | -import webbrowser |
16 | 9 | import yaml |
17 | 10 | # framework libs |
18 | 11 | from recon.core import framework |
@@ -117,29 +110,6 @@ def cidr_to_list(self, string): |
117 | 110 | import ipaddress |
118 | 111 | return [str(ip) for ip in ipaddress.ip_network(string)] |
119 | 112 |
|
120 | | - def parse_name(self, name): |
121 | | - elements = [self.html_unescape(x) for x in name.strip().split()] |
122 | | - # remove prefixes and suffixes |
123 | | - names = [] |
124 | | - for i in range(0,len(elements)): |
125 | | - # preserve initials |
126 | | - if re.search(r'^\w\.$', elements[i]): |
127 | | - elements[i] = elements[i][:-1] |
128 | | - # remove unecessary prefixes and suffixes |
129 | | - elif re.search(r'(?:\.|^the$|^jr$|^sr$|^I{2,3}$)', elements[i], re.IGNORECASE): |
130 | | - continue |
131 | | - names.append(elements[i]) |
132 | | - # make sense of the remaining elements |
133 | | - if len(names) > 3: |
134 | | - names[2:] = [' '.join(names[2:])] |
135 | | - # clean up any remaining garbage characters |
136 | | - names = [re.sub(r"[,']", '', x) for x in names] |
137 | | - # set values and return names |
138 | | - fname = names[0] if len(names) >= 1 else None |
139 | | - mname = names[1] if len(names) >= 3 else None |
140 | | - lname = names[-1] if len(names) >= 2 else None |
141 | | - return fname, mname, lname |
142 | | - |
143 | 113 | def hosts_to_domains(self, hosts, exclusions=[]): |
144 | 114 | domains = [] |
145 | 115 | for host in hosts: |
@@ -184,244 +154,6 @@ def _get_source(self, params, query=None): |
184 | 154 | raise framework.FrameworkException('Source contains no input.') |
185 | 155 | return sources |
186 | 156 |
|
187 | | - #================================================== |
188 | | - # 3RD PARTY API METHODS |
189 | | - #================================================== |
190 | | - |
191 | | - def get_explicit_oauth_token(self, resource, scope, authorize_url, access_url): |
192 | | - token_name = resource+'_token' |
193 | | - token = self.get_key(token_name) |
194 | | - if token: |
195 | | - return token |
196 | | - client_id = self.get_key(resource+'_api') |
197 | | - client_secret = self.get_key(resource+'_secret') |
198 | | - port = 31337 |
199 | | - redirect_uri = f"http://localhost:{port}" |
200 | | - payload = {'response_type': 'code', 'client_id': client_id, 'scope': scope, 'state': self.get_random_str(40), 'redirect_uri': redirect_uri} |
201 | | - authorize_url = f"{authorize_url}?{urllib.parse.urlencode(payload)}" |
202 | | - w = webbrowser.get() |
203 | | - w.open(authorize_url) |
204 | | - # open a socket to receive the access token callback |
205 | | - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
206 | | - sock.bind(('127.0.0.1', port)) |
207 | | - sock.listen(1) |
208 | | - conn, addr = sock.accept() |
209 | | - data = conn.recv(1024) |
210 | | - conn.sendall('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><head><title>Recon-ng</title></head><body>Response received. Return to Recon-ng.</body></html>') |
211 | | - conn.close() |
212 | | - # process the received data |
213 | | - if 'error_description' in data: |
214 | | - self.error(urllib.parse.unquote_plus(re.search(r'error_description=([^\s&]*)', data).group(1))) |
215 | | - return None |
216 | | - authorization_code = re.search(r'code=([^\s&]*)', data).group(1) |
217 | | - payload = {'grant_type': 'authorization_code', 'code': authorization_code, 'redirect_uri': redirect_uri, 'client_id': client_id, 'client_secret': client_secret} |
218 | | - resp = self.request('POST', access_url, data=payload) |
219 | | - if 'error' in resp.json(): |
220 | | - self.error(resp.json()['error_description']) |
221 | | - return None |
222 | | - access_token = resp.json()['access_token'] |
223 | | - self.add_key(token_name, access_token) |
224 | | - return access_token |
225 | | - |
226 | | - def get_twitter_oauth_token(self): |
227 | | - token_name = 'twitter_token' |
228 | | - token = self.get_key(token_name) |
229 | | - if token: |
230 | | - return token |
231 | | - twitter_key = self.get_key('twitter_api') |
232 | | - twitter_secret = self.get_key('twitter_secret') |
233 | | - url = 'https://api.twitter.com/oauth2/token' |
234 | | - auth = (twitter_key, twitter_secret) |
235 | | - headers = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'} |
236 | | - payload = {'grant_type': 'client_credentials'} |
237 | | - resp = self.request('POST', url, auth=auth, headers=headers, data=payload) |
238 | | - if 'errors' in resp.json(): |
239 | | - raise framework.FrameworkException(f"{resp.json()['errors'][0]['message']}, {resp.json()['errors'][0]['label']}") |
240 | | - access_token = resp.json()['access_token'] |
241 | | - self.add_key(token_name, access_token) |
242 | | - return access_token |
243 | | - |
244 | | - def build_pwnedlist_payload(self, payload, method, key, secret): |
245 | | - timestamp = int(time.time()) |
246 | | - payload['ts'] = timestamp |
247 | | - payload['key'] = key |
248 | | - msg = f"{key}{timestamp}{method}{secret}" |
249 | | - encoding = sys.getdefaultencoding() |
250 | | - hm = hmac.new(bytes(secret, encoding), bytes(msg, encoding), hashlib.sha1) |
251 | | - payload['hmac'] = hm.hexdigest() |
252 | | - return payload |
253 | | - |
254 | | - def get_pwnedlist_leak(self, leak_id): |
255 | | - # check if the leak has already been retrieved |
256 | | - leak = self.query('SELECT * FROM leaks WHERE leak_id=?', (leak_id,)) |
257 | | - if leak: |
258 | | - leak = dict(zip([x[0] for x in self.get_columns('leaks')], leak[0])) |
259 | | - del leak['module'] |
260 | | - return leak |
261 | | - # set up the API call |
262 | | - key = self.get_key('pwnedlist_api') |
263 | | - secret = self.get_key('pwnedlist_secret') |
264 | | - url = 'https://api.pwnedlist.com/api/1/leaks/info' |
265 | | - base_payload = {'leakId': leak_id} |
266 | | - payload = self.build_pwnedlist_payload(base_payload, 'leaks.info', key, secret) |
267 | | - # make the request |
268 | | - resp = self.request('GET', url, params=payload) |
269 | | - if resp.status_code != 200: |
270 | | - self.error(f"Error retrieving leak data.{os.linesep}{resp.text}") |
271 | | - return |
272 | | - leak = resp.json()['leaks'][0] |
273 | | - # normalize the leak for storage |
274 | | - normalized_leak = {} |
275 | | - for item in leak: |
276 | | - value = leak[item] |
277 | | - if type(value) == list: |
278 | | - value = ', '.join(value) |
279 | | - normalized_leak[item] = value |
280 | | - return normalized_leak |
281 | | - |
282 | | - def search_twitter_api(self, payload, limit=False): |
283 | | - headers = {'Authorization': f"Bearer {self.get_twitter_oauth_token()}"} |
284 | | - url = 'https://api.twitter.com/1.1/search/tweets.json' |
285 | | - results = [] |
286 | | - while True: |
287 | | - resp = self.request('GET', url, params=payload, headers=headers) |
288 | | - if limit: |
289 | | - # app auth rate limit for search/tweets is 450/15min |
290 | | - time.sleep(2) |
291 | | - jsonobj = resp.json() |
292 | | - for item in ['error', 'errors']: |
293 | | - if item in jsonobj: |
294 | | - raise framework.FrameworkException(jsonobj[item]) |
295 | | - results += jsonobj['statuses'] |
296 | | - if 'next_results' in jsonobj['search_metadata']: |
297 | | - max_id = urllib.parse.parse_qs(jsonobj['search_metadata']['next_results'][1:])['max_id'][0] |
298 | | - payload['max_id'] = max_id |
299 | | - continue |
300 | | - break |
301 | | - return results |
302 | | - |
303 | | - def search_shodan_api(self, query, limit=0): |
304 | | - api_key = self.get_key('shodan_api') |
305 | | - url = 'https://api.shodan.io/shodan/host/search' |
306 | | - payload = {'query': query, 'key': api_key} |
307 | | - results = [] |
308 | | - cnt = 0 |
309 | | - page = 1 |
310 | | - self.verbose(f"Searching Shodan API for: {query}") |
311 | | - while True: |
312 | | - time.sleep(1) |
313 | | - resp = self.request('GET', url, params=payload) |
314 | | - if resp.json() == None: |
315 | | - raise framework.FrameworkException(f"Invalid JSON response.{os.linesep}{resp.text}") |
316 | | - if 'error' in resp.json(): |
317 | | - raise framework.FrameworkException(resp.json()['error']) |
318 | | - if not resp.json()['matches']: |
319 | | - break |
320 | | - # add new results |
321 | | - results.extend(resp.json()['matches']) |
322 | | - # increment and check the limit |
323 | | - cnt += 1 |
324 | | - if limit == cnt: |
325 | | - break |
326 | | - # next page |
327 | | - page += 1 |
328 | | - payload['page'] = page |
329 | | - return results |
330 | | - |
331 | | - def search_bing_api(self, query, limit=0): |
332 | | - url = 'https://api.cognitive.microsoft.com/bing/v7.0/search' |
333 | | - payload = {'q': query, 'count': 50, 'offset': 0, 'responseFilter': 'WebPages'} |
334 | | - headers = {'Ocp-Apim-Subscription-Key': self.get_key('bing_api')} |
335 | | - results = [] |
336 | | - cnt = 0 |
337 | | - self.verbose(f"Searching Bing API for: {query}") |
338 | | - while True: |
339 | | - resp = self.request('GET', url, params=payload, headers=headers) |
340 | | - if resp.json() == None: |
341 | | - raise framework.FrameworkException(f"Invalid JSON response.{os.linesep}{resp.text}") |
342 | | - #elif 'error' in resp.json(): |
343 | | - elif resp.status_code == 401: |
344 | | - raise framework.FrameworkException(f"{resp.json()['statusCode']}: {resp.json()['message']}") |
345 | | - # add new results, or if there's no more, return what we have... |
346 | | - if 'webPages' in resp.json(): |
347 | | - results.extend(resp.json()['webPages']['value']) |
348 | | - else: |
349 | | - return results |
350 | | - # increment and check the limit |
351 | | - cnt += 1 |
352 | | - if limit == cnt: |
353 | | - break |
354 | | - # check for more pages |
355 | | - # https://msdn.microsoft.com/en-us/library/dn760787.aspx |
356 | | - if payload['offset'] > (resp.json()['webPages']['totalEstimatedMatches'] - payload['count']): |
357 | | - break |
358 | | - # set the payload for the next request |
359 | | - payload['offset'] += payload['count'] |
360 | | - return results |
361 | | - |
362 | | - def search_google_api(self, query, limit=0): |
363 | | - api_key = self.get_key('google_api') |
364 | | - cse_id = self.get_key('google_cse') |
365 | | - url = 'https://www.googleapis.com/customsearch/v1' |
366 | | - payload = {'alt': 'json', 'prettyPrint': 'false', 'key': api_key, 'cx': cse_id, 'q': query} |
367 | | - results = [] |
368 | | - cnt = 0 |
369 | | - self.verbose(f"Searching Google API for: {query}") |
370 | | - while True: |
371 | | - resp = self.request('GET', url, params=payload) |
372 | | - if resp.json() == None: |
373 | | - raise framework.FrameworkException(f"Invalid JSON response.{os.linesep}{resp.text}") |
374 | | - # add new results |
375 | | - if 'items' in resp.json(): |
376 | | - results.extend(resp.json()['items']) |
377 | | - # increment and check the limit |
378 | | - cnt += 1 |
379 | | - if limit == cnt: |
380 | | - break |
381 | | - # check for more pages |
382 | | - if not 'nextPage' in resp.json()['queries']: |
383 | | - break |
384 | | - payload['start'] = resp.json()['queries']['nextPage'][0]['startIndex'] |
385 | | - return results |
386 | | - |
387 | | - def search_github_api(self, query): |
388 | | - self.verbose(f"Searching Github for: {query}") |
389 | | - results = self.query_github_api(endpoint='/search/code', payload={'q': query}) |
390 | | - # reduce the nested lists of search results and return |
391 | | - results = [result['items'] for result in results] |
392 | | - return [x for sublist in results for x in sublist] |
393 | | - |
394 | | - def query_github_api(self, endpoint, payload={}, options={}): |
395 | | - opts = {'max_pages': None} |
396 | | - opts.update(options) |
397 | | - headers = {'Authorization': f"token {self.get_key('github_api')}"} |
398 | | - base_url = 'https://api.github.com' |
399 | | - url = base_url + endpoint |
400 | | - results = [] |
401 | | - page = 1 |
402 | | - while True: |
403 | | - # Github rate limit is 30 requests per minute |
404 | | - time.sleep(2) # 60s / 30r = 2s/r |
405 | | - payload['page'] = page |
406 | | - resp = self.request('GET', url, headers=headers, params=payload) |
407 | | - # check for errors |
408 | | - if resp.status_code != 200: |
409 | | - # skip 404s returned for no results |
410 | | - if resp.status_code != 404: |
411 | | - self.error(f"Message from Github: {resp.json()['message']}") |
412 | | - break |
413 | | - # some APIs return lists, and others a single dictionary |
414 | | - method = 'extend' |
415 | | - if type(resp.json()) == dict: |
416 | | - method = 'append' |
417 | | - getattr(results, method)(resp.json()) |
418 | | - # paginate |
419 | | - if 'link' in resp.headers and 'rel="next"' in resp.headers['link'] and (opts['max_pages'] is None or page < opts['max_pages']): |
420 | | - page += 1 |
421 | | - continue |
422 | | - break |
423 | | - return results |
424 | | - |
425 | 157 | #================================================== |
426 | 158 | # REQUEST METHODS |
427 | 159 | #================================================== |
|
0 commit comments