diff --git a/build.sh b/build.sh index d90ad51f..5d4dd93a 100755 --- a/build.sh +++ b/build.sh @@ -187,7 +187,7 @@ build_macos_amd64 && create_archive bettercap_macos_amd64_$VERSION.zip build_android_arm && create_archive bettercap_android_arm_$VERSION.zip build_windows_amd64 && create_exe_archive bettercap_windows_amd64_$VERSION.zip build_linux_arm7_static && create_archive bettercap_linux_arm7_$VERSION.zip -build_linux_arm7hf_static && create_archive bettercap_linux_arm7hf_$VERSION.zip +# build_linux_arm7hf_static && create_archive bettercap_linux_arm7hf_$VERSION.zip build_linux_mips_static && create_archive bettercap_linux_mips_$VERSION.zip build_linux_mipsle_static && create_archive bettercap_linux_mipsle_$VERSION.zip build_linux_mips64_static && create_archive bettercap_linux_mips64_$VERSION.zip diff --git a/core/banner.go b/core/banner.go index 791ad06f..22d52c78 100644 --- a/core/banner.go +++ b/core/banner.go @@ -2,7 +2,7 @@ package core const ( Name = "bettercap" - Version = "2.1" + Version = "2.2" Author = "Simone 'evilsocket' Margaritelli" Website = "https://bettercap.org/" ) diff --git a/core/options.go b/core/options.go index 261bf172..c26d9236 100644 --- a/core/options.go +++ b/core/options.go @@ -19,7 +19,7 @@ type Options struct { func ParseOptions() (Options, error) { o := Options{ InterfaceName: flag.String("iface", "", "Network interface to bind to, if empty the default interface will be auto selected."), - AutoStart: flag.String("autostart", "events.stream, net.recon", "Comma separated list of modules to auto start."), + AutoStart: flag.String("autostart", "events.stream, net.recon, update.check", "Comma separated list of modules to auto start."), Caplet: flag.String("caplet", "", "Read commands from this file and execute them in the interactive session."), Debug: flag.Bool("debug", false, "Print debug messages."), Silent: flag.Bool("silent", false, "Suppress all logs which are not errors."), diff --git a/core/swag.go b/core/swag.go index 95d71e06..1de2733c 100644 --- a/core/swag.go +++ b/core/swag.go @@ -5,6 +5,15 @@ import ( "os" ) +const ( + DEBUG = iota + INFO + IMPORTANT + WARNING + ERROR + FATAL +) + // https://misc.flogisoft.com/bash/tip_colors_and_formatting var ( BOLD = "\033[1m" @@ -26,6 +35,24 @@ var ( RESET = "\033[0m" + LogLabels = map[int]string{ + DEBUG: "dbg", + INFO: "inf", + IMPORTANT: "imp", + WARNING: "war", + ERROR: "err", + FATAL: "!!!", + } + + LogColors = map[int]string{ + DEBUG: DIM + FG_BLACK + BG_DGRAY, + INFO: FG_WHITE + BG_GREEN, + IMPORTANT: FG_WHITE + BG_LBLUE, + WARNING: FG_WHITE + BG_YELLOW, + ERROR: FG_WHITE + BG_RED, + FATAL: FG_WHITE + BG_RED + BOLD, + } + HasColors = true ) @@ -51,38 +78,20 @@ func InitSwag(disableColors bool) { BG_YELLOW = "" BG_LBLUE = "" RESET = "" + + LogColors = map[int]string{ + DEBUG: "", + INFO: "", + IMPORTANT: "", + WARNING: "", + ERROR: "", + FATAL: "", + } + HasColors = false } } -const ( - DEBUG = iota - INFO - IMPORTANT - WARNING - ERROR - FATAL -) - -var ( - LogLabels = map[int]string{ - DEBUG: "dbg", - INFO: "inf", - IMPORTANT: "imp", - WARNING: "war", - ERROR: "err", - FATAL: "!!!", - } - LogColors = map[int]string{ - DEBUG: DIM + FG_BLACK + BG_DGRAY, - INFO: FG_WHITE + BG_GREEN, - IMPORTANT: FG_WHITE + BG_LBLUE, - WARNING: FG_WHITE + BG_YELLOW, - ERROR: FG_WHITE + BG_RED, - FATAL: FG_WHITE + BG_RED + BOLD, - } -) - // W for Wrap func W(e, s string) string { return e + s + RESET diff --git a/firewall/firewall_windows.go b/firewall/firewall_windows.go index 8623f551..753a2b39 100644 --- a/firewall/firewall_windows.go +++ b/firewall/firewall_windows.go @@ -35,26 +35,14 @@ func (f WindowsFirewall) IsForwardingEnabled() bool { } } -func (f WindowsFirewall) isSuccess(output string) bool { - if trimmed := core.Trim(strings.ToUpper(output)); trimmed == "" || strings.Contains(trimmed, "OK") == true { - return true - } else { - return false - } -} - func (f WindowsFirewall) EnableForwarding(enabled bool) error { v := "enabled" if enabled == false { v = "disabled" } - out, err := core.Exec("netsh", []string{"interface", "ipv4", "set", "interface", fmt.Sprintf("%d", f.iface.Index), fmt.Sprintf("forwarding=\"%s\"", v)}) - if err != nil { - return err - } - if f.isSuccess(out) == false { - return fmt.Errorf("Unexpected netsh output: %s", out) + if _, err := core.Exec("netsh", []string{"interface", "ipv4", "set", "interface", fmt.Sprintf("%d", f.iface.Index), fmt.Sprintf("forwarding=\"%s\"", v)}); err != nil { + return err } return nil @@ -90,15 +78,10 @@ func (f *WindowsFirewall) AllowPort(port int, address string, proto string, allo cmd = []string{"advfirewall", "firewall", "delete", "rule", nameField, protoField, portField} } - out, err := core.Exec("netsh", cmd) - if err != nil { + if _, err := core.Exec("netsh", cmd); err != nil { return err } - if f.isSuccess(out) == false { - return fmt.Errorf("Unexpected netsh output: %s", out) - } - return nil } @@ -116,14 +99,10 @@ func (f *WindowsFirewall) EnableRedirection(r *Redirection, enabled bool) error rule = append([]string{"interface", "portproxy", "delete", "v4tov4"}, rule...) } - out, err := core.Exec("netsh", rule) - if err != nil { + if _, err := core.Exec("netsh", rule); err != nil { return err } - if f.isSuccess(out) == false { - return fmt.Errorf("Unexpected netsh output: %s", out) - } return nil } diff --git a/main.go b/main.go index 9dc1e5a6..cf3cea8e 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ func main() { sess.Register(modules.NewEventsStream(sess)) sess.Register(modules.NewTicker(sess)) + sess.Register(modules.NewUpdateModule(sess)) sess.Register(modules.NewMacChanger(sess)) sess.Register(modules.NewProber(sess)) sess.Register(modules.NewDiscovery(sess)) diff --git a/modules/arp_spoof.go b/modules/arp_spoof.go index 75e2042f..27d644ad 100644 --- a/modules/arp_spoof.go +++ b/modules/arp_spoof.go @@ -153,12 +153,11 @@ func (p *ArpSpoofer) parseTargets(targets string) (err error) { targets = strings.Replace(targets, mac, "", -1) } - targets = strings.TrimLeft(targets, ", ") - targets = strings.TrimRight(targets, ", ") + targets = strings.Trim(targets, ", ") log.Debug("Parsing IP range %s", targets) if len(p.macs) == 0 || targets != "" { - list, err := iprange.Parse(targets) + list, err := iprange.ParseList(targets) if err != nil { return fmt.Errorf("Error while parsing arp.spoof.targets variable '%s': %s.", targets, err) } diff --git a/modules/base_proxy_script.go b/modules/base_proxy_script.go index 4cee92d8..cc35305b 100644 --- a/modules/base_proxy_script.go +++ b/modules/base_proxy_script.go @@ -1,6 +1,7 @@ package modules import ( + "encoding/base64" "io/ioutil" "sync" @@ -109,6 +110,69 @@ func (s *ProxyScript) defineBuiltins() error { return otto.Value{} }) + // log debug + s.VM.Set("log_debug", func(call otto.FunctionCall) otto.Value { + for _, v := range call.ArgumentList { + log.Debug("%s", v.String()) + } + return otto.Value{} + }) + + // log info + s.VM.Set("log_info", func(call otto.FunctionCall) otto.Value { + for _, v := range call.ArgumentList { + log.Info("%s", v.String()) + } + return otto.Value{} + }) + + // log warning + s.VM.Set("log_warn", func(call otto.FunctionCall) otto.Value { + for _, v := range call.ArgumentList { + log.Warning("%s", v.String()) + } + return otto.Value{} + }) + + // log error + s.VM.Set("log_error", func(call otto.FunctionCall) otto.Value { + for _, v := range call.ArgumentList { + log.Error("%s", v.String()) + } + return otto.Value{} + }) + + // log fatal + s.VM.Set("log_fatal", func(call otto.FunctionCall) otto.Value { + for _, v := range call.ArgumentList { + log.Fatal("%s", v.String()) + } + return otto.Value{} + }) + + // javascript btoa function + s.VM.Set("btoa", func(call otto.FunctionCall) otto.Value { + varValue := base64.StdEncoding.EncodeToString([]byte(call.Argument(0).String())) + v, err := s.VM.ToValue(varValue) + if err != nil { + return errOtto("Could not convert to string: %s", varValue) + } + return v + }) + + // javascript atob function + s.VM.Set("atob", func(call otto.FunctionCall) otto.Value { + varValue, err := base64.StdEncoding.DecodeString(call.Argument(0).String()) + if err != nil { + return errOtto("Could not decode string: %s", call.Argument(0).String()) + } + v, err := s.VM.ToValue(string(varValue)) + if err != nil { + return errOtto("Could not convert to string: %s", varValue) + } + return v + }) + // read or write environment variable s.VM.Set("env", func(call otto.FunctionCall) otto.Value { argv := call.ArgumentList diff --git a/modules/events_view.go b/modules/events_view.go index 4ac29e80..967db03f 100644 --- a/modules/events_view.go +++ b/modules/events_view.go @@ -10,6 +10,8 @@ import ( "github.com/bettercap/bettercap/core" "github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/session" + + "github.com/google/go-github/github" ) const eventTimeFormat = "15:04:05" @@ -23,7 +25,6 @@ func (s *EventsStream) viewLogEvent(e session.Event) { } func (s *EventsStream) viewWiFiEvent(e session.Event) { - if strings.HasPrefix(e.Tag, "wifi.ap.") { ap := e.Data.(*network.AccessPoint) vend := "" @@ -167,6 +168,16 @@ func (s *EventsStream) viewSynScanEvent(e session.Event) { core.Bold(se.Address)) } +func (s *EventsStream) viewUpdateEvent(e session.Event) { + update := e.Data.(*github.RepositoryRelease) + + fmt.Fprintf(s.output, "[%s] [%s] An update to version %s is available at %s\n", + e.Time.Format(eventTimeFormat), + core.Bold(core.Yellow(e.Tag)), + core.Bold(*update.TagName), + *update.HTMLURL) +} + func (s *EventsStream) View(e session.Event, refresh bool) { if e.Tag == "sys.log" { s.viewLogEvent(e) @@ -180,8 +191,10 @@ func (s *EventsStream) View(e session.Event, refresh bool) { s.viewModuleEvent(e) } else if strings.HasPrefix(e.Tag, "net.sniff.") { s.viewSnifferEvent(e) - } else if strings.HasPrefix(e.Tag, "syn.scan.") { + } else if e.Tag == "syn.scan" { s.viewSynScanEvent(e) + } else if e.Tag == "update.available" { + s.viewUpdateEvent(e) } else { fmt.Fprintf(s.output, "[%s] [%s] %v\n", e.Time.Format(eventTimeFormat), core.Green(e.Tag), e) } diff --git a/modules/http_proxy_base_cookietracker.go b/modules/http_proxy_base_cookietracker.go index bd82a33b..34369ce4 100644 --- a/modules/http_proxy_base_cookietracker.go +++ b/modules/http_proxy_base_cookietracker.go @@ -39,9 +39,6 @@ func (t *CookieTracker) keyOf(req *http.Request) string { } func (t *CookieTracker) IsClean(req *http.Request) bool { - // t.RLock() - // defer t.RUnlock() - // we only clean GET requests if req.Method != "GET" { return true @@ -53,6 +50,9 @@ func (t *CookieTracker) IsClean(req *http.Request) bool { return true } + t.RLock() + defer t.RUnlock() + // was it already processed? if _, found := t.set[t.keyOf(req)]; found == true { return true @@ -65,8 +65,7 @@ func (t *CookieTracker) IsClean(req *http.Request) bool { func (t *CookieTracker) Track(req *http.Request) { t.Lock() defer t.Unlock() - reqKey := t.keyOf(req) - t.set[reqKey] = true + t.set[t.keyOf(req)] = true } func (t *CookieTracker) Expire(req *http.Request) *http.Response { diff --git a/modules/http_proxy_js_request.go b/modules/http_proxy_js_request.go index f389db29..f87ede18 100644 --- a/modules/http_proxy_js_request.go +++ b/modules/http_proxy_js_request.go @@ -88,6 +88,16 @@ func (j *JSRequest) WasModified() bool { return false } +func (j *JSRequest) Header(name, deflt string) string { + name = strings.ToLower(name) + for _, h := range j.Headers { + if name == strings.ToLower(h.Name) { + return h.Value + } + } + return deflt +} + func (j *JSRequest) ReadBody() string { raw, err := ioutil.ReadAll(j.req.Body) if err != nil { diff --git a/modules/http_proxy_js_response.go b/modules/http_proxy_js_response.go index c393e82e..43034158 100644 --- a/modules/http_proxy_js_response.go +++ b/modules/http_proxy_js_response.go @@ -73,6 +73,17 @@ func (j *JSResponse) WasModified() bool { return false } +func (j *JSResponse) Header(name, deflt string) string { + name = strings.ToLower(name) + for _, header := range strings.Split(j.Headers, "\n") { + parts := strings.SplitN(core.Trim(header), ":", 2) + if len(parts) == 2 && strings.ToLower(parts[0]) == name { + return parts[1] + } + } + return deflt +} + func (j *JSResponse) ToResponse(req *http.Request) (resp *http.Response) { resp = goproxy.NewResponse(req, j.ContentType, j.Status, j.Body) if j.Headers != "" { diff --git a/modules/syn_scan.go b/modules/syn_scan.go index ba25bf7c..89b691c1 100644 --- a/modules/syn_scan.go +++ b/modules/syn_scan.go @@ -38,7 +38,7 @@ func NewSynScanner(s *session.Session) *SynScanner { waitGroup: &sync.WaitGroup{}, } - ss.AddHandler(session.NewModuleHandler("syn.scan IP-RANGE START-PORT END-PORT", "syn.scan ([^\\s]+) (\\d+)([\\s\\d]*)", + ss.AddHandler(session.NewModuleHandler("syn.scan IP-RANGE [START-PORT] [END-PORT]", "syn.scan ([^\\s]+) ?(\\d+)?([\\s\\d]*)?", "Perform a syn port scanning against an IP address within the provided ports range.", func(args []string) error { if ss.Running() == true { @@ -50,20 +50,22 @@ func NewSynScanner(s *session.Session) *SynScanner { return fmt.Errorf("Error while parsing IP range '%s': %s", args[0], err) } - ss.addresses = list.Expand() - ss.startPort = 0 - ss.endPort = 0 - - if ss.startPort, err = strconv.Atoi(core.Trim(args[1])); err != nil { - return fmt.Errorf("Invalid START-PORT: %s", err) - } - - if ss.startPort > 65535 { - ss.startPort = 65535 - } - ss.endPort = ss.startPort - argc := len(args) + ss.addresses = list.Expand() + ss.startPort = 1 + ss.endPort = 65535 + + if argc > 1 && core.Trim(args[1]) != "" { + if ss.startPort, err = strconv.Atoi(core.Trim(args[1])); err != nil { + return fmt.Errorf("Invalid START-PORT: %s", err) + } + + if ss.startPort > 65535 { + ss.startPort = 65535 + } + ss.endPort = ss.startPort + } + if argc > 2 && core.Trim(args[2]) != "" { if ss.endPort, err = strconv.Atoi(core.Trim(args[2])); err != nil { return fmt.Errorf("Invalid END-PORT: %s", err) diff --git a/modules/update.go b/modules/update.go new file mode 100644 index 00000000..0b15232a --- /dev/null +++ b/modules/update.go @@ -0,0 +1,96 @@ +package modules + +import ( + "context" + "math" + "strconv" + "strings" + + "github.com/bettercap/bettercap/core" + "github.com/bettercap/bettercap/log" + "github.com/bettercap/bettercap/session" + + "github.com/google/go-github/github" +) + +type UpdateModule struct { + session.SessionModule + client *github.Client +} + +func NewUpdateModule(s *session.Session) *UpdateModule { + u := &UpdateModule{ + SessionModule: session.NewSessionModule("update", s), + client: github.NewClient(nil), + } + + u.AddHandler(session.NewModuleHandler("update.check on", "", + "Check latest available stable version and compare it with the one being used.", + func(args []string) error { + return u.Start() + })) + + return u +} + +func (u *UpdateModule) Name() string { + return "update" +} + +func (u *UpdateModule) Description() string { + return "A module to check for bettercap's updates." +} + +func (u *UpdateModule) Author() string { + return "Simone Margaritelli " +} + +func (u *UpdateModule) Configure() error { + return nil +} + +func (u *UpdateModule) Stop() error { + return nil +} + +func (u *UpdateModule) versionToNum(ver string) float64 { + if ver[0] == 'v' { + ver = ver[1:] + } + + n := 0.0 + parts := strings.Split(ver, ".") + nparts := len(parts) + + // reverse + for i := nparts/2 - 1; i >= 0; i-- { + opp := nparts - 1 - i + parts[i], parts[opp] = parts[opp], parts[i] + } + + for i, e := range parts { + ev, _ := strconv.Atoi(e) + n += float64(ev) * math.Pow10(i) + } + + return n +} + +func (u *UpdateModule) Start() error { + return u.SetRunning(true, func() { + defer u.SetRunning(false, nil) + + log.Info("Checking latest stable release ...") + + if releases, _, err := u.client.Repositories.ListReleases(context.Background(), "bettercap", "bettercap", nil); err == nil { + latest := releases[0] + if u.versionToNum(core.Version) < u.versionToNum(*latest.TagName) { + u.Session.Events.Add("update.available", latest) + } else { + log.Info("You are running %s which is the latest stable version.", core.Bold(core.Version)) + } + } else { + log.Error("Error while fetching latest release info from GitHub: %s", err) + } + }) +} diff --git a/network/ble_unsupported.go b/network/ble_unsupported.go index 7c29f385..0a22772f 100644 --- a/network/ble_unsupported.go +++ b/network/ble_unsupported.go @@ -38,6 +38,10 @@ func NewBLE(newcb BLEDevNewCallback, lostcb BLEDevLostCallback) *BLE { } } +func (b *BLE) Get(id string) (dev *BLEDevice, found bool) { + return +} + func (b *BLE) MarshalJSON() ([]byte, error) { doc := bleJSON{ Devices: make([]*BLEDevice, 0), diff --git a/network/oui.dat b/network/oui.dat index 645d4add..2abf3097 100644 --- a/network/oui.dat +++ b/network/oui.dat @@ -8493,6 +8493,7 @@ F88E85 Comtrend 607EDD Microsoft Mobile Oy F88096 Elsys Equipamentos EletrĂ´nicos Ltda E0B9E5 Technicolor +E2B9E5 Technicolor 0CBF15 Genetec 000B5D Fujitsu Limited F4CAE5 Freebox SAS diff --git a/network/oui.go b/network/oui.go index 5af69b9c..4a1b5c37 100644 --- a/network/oui.go +++ b/network/oui.go @@ -10315,6 +10315,7 @@ var oui = map[string]string { "5cf286": "Ieee Registration Authority", "001098": "Starnet Technologies", "001099": "InnoMedia", + "e2b9e5": "Technicolor", "24ee3a": "Chengdu Yingji Electronic Hi-tech Co", "00093f": "Double-Win Enterpirse CO.", "0026c6": "Intel Corporate", diff --git a/session/events.go b/session/events.go index 24fe1899..9fe075ab 100644 --- a/session/events.go +++ b/session/events.go @@ -57,7 +57,7 @@ func NewEventPool(debug bool, silent bool) *EventPool { func (p *EventPool) Listen() <-chan Event { p.Lock() defer p.Unlock() - l := make(chan Event, 1) + l := make(chan Event, 255) p.listeners = append(p.listeners, l) return l } @@ -86,6 +86,7 @@ func (p *EventPool) Add(tag string, data interface{}) { select { case l <- e: default: + fmt.Fprintf(os.Stderr, "Message not sent!\n") } } } diff --git a/session/session.go b/session/session.go index 01d3e6c4..46b42b95 100644 --- a/session/session.go +++ b/session/session.go @@ -8,6 +8,7 @@ import ( "net" "os" "os/signal" + "regexp" "runtime" "runtime/pprof" "sort" @@ -32,6 +33,8 @@ var ( ErrAlreadyStarted = errors.New("Module is already running.") ErrAlreadyStopped = errors.New("Module is not running.") + + reCmdSpaceCleaner = regexp.MustCompile(`^([^\s]+)\s+(.+)$`) ) type Session struct { @@ -254,7 +257,6 @@ func (s *Session) Close() { } s.Firewall.Restore() - s.Queue.Stop() if *s.Options.EnvFile != "" { envFile, _ := core.ExpandPath(*s.Options.EnvFile) @@ -480,6 +482,11 @@ func (s *Session) RunCaplet(filename string) error { func (s *Session) Run(line string) error { line = core.TrimRight(line) + // remove extra spaces after the first command + // so that 'arp.spoof on' is normalized + // to 'arp.spoof on' (fixes #178) + line = reCmdSpaceCleaner.ReplaceAllString(line, "$1 $2") + for _, h := range s.CoreHandlers { if parsed, args := h.Parse(line); parsed == true { return h.Exec(args, s)