Browser forensic artefacts on Linux. Profile paths, SQLite databases, the queries that turn schema into history / downloads / cookies / search activity. Firefox uses Unix epoch microseconds. Chrome uses FILETIME (1601) microseconds. Same column type, different zero point.


Profile locations

BrowserPath
Firefox~/.mozilla/firefox/<profile>/ (profile names from profiles.ini)
Chrome~/.config/google-chrome/Default/ or <ProfileName>/
Chromium~/.config/chromium/Default/
Brave~/.config/BraveSoftware/Brave-Browser/Default/
Edge (Linux)~/.config/microsoft-edge/Default/
cat ~/.mozilla/firefox/profiles.ini  # find Firefox profile dirname
ls -la ~/.config/google-chrome/      # find Chrome profile dirs

Firefox - places.sqlite

navigation history, bookmarks, download annotations all live here. timestamps are microseconds since 1970-01-01 UTC.

cp ~/.mozilla/firefox/<profile>/*.sqlite ./working/  # close Firefox first - locks block reads
sqlite3 ./working/places.sqlite                       # open shell
SELECT url, title, visit_count
FROM moz_places
ORDER BY visit_count DESC
LIMIT 10;
Last visited site
SELECT url, datetime(last_visit_date / 1000000, 'unixepoch') AS last_utc
FROM moz_places
ORDER BY last_visit_date DESC
LIMIT 1;
Full visit history
SELECT p.url, datetime(v.visit_date / 1000000, 'unixepoch') AS d, v.visit_type
FROM moz_historyvisits v
JOIN moz_places p ON p.id = v.place_id
ORDER BY v.visit_date DESC;
Downloads (visit_type 7) and via moz_annos
SELECT p.url, datetime(v.visit_date / 1000000, 'unixepoch') AS d
FROM moz_historyvisits v JOIN moz_places p ON v.place_id = p.id
WHERE v.visit_type = 7 ORDER BY v.visit_date DESC;
 
SELECT datetime(lastModified / 1000000, 'unixepoch') AS down_date,
       content AS file, url
FROM moz_places, moz_annos
WHERE moz_places.id = moz_annos.place_id;
Bookmarks
SELECT b.title, p.url, datetime(b.dateAdded / 1000000, 'unixepoch') AS added
FROM moz_bookmarks b JOIN moz_places p ON p.id = b.fk
WHERE b.type = 1
ORDER BY b.dateAdded DESC;
Search activity (Google example)
SELECT p.url, datetime(v.visit_date / 1000000, 'unixepoch') AS d
FROM moz_historyvisits v JOIN moz_places p ON v.place_id = p.id
WHERE p.url LIKE '%google%search%'
ORDER BY v.visit_date DESC;
Firefox visit_type codes
CodeMeaning
1followed link
2typed URL
3bookmark
4from iframe
5HTTP 301 redirect
6HTTP 302 redirect
7downloaded file
8followed link inside iframe

filter WHERE visit_type IN (2, 3) for user-driven navigation only.


Firefox - other databases

FileHolds
cookies.sqlitemoz_cookies - host, name, value, expiry
formhistory.sqliteautofill form data
favicons.sqlitefavicon images
webappsstore.sqliteHTML5 localStorage / sessionStorage
permissions.sqliteper-site permissions
logins.json + key4.dbencrypted credentials - decrypt with firefox_decrypt
cache2/entries/cache content - parse with MozillaCacheView (Win) or dejavu (Linux)
# anti-phishing settings - findings if disabled
grep safebrowsing ~/.mozilla/firefox/*/prefs.js
# expect safebrowsing.malware.enabled=true. false value = tampering tell

Chrome - History (no extension)

timestamps are microseconds since 1601-01-01 (Windows FILETIME). conversion needs the offset to Unix epoch.

cp ~/.config/google-chrome/Default/History ./working/  # close Chrome first
sqlite3 ./working/History
SELECT url, title, visit_count
FROM urls
ORDER BY visit_count DESC
LIMIT 10;
Visit history (FILETIME UTC string)
SELECT p.url,
       datetime(v.visit_time / 1000000 + (strftime('%s', '1601-01-01')), 'unixepoch') AS d
FROM visits v
LEFT JOIN urls p ON v.url = p.id
ORDER BY d DESC;
Last download
SELECT tab_url,
       datetime(start_time / 1000000 + (strftime('%s', '1601-01-01')), 'unixepoch') AS d
FROM downloads
ORDER BY start_time DESC
LIMIT 5;
Search activity
SELECT p.url,
       datetime(v.visit_time / 1000000 + (strftime('%s', '1601-01-01')), 'unixepoch') AS d
FROM visits v LEFT JOIN urls p ON v.url = p.id
WHERE p.url LIKE '%google%search%'
ORDER BY d DESC;

strftime('%s', '1601-01-01') returns -11644473600 (Unix-epoch seconds for 1601-01-01). visit_time / 1000000 converts microseconds since 1601 into seconds since 1601. adding the two yields Unix-epoch seconds, which datetime(..., 'unixepoch') renders.


Chrome - other files

FileHolds
Cookies (or Network/Cookies from Chrome 96+)cookies table
BookmarksJSON, not SQLite
Login Dataencrypted credentials - OS keychain holds the master key
Web Dataautofill form history (autofill table)
PreferencesJSON config - search for safebrowsing settings
Extensions/<id>/per-extension manifest + assets
Cache/cache data, custom format
# Chrome anti-phishing check
grep safebrowsing ~/.config/google-chrome/Default/Preferences

Shell + interactive history files

FileShell / tool
~/.bash_historybash
~/.zsh_historyzsh - extended format : <epoch>:<duration>;<command> if EXTENDED_HISTORY set
~/.sh_historysh, ksh, ash
~/.historycsh, tcsh
~/.mysql_historymysql client
~/.psql_historypsql client
~/.python_historypython REPL
~/.lesshstless search patterns
# extract last 50 commands
tail -50 ~/.bash_history
 
# zsh extended format - convert epochs to readable
awk -F'[:;]' '/^: [0-9]/ {cmd=""; for(i=4;i<=NF;i++) cmd=cmd" "$i; print strftime("%Y-%m-%d %H:%M:%S", $2), cmd}' ~/.zsh_history
 
# search every history file in a home directory
find /home/<user> -maxdepth 2 -type f -name '.*_history' -o -name '.bash_history' -o -name '.history'
History tampering tells
  • .bash_history reduced to single exit line on an actively-used account = history -c; exit (T1070.003)
  • .bash_history symlinked to /dev/null
  • HISTFILE unset in user’s shell rc files
  • .bash_history size 0 with recent atime (read-and-truncate)

Decision flow

graph TD
    q["browser-artefact question"] --> b{"which browser?"}
    b -->|"Firefox"| ff["~/.mozilla/firefox/<profile>/"]
    b -->|"Chrome / Brave / Edge"| cr["~/.config/<browser>/Default/"]
    ff --> ffq["which artefact?"]
    cr --> crq["which artefact?"]
    ffq -->|"history / bookmarks / downloads"| pl["places.sqlite"]
    ffq -->|"cookies"| ck["cookies.sqlite"]
    ffq -->|"credentials"| ck2["logins.json + key4.db<br/>(firefox_decrypt)"]
    crq -->|"history / downloads"| ch["History (no ext)"]
    crq -->|"cookies"| cc["Cookies / Network/Cookies"]
    crq -->|"credentials"| ld["Login Data + OS keychain"]

Pitfalls

  • timestamp epoch confusion: Firefox = 1970, Chrome = 1601. wrong epoch produces dates in the wrong century
  • running queries against an open browser: SQLite locks block reads. always copy out first or close the browser
  • multi-profile Firefox: profiles.ini lists every profile. process each separately
  • Chrome History has no extension: easy to miss when ls-greping for *.sqlite or *.db
  • moz_places.last_visit_date vs moz_historyvisits.visit_date: the former is the most-recent visit only. the latter is the per-visit log
  • clear-history does not clear logins.json or Login Data. credential stores survive a partial wipe
  • cache files need rendering, not just extraction: cache2/entries/<hash> files have a Mozilla wrapper around the response body. raw cat will not produce readable HTML

Field Manual | Log Triage | Filesystem Metadata