mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
refactor: Optimize traversal browser data logic (#311)
* refactor: Refactor package names and imports for better code organization. * refactor: Package imports and variable types for consistency * chore: Disable unused-parameter rule in revive. * refactor: Refactor and organize data extraction and browserdata parse. * fix: rename wrong error message info
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package bookmark
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
_ "modernc.org/sqlite" // import sqlite3 driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumBookmark, func() extractor.Extractor {
|
||||
return new(ChromiumBookmark)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxBookmark, func() extractor.Extractor {
|
||||
return new(FirefoxBookmark)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumBookmark []bookmark
|
||||
|
||||
type bookmark struct {
|
||||
ID int64
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
DateAdded time.Time
|
||||
}
|
||||
|
||||
func (c *ChromiumBookmark) Extract(_ []byte) error {
|
||||
bookmarks, err := fileutil.ReadFile(types.ChromiumBookmark.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumBookmark.TempFilename())
|
||||
r := gjson.Parse(bookmarks)
|
||||
if r.Exists() {
|
||||
roots := r.Get("roots")
|
||||
roots.ForEach(func(key, value gjson.Result) bool {
|
||||
getBookmarkChildren(value, c)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].DateAdded.After((*c)[j].DateAdded)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
bookmarkID = "id"
|
||||
bookmarkAdded = "date_added"
|
||||
bookmarkURL = "url"
|
||||
bookmarkName = "name"
|
||||
bookmarkType = "type"
|
||||
bookmarkChildren = "children"
|
||||
)
|
||||
|
||||
func getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (children gjson.Result) {
|
||||
nodeType := value.Get(bookmarkType)
|
||||
children = value.Get(bookmarkChildren)
|
||||
|
||||
bm := bookmark{
|
||||
ID: value.Get(bookmarkID).Int(),
|
||||
Name: value.Get(bookmarkName).String(),
|
||||
URL: value.Get(bookmarkURL).String(),
|
||||
DateAdded: typeutil.TimeEpoch(value.Get(bookmarkAdded).Int()),
|
||||
}
|
||||
if nodeType.Exists() {
|
||||
bm.Type = nodeType.String()
|
||||
*w = append(*w, bm)
|
||||
if children.Exists() && children.IsArray() {
|
||||
for _, v := range children.Array() {
|
||||
children = getBookmarkChildren(v, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *ChromiumBookmark) Name() string {
|
||||
return "bookmark"
|
||||
}
|
||||
|
||||
func (c *ChromiumBookmark) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxBookmark []bookmark
|
||||
|
||||
const (
|
||||
queryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id)`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxBookmark) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxBookmark.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxBookmark.TempFilename())
|
||||
defer db.Close()
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
slog.Error("close journal mode error", "err", err)
|
||||
}
|
||||
rows, err := db.Query(queryFirefoxBookMark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, bt, dateAdded int64
|
||||
title, url string
|
||||
)
|
||||
if err = rows.Scan(&id, &url, &bt, &dateAdded, &title); err != nil {
|
||||
slog.Error("scan bookmark error", "err", err)
|
||||
}
|
||||
*f = append(*f, bookmark{
|
||||
ID: id,
|
||||
Name: title,
|
||||
Type: linkType(bt),
|
||||
URL: url,
|
||||
DateAdded: typeutil.TimeStamp(dateAdded / 1000000),
|
||||
})
|
||||
}
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].DateAdded.After((*f)[j].DateAdded)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxBookmark) Name() string {
|
||||
return "bookmark"
|
||||
}
|
||||
|
||||
func (f *FirefoxBookmark) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
|
||||
func linkType(a int64) string {
|
||||
switch a {
|
||||
case 1:
|
||||
return "url"
|
||||
default:
|
||||
return "folder"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
type BrowserData struct {
|
||||
extractors map[types.DataType]extractor.Extractor
|
||||
}
|
||||
|
||||
func New(items []types.DataType) *BrowserData {
|
||||
bd := &BrowserData{
|
||||
extractors: make(map[types.DataType]extractor.Extractor),
|
||||
}
|
||||
bd.addExtractors(items)
|
||||
return bd
|
||||
}
|
||||
|
||||
func (d *BrowserData) Recovery(masterKey []byte) error {
|
||||
for _, source := range d.extractors {
|
||||
if err := source.Extract(masterKey); err != nil {
|
||||
slog.Error("parse error", "source_data", source.Name(), "err", err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BrowserData) Output(dir, browserName, flag string) {
|
||||
output := newOutPutter(flag)
|
||||
|
||||
for _, source := range d.extractors {
|
||||
if source.Len() == 0 {
|
||||
// if the length of the export data is 0, then it is not necessary to output
|
||||
continue
|
||||
}
|
||||
filename := fileutil.Filename(browserName, source.Name(), output.Ext())
|
||||
|
||||
f, err := output.CreateFile(dir, filename)
|
||||
if err != nil {
|
||||
slog.Error("create file error", "filename", filename, "err", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := output.Write(source, f); err != nil {
|
||||
slog.Error("write to file error", "filename", filename, "err", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
slog.Error("close file error", "filename", filename, "err", err.Error())
|
||||
continue
|
||||
}
|
||||
slog.Warn("export success", "filename", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *BrowserData) addExtractors(items []types.DataType) {
|
||||
for _, itemType := range items {
|
||||
if source := extractor.CreateExtractor(itemType); source != nil {
|
||||
d.extractors[itemType] = source
|
||||
} else {
|
||||
slog.Debug("source not found", "source", itemType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
// import sqlite3 driver
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumCookie, func() extractor.Extractor {
|
||||
return new(ChromiumCookie)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxCookie, func() extractor.Extractor {
|
||||
return new(FirefoxCookie)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumCookie []cookie
|
||||
|
||||
type cookie struct {
|
||||
Host string
|
||||
Path string
|
||||
KeyName string
|
||||
encryptValue []byte
|
||||
Value string
|
||||
IsSecure bool
|
||||
IsHTTPOnly bool
|
||||
HasExpire bool
|
||||
IsPersistent bool
|
||||
CreateDate time.Time
|
||||
ExpireDate time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies`
|
||||
)
|
||||
|
||||
func (c *ChromiumCookie) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumCookie.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumCookie.TempFilename())
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryChromiumCookie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
key, host, path string
|
||||
isSecure, isHTTPOnly, hasExpire, isPersistent int
|
||||
createDate, expireDate int64
|
||||
value, encryptValue []byte
|
||||
)
|
||||
if err = rows.Scan(&key, &encryptValue, &host, &path, &createDate, &expireDate, &isSecure, &isHTTPOnly, &hasExpire, &isPersistent); err != nil {
|
||||
slog.Error("scan chromium cookie error", "err", err)
|
||||
}
|
||||
|
||||
cookie := cookie{
|
||||
KeyName: key,
|
||||
Host: host,
|
||||
Path: path,
|
||||
encryptValue: encryptValue,
|
||||
IsSecure: typeutil.IntToBool(isSecure),
|
||||
IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
|
||||
HasExpire: typeutil.IntToBool(hasExpire),
|
||||
IsPersistent: typeutil.IntToBool(isPersistent),
|
||||
CreateDate: typeutil.TimeEpoch(createDate),
|
||||
ExpireDate: typeutil.TimeEpoch(expireDate),
|
||||
}
|
||||
if len(encryptValue) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
value, err = crypto.DecryptWithDPAPI(encryptValue)
|
||||
} else {
|
||||
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("decrypt chromium cookie error", "err", err)
|
||||
}
|
||||
}
|
||||
cookie.Value = string(value)
|
||||
*c = append(*c, cookie)
|
||||
}
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumCookie) Name() string {
|
||||
return "cookie"
|
||||
}
|
||||
|
||||
func (c *ChromiumCookie) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxCookie []cookie
|
||||
|
||||
const (
|
||||
queryFirefoxCookie = `SELECT name, value, host, path, creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies`
|
||||
)
|
||||
|
||||
func (f *FirefoxCookie) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxCookie.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxCookie.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryFirefoxCookie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, value, host, path string
|
||||
isSecure, isHTTPOnly int
|
||||
creationTime, expiry int64
|
||||
)
|
||||
if err = rows.Scan(&name, &value, &host, &path, &creationTime, &expiry, &isSecure, &isHTTPOnly); err != nil {
|
||||
slog.Error("scan firefox cookie error", "err", err)
|
||||
}
|
||||
*f = append(*f, cookie{
|
||||
KeyName: name,
|
||||
Host: host,
|
||||
Path: path,
|
||||
IsSecure: typeutil.IntToBool(isSecure),
|
||||
IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
|
||||
CreateDate: typeutil.TimeStamp(creationTime / 1000000),
|
||||
ExpireDate: typeutil.TimeStamp(expiry),
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].CreateDate.After((*f)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxCookie) Name() string {
|
||||
return "cookie"
|
||||
}
|
||||
|
||||
func (f *FirefoxCookie) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package creditcard
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
// import sqlite3 driver
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumCreditCard, func() extractor.Extractor {
|
||||
return new(ChromiumCreditCard)
|
||||
})
|
||||
extractor.RegisterExtractor(types.YandexCreditCard, func() extractor.Extractor {
|
||||
return new(YandexCreditCard)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumCreditCard []card
|
||||
|
||||
type card struct {
|
||||
GUID string
|
||||
Name string
|
||||
ExpirationYear string
|
||||
ExpirationMonth string
|
||||
CardNumber string
|
||||
Address string
|
||||
NickName string
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted, billing_address_id, nickname FROM credit_cards`
|
||||
)
|
||||
|
||||
func (c *ChromiumCreditCard) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumCreditCard.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumCreditCard.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryChromiumCredit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, month, year, guid, address, nickname string
|
||||
value, encryptValue []byte
|
||||
)
|
||||
if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
|
||||
slog.Error("scan chromium credit card error", "err", err)
|
||||
}
|
||||
ccInfo := card{
|
||||
GUID: guid,
|
||||
Name: name,
|
||||
ExpirationMonth: month,
|
||||
ExpirationYear: year,
|
||||
Address: address,
|
||||
NickName: nickname,
|
||||
}
|
||||
if len(encryptValue) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
value, err = crypto.DecryptWithDPAPI(encryptValue)
|
||||
} else {
|
||||
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("decrypt chromium credit card error", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
ccInfo.CardNumber = string(value)
|
||||
*c = append(*c, ccInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumCreditCard) Name() string {
|
||||
return "creditcard"
|
||||
}
|
||||
|
||||
func (c *ChromiumCreditCard) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type YandexCreditCard []card
|
||||
|
||||
func (c *YandexCreditCard) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.YandexCreditCard.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.YandexCreditCard.TempFilename())
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryChromiumCredit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, month, year, guid, address, nickname string
|
||||
value, encryptValue []byte
|
||||
)
|
||||
if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
|
||||
slog.Error("scan chromium credit card error", "err", err)
|
||||
}
|
||||
ccInfo := card{
|
||||
GUID: guid,
|
||||
Name: name,
|
||||
ExpirationMonth: month,
|
||||
ExpirationYear: year,
|
||||
Address: address,
|
||||
NickName: nickname,
|
||||
}
|
||||
if len(encryptValue) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
value, err = crypto.DecryptWithDPAPI(encryptValue)
|
||||
} else {
|
||||
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("decrypt chromium credit card error", "err", err)
|
||||
}
|
||||
}
|
||||
ccInfo.CardNumber = string(value)
|
||||
*c = append(*c, ccInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YandexCreditCard) Name() string {
|
||||
return "creditcard"
|
||||
}
|
||||
|
||||
func (c *YandexCreditCard) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
_ "modernc.org/sqlite" // import sqlite3 driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumDownload, func() extractor.Extractor {
|
||||
return new(ChromiumDownload)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxDownload, func() extractor.Extractor {
|
||||
return new(FirefoxDownload)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumDownload []download
|
||||
|
||||
type download struct {
|
||||
TargetPath string
|
||||
URL string
|
||||
TotalBytes int64
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
MimeType string
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start_time, end_time, mime_type FROM downloads`
|
||||
)
|
||||
|
||||
func (c *ChromiumDownload) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumDownload.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumDownload.TempFilename())
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryChromiumDownload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
targetPath, tabURL, mimeType string
|
||||
totalBytes, startTime, endTime int64
|
||||
)
|
||||
if err := rows.Scan(&targetPath, &tabURL, &totalBytes, &startTime, &endTime, &mimeType); err != nil {
|
||||
slog.Warn("scan chromium download error", "err", err)
|
||||
}
|
||||
data := download{
|
||||
TargetPath: targetPath,
|
||||
URL: tabURL,
|
||||
TotalBytes: totalBytes,
|
||||
StartTime: typeutil.TimeEpoch(startTime),
|
||||
EndTime: typeutil.TimeEpoch(endTime),
|
||||
MimeType: mimeType,
|
||||
}
|
||||
*c = append(*c, data)
|
||||
}
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].TotalBytes > (*c)[j].TotalBytes
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumDownload) Name() string {
|
||||
return "download"
|
||||
}
|
||||
|
||||
func (c *ChromiumDownload) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxDownload []download
|
||||
|
||||
const (
|
||||
queryFirefoxDownload = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxDownload) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxDownload.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxDownload.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
slog.Error("close journal mode error", "err", err)
|
||||
}
|
||||
rows, err := db.Query(queryFirefoxDownload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
content, url string
|
||||
placeID, dateAdded int64
|
||||
)
|
||||
if err = rows.Scan(&placeID, &content, &url, &dateAdded); err != nil {
|
||||
slog.Warn("scan firefox download error", "err", err)
|
||||
}
|
||||
contentList := strings.Split(content, ",{")
|
||||
if len(contentList) > 1 {
|
||||
path := contentList[0]
|
||||
json := "{" + contentList[1]
|
||||
endTime := gjson.Get(json, "endTime")
|
||||
fileSize := gjson.Get(json, "fileSize")
|
||||
*f = append(*f, download{
|
||||
TargetPath: path,
|
||||
URL: url,
|
||||
TotalBytes: fileSize.Int(),
|
||||
StartTime: typeutil.TimeStamp(dateAdded / 1000000),
|
||||
EndTime: typeutil.TimeStamp(endTime.Int() / 1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].TotalBytes < (*f)[j].TotalBytes
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxDownload) Name() string {
|
||||
return "download"
|
||||
}
|
||||
|
||||
func (f *FirefoxDownload) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumExtension, func() extractor.Extractor {
|
||||
return new(ChromiumExtension)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxExtension, func() extractor.Extractor {
|
||||
return new(FirefoxExtension)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumExtension []*extension
|
||||
|
||||
type extension struct {
|
||||
ID string
|
||||
URL string
|
||||
Enabled bool
|
||||
Name string
|
||||
Description string
|
||||
Version string
|
||||
HomepageURL string
|
||||
}
|
||||
|
||||
func (c *ChromiumExtension) Extract(_ []byte) error {
|
||||
extensionFile, err := fileutil.ReadFile(types.ChromiumExtension.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumExtension.TempFilename())
|
||||
|
||||
result, err := parseChromiumExtensions(extensionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = result
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseChromiumExtensions(content string) ([]*extension, error) {
|
||||
settingKeys := []string{
|
||||
"settings.extensions",
|
||||
"settings.settings",
|
||||
"extensions.settings",
|
||||
}
|
||||
var settings gjson.Result
|
||||
for _, key := range settingKeys {
|
||||
settings = gjson.Parse(content).Get(key)
|
||||
if settings.Exists() {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !settings.Exists() {
|
||||
return nil, fmt.Errorf("cannot find extensions in settings")
|
||||
}
|
||||
var c []*extension
|
||||
|
||||
settings.ForEach(func(id, ext gjson.Result) bool {
|
||||
location := ext.Get("location")
|
||||
if !location.Exists() {
|
||||
return true
|
||||
}
|
||||
switch location.Int() {
|
||||
case 5, 10: // https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom
|
||||
return true
|
||||
}
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/disable_reason.h
|
||||
enabled := !ext.Get("disable_reasons").Exists()
|
||||
b := ext.Get("manifest")
|
||||
if !b.Exists() {
|
||||
c = append(c, &extension{
|
||||
ID: id.String(),
|
||||
Enabled: enabled,
|
||||
Name: ext.Get("path").String(),
|
||||
})
|
||||
return true
|
||||
}
|
||||
c = append(c, &extension{
|
||||
ID: id.String(),
|
||||
URL: getChromiumExtURL(id.String(), b.Get("update_url").String()),
|
||||
Enabled: enabled,
|
||||
Name: b.Get("name").String(),
|
||||
Description: b.Get("description").String(),
|
||||
Version: b.Get("version").String(),
|
||||
HomepageURL: b.Get("homepage_url").String(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getChromiumExtURL(id, updateURL string) string {
|
||||
if strings.HasSuffix(updateURL, "clients2.google.com/service/update2/crx") {
|
||||
return "https://chrome.google.com/webstore/detail/" + id
|
||||
} else if strings.HasSuffix(updateURL, "edge.microsoft.com/extensionwebstorebase/v1/crx") {
|
||||
return "https://microsoftedge.microsoft.com/addons/detail/" + id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *ChromiumExtension) Name() string {
|
||||
return "extension"
|
||||
}
|
||||
|
||||
func (c *ChromiumExtension) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxExtension []*extension
|
||||
|
||||
var lang = language.Und
|
||||
|
||||
func (f *FirefoxExtension) Extract(_ []byte) error {
|
||||
s, err := fileutil.ReadFile(types.FirefoxExtension.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(types.FirefoxExtension.TempFilename())
|
||||
j := gjson.Parse(s)
|
||||
for _, v := range j.Get("addons").Array() {
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIDatabase.jsm#157
|
||||
if v.Get("location").String() != "app-profile" {
|
||||
continue
|
||||
}
|
||||
|
||||
if lang != language.Und {
|
||||
locale := findFirefoxLocale(v.Get("locales").Array(), lang)
|
||||
*f = append(*f, &extension{
|
||||
ID: v.Get("id").String(),
|
||||
Enabled: v.Get("active").Bool(),
|
||||
Name: locale.Get("name").String(),
|
||||
Description: locale.Get("description").String(),
|
||||
Version: v.Get("version").String(),
|
||||
HomepageURL: locale.Get("homepageURL").String(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
*f = append(*f, &extension{
|
||||
ID: v.Get("id").String(),
|
||||
Enabled: v.Get("active").Bool(),
|
||||
Name: v.Get("defaultLocale.name").String(),
|
||||
Description: v.Get("defaultLocale.description").String(),
|
||||
Version: v.Get("version").String(),
|
||||
HomepageURL: v.Get("defaultLocale.homepageURL").String(),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) gjson.Result {
|
||||
tags := make([]language.Tag, 0, len(locales))
|
||||
indices := make([]int, 0, len(locales))
|
||||
for i, locale := range locales {
|
||||
for _, tagStr := range locale.Get("locales").Array() {
|
||||
tag, _ := language.Parse(tagStr.String())
|
||||
if tag == language.Und {
|
||||
continue
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
indices = append(indices, i)
|
||||
}
|
||||
}
|
||||
_, tagIndex, _ := language.NewMatcher(tags).Match(targetLang)
|
||||
return locales[indices[tagIndex]]
|
||||
}
|
||||
|
||||
func (f *FirefoxExtension) Name() string {
|
||||
return "extension"
|
||||
}
|
||||
|
||||
func (f *FirefoxExtension) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
// import sqlite3 driver
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumHistory, func() extractor.Extractor {
|
||||
return new(ChromiumHistory)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxHistory, func() extractor.Extractor {
|
||||
return new(FirefoxHistory)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumHistory []history
|
||||
|
||||
type history struct {
|
||||
Title string
|
||||
URL string
|
||||
VisitCount int
|
||||
LastVisitTime time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumHistory = `SELECT url, title, visit_count, last_visit_time FROM urls`
|
||||
)
|
||||
|
||||
func (c *ChromiumHistory) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumHistory.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumHistory.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryChromiumHistory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
url, title string
|
||||
visitCount int
|
||||
lastVisitTime int64
|
||||
)
|
||||
if err := rows.Scan(&url, &title, &visitCount, &lastVisitTime); err != nil {
|
||||
slog.Warn("scan chromium history error", "err", err)
|
||||
}
|
||||
data := history{
|
||||
URL: url,
|
||||
Title: title,
|
||||
VisitCount: visitCount,
|
||||
LastVisitTime: typeutil.TimeEpoch(lastVisitTime),
|
||||
}
|
||||
*c = append(*c, data)
|
||||
}
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].VisitCount > (*c)[j].VisitCount
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumHistory) Name() string {
|
||||
return "history"
|
||||
}
|
||||
|
||||
func (c *ChromiumHistory) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxHistory []history
|
||||
|
||||
const (
|
||||
queryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COALESCE(title, ''), visit_count FROM moz_places`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxHistory) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxHistory.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxHistory.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryFirefoxHistory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, visitDate int64
|
||||
url, title string
|
||||
visitCount int
|
||||
)
|
||||
if err = rows.Scan(&id, &url, &visitDate, &title, &visitCount); err != nil {
|
||||
slog.Error("scan firefox history error", "err", err)
|
||||
}
|
||||
*f = append(*f, history{
|
||||
Title: title,
|
||||
URL: url,
|
||||
VisitCount: visitCount,
|
||||
LastVisitTime: typeutil.TimeStamp(visitDate / 1000000),
|
||||
})
|
||||
}
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].VisitCount < (*f)[j].VisitCount
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxHistory) Name() string {
|
||||
return "history"
|
||||
}
|
||||
|
||||
func (f *FirefoxHistory) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Package browserdata is responsible for initializing all the necessary
|
||||
// components that handle different types of browser data extraction.
|
||||
// This file, imports.go, is specifically used to import various data
|
||||
// handler packages to ensure their initialization logic is executed.
|
||||
// These imports are crucial as they trigger the `init()` functions
|
||||
// within each package, which typically handle registration of their
|
||||
// specific data handlers to a central registry.
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/bookmark"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/cookie"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/creditcard"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/download"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/extension"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/history"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/localstorage"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/password"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/sessionstorage"
|
||||
)
|
||||
@@ -0,0 +1,167 @@
|
||||
package localstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/byteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumLocalStorage, func() extractor.Extractor {
|
||||
return new(ChromiumLocalStorage)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxLocalStorage, func() extractor.Extractor {
|
||||
return new(FirefoxLocalStorage)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumLocalStorage []storage
|
||||
|
||||
type storage struct {
|
||||
IsMeta bool
|
||||
URL string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
const maxLocalStorageValueLength = 1024 * 2
|
||||
|
||||
func (c *ChromiumLocalStorage) Extract(_ []byte) error {
|
||||
db, err := leveldb.OpenFile(types.ChromiumLocalStorage.TempFilename(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(types.ChromiumLocalStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
s := new(storage)
|
||||
s.fillKey(key)
|
||||
// don't all value upper than 2KB
|
||||
if len(value) < maxLocalStorageValueLength {
|
||||
s.fillValue(value)
|
||||
} else {
|
||||
s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
|
||||
}
|
||||
if s.IsMeta {
|
||||
s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
|
||||
}
|
||||
*c = append(*c, *s)
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ChromiumLocalStorage) Name() string {
|
||||
return "localStorage"
|
||||
}
|
||||
|
||||
func (c *ChromiumLocalStorage) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
func (s *storage) fillKey(b []byte) {
|
||||
keys := bytes.Split(b, []byte("\x00"))
|
||||
if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
|
||||
s.IsMeta = true
|
||||
s.fillMetaHeader(keys[0])
|
||||
}
|
||||
if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
|
||||
s.fillHeader(keys[0], keys[1])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *storage) fillMetaHeader(b []byte) {
|
||||
s.URL = string(bytes.Trim(b, "META:"))
|
||||
}
|
||||
|
||||
func (s *storage) fillHeader(url, key []byte) {
|
||||
s.URL = string(bytes.Trim(url, "_"))
|
||||
s.Key = string(bytes.Trim(key, "\x01"))
|
||||
}
|
||||
|
||||
func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
|
||||
r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// fillValue fills value of the storage
|
||||
// TODO: support unicode charter
|
||||
func (s *storage) fillValue(b []byte) {
|
||||
value := bytes.Map(byteutil.OnSplitUTF8Func, b)
|
||||
s.Value = string(value)
|
||||
}
|
||||
|
||||
type FirefoxLocalStorage []storage
|
||||
|
||||
const (
|
||||
queryLocalStorage = `SELECT originKey, key, value FROM webappsstore2`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxLocalStorage) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxLocalStorage.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxLocalStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
slog.Error("close journal mode error", "err", err)
|
||||
}
|
||||
rows, err := db.Query(queryLocalStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var originKey, key, value string
|
||||
if err = rows.Scan(&originKey, &key, &value); err != nil {
|
||||
slog.Error("scan firefox local storage error", "err", err)
|
||||
}
|
||||
s := new(storage)
|
||||
s.fillFirefox(originKey, key, value)
|
||||
*f = append(*f, *s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *storage) fillFirefox(originKey, key, value string) {
|
||||
// originKey = moc.buhtig.:https:443
|
||||
p := strings.Split(originKey, ":")
|
||||
h := typeutil.Reverse([]byte(p[0]))
|
||||
if bytes.HasPrefix(h, []byte(".")) {
|
||||
h = h[1:]
|
||||
}
|
||||
if len(p) == 3 {
|
||||
s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
|
||||
}
|
||||
s.Key = key
|
||||
s.Value = value
|
||||
}
|
||||
|
||||
func (f *FirefoxLocalStorage) Name() string {
|
||||
return "localStorage"
|
||||
}
|
||||
|
||||
func (f *FirefoxLocalStorage) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package localstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
var testCases = []struct {
|
||||
in []byte
|
||||
wanted []byte
|
||||
actual []byte
|
||||
}{
|
||||
{
|
||||
in: []byte{0x0, 0x7b, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x68, 0x0, 0x74, 0x0, 0x74, 0x0, 0x70, 0x0, 0x73, 0x0, 0x3a, 0x0, 0x2f, 0x0, 0x2f, 0x0, 0x77, 0x0, 0x77, 0x0, 0x77, 0x0, 0x2e, 0x0, 0x76, 0x0, 0x6f, 0x0, 0x6c, 0x0, 0x63, 0x0, 0x65, 0x0, 0x6e, 0x0, 0x67, 0x0, 0x69, 0x0, 0x6e, 0x0, 0x65, 0x0, 0x2e, 0x0, 0x63, 0x0, 0x6f, 0x0, 0x6d, 0x0, 0x2f, 0x0, 0x70, 0x0, 0x72, 0x0, 0x6f, 0x0, 0x64, 0x0, 0x75, 0x0, 0x63, 0x0, 0x74, 0x0, 0x73, 0x0, 0x2f, 0x0, 0x66, 0x0, 0x65, 0x0, 0x69, 0x0, 0x6c, 0x0, 0x69, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x74, 0x0, 0x69, 0x0, 0x74, 0x0, 0x6c, 0x0, 0x65, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0xde, 0x98, 0xde, 0x8f, 0x2d, 0x0, 0x6b, 0x70, 0x71, 0x5c, 0x15, 0x5f, 0xce, 0x64, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6d, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x75, 0x0, 0x61, 0x0, 0x6c, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x22, 0x0, 0x7d, 0x0},
|
||||
wanted: []byte(`{"refer_key":"https://www.volcengine.com/product/feilian","refer_title":"飞连_SSO单点登录_VPN_终端安全合规_便捷Wifi认证-火山引擎","refer_manual_key":""}`),
|
||||
actual: []byte{0x7b, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x6f, 0x6c, 0x63, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x73, 0x2f, 0x66, 0x65, 0x69, 0x6c, 0x69, 0x61, 0x6e, 0x22, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3a, 0x22, 0xc3, 0x9e, 0xe9, 0xa3, 0x9e, 0xe8, 0xbc, 0xad, 0x6b, 0xe7, 0x81, 0xb1, 0xe5, 0xb0, 0x95, 0xe5, 0xbf, 0x8e, 0xe6, 0x90, 0xa2, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x22, 0x7d, 0xef, 0xbf, 0xbd},
|
||||
},
|
||||
}
|
||||
|
||||
func TestLocalStorageKeyToUTF8(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range testCases {
|
||||
actual, err := convertUTF16toUTF8(tc.in, unicode.BigEndian)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// TODO: fix this, value from local storage if contains chinese characters, need convert utf16 to utf8
|
||||
// but now, it can't convert, so just skip it.
|
||||
assert.Equal(t, tc.actual, actual, "chinese characters can't actual convert")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
)
|
||||
|
||||
type outPutter struct {
|
||||
json bool
|
||||
csv bool
|
||||
}
|
||||
|
||||
func newOutPutter(flag string) *outPutter {
|
||||
o := &outPutter{}
|
||||
if flag == "json" {
|
||||
o.json = true
|
||||
} else {
|
||||
o.csv = true
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *outPutter) Write(data extractor.Extractor, writer io.Writer) error {
|
||||
switch o.json {
|
||||
case true:
|
||||
encoder := json.NewEncoder(writer)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.SetEscapeHTML(false)
|
||||
return encoder.Encode(data)
|
||||
default:
|
||||
gocsv.SetCSVWriter(func(w io.Writer) *gocsv.SafeCSVWriter {
|
||||
writer := csv.NewWriter(transform.NewWriter(w, unicode.UTF8BOM.NewEncoder()))
|
||||
writer.Comma = ','
|
||||
return gocsv.NewSafeCSVWriter(writer)
|
||||
})
|
||||
return gocsv.Marshal(data, writer)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *outPutter) CreateFile(dir, filename string) (*os.File, error) {
|
||||
if filename == "" {
|
||||
return nil, errors.New("empty filename")
|
||||
}
|
||||
|
||||
if dir != "" {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err := os.MkdirAll(dir, 0o750)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
var err error
|
||||
p := filepath.Join(dir, filename)
|
||||
file, err = os.OpenFile(filepath.Clean(p), os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (o *outPutter) Ext() string {
|
||||
if o.json {
|
||||
return "json"
|
||||
}
|
||||
return "csv"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewOutPutter(t *testing.T) {
|
||||
t.Parallel()
|
||||
out := newOutPutter("json")
|
||||
if out == nil {
|
||||
t.Error("New() returned nil")
|
||||
}
|
||||
f, err := out.CreateFile("results", "test.json")
|
||||
if err != nil {
|
||||
t.Error("CreateFile() returned an error", err)
|
||||
}
|
||||
defer os.RemoveAll("results")
|
||||
err = out.Write(nil, f)
|
||||
if err != nil {
|
||||
t.Error("Write() returned an error", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
_ "modernc.org/sqlite" // import sqlite3 driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumPassword, func() extractor.Extractor {
|
||||
return new(ChromiumPassword)
|
||||
})
|
||||
extractor.RegisterExtractor(types.YandexPassword, func() extractor.Extractor {
|
||||
return new(YandexPassword)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxPassword, func() extractor.Extractor {
|
||||
return new(FirefoxPassword)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumPassword []loginData
|
||||
|
||||
type loginData struct {
|
||||
UserName string
|
||||
encryptPass []byte
|
||||
encryptUser []byte
|
||||
Password string
|
||||
LoginURL string
|
||||
CreateDate time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins`
|
||||
)
|
||||
|
||||
func (c *ChromiumPassword) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumPassword.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumPassword.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryChromiumLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
url, username string
|
||||
pwd, password []byte
|
||||
create int64
|
||||
)
|
||||
if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
|
||||
slog.Error("scan chromium password error", "err", err)
|
||||
}
|
||||
login := loginData{
|
||||
UserName: username,
|
||||
encryptPass: pwd,
|
||||
LoginURL: url,
|
||||
}
|
||||
if len(pwd) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
password, err = crypto.DecryptWithDPAPI(pwd)
|
||||
} else {
|
||||
password, err = crypto.DecryptWithChromium(masterKey, pwd)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("decrypt chromium password error", "err", err)
|
||||
}
|
||||
}
|
||||
if create > time.Now().Unix() {
|
||||
login.CreateDate = typeutil.TimeEpoch(create)
|
||||
} else {
|
||||
login.CreateDate = typeutil.TimeStamp(create)
|
||||
}
|
||||
login.Password = string(password)
|
||||
*c = append(*c, login)
|
||||
}
|
||||
// sort with create date
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumPassword) Name() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (c *ChromiumPassword) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type YandexPassword []loginData
|
||||
|
||||
const (
|
||||
queryYandexLogin = `SELECT action_url, username_value, password_value, date_created FROM logins`
|
||||
)
|
||||
|
||||
func (c *YandexPassword) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.YandexPassword.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.YandexPassword.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryYandexLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
url, username string
|
||||
pwd, password []byte
|
||||
create int64
|
||||
)
|
||||
if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
|
||||
slog.Error("scan yandex password error", "err", err)
|
||||
}
|
||||
login := loginData{
|
||||
UserName: username,
|
||||
encryptPass: pwd,
|
||||
LoginURL: url,
|
||||
}
|
||||
|
||||
if len(pwd) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
password, err = crypto.DecryptWithDPAPI(pwd)
|
||||
} else {
|
||||
password, err = crypto.DecryptWithChromium(masterKey, pwd)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("decrypt yandex password error", "err", err)
|
||||
}
|
||||
}
|
||||
if create > time.Now().Unix() {
|
||||
login.CreateDate = typeutil.TimeEpoch(create)
|
||||
} else {
|
||||
login.CreateDate = typeutil.TimeStamp(create)
|
||||
}
|
||||
login.Password = string(password)
|
||||
*c = append(*c, login)
|
||||
}
|
||||
// sort with create date
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YandexPassword) Name() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (c *YandexPassword) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxPassword []loginData
|
||||
|
||||
func (f *FirefoxPassword) Extract(globalSalt []byte) error {
|
||||
logins, err := getFirefoxLoginData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range logins {
|
||||
userPBE, err := crypto.NewASN1PBE(v.encryptUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pwdPBE, err := crypto.NewASN1PBE(v.encryptPass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := userPBE.Decrypt(globalSalt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pwd, err := pwdPBE.Decrypt(globalSalt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = append(*f, loginData{
|
||||
LoginURL: v.LoginURL,
|
||||
UserName: string(user),
|
||||
Password: string(pwd),
|
||||
CreateDate: v.CreateDate,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].CreateDate.After((*f)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFirefoxLoginData() ([]loginData, error) {
|
||||
s, err := os.ReadFile(types.FirefoxPassword.TempFilename())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(types.FirefoxPassword.TempFilename())
|
||||
loginsJSON := gjson.GetBytes(s, "logins")
|
||||
var logins []loginData
|
||||
if loginsJSON.Exists() {
|
||||
for _, v := range loginsJSON.Array() {
|
||||
var (
|
||||
m loginData
|
||||
user []byte
|
||||
pass []byte
|
||||
)
|
||||
m.LoginURL = v.Get("formSubmitURL").String()
|
||||
user, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.encryptUser = user
|
||||
m.encryptPass = pass
|
||||
m.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000)
|
||||
logins = append(logins, m)
|
||||
}
|
||||
}
|
||||
return logins, nil
|
||||
}
|
||||
|
||||
func (f *FirefoxPassword) Name() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (f *FirefoxPassword) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package sessionstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/byteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumSessionStorage, func() extractor.Extractor {
|
||||
return new(ChromiumSessionStorage)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxSessionStorage, func() extractor.Extractor {
|
||||
return new(FirefoxSessionStorage)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumSessionStorage []session
|
||||
|
||||
type session struct {
|
||||
IsMeta bool
|
||||
URL string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
const maxLocalStorageValueLength = 1024 * 2
|
||||
|
||||
func (c *ChromiumSessionStorage) Extract(_ []byte) error {
|
||||
db, err := leveldb.OpenFile(types.ChromiumSessionStorage.TempFilename(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(types.ChromiumSessionStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
s := new(session)
|
||||
s.fillKey(key)
|
||||
// don't all value upper than 2KB
|
||||
if len(value) < maxLocalStorageValueLength {
|
||||
s.fillValue(value)
|
||||
} else {
|
||||
s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
|
||||
}
|
||||
if s.IsMeta {
|
||||
s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
|
||||
}
|
||||
*c = append(*c, *s)
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ChromiumSessionStorage) Name() string {
|
||||
return "sessionStorage"
|
||||
}
|
||||
|
||||
func (c *ChromiumSessionStorage) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
func (s *session) fillKey(b []byte) {
|
||||
keys := bytes.Split(b, []byte("-"))
|
||||
if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
|
||||
s.IsMeta = true
|
||||
s.fillMetaHeader(keys[0])
|
||||
}
|
||||
if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
|
||||
s.fillHeader(keys[0], keys[1])
|
||||
}
|
||||
if len(keys) == 3 {
|
||||
if string(keys[0]) == "map" {
|
||||
s.Key = string(keys[2])
|
||||
} else if string(keys[0]) == "namespace" {
|
||||
s.URL = string(keys[2])
|
||||
s.Key = string(keys[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) fillMetaHeader(b []byte) {
|
||||
s.URL = string(bytes.Trim(b, "META:"))
|
||||
}
|
||||
|
||||
func (s *session) fillHeader(url, key []byte) {
|
||||
s.URL = string(bytes.Trim(url, "_"))
|
||||
s.Key = string(bytes.Trim(key, "\x01"))
|
||||
}
|
||||
|
||||
func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
|
||||
r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// fillValue fills value of the storage
|
||||
// TODO: support unicode charter
|
||||
func (s *session) fillValue(b []byte) {
|
||||
value := bytes.Map(byteutil.OnSplitUTF8Func, b)
|
||||
s.Value = string(value)
|
||||
}
|
||||
|
||||
type FirefoxSessionStorage []session
|
||||
|
||||
const (
|
||||
querySessionStorage = `SELECT originKey, key, value FROM webappsstore2`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxSessionStorage) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxSessionStorage.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxSessionStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
slog.Error("close journal mode error", "err", err)
|
||||
}
|
||||
rows, err := db.Query(querySessionStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var originKey, key, value string
|
||||
if err = rows.Scan(&originKey, &key, &value); err != nil {
|
||||
slog.Error("scan session storage error", "err", err)
|
||||
}
|
||||
s := new(session)
|
||||
s.fillFirefox(originKey, key, value)
|
||||
*f = append(*f, *s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) fillFirefox(originKey, key, value string) {
|
||||
// originKey = moc.buhtig.:https:443
|
||||
p := strings.Split(originKey, ":")
|
||||
h := typeutil.Reverse([]byte(p[0]))
|
||||
if bytes.HasPrefix(h, []byte(".")) {
|
||||
h = h[1:]
|
||||
}
|
||||
if len(p) == 3 {
|
||||
s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
|
||||
}
|
||||
s.Key = key
|
||||
s.Value = value
|
||||
}
|
||||
|
||||
func (f *FirefoxSessionStorage) Name() string {
|
||||
return "sessionStorage"
|
||||
}
|
||||
|
||||
func (f *FirefoxSessionStorage) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
Reference in New Issue
Block a user