├── HFF.py ├── README.md ├── modules ├── chrome.py ├── chromedriver ├── chromedriver.exe ├── core.py ├── elements.py ├── printer.py └── tests.py └── requirements.txt /HFF.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os, click 4 | from pprint import pprint 5 | 6 | from modules import chrome 7 | from modules import core 8 | from modules import printer 9 | 10 | 11 | @click.command() 12 | @click.option('-l', '--login', prompt='\nFacebook login', help='Email you use to login to facebook.', required=True) 13 | @click.option('-p', '--password', prompt='\nFacebook password', help="Password to facebook account.", required=True) 14 | @click.option('-t', '--target', prompt='Target user ID (only numbers)', help="Facebook user ID - ex. 100003743435526", required=True, type=int) 15 | def main(login, password, target): 16 | browser = chrome.setup() 17 | core.login(login,password,browser) 18 | 19 | printer.print_banner("Verifying data") 20 | errors = chrome.scroll_page(browser, target) 21 | profile_name = core.get_profile_name(browser) 22 | 23 | printer.print_banner("Gathering data") 24 | if "reactions" not in errors: 25 | reaction_accounts = core.get_reactions(browser) 26 | else: 27 | reaction_accounts = [] 28 | if "comment" not in errors: 29 | comment_accounts = core.get_comments(browser) 30 | else: 31 | comment_accounts = [] 32 | combined_accounts = list({x['FB User ID']:x for x in reaction_accounts + comment_accounts}.values()) 33 | 34 | printer.print_banner("Analysing data") 35 | hidden_friends = core.find_mutual_friends(browser, target, combined_accounts) 36 | unconfirmed_friends = [x for x in combined_accounts if x not in hidden_friends] 37 | printer.print_good("Found {} accounts directly connected to {}: {} - confirmed friends, {} - probable friends ".format(len(hidden_friends+unconfirmed_friends), profile_name, len(hidden_friends), len(unconfirmed_friends))) 38 | 39 | printer.print_banner("Printing data") 40 | printer.save_friends(browser, target, hidden_friends, "confirmed") 41 | printer.save_friends(browser, target, unconfirmed_friends, "probable") 42 | 43 | browser.close() 44 | exit() 45 | 46 | if __name__ == '__main__': 47 | printer.print_art() 48 | main() 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### *DEPRECATED* 2 | Hidden Friends Finder (HFF) for Facebook. 3 | 4 | HFF automates search for friends of targeted profile based on graph search method. It's designed as OSINT tool for online investigations (not stalking your ex :wink:). Depends on profile activity and privacy settings it can "extract" from 30% to 90% of private friends list. 5 | 6 | #### _Requirements_: 7 | - Python 3 plus additional libraries (check requirements.txt). 8 | - Chrome driver included only for Windows and Mac OS. If you want to use HFF.py with linux, plese download proper driver and modify if statement in chrome.py. 9 | - Chrome browser installed. Chrome webdriver (version 73) is included for Windows and MacOS. HFF checks for OS type and choose correct driver. 10 | - Target profile must have at least one post and one comment or reaction visible publicly. 11 | - Only works with english version of Facebook so check your language settings. 12 | - HFF works much better on accounts where "friends list" visibility is set to - "only friends" (most cases). Privacy setting "only me" works as well but returns fewer accounts. 13 | - Tested on Mac and Win terminals. 14 | 15 | _____ 16 | ### RUN: 17 | `$ python HFF.py` 18 | 19 | `$ python HFF.py -l [login email] -p [password to facebook] -t [target Facebook ID]` 20 | 21 | After the launch program runs in 5 phases: 22 | 23 | 24 | #### INPUT PHASE (if no options given with run command): 25 | Facebook login: *[email you use to login to facebook]* 26 | 27 | Facebook password: *[no need to explain]* 28 | 29 | Target user ID: *[Facebook user ID in numerical format]* (you can find one easily here: [Findmyfbid.in](https://findmyfbid.in/)) 30 | 31 | 32 | #### VERIFYING DATA: 33 | The program automatically scrolls through a Facebook profile to reveal all posts and comments. 34 | At the same time, it tests the most critical elements of the Facebook code. The reason is, facebook changes classes and names of HTML elements quite frequently. In that case, program lets you know about changes and ask to edit values in file modules/elements.py. 35 | 36 | 37 | #### GATHERING DATA: 38 | HFF tries to gather as much data as possible. It collects all comments and reactions on a profile. 39 | 40 | 41 | #### ANALYSING DATA: 42 | HFF runs analysis based on graph search for every account gathered in the previous step. 43 | 44 | 45 | #### PRINTING DATA: 46 | Results are save to two CSV files. 47 | 48 | File Name | Description 49 | ------------- | ------------- 50 | [target_username]-confirmed-friends.csv | Profiles found and confirmed using graph search. 51 | [target_username]-unconfirmed-friends.csv | Profiles collected during data collecting phase, not confirmed by graph search. 52 | -------------------------------------------------------------------------------- /modules/chrome.py: -------------------------------------------------------------------------------- 1 | import platform, os, time, sys 2 | from tqdm import tqdm 3 | 4 | from . import printer 5 | from . import core 6 | from . import tests 7 | from . import elements 8 | 9 | from selenium import webdriver 10 | from selenium.webdriver.chrome.options import Options 11 | from selenium.webdriver.common.action_chains import ActionChains 12 | 13 | def setup(): 14 | options = Options() 15 | options.add_argument("--headless") 16 | options.add_argument("--no-sandbox") 17 | options.add_argument("--ignore-certificate-errors") 18 | options.add_argument("--disable-notifications") 19 | options.add_argument("--incognito") 20 | options.add_argument("--log-level=3") 21 | 22 | current_path = os.path.dirname(os.path.realpath(__file__)) 23 | 24 | os_check = platform.system() 25 | if "Darwin" in os_check: 26 | chromedrive_check = os.path.isfile(current_path + "/chromedriver") 27 | if not chromedrive_check: 28 | printer.print_bad("Missing Mac OS chromedriver or damaged file.") 29 | exit() 30 | os.chmod(current_path + "/chromedriver", 0o775) 31 | chromedriver = current_path + '/chromedriver' 32 | 33 | elif "Win" in os_check: 34 | chromedrive_check = os.path.isfile(current_path + "/chromedriver.exe") 35 | if not chromedrive_check: 36 | printer.print_bad("Missing Windows chromedriver or damaged file.") 37 | exit() 38 | chromedriver = current_path + '/chromedriver.exe' 39 | else: 40 | printer.print_bad("Driver for your system is not included. Add driver manualy and modify code in chrome.py file") 41 | exit() 42 | 43 | browser = webdriver.Chrome(executable_path=chromedriver, options=options) 44 | return browser 45 | 46 | 47 | def scroll_page(browser, target): 48 | 49 | url = "https://www.facebook.com/{}".format(target) 50 | errors = [] 51 | browser.get(url) 52 | time.sleep(5) 53 | 54 | try: 55 | profile_name = core.get_profile_name(browser) 56 | tests.profile_prompt(browser, profile_name, target) 57 | printer.print_info("Data verification in progress. It might take a while... ".format(profile_name)) 58 | except Exception as e: 59 | printer.print_bad("Cannot open profile page. Error message: {}. Are you sure user ID is correct? Please check and start again.".format(e)) 60 | browser.close() 61 | exit() 62 | 63 | tests.test_elements(browser, elements.post) 64 | 65 | browser.execute_script("window.scrollTo(0,document.body.scrollHeight);") 66 | time.sleep(2) 67 | 68 | posts_after_scrolling = len(core.get_elements(browser, **elements.post)) 69 | 70 | t = tqdm(ncols=0) 71 | while True: 72 | posts_limit = 100 73 | posts_before_scrolling = posts_after_scrolling 74 | browser.execute_script("window.scrollTo(0,document.body.scrollHeight);") 75 | time.sleep(4) 76 | posts_after_scrolling = len(core.get_elements(browser, **elements.post)) 77 | browser.execute_script("window.scrollTo(0,0);") 78 | t.update() 79 | if posts_after_scrolling >= posts_limit: 80 | break 81 | 82 | if posts_after_scrolling <= posts_before_scrolling: 83 | if len(core.get_elements(browser, **elements.spinner)) > 0: 84 | spinners = core.get_elements(browser, **elements.spinner) 85 | for spinner in spinners: 86 | scroll_into_view(browser, spinner) 87 | time.sleep(3) 88 | else: 89 | errors = tests.test_elements(browser, elements.reactions_link, elements.comment) 90 | break 91 | t.close() 92 | printer.print_info("Completed. Verified {} posts with {} errors.".format(posts_after_scrolling, len(errors))) 93 | return errors 94 | 95 | 96 | def scroll_into_view(browser, element): 97 | scroll = ActionChains(browser).move_to_element(element) 98 | scroll.perform() 99 | return 100 | 101 | def click_it(element): 102 | n = 1 103 | while n < 5: 104 | try: 105 | time.sleep(1) 106 | element.click() 107 | time.sleep(1) 108 | break 109 | except Exception as e: 110 | printer.print_bad("Error when clicking {}".format(element.text)) 111 | time.sleep(5) 112 | n += 1 113 | continue 114 | -------------------------------------------------------------------------------- /modules/chromedriver: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntelTakes/Hidden-Friends-Finder/0be34523a4a805dce20f84f57504be46578a751c/modules/chromedriver -------------------------------------------------------------------------------- /modules/chromedriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntelTakes/Hidden-Friends-Finder/0be34523a4a805dce20f84f57504be46578a751c/modules/chromedriver.exe -------------------------------------------------------------------------------- /modules/core.py: -------------------------------------------------------------------------------- 1 | import time, json, operator, re 2 | from tqdm import tqdm 3 | 4 | from . import printer 5 | from . import chrome 6 | from . import elements 7 | 8 | from selenium.webdriver.common.keys import Keys 9 | 10 | # Login to facebook 11 | def login(login,password,browser): 12 | 13 | printer.print_info("Logging in to facebook account...") 14 | try: 15 | browser.get("https://www.facebook.com/") 16 | elem = browser.find_element_by_id('email') 17 | elem.send_keys(login) 18 | elem = browser.find_element_by_id('pass') 19 | elem.send_keys(password) 20 | elem.send_keys(Keys.RETURN) 21 | time.sleep(3) 22 | 23 | except Exception as e: 24 | print(e) 25 | browser.save_screenshot('screenshot.png') 26 | printer.print_bad("Something went seriously wrong during login - check internet connection and python configuration") 27 | browser.close() 28 | exit() 29 | 30 | try: 31 | login_test = get_elements(browser, **elements.recover) 32 | if len(login_test) == 0: 33 | printer.print_good("Login to facebook account ({}) successful".format(login)) 34 | else: 35 | printer.print_bad("Login unsuccessful - check login and/or password") 36 | browser.close() 37 | exit() 38 | except Exception as e: 39 | printer.print_bad("Login test error: {}".format(e)) 40 | 41 | return 42 | 43 | def get_elements(browser, **kwargs): 44 | elements = browser.find_elements_by_xpath("//{}[@{}='{}']".format(kwargs['html'], kwargs['attribute'], kwargs['value'])) 45 | return elements 46 | 47 | 48 | def get_profile_name(browser): 49 | profile_name_elements = get_elements(browser, **elements.profile_name) 50 | name = profile_name_elements[0].text 51 | return name 52 | 53 | def get_reactions(browser): 54 | printer.print_info("Processing reactions") 55 | reaction_accounts = [] 56 | reactions_links = get_elements(browser, **elements.reactions_link) 57 | 58 | with tqdm(total=len(reactions_links), ncols=100) as pbar: 59 | for reaction_link in reactions_links: 60 | try: 61 | while reaction_link.is_displayed() == False: 62 | chrome.scroll_into_view(reaction_link, browser) 63 | browser.execute_script("arguments[0].click();", reaction_link) 64 | time.sleep(2) 65 | 66 | see_more = get_elements(browser, **elements.see_more) 67 | 68 | if len(see_more) > 0: 69 | chrome.scroll_into_view(browser, see_more[0]) 70 | time.sleep(1) 71 | browser.execute_script("arguments[0].click();", see_more[0]) 72 | time.sleep(1) 73 | 74 | reaction_profiles = get_elements(browser, **elements.reaction_profile) 75 | if len(reaction_profiles) == 0: 76 | time.sleep(5) #additional verification. Just in case code in pop-up window didn't load quickly enough. 77 | reaction_profiles = get_elements(browser, **elements.reaction_profile) 78 | 79 | except Exception as e: 80 | printer.print_bad("Error while trying to collect reactions") 81 | print(e) 82 | pass 83 | 84 | try: 85 | for profile in reaction_profiles: 86 | if profile.text: 87 | link = profile.find_element_by_tag_name("a") 88 | user_data_json = link.get_attribute("data-gt") 89 | user_data_dict = json.loads(user_data_json) 90 | user_id = user_data_dict['engagement']['eng_tid'] 91 | 92 | account = {} 93 | account['Name'] = profile.text 94 | account['Profile URL'] = "https://www.facebook.com/"+user_id 95 | account['FB User ID'] = user_id 96 | 97 | if account not in reaction_accounts: 98 | reaction_accounts.append(account) 99 | 100 | except Exception as e: 101 | print(e) 102 | pass 103 | 104 | 105 | close_btn = get_elements(browser, **elements.close_button) 106 | while len(close_btn) == 0: 107 | time.sleep(1) 108 | close_btn = get_elements(browser, **elements.close_button) 109 | 110 | time.sleep(1) 111 | chrome.click_it(close_btn[0]) 112 | pbar.update() 113 | 114 | # printer.print_good("Found {} unique profiles in reactions".format(len(reaction_accounts))) 115 | return reaction_accounts 116 | 117 | 118 | def get_comments(browser): 119 | printer.print_info("Processing comments") 120 | comment_accounts = [] 121 | comment_profiles = [] 122 | comments_links = get_elements(browser, **elements.comments_link) 123 | 124 | with tqdm(total=len(comments_links), ncols=100) as pbar: 125 | for comment_link in comments_links: 126 | try: 127 | chrome.scroll_into_view(browser, comment_link) 128 | browser.execute_script("arguments[0].click();", comment_link) 129 | time.sleep(2) 130 | comment_profiles = get_elements(browser, **elements.comment) 131 | except Exception as e: 132 | print(e) 133 | pass 134 | pbar.update() 135 | 136 | for profile in comment_profiles: 137 | try: 138 | regex = re.compile("id=[0-9]*") 139 | hovercard = profile.get_attribute("data-hovercard") 140 | r = regex.findall(hovercard) 141 | user_id = r[0][3:] 142 | account = {} 143 | account['Name'] = profile.text 144 | account['Profile URL'] = "https://www.facebook.com/"+user_id 145 | account['FB User ID'] = user_id 146 | if account not in comment_accounts: 147 | comment_accounts.append(account) 148 | 149 | except Exception as e: 150 | print(e) 151 | pass 152 | 153 | # printer.print_good("Found {} unique profiles in comments".format(len(comment_accounts))) 154 | return comment_accounts 155 | 156 | 157 | def find_mutual_friends(browser, target, accounts): 158 | hidden_friends = [] 159 | 160 | 161 | with tqdm(total=len(accounts), ncols=100) as pbar: 162 | for account in accounts: 163 | try: 164 | browser.get("https://www.facebook.com/browse/mutual_friends/?uid={}&node={}".format(target, account['FB User ID'])) 165 | mutual_friends = get_elements(browser, **elements.mutual_friend) 166 | 167 | for bff in mutual_friends: 168 | link = bff.find_element_by_tag_name('a') 169 | user_data_json = link.get_attribute("data-gt") 170 | user_data_dict = json.loads(user_data_json) 171 | user_id = user_data_dict['engagement']['eng_tid'] 172 | 173 | friend = {} 174 | friend['Name'] = link.text 175 | friend['Profile URL'] = "https://www.facebook.com/"+user_id 176 | friend['FB User ID'] = user_id 177 | 178 | if account not in hidden_friends: 179 | hidden_friends.append(account) 180 | except Exception as e: 181 | print(e) 182 | pbar.update() 183 | 184 | hidden_friends.sort(key=operator.itemgetter('Name')) 185 | printer.print_good("Found {} hidden friends".format(len(hidden_friends))) 186 | return hidden_friends 187 | 188 | -------------------------------------------------------------------------------- /modules/elements.py: -------------------------------------------------------------------------------- 1 | ### Facebook elements values. Chanage here when facebook changes code. 2 | 3 | spinner = {"value":"_6z7p", 4 | "html":"div", 5 | "attribute":"class", 6 | "verbose":"loading spinner", 7 | "plural":"loading spinners"} 8 | 9 | post = {"value":"_5pcr userContentWrapper", 10 | "html":"div", 11 | "attribute":"class", 12 | "verbose":"post", 13 | "plural":"posts"} 14 | 15 | profile_name = {"value":"fb-timeline-cover-name", 16 | "html":"*", 17 | "attribute":"id", 18 | "verbose":"target profile name", 19 | "plural":"target profile name"} 20 | 21 | reactions_link = {"value":"UFI2ReactionsCount/root", 22 | "html":"a", 23 | "attribute":"data-testid", 24 | "verbose":"reactions", 25 | "plural":"reactions"} 26 | 27 | comment = {"value":"_6qw4", 28 | "html":"a", 29 | "attribute":"class", 30 | "verbose":"comment", 31 | "plural":"comments"} 32 | 33 | comments_link = {"value":"_4sxc _42ft", 34 | "html":"a", 35 | "attribute":"class", 36 | "verbose":"comments link", 37 | "plural":"comments links"} 38 | 39 | reaction_profile = {"value":"_5j0e fsl fwb fcb", 40 | "html":"div", 41 | "attribute":"class", 42 | "verbose":"reaction profile", 43 | "plural":"reaction profiles"} 44 | 45 | mutual_friend = {"value":"fsl fwb fcb", 46 | "html":"div", 47 | "attribute":"class", 48 | "verbose":"mutual friend", 49 | "plural":"mutual friends"} 50 | 51 | close_button = {"value":"reactions_profile_browser:close", 52 | "html":"a", 53 | "attribute":"data-testid", 54 | "verbose":"close reactions button", 55 | "plural":"close reactions buttons"} 56 | 57 | see_more = {"value":"pam uiBoxLightblue uiMorePagerPrimary", 58 | "html":"a", 59 | "attribute":"class", 60 | "verbose":"see more reactions button", 61 | "plural":"see more reactions buttons"} 62 | 63 | recover = {"value":"_42ft _4jy0 _62c3 _4jy4 _517h _51sy", 64 | "html":"a", 65 | "attribute":"class"} 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /modules/printer.py: -------------------------------------------------------------------------------- 1 | import codecs, csv 2 | from colorama import init 3 | from . import core 4 | init() 5 | author = 'Musafir.py' 6 | version = '1.0' 7 | 8 | # Console colors 9 | W = '\033[1;0m' # white 10 | R = '\033[1;31m' # red 11 | G = '\033[1;32m' # green 12 | O = '\033[1;33m' # orange 13 | B = '\033[1;34m' # blue 14 | Y = '\033[1;93m' # yellow 15 | P = '\033[1;35m' # purple 16 | C = '\033[1;36m' # cyan 17 | GR = '\033[1;37m' # gray 18 | colors = [G,R,B,P,C,O,GR] 19 | 20 | info = '{}[*]{} '.format(B,GR) 21 | ques = '{}[?]{} '.format(Y,GR) 22 | bad = '{}[-]{} '.format(R,GR) 23 | good = '{}[+]{} '.format(G,GR) 24 | 25 | 26 | def print_art(): 27 | 28 | print(""" 29 | {}¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶ 30 | ¶¶¶¶¶¶¶{}____{}¶¶¶¶ 31 | ¶¶¶¶¶¶{}___{}¶¶¶¶¶¶ 32 | ¶¶¶¶{}_______{}¶¶¶¶ 33 | ¶¶¶¶¶¶{}___{}¶¶¶¶¶¶ 34 | ¶¶¶¶¶¶{}___{}¶¶¶¶¶¶ {}HIDDEN 35 | {}¶¶¶¶¶¶{}___{}¶¶¶¶¶¶ {}FRIENDS 36 | {}¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶ {}FINDER v{} 37 | by {}""".format(B, GR, B, GR, B, GR, B, GR, B, GR, B, GR, B, GR, B, GR, B, GR, version,author)) 38 | 39 | 40 | def print_banner(text): 41 | print('\n{1}[ {2}{0}{1} ]{3}'.format(text, G, C, GR)) 42 | 43 | def print_info(text): 44 | print(info + text) 45 | 46 | def print_ques(text): 47 | print(ques + text) 48 | 49 | def print_good(text): 50 | print(good + text) 51 | 52 | def print_bad(text): 53 | print(bad + text) 54 | 55 | 56 | def save_friends(browser, name, friends_list, category): 57 | try: 58 | headers = ["Name","FB User ID","Profile URL",] 59 | with open("{}-{}-friends.csv".format(name, category),"w") as fd: 60 | spreadsheet = csv.DictWriter(fd,fieldnames=headers) 61 | spreadsheet.writeheader() 62 | for friend in friends_list: 63 | spreadsheet.writerow(friend) 64 | except Exception as e: 65 | print_bad("Error when writing to file: {}".format(e)) 66 | pass 67 | print_good("Task completed! {} {} friends saved to {}-{}-friends.csv".format(len(friends_list), category, name, category)) 68 | 69 | return 70 | -------------------------------------------------------------------------------- /modules/tests.py: -------------------------------------------------------------------------------- 1 | from . import printer 2 | 3 | def test_elements(browser, *elements): 4 | errors = [] 5 | for element in elements: 6 | if len(browser.find_elements_by_xpath("//{}[@{}='{}']".format(element['html'], element['attribute'], element['value']))) > 0: 7 | pass 8 | # printer.print_good("{} test - ok".format(element['verbose'])) 9 | else: 10 | if element['verbose'] == "post": 11 | printer.print_bad('Critical error. No posts were found. Reasons: no posts in profile or post value "{}" was changed in page code'.format(element['value'])) 12 | printer.print_bad('Terminating program...') 13 | browser.close() 14 | exit() 15 | else: 16 | printer.print_bad('Error. No {} found. Reasons: no {} in profile or {} value "{}" was changed in page code'.format(element['verbose'], element['plural'], element['plural'], element['value'])) 17 | errors.append(element['verbose']) 18 | test_failed_prompt(browser) 19 | return errors 20 | 21 | def test_failed_prompt(browser): 22 | answer = None 23 | while answer not in ("y", "n"): 24 | answer = input("Would you like to continue? [y/n] ") 25 | if answer == "y": 26 | pass 27 | elif answer == "n": 28 | browser.close() 29 | exit() 30 | else: 31 | print("Please press 'y' or 'n'.") 32 | 33 | def profile_prompt(browser, profile_name, target): 34 | answer = None 35 | while answer not in ("y", "n"): 36 | print("{}".format(printer.ques), end="", flush=True) 37 | answer = input('User ID: {} is connected to profile name: {} - is this a correct profile? [y/n] '.format(target, profile_name)) 38 | if answer == "y": 39 | pass 40 | elif answer == "n": 41 | browser.close() 42 | exit() 43 | else: 44 | print("Please press 'y' or 'n'.") 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2018.11.29 2 | chardet==3.0.4 3 | Click==7.0 4 | colorama==0.4.1 5 | idna==2.8 6 | requests==2.21.0 7 | selenium==3.141.0 8 | tqdm==4.31.1 9 | urllib3>=1.24.2 10 | wincertstore==0.2 11 | --------------------------------------------------------------------------------