|
13 | 13 | import re |
14 | 14 | import time |
15 | 15 | import argparse |
16 | | -import getpass |
17 | 16 | import json |
18 | 17 | import urllib.parse |
19 | 18 | import requests |
20 | 19 | import urllib3 |
21 | 20 |
|
| 21 | +from selenium import webdriver |
| 22 | +from selenium.common.exceptions import WebDriverException |
| 23 | + |
22 | 24 | BANNER = r""" |
23 | 25 |
|
24 | 26 | .__ .__________ |
@@ -192,17 +194,10 @@ def parse_arguments(): |
192 | 194 | ' to report any inconsistencies, and they will be quickly fixed.') |
193 | 195 | parser = argparse.ArgumentParser(description=desc) |
194 | 196 |
|
195 | | - parser.add_argument('-u', '--username', type=str, action='store', |
196 | | - required=True, |
197 | | - help='A valid LinkedIn username.') |
198 | 197 | parser.add_argument('-c', '--company', type=str, action='store', |
199 | 198 | required=True, |
200 | 199 | help='Company name exactly as typed in the company ' |
201 | 200 | 'linkedin profile page URL.') |
202 | | - parser.add_argument('-p', '--password', type=str, action='store', |
203 | | - help='Specify your password in clear-text on the ' |
204 | | - 'command line. If not specified, will prompt and ' |
205 | | - 'obfuscate as you type.') |
206 | 201 | parser.add_argument('-n', '--domain', type=str, action='store', |
207 | 202 | default='', |
208 | 203 | help='Append a domain name to username output. ' |
@@ -251,116 +246,59 @@ def parse_arguments(): |
251 | 246 | print("Sorry, keywords and geoblast are currently not compatible. Use one or the other.") |
252 | 247 | sys.exit() |
253 | 248 |
|
254 | | - # If password is not passed in the command line, prompt for it |
255 | | - # in a more secure fashion (not shown on screen) |
256 | | - args.password = args.password or getpass.getpass() |
257 | | - |
258 | 249 | return args |
259 | 250 |
|
260 | 251 |
|
261 | | -def login(args): |
262 | | - """Creates a new authenticated session. |
| 252 | +def get_webdriver(): |
| 253 | + """ |
| 254 | + Try to get a working Selenium browser driver |
| 255 | + """ |
| 256 | + for browser in [webdriver.Firefox, webdriver.Chrome]: |
| 257 | + try: |
| 258 | + return browser() |
| 259 | + except WebDriverException: |
| 260 | + continue |
| 261 | + return None |
263 | 262 |
|
264 | | - Note that a mobile user agent is used. Parsing using the desktop results |
265 | | - proved extremely difficult, as shared connections would be returned in |
266 | | - a manner that was indistinguishable from the desired targets. |
267 | 263 |
|
268 | | - The other header matters as well, otherwise advanced search functions |
269 | | - (region and keyword) will not work. |
| 264 | +def login(): |
| 265 | + """Creates a new authenticated session. |
270 | 266 |
|
271 | | - The function will check for common failure scenarios - the most common is |
272 | | - logging in from a new location. Accounts using multi-factor auth are not |
273 | | - yet supported and will produce an error. |
| 267 | + This now uses Selenium because I got very tired playing cat/mouse |
| 268 | + with LinkedIn's login process. |
274 | 269 | """ |
275 | | - session = requests.session() |
| 270 | + driver = get_webdriver() |
276 | 271 |
|
277 | | - # The following are known errors that require the user to log in via the web |
278 | | - login_problems = ['challenge', 'captcha', 'manage-account', 'add-email'] |
| 272 | + if driver is None: |
| 273 | + print("[!] Could not find a supported browser for Selenium. Exiting.") |
| 274 | + sys.exit(1) |
279 | 275 |
|
280 | | - # Special options below when using a proxy server. Helpful for debugging |
281 | | - # the application in Burp Suite. |
282 | | - if args.proxy: |
283 | | - print("[!] Using a proxy, ignoring SSL errors. Don't get pwned.") |
284 | | - session.verify = False |
285 | | - urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning) |
286 | | - session.proxies.update(args.proxy_dict) |
| 276 | + driver.get("https://linkedin.com/login") |
| 277 | + |
| 278 | + # Pause until the user lets us know the session is good. |
| 279 | + print("[*] Log in to LinkedIn. Leave the browser open and press enter when ready...") |
| 280 | + input("Ready? Press Enter!") |
| 281 | + |
| 282 | + selenium_cookies = driver.get_cookies() |
| 283 | + driver.quit() |
| 284 | + |
| 285 | + # Initialize and return a requests session |
| 286 | + session = requests.Session() |
| 287 | + for cookie in selenium_cookies: |
| 288 | + session.cookies.set(cookie['name'], cookie['value']) |
287 | 289 |
|
288 | | - # Our search and regex will work only with a mobile user agent and |
289 | | - # the correct REST protocol specified below. |
| 290 | + # Add headers required for this tool to function |
290 | 291 | mobile_agent = ('Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; SCH-I535 ' |
291 | 292 | 'Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) ' |
292 | 293 | 'Version/4.0 Mobile Safari/534.30') |
293 | 294 | session.headers.update({'User-Agent': mobile_agent, |
294 | 295 | 'X-RestLi-Protocol-Version': '2.0.0', |
295 | 296 | 'X-Li-Track': '{"clientVersion":"1.13.1665"}'}) |
296 | 297 |
|
297 | | - # We wll grab an anonymous response to look for the CSRF token, which |
298 | | - # is required for our logon attempt. |
299 | | - anon_response = session.get('https://www.linkedin.com/login') |
300 | | - login_csrf = re.findall(r'name="loginCsrfParam" value="(.*?)"', |
301 | | - anon_response.text) |
302 | | - if login_csrf: |
303 | | - login_csrf = login_csrf[0] |
304 | | - else: |
305 | | - print("Having trouble loading login page... try the command again.") |
306 | | - sys.exit() |
307 | | - |
308 | | - # Define the data we will POST for our login. |
309 | | - auth_payload = { |
310 | | - 'session_key': args.username, |
311 | | - 'session_password': args.password, |
312 | | - 'isJsEnabled': 'false', |
313 | | - 'loginCsrfParam': login_csrf |
314 | | - } |
315 | | - |
316 | | - # Perform the actual login. We disable redirects as we will use that 302 |
317 | | - # as an indicator of a successful logon. |
318 | | - response = session.post('https://www.linkedin.com/checkpoint/lg/login-submit' |
319 | | - '?loginSubmitSource=GUEST_HOME', |
320 | | - data=auth_payload, allow_redirects=False) |
321 | | - |
322 | | - # Define a successful login by the 302 redirect to the 'feed' page. Try |
323 | | - # to detect some other common logon failures and alert the user. |
324 | | - if response.status_code in (302, 303): |
325 | | - # Add CSRF token for all additional requests |
326 | | - session = set_csrf_token(session) |
327 | | - redirect = response.headers['Location'] |
328 | | - if 'feed' in redirect: |
329 | | - return session |
330 | | - if 'add-phone' in redirect: |
331 | | - # Skip the prompt to add a phone number |
332 | | - url = 'https://www.linkedin.com/checkpoint/post-login/security/dismiss-phone-event' |
333 | | - response = session.post(url) |
334 | | - if response.status_code == 200: |
335 | | - return session |
336 | | - print("[!] Could not skip phone prompt. Log in via the web and then try again.\n") |
337 | | - |
338 | | - elif any(x in redirect for x in login_problems): |
339 | | - print("[!] LinkedIn has a message for you that you need to address. " |
340 | | - "Please log in using a web browser first, and then come back and try again.") |
341 | | - else: |
342 | | - # The below will detect some 302 that I don't yet know about. |
343 | | - print("[!] Some unknown redirection occurred. If this persists, please open an issue " |
344 | | - "and include the info below:") |
345 | | - print("DEBUG INFO:") |
346 | | - print(f"LOCATION: {redirect}") |
347 | | - print(f"RESPONSE TEXT:\n{response.text}") |
348 | | - |
349 | | - return False |
350 | | - |
351 | | - # A failed logon doesn't generate a 302 at all, but simply responds with |
352 | | - # the logon page. We detect this here. |
353 | | - if '<title>LinkedIn Login' in response.text: |
354 | | - print("[!] Check your username and password and try again.\n") |
355 | | - return False |
| 298 | + # Set the CSRF token |
| 299 | + session = set_csrf_token(session) |
356 | 300 |
|
357 | | - # If we make it past everything above, we have no idea what happened. |
358 | | - # Oh well, we fail. |
359 | | - print("[!] Some unknown error logging in. If this persists, please open an issue on github.\n") |
360 | | - print("DEBUG INFO:") |
361 | | - print(f"RESPONSE CODE: {response.status_code}") |
362 | | - print(f"RESPONSE TEXT:\n{response.text}") |
363 | | - return False |
| 301 | + return session |
364 | 302 |
|
365 | 303 |
|
366 | 304 | def set_csrf_token(session): |
@@ -717,14 +655,20 @@ def main(): |
717 | 655 | args = parse_arguments() |
718 | 656 |
|
719 | 657 | # Instantiate a session by logging in to LinkedIn. |
720 | | - session = login(args) |
| 658 | + session = login() |
721 | 659 |
|
722 | 660 | # If we can't get a valid session, we quit now. Specific errors are |
723 | 661 | # printed to the console inside the login() function. |
724 | 662 | if not session: |
725 | 663 | sys.exit() |
726 | 664 |
|
727 | | - print("[*] Successfully logged in.") |
| 665 | + # Special options below when using a proxy server. Helpful for debugging |
| 666 | + # the application in Burp Suite. |
| 667 | + if args.proxy: |
| 668 | + print("[!] Using a proxy, ignoring SSL errors. Don't get pwned.") |
| 669 | + session.verify = False |
| 670 | + urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning) |
| 671 | + session.proxies.update(args.proxy_dict) |
728 | 672 |
|
729 | 673 | # Get basic company info |
730 | 674 | print("[*] Trying to get company info...") |
|
0 commit comments