digital forensics

Extracting Browser History Artifacts

Extract and analyze browser history, cookies, cache, downloads, and bookmarks from Chrome, Firefox, and Edge for forensic evidence of user web activity.

artifact-extractionbrowser-forensicschromeedgefirefoxforensicsweb-history
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When investigating user web activity as part of a forensic examination
  • During insider threat investigations to establish patterns of data exfiltration
  • When tracing user visits to malicious or policy-violating websites
  • For correlating browser activity with other forensic artifacts and timelines
  • When investigating phishing attacks to identify which links were clicked

Prerequisites

  • Forensic image or access to user profile directories
  • SQLite3 for querying browser databases
  • Hindsight, BrowsingHistoryView, or DB Browser for SQLite
  • Knowledge of browser artifact file locations per OS
  • Python 3 with sqlite3 module for automated extraction
  • Understanding of Chrome, Firefox, and Edge storage formats

Workflow

Step 1: Locate Browser Artifact Files

# Mount forensic image
mount -o ro,loop,offset=$((2048*512)) /cases/case-2024-001/images/evidence.dd /mnt/evidence
 
# Chrome artifact locations (Windows)
CHROME_WIN="/mnt/evidence/Users/suspect/AppData/Local/Google/Chrome/User Data/Default"
# Key files: History, Cookies, Login Data, Web Data, Bookmarks, Preferences,
#            Cache/, GPUCache/, Local Storage/, Session Storage/, IndexedDB/
 
# Firefox artifact locations (Windows)
FIREFOX_WIN="/mnt/evidence/Users/suspect/AppData/Roaming/Mozilla/Firefox/Profiles/*.default-release"
# Key files: places.sqlite, cookies.sqlite, formhistory.sqlite, logins.json,
#            key4.db, sessionstore.jsonlz4, webappsstore.sqlite
 
# Edge (Chromium) artifact locations (Windows)
EDGE_WIN="/mnt/evidence/Users/suspect/AppData/Local/Microsoft/Edge/User Data/Default"
 
# Copy artifacts to working directory
mkdir -p /cases/case-2024-001/browser/{chrome,firefox,edge}
cp -r "$CHROME_WIN"/{History,Cookies,Downloads,"Login Data","Web Data",Bookmarks} \
   /cases/case-2024-001/browser/chrome/ 2>/dev/null
cp -r $FIREFOX_WIN/{places.sqlite,cookies.sqlite,formhistory.sqlite,logins.json} \
   /cases/case-2024-001/browser/firefox/ 2>/dev/null
cp -r "$EDGE_WIN"/{History,Cookies,Downloads} \
   /cases/case-2024-001/browser/edge/ 2>/dev/null
 
# Hash artifacts for integrity
find /cases/case-2024-001/browser/ -type f -exec sha256sum {} \; \
   > /cases/case-2024-001/browser/artifact_hashes.txt

Step 2: Extract Chrome Browsing History and Downloads

# Query Chrome History database
sqlite3 /cases/case-2024-001/browser/chrome/History << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/chrome_history.csv
 
SELECT
    urls.url,
    urls.title,
    datetime(urls.last_visit_time/1000000-11644473600, 'unixepoch') AS last_visit,
    urls.visit_count,
    urls.typed_count,
    visits.transition & 0xFF AS transition_type
FROM urls
LEFT JOIN visits ON urls.id = visits.url
ORDER BY urls.last_visit_time DESC;
SQL
 
# Extract Chrome downloads
sqlite3 /cases/case-2024-001/browser/chrome/History << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/chrome_downloads.csv
 
SELECT
    current_path,
    tab_url AS source_url,
    total_bytes,
    datetime(start_time/1000000-11644473600, 'unixepoch') AS start_time,
    datetime(end_time/1000000-11644473600, 'unixepoch') AS end_time,
    state,
    danger_type,
    mime_type
FROM downloads
ORDER BY start_time DESC;
SQL
 
# Extract Chrome search terms
sqlite3 /cases/case-2024-001/browser/chrome/History << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/chrome_searches.csv
 
SELECT
    term,
    urls.url,
    datetime(urls.last_visit_time/1000000-11644473600, 'unixepoch') AS search_time
FROM keyword_search_terms
JOIN urls ON keyword_search_terms.url_id = urls.id
ORDER BY urls.last_visit_time DESC;
SQL

Step 3: Extract Firefox Browsing History

# Query Firefox places.sqlite for history
sqlite3 /cases/case-2024-001/browser/firefox/places.sqlite << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/firefox_history.csv
 
SELECT
    moz_places.url,
    moz_places.title,
    datetime(moz_historyvisits.visit_date/1000000, 'unixepoch') AS visit_date,
    moz_places.visit_count,
    moz_historyvisits.visit_type
FROM moz_places
JOIN moz_historyvisits ON moz_places.id = moz_historyvisits.place_id
ORDER BY moz_historyvisits.visit_date DESC;
SQL
 
# Extract Firefox bookmarks
sqlite3 /cases/case-2024-001/browser/firefox/places.sqlite << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/firefox_bookmarks.csv
 
SELECT
    moz_bookmarks.title,
    moz_places.url,
    datetime(moz_bookmarks.dateAdded/1000000, 'unixepoch') AS date_added,
    datetime(moz_bookmarks.lastModified/1000000, 'unixepoch') AS last_modified
FROM moz_bookmarks
JOIN moz_places ON moz_bookmarks.fk = moz_places.id
WHERE moz_bookmarks.type = 1
ORDER BY moz_bookmarks.dateAdded DESC;
SQL
 
# Extract Firefox form history (search terms, form fills)
sqlite3 /cases/case-2024-001/browser/firefox/formhistory.sqlite << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/firefox_forms.csv
 
SELECT
    fieldname,
    value,
    timesUsed,
    datetime(firstUsed/1000000, 'unixepoch') AS first_used,
    datetime(lastUsed/1000000, 'unixepoch') AS last_used
FROM moz_formhistory
ORDER BY lastUsed DESC;
SQL

Step 4: Extract Cookies and Stored Credentials

# Extract Chrome cookies
sqlite3 /cases/case-2024-001/browser/chrome/Cookies << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/chrome_cookies.csv
 
SELECT
    host_key,
    name,
    path,
    datetime(creation_utc/1000000-11644473600, 'unixepoch') AS created,
    datetime(expires_utc/1000000-11644473600, 'unixepoch') AS expires,
    datetime(last_access_utc/1000000-11644473600, 'unixepoch') AS last_access,
    is_secure,
    is_httponly,
    is_persistent
FROM cookies
ORDER BY last_access_utc DESC;
SQL
 
# Extract Firefox cookies
sqlite3 /cases/case-2024-001/browser/firefox/cookies.sqlite << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/firefox_cookies.csv
 
SELECT
    host,
    name,
    path,
    datetime(creationTime/1000000, 'unixepoch') AS created,
    datetime(expiry, 'unixepoch') AS expires,
    datetime(lastAccessed/1000000, 'unixepoch') AS last_access,
    isSecure,
    isHttpOnly
FROM moz_cookies
ORDER BY lastAccessed DESC;
SQL
 
# Note: Chrome Login Data is encrypted with DPAPI (Windows) or keychain (Mac)
# Extract stored login URLs (passwords are encrypted)
sqlite3 /cases/case-2024-001/browser/chrome/"Login Data" << 'SQL'
.headers on
.mode csv
.output /cases/case-2024-001/analysis/chrome_logins.csv
 
SELECT
    origin_url,
    action_url,
    username_value,
    datetime(date_created/1000000-11644473600, 'unixepoch') AS date_created,
    datetime(date_last_used/1000000-11644473600, 'unixepoch') AS date_last_used,
    times_used
FROM logins
ORDER BY date_last_used DESC;
SQL

Step 5: Use Hindsight for Comprehensive Chrome Analysis

# Install Hindsight
pip install pyhindsight
 
# Run Hindsight against Chrome profile
hindsight -i "/cases/case-2024-001/browser/chrome/" \
   -o /cases/case-2024-001/analysis/hindsight_report \
   -f xlsx
 
# Hindsight automatically extracts:
# - Browsing history with timestamps
# - Downloads with source URLs
# - Cookies with decryption (where possible)
# - Cache records
# - Local Storage entries
# - Autofill data
# - Saved passwords (encrypted)
# - Preferences and extensions
# - Session/tab recovery data
 
# For JSONL output (easier to parse)
hindsight -i "/cases/case-2024-001/browser/chrome/" \
   -o /cases/case-2024-001/analysis/hindsight_report \
   -f jsonl

Key Concepts

Concept Description
Chrome timestamp Microseconds since January 1, 1601 (WebKit/Chrome epoch)
Firefox timestamp Microseconds since January 1, 1970 (Unix epoch in microseconds)
Transition types How a URL was accessed: typed (1), link (0), bookmark (1), redirect (5/6)
DPAPI encryption Windows Data Protection API encrypting stored passwords and cookies
places.sqlite Firefox combined history and bookmark database
SQLite WAL Write-Ahead Log that may contain recently deleted browser records
Session restore Browser data preserving open tabs across restarts
IndexedDB Browser-based database that may contain web application data

Tools & Systems

Tool Purpose
Hindsight Comprehensive Chrome/Chromium forensic analysis tool
sqlite3 Command-line SQLite database query tool
DB Browser for SQLite GUI tool for browsing SQLite databases
BrowsingHistoryView NirSoft tool for viewing browser history across all browsers
ChromeCacheView NirSoft tool for examining Chrome cache contents
MZCacheView NirSoft tool for Firefox cache analysis
KAPE Automated artifact collection including browser data
Autopsy Full forensic platform with browser artifact ingest modules

Common Scenarios

Scenario 1: Phishing Investigation Extract browser history around the reported phishing timeframe, identify the phishing URL that was visited, check downloads for malicious attachments, examine cookies for session tokens that may have been stolen, correlate with email header analysis.

Scenario 2: Data Exfiltration via Cloud Services Search history for cloud storage URLs (Dropbox, Google Drive, OneDrive, Mega), examine downloads and uploads, check form history for file names entered, review cookies for active cloud service sessions during the investigation period.

Scenario 3: Policy Violation Investigation Extract complete browsing history for the investigation period, categorize sites visited, identify access to prohibited content categories, document timestamps and visit duration, correlate with network proxy logs for verification.

Scenario 4: Malware Delivery Vector Analysis Trace the chain of redirects leading to a drive-by download, examine the downloads database for the malware payload, check cache for exploit kit landing pages, identify the initial referrer URL that started the infection chain.

Output Format

Browser Forensics Summary:
  User Profile: suspect (Windows 10)
  Browsers Found: Chrome 120, Firefox 121, Edge 120
 
  Chrome Analysis:
    History Entries:    12,456
    Downloads:          234
    Saved Passwords:    67 sites (encrypted)
    Cookies:            3,456
    Bookmarks:          89
 
  Firefox Analysis:
    History Entries:    5,678
    Form Entries:       234
    Bookmarks:          45
    Cookies:            1,234
 
  Suspicious Findings:
    - Visited known phishing URL at 2024-01-15 14:32 UTC
    - Downloaded "invoice_update.exe" from suspicious domain
    - Cloud storage (mega.nz) accessed 15 times in 2-hour window
    - Search queries: "how to encrypt files", "secure file transfer"
 
  Reports:
    Chrome History:   /analysis/chrome_history.csv
    Firefox History:  /analysis/firefox_history.csv
    Full Report:      /analysis/hindsight_report.xlsx
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.1 KB

API Reference: Browser History Extraction Agent

Dependencies

Library Version Purpose
sqlite3 stdlib Query Chrome/Firefox SQLite databases
csv stdlib Export results to CSV format

CLI Usage

python scripts/agent.py \
  --chrome-dir "/mnt/evidence/Users/suspect/AppData/Local/Google/Chrome/User Data/Default" \
  --firefox-dir "/mnt/evidence/Users/suspect/AppData/Roaming/Mozilla/Firefox/Profiles/abc.default" \
  --output-dir /cases/analysis/ \
  --output browser_report.json

Functions

chrome_time_to_utc(chrome_ts) -> str

Converts Chrome/WebKit timestamp (microseconds since 1601-01-01) to ISO-8601 UTC string.

firefox_time_to_utc(ff_ts) -> str

Converts Firefox timestamp (microseconds since Unix epoch) to ISO-8601 UTC string.

extract_chrome_history(db_path, limit) -> list

Queries the urls table from Chrome's History SQLite DB. Returns URL, title, last_visit, visit_count.

extract_chrome_downloads(db_path, limit) -> list

Queries the downloads table for file path, source URL, size, timestamps, and danger type.

extract_chrome_cookies(db_path, limit) -> list

Queries the cookies table. Note: cookie values are DPAPI-encrypted on Windows.

extract_firefox_history(db_path, limit) -> list

Queries moz_places JOIN moz_historyvisits from Firefox places.sqlite.

extract_firefox_cookies(db_path, limit) -> list

Queries moz_cookies from Firefox cookies.sqlite.

export_to_csv(data, output_path)

Writes list of dicts to CSV with headers.

generate_report(chrome_dir, firefox_dir, output_dir) -> dict

Orchestrates extraction from both browsers and exports CSVs.

Browser Database Locations (Windows)

Browser Path
Chrome %LOCALAPPDATA%\Google\Chrome\User Data\Default\History
Edge %LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History
Firefox %APPDATA%\Mozilla\Firefox\Profiles\*.default\places.sqlite

Timestamp Formats

Browser Epoch Unit
Chrome/Edge 1601-01-01 Microseconds
Firefox 1970-01-01 Microseconds

Scripts 1

agent.py7.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Browser history artifact extraction agent using sqlite3 for Chrome/Firefox/Edge forensics."""

import argparse
import csv
import json
import logging
import os
import sqlite3
from datetime import datetime, timedelta
from typing import List

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

CHROME_EPOCH = datetime(1601, 1, 1)
UNIX_EPOCH = datetime(1970, 1, 1)


def chrome_time_to_utc(chrome_ts: int) -> str:
    """Convert Chrome/WebKit timestamp (microseconds since 1601-01-01) to ISO UTC."""
    if not chrome_ts or chrome_ts < 0:
        return ""
    try:
        dt = CHROME_EPOCH + timedelta(microseconds=chrome_ts)
        return dt.isoformat() + "Z"
    except (OverflowError, ValueError):
        return ""


def firefox_time_to_utc(ff_ts: int) -> str:
    """Convert Firefox timestamp (microseconds since Unix epoch) to ISO UTC."""
    if not ff_ts or ff_ts < 0:
        return ""
    try:
        dt = UNIX_EPOCH + timedelta(microseconds=ff_ts)
        return dt.isoformat() + "Z"
    except (OverflowError, ValueError):
        return ""


def extract_chrome_history(db_path: str, limit: int = 5000) -> List[dict]:
    """Extract browsing history from Chrome/Edge History database."""
    if not os.path.exists(db_path):
        logger.warning("Chrome History DB not found: %s", db_path)
        return []
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT urls.url, urls.title, urls.last_visit_time, urls.visit_count, urls.typed_count
        FROM urls ORDER BY urls.last_visit_time DESC LIMIT ?
    """, (limit,))
    rows = cursor.fetchall()
    conn.close()
    results = []
    for url, title, last_visit, visit_count, typed_count in rows:
        results.append({
            "url": url, "title": title or "",
            "last_visit": chrome_time_to_utc(last_visit),
            "visit_count": visit_count, "typed_count": typed_count,
        })
    logger.info("Extracted %d Chrome history entries from %s", len(results), db_path)
    return results


def extract_chrome_downloads(db_path: str, limit: int = 1000) -> List[dict]:
    """Extract downloads from Chrome/Edge History database."""
    if not os.path.exists(db_path):
        return []
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT current_path, tab_url, total_bytes, start_time, end_time, mime_type, danger_type
        FROM downloads ORDER BY start_time DESC LIMIT ?
    """, (limit,))
    rows = cursor.fetchall()
    conn.close()
    return [{
        "path": r[0], "source_url": r[1], "size_bytes": r[2],
        "start_time": chrome_time_to_utc(r[3]), "end_time": chrome_time_to_utc(r[4]),
        "mime_type": r[5], "danger_type": r[6],
    } for r in rows]


def extract_chrome_cookies(db_path: str, limit: int = 5000) -> List[dict]:
    """Extract cookies from Chrome Cookies database."""
    if not os.path.exists(db_path):
        return []
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT host_key, name, path, creation_utc, last_access_utc, is_secure, is_httponly
        FROM cookies ORDER BY last_access_utc DESC LIMIT ?
    """, (limit,))
    rows = cursor.fetchall()
    conn.close()
    return [{
        "host": r[0], "name": r[1], "path": r[2],
        "created": chrome_time_to_utc(r[3]), "last_access": chrome_time_to_utc(r[4]),
        "secure": bool(r[5]), "httponly": bool(r[6]),
    } for r in rows]


def extract_firefox_history(db_path: str, limit: int = 5000) -> List[dict]:
    """Extract browsing history from Firefox places.sqlite."""
    if not os.path.exists(db_path):
        logger.warning("Firefox places.sqlite not found: %s", db_path)
        return []
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT moz_places.url, moz_places.title, moz_historyvisits.visit_date,
               moz_places.visit_count, moz_historyvisits.visit_type
        FROM moz_places
        JOIN moz_historyvisits ON moz_places.id = moz_historyvisits.place_id
        ORDER BY moz_historyvisits.visit_date DESC LIMIT ?
    """, (limit,))
    rows = cursor.fetchall()
    conn.close()
    return [{
        "url": r[0], "title": r[1] or "",
        "visit_date": firefox_time_to_utc(r[2]),
        "visit_count": r[3], "visit_type": r[4],
    } for r in rows]


def extract_firefox_cookies(db_path: str, limit: int = 5000) -> List[dict]:
    """Extract cookies from Firefox cookies.sqlite."""
    if not os.path.exists(db_path):
        return []
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT host, name, path, creationTime, lastAccessed, isSecure, isHttpOnly
        FROM moz_cookies ORDER BY lastAccessed DESC LIMIT ?
    """, (limit,))
    rows = cursor.fetchall()
    conn.close()
    return [{
        "host": r[0], "name": r[1], "path": r[2],
        "created": firefox_time_to_utc(r[3]), "last_access": firefox_time_to_utc(r[4]),
        "secure": bool(r[5]), "httponly": bool(r[6]),
    } for r in rows]


def export_to_csv(data: List[dict], output_path: str) -> None:
    """Export extracted data to CSV."""
    if not data:
        return
    with open(output_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=data[0].keys())
        writer.writeheader()
        writer.writerows(data)
    logger.info("Exported %d rows to %s", len(data), output_path)


def generate_report(chrome_dir: str = "", firefox_dir: str = "",
                     output_dir: str = ".") -> dict:
    """Generate comprehensive browser forensics report."""
    report = {"analysis_date": datetime.utcnow().isoformat(), "browsers": {}}

    if chrome_dir and os.path.isdir(chrome_dir):
        history = extract_chrome_history(os.path.join(chrome_dir, "History"))
        downloads = extract_chrome_downloads(os.path.join(chrome_dir, "History"))
        cookies = extract_chrome_cookies(os.path.join(chrome_dir, "Cookies"))
        report["browsers"]["chrome"] = {
            "history_count": len(history), "download_count": len(downloads),
            "cookie_count": len(cookies),
        }
        export_to_csv(history, os.path.join(output_dir, "chrome_history.csv"))
        export_to_csv(downloads, os.path.join(output_dir, "chrome_downloads.csv"))

    if firefox_dir and os.path.isdir(firefox_dir):
        history = extract_firefox_history(os.path.join(firefox_dir, "places.sqlite"))
        cookies = extract_firefox_cookies(os.path.join(firefox_dir, "cookies.sqlite"))
        report["browsers"]["firefox"] = {
            "history_count": len(history), "cookie_count": len(cookies),
        }
        export_to_csv(history, os.path.join(output_dir, "firefox_history.csv"))

    return report


def main():
    parser = argparse.ArgumentParser(description="Browser History Extraction Agent")
    parser.add_argument("--chrome-dir", default="", help="Path to Chrome/Edge User Data/Default")
    parser.add_argument("--firefox-dir", default="", help="Path to Firefox profile directory")
    parser.add_argument("--output-dir", default=".", help="Output directory for CSVs and report")
    parser.add_argument("--output", default="browser_report.json")
    args = parser.parse_args()

    os.makedirs(args.output_dir, exist_ok=True)
    report = generate_report(args.chrome_dir, args.firefox_dir, args.output_dir)
    with open(os.path.join(args.output_dir, args.output), "w") as f:
        json.dump(report, f, indent=2)
    logger.info("Report saved")
    print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
Keep exploring