123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- package http_proxy
- import (
- "bufio"
- "bytes"
- "context"
- "crypto/tls"
- "crypto/x509"
- "fmt"
- "io/ioutil"
- "net"
- "net/http"
- "net/url"
- "path/filepath"
- "strconv"
- "strings"
- "time"
- "github.com/bettercap/bettercap/firewall"
- "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/log"
- "github.com/evilsocket/islazy/str"
- "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
- Blacklist []string
- Whitelist []string
- Sess *session.Session
- Stripper *SSLStripper
- jsHook string
- isTLS bool
- isRunning bool
- doRedirect bool
- sniListener net.Listener
- tag string
- }
- func stripPort(s string) string {
- ix := strings.IndexRune(s, ':')
- if ix == -1 {
- return s
- }
- return s[:ix]
- }
- type dummyLogger struct {
- p *HTTPProxy
- }
- 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, tag string) *HTTPProxy {
- p := &HTTPProxy{
- Name: "http.proxy",
- Proxy: goproxy.NewProxyHttpServer(),
- Sess: s,
- Stripper: NewSSLStripper(s, false),
- isTLS: false,
- doRedirect: true,
- Server: nil,
- Blacklist: make([]string, 0),
- Whitelist: make([]string, 0),
- tag: session.AsTag(tag),
- }
- p.Proxy.Verbose = false
- p.Proxy.Logger = dummyLogger{p}
- 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) Debug(format string, args ...interface{}) {
- 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...)
- }
- func (p *HTTPProxy) Warning(format string, args ...interface{}) {
- 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...)
- }
- func (p *HTTPProxy) Fatal(format string, args ...interface{}) {
- p.Sess.Events.Log(log.FATAL, p.tag+format, args...)
- }
- func (p *HTTPProxy) doProxy(req *http.Request) bool {
- if req.Host == "" {
- p.Error("got request with empty host: %v", req)
- return false
- }
- hostname := strings.Split(req.Host, ":")[0]
- for _, local := range []string{"localhost", "127.0.0.1"} {
- if hostname == local {
- p.Error("got request with localed host: %s", req.Host)
- return false
- }
- }
- return true
- }
- func (p *HTTPProxy) shouldProxy(req *http.Request) bool {
- hostname := strings.Split(req.Host, ":")[0]
- // check for the whitelist
- for _, expr := range p.Whitelist {
- if matched, err := filepath.Match(expr, hostname); err != nil {
- p.Error("error while using proxy whitelist expression '%s': %v", expr, err)
- } else if matched {
- p.Debug("hostname '%s' matched whitelisted element '%s'", hostname, expr)
- return true
- }
- }
- // then the blacklist
- for _, expr := range p.Blacklist {
- if matched, err := filepath.Match(expr, hostname); err != nil {
- p.Error("error while using proxy blacklist expression '%s': %v", expr, err)
- } else if matched {
- p.Debug("hostname '%s' matched blacklisted element '%s'", hostname, expr)
- return false
- }
- }
- return true
- }
- func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string,
- jsToInject string, stripSSL bool) error {
- var err error
- // 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)
- p.Address = address
- p.doRedirect = doRedirect
- p.jsHook = ""
- 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 {
- p.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.doRedirect {
- if !p.Sess.Firewall.IsForwardingEnabled() {
- p.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
- }
- p.Debug("applied redirection %s", p.Redirection.String())
- } else {
- p.Warning("port redirection disabled, the proxy must be set manually to work")
- }
- p.Sess.UnkCmdCallback = func(cmd string) bool {
- if p.Script != nil {
- return p.Script.OnCommand(cmd)
- }
- return false
- }
- return nil
- }
- func (p *HTTPProxy) 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 {
- p.Info("creating spoofed certificate for %s:%d", tui.Yellow(hostname), port)
- cert, err = btls.SignCertificateForHost(ca, hostname, port)
- if err != nil {
- p.Warning("cannot sign host certificate with provided CA: %s", err)
- return nil, err
- }
- setCachedCert(hostname, port, cert)
- } else {
- p.Debug("serving spoofed certificate for %s:%d", tui.Yellow(hostname), port)
- }
- config := tls.Config{
- InsecureSkipVerify: true,
- Certificates: []tls.Certificate{*cert},
- }
- return &config, nil
- }
- }
- 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 {
- 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: p.TLSConfigFromCA(&ourCa)}
- goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: p.TLSConfigFromCA(&ourCa)}
- goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: p.TLSConfigFromCA(&ourCa)}
- goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: p.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 {
- p.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 {
- p.Warning("error reading SNI: %s.", err)
- return
- }
- hostname := tlsConn.Host()
- if hostname == "" {
- p.Warning("client does not support SNI.")
- return
- }
- p.Debug("proxying connection from %s to %s", 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),
- RemoteAddr: c.RemoteAddr().String(),
- }
- 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")
- }
- p.Info("started on %s (sslstrip %s)", p.Server.Addr, strip)
- if p.isTLS {
- err = p.httpsWorker()
- } else {
- err = p.httpWorker()
- }
- if err != nil && err.Error() != "http: Server closed" {
- p.Fatal("%s", err)
- }
- }()
- }
- 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 {
- 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)
- }
- }
|