├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── conftest.py │ ├── test_import.py │ ├── test_aireplay.py │ ├── test_airodump.py │ ├── test_executor.py │ └── test_airmon.py └── features │ ├── package.feature │ └── steps │ └── steps_package.py ├── docs ├── examples │ ├── scan.jpg │ ├── index.md │ └── example.ipynb ├── api │ ├── airmon.md │ ├── airbase.md │ ├── aircrack.md │ ├── airdecap.md │ ├── aireplay.md │ ├── airodump.md │ ├── airdecloack.md │ ├── index.md │ ├── executor.md │ └── models.md ├── pythonlovesaircrack.png ├── custom.css ├── SUMMARY.md ├── intro │ ├── features.md │ └── index.md ├── index.md └── icon.svg ├── .gitignore ├── pyrcrack ├── __init__.py ├── airdecap.py ├── airdecloack.py ├── airbase.py ├── aircrack.py ├── airmon.py ├── aireplay.py ├── airodump.py ├── models.py └── executor.py ├── .github ├── CONTRIBUTING.md ├── workflows │ ├── test.yml │ └── release.yml └── ISSUE_TEMPLATE │ └── bug.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── docs_src ├── init_select_recolect.py └── aireplay_deauth.py ├── mkdocs.yml ├── pyproject.toml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | -------------------------------------------------------------------------------- /docs/examples/scan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XayOn/pyrcrack/HEAD/docs/examples/scan.jpg -------------------------------------------------------------------------------- /docs/api/airmon.md: -------------------------------------------------------------------------------- 1 | # airmon 2 | ::: pyrcrack.airmon.AirmonNg 3 | :docstring: 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/pythonlovesaircrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XayOn/pyrcrack/HEAD/docs/pythonlovesaircrack.png -------------------------------------------------------------------------------- /docs/api/airbase.md: -------------------------------------------------------------------------------- 1 | # airbase 2 | 3 | ::: pyrcrack.airbase.AirbaseNg 4 | :docstring: 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/aircrack.md: -------------------------------------------------------------------------------- 1 | # aircrack 2 | ::: pyrcrack.aircrack.AircrackNg 3 | :docstring: 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/airdecap.md: -------------------------------------------------------------------------------- 1 | # airdecap 2 | ::: pyrcrack.airdecap.AirdecapNg 3 | :docstring: 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/aireplay.md: -------------------------------------------------------------------------------- 1 | # aireplay 2 | ::: pyrcrack.aireplay.AireplayNg 3 | :docstring: 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/airodump.md: -------------------------------------------------------------------------------- 1 | # airodump 2 | ::: pyrcrack.airodump.AirodumpNg 3 | :docstring: 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/airdecloack.md: -------------------------------------------------------------------------------- 1 | # airdecloack 2 | ::: pyrcrack.airdecloack.AirdecloackNg 3 | :docstring: 4 | :members: 5 | -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["SKIP_ROOT_CHECK"] = "1" 4 | os.environ["SKIP_VERSION_CHECK"] = "1" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | .tox 3 | *.egg-info 4 | htmlcov 5 | dist 6 | .cache 7 | .eggs 8 | .coverage 9 | __pycache__ 10 | 11 | # IDE 12 | .vscode 13 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | Pyrcrack's structure is as follows: 4 | 5 | - executor.py - Common python methods 6 | - models.py - Data models 7 | - a file for each aircrack-ng utility 8 | -------------------------------------------------------------------------------- /docs/api/executor.md: -------------------------------------------------------------------------------- 1 | # executor 2 | 3 | ::: pyrcrack.executor.ExecutorHelper 4 | :docstring: 5 | :members: 6 | 7 | 8 | ::: pyrcrack.executor.Option 9 | :docstring: 10 | :members: 11 | -------------------------------------------------------------------------------- /tests/features/package.feature: -------------------------------------------------------------------------------- 1 | Feature: Our software must be easily redistributable 2 | Scenario: Check that we provide an importable package 3 | When: I import the package pyrcrack 4 | Then: I see no errors 5 | 6 | -------------------------------------------------------------------------------- /docs/custom.css: -------------------------------------------------------------------------------- 1 | div.autodoc-docstring { 2 | padding-left: 20px; 3 | margin-bottom: 30px; 4 | border-left: 5px solid rgba(230, 230, 230); 5 | } 6 | 7 | div.autodoc-members { 8 | padding-left: 20px; 9 | margin-bottom: 15px; 10 | } 11 | -------------------------------------------------------------------------------- /tests/unit/test_import.py: -------------------------------------------------------------------------------- 1 | """Base library tests.""" 2 | 3 | 4 | def test_import(): 5 | """Test basic import.""" 6 | import importlib 7 | 8 | try: 9 | importlib.import_module("pyrcrack") 10 | except ImportError: 11 | assert False 12 | -------------------------------------------------------------------------------- /docs/api/models.md: -------------------------------------------------------------------------------- 1 | # models 2 | 3 | 4 | ## Result model 5 | 6 | ::: pyrcrack.models.Result 7 | :docstring: 8 | :members: 9 | 10 | 11 | ## Wifi Interface model 12 | 13 | ::: pyrcrack.models.Interface 14 | :docstring: 15 | :members: 16 | 17 | 18 | 19 | ## Wifi Interfaces List 20 | 21 | ::: pyrcrack.models.Interfaces 22 | :docstring: 23 | :members: 24 | 25 | 26 | ## AP's Client 27 | 28 | ::: pyrcrack.models.Client 29 | :docstring: 30 | :members: 31 | -------------------------------------------------------------------------------- /pyrcrack/__init__.py: -------------------------------------------------------------------------------- 1 | """pyrcrack. 2 | 3 | Aircrack-NG python bindings 4 | """ 5 | 6 | from contextvars import ContextVar 7 | 8 | from .airbase import AirbaseNg # noqa 9 | from .aircrack import AircrackNg # noqa 10 | from .airdecap import AirdecapNg # noqa 11 | from .airdecloack import AirdecloackNg # noqa 12 | from .aireplay import AireplayNg # noqa 13 | from .airmon import AirmonNg # noqa 14 | from .airodump import AirodumpNg # noqa 15 | 16 | MONITOR = ContextVar("monitor_interface") 17 | 18 | __version__ = "1.2.6" 19 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Home](index.md) 2 | * [Intro](intro/index.md) 3 | * [Features](intro/features.md) 4 | * [Examples](examples/index.md) 5 | * [Notebook](examples/example.ipynb) 6 | * [API DOCUMENTATION](api/index.md) 7 | * [Data models](api/models.md) 8 | * [Base executor](api/executor.md) 9 | * [Airmon-ng](api/airmon.md) 10 | * [Aircrack-ng](api/aircrack.md) 11 | * [Airodump-ng](api/airodump.md) 12 | * [Airdecloack-ng](api/airdecloack.md) 13 | * [Aireplay-ng](api/aireplay.md) 14 | * [Airbase-ng](api/airbase.md) 15 | * [Airdecap-ng](api/airdecap.md) 16 | -------------------------------------------------------------------------------- /tests/features/steps/steps_package.py: -------------------------------------------------------------------------------- 1 | from behave import then, when 2 | 3 | 4 | @when("I import the package pyrcrack") 5 | def import_package(context): 6 | """Try to import the package. 7 | 8 | If an exception happened push to context. 9 | """ 10 | context.exceptions = [] 11 | try: 12 | import pyrcrack # noqa 13 | except ImportError as excp: 14 | context.exceptions = [excp] 15 | 16 | 17 | @then("I see no errors") 18 | def no_exceptions_in_context(context): 19 | """Check that no previous exceptions have been captured.""" 20 | assert not context.exceptions 21 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Coding styles and coverage 2 | 3 | - Yapf with default options must be run before submitting any PR 4 | - A bug must exist on tracker, and it must be mentioned in each commit in the form \[4\] COMMIT\_MSG 5 | - Use conventional commits ( https://www.conventionalcommits.org/en/v1.0.0-beta.2/ ) 6 | - You can use emojis on commits 7 | 8 | # Testing 9 | 10 | Make sure you test anything before submitting a PR, and please, do not lower 11 | the coverage levels 12 | 13 | # Notes 14 | 15 | This is all subjective, and most PRs wich add any value will be probably 16 | merged. Don't be afraid to contribute! 17 | 18 | Artwork, testing (are you using pyrcrack on an opensource project? Give me a 19 | note!), etc are also good contributions. 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | files: "(.*).py" 2 | exclude: "docker/(.*)" 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.3.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: check-docstring-first 9 | - id: check-added-large-files 10 | - id: check-yaml 11 | - id: debug-statements 12 | - id: check-merge-conflict 13 | - id: end-of-file-fixer 14 | - hooks: 15 | - id: check-hooks-apply 16 | - id: check-useless-excludes 17 | repo: meta 18 | - repo: https://github.com/commitizen-tools/commitizen 19 | rev: v2.35.0 20 | hooks: 21 | - id: commitizen 22 | - repo: https://github.com/PyCQA/docformatter 23 | rev: v1.5.0 24 | hooks: 25 | - id: docformatter 26 | - repo: https://github.com/charliermarsh/ruff-pre-commit 27 | rev: v0.0.150 28 | hooks: 29 | - id: ruff 30 | -------------------------------------------------------------------------------- /tests/unit/test_aireplay.py: -------------------------------------------------------------------------------- 1 | """Test specific aireplay functions.""" 2 | import pytest 3 | 4 | 5 | def test_aireplay_props(): 6 | """Props.""" 7 | from pyrcrack import AireplayNg 8 | 9 | assert not AireplayNg.requires_tempdir 10 | assert not AireplayNg.requires_tempfile 11 | assert AireplayNg.command == "aireplay-ng" 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_run_aireplay(): 16 | """Test main run method.""" 17 | import asynctest 18 | 19 | from pyrcrack import AireplayNg 20 | 21 | async with AireplayNg() as aireplay: 22 | with asynctest.mock.patch("asyncio.create_subprocess_exec") as runmock: 23 | aireplay.result_updater = asynctest.mock.CoroutineMock() 24 | await aireplay.run() 25 | assert runmock.called 26 | runmock.assert_called_with( 27 | "aireplay-ng", stderr=-1, stdout=-1, stdin=-1) 28 | -------------------------------------------------------------------------------- /pyrcrack/airdecap.py: -------------------------------------------------------------------------------- 1 | """Airdecap-ng.""" 2 | from .executor import ExecutorHelper 3 | 4 | 5 | class AirdecapNg(ExecutorHelper): 6 | """Airdecap-ng 1.6 - (C) 2006-2020 Thomas d'Otreppe 7 | 8 | https://www.aircrack-ng.org 9 | 10 | Usage: airdecap-ng [options] 11 | 12 | Options: 13 | 14 | -l : don't remove the 802.11 header 15 | -b : access point MAC address filter 16 | -e : target network SSID 17 | -o : output file for decrypted packets (default -dec) 18 | -w : target network WEP key in hex 19 | -c : output file for corrupted WEP packets (default -bad) 20 | -p : target network WPA passphrase 21 | -k : WPA Pairwise Master Key in hex 22 | --help : Displays this usage screen 23 | 24 | """ 25 | 26 | command = "airdecap-ng" 27 | sync = False 28 | requires_tempfile = False 29 | requires_tempdir = False 30 | requires_root = False 31 | -------------------------------------------------------------------------------- /docs/intro/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | 4 | ## Releases 5 | - [X] Aircrack-ng 1.6 support 6 | - [X] Aircrack-ng 1.7 support 7 | 8 | ## Supported tools 9 | - [X] [Airbase-Ng](https://www.aircrack-ng.org/doku.php?id=airbase-ng) 10 | - [X] [Airdecap-Ng](https://www.aircrack-ng.org/doku.php?id=airdecap-ng) 11 | - [X] [Aircrack-Ng](https://www.aircrack-ng.org/doku.php?id=aircrack-ng) 12 | * [X] Custom data models 13 | * [X] Shortcuts for cracking specific APs 14 | - [X] [Airdecloack-Ng](https://www.aircrack-ng.org/doku.php?id=airdecloak-ng) 15 | - [X] [Aireplay-Ng](https://www.aircrack-ng.org/doku.php?id=aireplay-ng) 16 | - [X] [Airmon-Ng](https://www.aircrack-ng.org/doku.php?id=airmon-ng) 17 | - [X] [Airodump-Ng](https://www.aircrack-ng.org/doku.php?id=airodump-ng) 18 | * [X] Custom data models 19 | * [X] ".airodump" properties on models 20 | - [ ] airdecloak-ng 21 | - [ ] airdrop-ng 22 | - [ ] airgraph-ng 23 | - [ ] airolib-ng 24 | - [ ] airserv-ng 25 | - [ ] airtun-ng 26 | - [ ] besside-ng 27 | - [ ] easside-ng 28 | - [ ] wesside-ng 29 | - [ ] dcrack 30 | - [ ] packetforge-ng 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | branches: [develop] 8 | 9 | concurrency: 10 | group: test-${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | PYTHONUNBUFFERED: "1" 15 | FORCE_COLOR: "1" 16 | 17 | jobs: 18 | 19 | run: 20 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: [ubuntu-latest] 26 | python-version: ['3.10'] 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set up Python ${{ matrix.python-version }} 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | 36 | - name: Install Hatch 37 | run: pip install --upgrade hatch 38 | 39 | - name: Run tests 40 | run: hatch run cov 41 | 42 | - name: Code style 43 | run: hatch run ruff . 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.2.6 (2022-12-20) 2 | 3 | ### Fix 4 | 5 | - Fixed small typing issue 6 | 7 | ## v1.2.5 (2022-12-03) 8 | 9 | ### Fix 10 | 11 | - Fixed CI badge on readme 12 | 13 | ## v1.2.4 (2022-12-03) 14 | 15 | ### Fix 16 | 17 | - Reorganized workflows 18 | 19 | ## v1.2.3 (2022-12-03) 20 | 21 | ### Fix 22 | 23 | - I was re-checking out the repo 24 | 25 | ## v1.2.2 (2022-12-03) 26 | 27 | ### Fix 28 | 29 | - Changed action for softprops/action-gh-release 30 | 31 | ## v1.2.1 (2022-12-03) 32 | 33 | ### Fix 34 | 35 | - Added missing commitizen dep 36 | 37 | ## v1.2.0 (2022-12-03) 38 | 39 | >>>>>>> d12f20d9790c04622957a14d2304e149b78d49df 40 | ### Feat 41 | 42 | - Added CI autorelease 43 | - Upgrading workflows 44 | - Migrating to hatch 45 | - :sparkles: Check correct aircrack version and root (#41) 46 | - :sparkles: #29 Improved airmon-ng interface 47 | - :sparkles: #37 Improved aireplay-ng usability (#38) 48 | - Added debug mode. Don't delete tempfiles 49 | - closes: #11 Added pypi to github flow 50 | 51 | ### Fix 52 | 53 | - :bug: #29 Don't break on exit in most cases 54 | - Added run-as-root message 55 | - Fixed airmon test 56 | - [#10] Using codecov badge 57 | - [#10] Migrating to codecov 58 | -------------------------------------------------------------------------------- /docs/intro/index.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | As AircrackNg will run in background processes, and produce parseable output 3 | both in files and stdout, the most pythonical approach are context managers, 4 | **cleaning up after**. 5 | 6 | !!! note 7 | 8 | This library exports a basic aircrack-ng API aiming to keep always a small 9 | readable codebase. 10 | 11 | This has led to a simple library that executes each of the aircrack-ng's suite 12 | commands and auto-detects its usage instructions. Based on that, it dinamically 13 | builds classes inheriting that usage as docstring and a `run()` method that 14 | accepts keyword parameters and arguments, and checks them **before** trying to 15 | run them. 16 | 17 | Some classes expose themselves as async iterators, as airodump-ng's wich 18 | returns access points with its associated clients. 19 | 20 | # Common pitfals 21 | 22 | !!! danger 23 | 24 | Make sure you have aircrack-ng installed and **on path**. 25 | Most linux distros will not have any issue with this. Specialized distros 26 | like khali do this by default. 27 | On windows, you're basically on your own, but as far as you have 28 | aircrack-ng tools on path, you should be good to go 29 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Show me some code! 2 | 3 | 4 | You can have also have a look at the docs_src/ folder for some usage examples, 5 | such as the basic "scan for targets", that will list available interfaces, let 6 | you choose one, put it in monitor mode, and scan for targets updating results 7 | each 2 seconds. 8 | 9 | Be sure to check the python [notebook example](./example.ipynb) 10 | 11 | 12 | ``` py 13 | 14 | import asyncio 15 | 16 | import pyrcrack 17 | 18 | from rich.console import Console 19 | from rich.prompt import Prompt 20 | 21 | async def scan_for_targets(): 22 | """Scan for targets, return json.""" 23 | console = Console() 24 | console.clear() 25 | console.show_cursor(False) 26 | airmon = pyrcrack.AirmonNg() 27 | 28 | interface = Prompt.ask( 29 | 'Select an interface', 30 | choices=[a['interface'] for a in await airmon.interfaces]) 31 | 32 | async with airmon(interface) as mon: 33 | async with pyrcrack.AirodumpNg() as pdump: 34 | async for result in pdump(mon.monitor_interface): 35 | console.clear() 36 | console.print(result.table) 37 | await asyncio.sleep(2) 38 | 39 | 40 | asyncio.run(scan_for_targets()) 41 | 42 | ``` 43 | 44 | This snippet of code will produce the following results: 45 | 46 | ![scan](./scan.jpg) 47 | -------------------------------------------------------------------------------- /docs_src/init_select_recolect.py: -------------------------------------------------------------------------------- 1 | """Scan for targets and and pretty print some data.""" 2 | import asyncio 3 | import logging 4 | 5 | from rich.console import Console 6 | from rich.prompt import Prompt 7 | 8 | import pyrcrack 9 | 10 | logging.basicConfig(level=logging.DEBUG) 11 | 12 | 13 | async def scan_for_targets(): 14 | """Scan for targets, return json.""" 15 | console = Console() 16 | console.clear() 17 | console.show_cursor(show=False) 18 | airmon = pyrcrack.AirmonNg() 19 | interfaces = await airmon.interfaces 20 | console.print(interfaces.table) 21 | interface = Prompt.ask("Select an interface", 22 | choices=[a.interface for a in interfaces]) 23 | 24 | async with airmon(interface) as mon: 25 | async with pyrcrack.AirodumpNg() as pdump: 26 | async for aps in pdump(mon.monitor_interface): 27 | console.clear() 28 | console.print(aps.table) 29 | client = Prompt.ask( 30 | "Select an AP", 31 | choices=["continue", *[str(a) for a in range(len(aps))]], 32 | ) 33 | 34 | if client != "continue": 35 | break 36 | 37 | async with pyrcrack.AirodumpNg() as pdump: 38 | console.print( 39 | ":vampire:", 40 | f"Selected client: [red] {aps[int(client)].bssid} [/red]") 41 | 42 | async for result in pdump(mon.monitor_interface, 43 | **aps[int(client)].airodump): 44 | console.clear() 45 | console.print(result.table) 46 | await asyncio.sleep(3) 47 | 48 | 49 | asyncio.run(scan_for_targets()) 50 | -------------------------------------------------------------------------------- /docs_src/aireplay_deauth.py: -------------------------------------------------------------------------------- 1 | """Deauth""" 2 | import asyncio 3 | from contextlib import suppress 4 | 5 | from rich.console import Console 6 | 7 | import pyrcrack 8 | 9 | CONSOLE = Console() 10 | CONSOLE.clear() 11 | CONSOLE.show_cursor(show=False) 12 | 13 | 14 | async def attack(apo): 15 | """Run aireplay deauth attack.""" 16 | airmon = pyrcrack.AirmonNg() 17 | interfaces = await airmon.interfaces 18 | CONSOLE.print(f"Starting airmon-ng with channel {apo.channel}") 19 | interface = interfaces[0] 20 | async with airmon(interface.interface, apo.channel) as mon: 21 | async with pyrcrack.AireplayNg() as aireplay: 22 | CONSOLE.print("Starting aireplay on {} for {}".format( 23 | mon.monitor_interface, apo.bssid)) 24 | await aireplay.run(mon.monitor_interface, 25 | deauth=10, 26 | D=True, 27 | b=apo.bssid) 28 | while True: 29 | CONSOLE.print(aireplay.meta) 30 | await asyncio.sleep(2) 31 | 32 | 33 | async def deauth(): 34 | """Scan for targets, return json.""" 35 | airmon = pyrcrack.AirmonNg() 36 | interfaces = await airmon.interfaces 37 | async with airmon(interfaces[0].interface) as mon: 38 | async with pyrcrack.AirodumpNg() as pdump: 39 | async for result in pdump(mon.monitor_interface): 40 | CONSOLE.print(result.table) 41 | with suppress(KeyError): 42 | ap = result[0] 43 | CONSOLE.print(f"Selected AP {ap.bssid}") 44 | break 45 | await asyncio.sleep(3) 46 | await attack(ap) 47 | 48 | 49 | asyncio.run(deauth()) 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | PyrCrack is a Python library exposing a common aircrack-ng API. 6 | 7 | !!! danger 8 | 9 | pyrcrack-ng is a **library**, you need heavy python knowledge to be able to 10 | use it. It's intended as a base to work upon. 11 | 12 | 13 | # Status 14 | --- | --- 15 | ----- | ------- 16 | CI/CD | [![CI - Test](https://github.com/xayon/pyrcrack/actions/workflows/test.yml/badge.svg)](https://github.com/xayon/pyrcrack/actions/workflows/test.yml) [![CD - Build Package](https://github.com/xayon/pyrcrack/actions/workflows/release.yml/badge.svg)](https://github.com/xayon/pyrcrack/actions/workflows/release.yml) | 17 | Package | [![PyPI - Version](https://img.shields.io/pypi/v/pyrcrack.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/pyrcrack/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyrcrack.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/pyrcrack/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyrcrack.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/pyrcrack/) | 18 | Meta | [![code style - yapf](https://img.shields.io/badge/code%20style-yapf-000000.svg)](https://github.com/psf/black) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) [![imports - isort](https://img.shields.io/badge/imports-isort-ef8336.svg)](https://github.com/pycqa/isort) 19 | 20 | # Contributors 21 | 22 | ![contributors](https://contrib.rocks/image?repo=xayon/pyrcrack) 23 | 24 | 25 | ## License 26 | 27 | Pyrcrack is distributed under the terms of the [GPL2+](https://spdx.org/licenses/GPL2.html) license. 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: File a bug/issue 3 | title: "[BUG] " 4 | labels: [Bug, Needs Triage] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Is there an existing issue for this? 9 | description: Please search to see if an issue already exists for the bug you encountered. 10 | options: 11 | - label: I have searched the existing issues 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Current Behavior 16 | description: A concise description of what you're experiencing. 17 | validations: 18 | required: false 19 | - type: textarea 20 | attributes: 21 | label: Expected Behavior 22 | description: A concise description of what you expected to happen. 23 | validations: 24 | required: false 25 | - type: textarea 26 | attributes: 27 | label: Steps To Reproduce 28 | description: Steps to reproduce the behavior. 29 | placeholder: | 30 | 1. In this environment... 31 | 2. With this config... 32 | 3. Run '...' 33 | 4. See error... 34 | validations: 35 | required: false 36 | - type: textarea 37 | attributes: 38 | label: Environment 39 | description: | 40 | examples: 41 | - **OS**: Ubuntu 20.04 42 | - **Node**: 13.14.0 43 | - **npm**: 7.6.3 44 | value: | 45 | - OS: 46 | - Node: 47 | - npm: 48 | render: markdown 49 | validations: 50 | required: false 51 | - type: textarea 52 | attributes: 53 | label: Anything else? 54 | description: | 55 | Links? References? Anything that will give us more context about the issue you are encountering! 56 | 57 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 58 | validations: 59 | required: false 60 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release and publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | bump-version: 10 | if: "!startsWith(github.event.head_commit.message, 'bump:')" 11 | runs-on: ubuntu-latest 12 | name: "Bump version and create changelog with commitizen" 13 | steps: 14 | - name: Check out 15 | uses: actions/checkout@v2 16 | with: 17 | token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" 18 | fetch-depth: 0 19 | - name: Create bump and changelog 20 | uses: commitizen-tools/commitizen-action@master 21 | with: 22 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 23 | changelog_increment_filename: body.md 24 | - name: Dump GitHub context 25 | env: 26 | GITHUB_CONTEXT: ${{ toJson(github) }} 27 | run: echo "$GITHUB_CONTEXT" 28 | - name: Set up Python 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: "3.8" 32 | cache: "pip" 33 | cache-dependency-path: pyproject.toml 34 | - uses: actions/cache@v3 35 | id: cache 36 | with: 37 | path: ${{ env.pythonLocation }} 38 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-publish 39 | - name: Install build dependencies 40 | if: steps.cache.outputs.cache-hit != 'true' 41 | run: pip install build 42 | - name: Build distribution 43 | run: python -m build 44 | - name: Release 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | body_path: "body.md" 48 | tag_name: ${{ env.REVISION }} 49 | files: dist/* 50 | - name: Publish 51 | uses: pypa/gh-action-pypi-publish@v1.13.0 52 | with: 53 | password: ${{ secrets.PYPI_API_TOKEN }} 54 | -------------------------------------------------------------------------------- /pyrcrack/airdecloack.py: -------------------------------------------------------------------------------- 1 | """Airdecloack-ng.""" 2 | 3 | from .executor import ExecutorHelper 4 | 5 | 6 | class AirdecloackNg(ExecutorHelper): 7 | """Airdecloak-ng 1.6 - (C) 2008-2020 Thomas d'Otreppe 8 | https://www.aircrack-ng.org 9 | Available filters:: 10 | 11 | signal: Try to filter based on signal. 12 | duplicate_sn: Remove all duplicate sequence numbers 13 | for both the AP and the client. 14 | duplicate_sn_ap: Remove duplicate sequence number for 15 | the AP only. 16 | duplicate_sn_client: Remove duplicate sequence number for the 17 | client only. 18 | consecutive_sn: Filter based on the fact that IV should 19 | be consecutive (only for AP). 20 | duplicate_iv: Remove all duplicate IV. 21 | signal_dup_consec_sn: Use signal (if available), duplicate and 22 | consecutive sequence number (filtering is 23 | much more precise than using all these 24 | filters one by one). 25 | 26 | Usage: airdecloak-ng [options] 27 | 28 | Options: 29 | -i <file> : Input capture file 30 | --ssid <ESSID> : ESSID of the network to filter 31 | --bssid <BSSID> : BSSID of the network to filter 32 | -o <file> : Output packets (valid) file 33 | -c <file> : Output packets (cloaked) file 34 | -u <file> : Output packets (unknown/ignored) file 35 | --filters <filters> : Apply filters (separated by a comma). 36 | --null-packets : Assume that null packets can be cloaked. 37 | --disable-base_filter : Do not apply base filter. 38 | --drop-frag : Drop fragmented packets 39 | --help : Displays this usage screen 40 | """ 41 | 42 | command = "airdecloack-ng" 43 | requires_tempfile = False 44 | requires_tempdir = False 45 | requires_root = False 46 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pyrcrack Documentation 2 | theme: 3 | features: 4 | - navigation.instant 5 | - navigation.tracking 6 | - navigation.tabs 7 | - content.code.annotate 8 | - toc.follow 9 | - announce.dismiss 10 | - content.code.annotate 11 | - content.tabs.link 12 | - content.tooltips 13 | - header.autohide 14 | - navigation.expand 15 | - navigation.indexes 16 | - navigation.instant 17 | - navigation.prune 18 | - navigation.sections 19 | - navigation.tabs 20 | - navigation.tabs.sticky 21 | - navigation.top 22 | - navigation.tracking 23 | - search.highlight 24 | - search.share 25 | - search.suggest 26 | - toc.follow 27 | - toc.integrate 28 | name: material 29 | logo: icon.svg 30 | palette: 31 | - scheme: slate 32 | primary: deep-purple 33 | toggle: 34 | icon: material/brightness-4 35 | name: Switch to light mode 36 | - scheme: default 37 | primary: deep-purple 38 | toggle: 39 | icon: material/brightness-7 40 | name: Switch to dark mode 41 | extra_css: 42 | - ./custom.css 43 | plugins: 44 | - literate-nav: 45 | nav_file: SUMMARY.md 46 | - search 47 | - social 48 | - include-markdown 49 | - mkdocs-jupyter 50 | repo_url: https://github.com/XayOn/pyrcrack 51 | markdown_extensions: 52 | - mkautodoc 53 | - pymdownx.highlight: 54 | anchor_linenums: true 55 | - abbr 56 | - admonition 57 | - attr_list 58 | - def_list 59 | - footnotes 60 | - md_in_html 61 | - toc: 62 | permalink: true 63 | - pymdownx.arithmatex: 64 | generic: true 65 | - pymdownx.betterem: 66 | smart_enable: all 67 | - pymdownx.caret 68 | - pymdownx.details 69 | - pymdownx.emoji: 70 | emoji_generator: !!python/name:materialx.emoji.to_svg 71 | emoji_index: !!python/name:materialx.emoji.twemoji 72 | - pymdownx.highlight: 73 | anchor_linenums: true 74 | auto_title: true 75 | - pymdownx.inlinehilite 76 | - pymdownx.keys 77 | - pymdownx.magiclink: 78 | repo_url_shorthand: true 79 | user: squidfunk 80 | repo: mkdocs-material 81 | - pymdownx.mark 82 | - pymdownx.smartsymbols 83 | - pymdownx.superfences 84 | - pymdownx.tabbed: 85 | alternate_style: true 86 | - pymdownx.tasklist: 87 | custom_checkbox: true 88 | - pymdownx.tilde 89 | -------------------------------------------------------------------------------- /tests/unit/test_airodump.py: -------------------------------------------------------------------------------- 1 | """Test specific airodump functions.""" 2 | import pytest 3 | 4 | 5 | def test_props(): 6 | """Props.""" 7 | from pyrcrack import AirodumpNg 8 | 9 | assert AirodumpNg.requires_tempdir 10 | assert not AirodumpNg.requires_tempfile 11 | assert AirodumpNg.command == "airodump-ng" 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_run_airodump(): 16 | """Test main run method.""" 17 | import asynctest 18 | 19 | from pyrcrack import AirodumpNg 20 | 21 | async with AirodumpNg() as airodump: 22 | with asynctest.mock.patch("asyncio.create_subprocess_exec") as runmock: 23 | airodump.result_updater = asynctest.mock.CoroutineMock() 24 | await airodump.run() 25 | assert runmock.called 26 | runmock.assert_called_with( 27 | "airodump-ng", 28 | "--background", 29 | "1", 30 | "--write", 31 | airodump.tempdir.name + "/" + airodump.uuid, 32 | "--write-interval", 33 | "1", 34 | "--output-format", 35 | "netxml,logcsv,netxml", 36 | stderr=-1, 37 | stdout=-1, 38 | stdin=-1, 39 | ) 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_run_airodump_with_write(): 44 | """Test main run method.""" 45 | import asynctest 46 | 47 | from pyrcrack import AirodumpNg 48 | 49 | async with AirodumpNg() as airodump: 50 | with asynctest.mock.patch("asyncio.create_subprocess_exec") as runmock: 51 | await airodump.run(write="foo") 52 | runmock.assert_called_with( 53 | "airodump-ng", 54 | "--background", 55 | "1", 56 | "--write", 57 | "foo", 58 | "--write-interval", 59 | "1", 60 | "--output-format", 61 | "netxml,logcsv,netxml", 62 | stderr=-1, 63 | stdout=-1, 64 | stdin=-1, 65 | ) 66 | 67 | 68 | @pytest.mark.asyncio 69 | async def test_csv(): 70 | """Test main run method.""" 71 | from unittest.mock import MagicMock 72 | 73 | from pyrcrack import AirodumpNg 74 | 75 | async with AirodumpNg() as airodump: 76 | expected = ( 77 | f"{airodump.tempdir.name}/{airodump.uuid}" 78 | f"-{airodump.execn:02}.kismet.csv" 79 | ) 80 | airodump.proc = MagicMock() 81 | assert airodump.get_file("kismet.csv") == expected 82 | -------------------------------------------------------------------------------- /pyrcrack/airbase.py: -------------------------------------------------------------------------------- 1 | """Airbase-ng.""" 2 | 3 | from .executor import ExecutorHelper 4 | 5 | 6 | class AirbaseNg(ExecutorHelper): 7 | """Airbase-ng 1.6 - (C) 2008-2020 Thomas d'Otreppe 8 | Original work: Martin Beck 9 | https://www.aircrack-ng.org 10 | 11 | Usage: airbase-ng <options> <replay interface> 12 | 13 | Options: 14 | 15 | -a <bssid> : set Access Point MAC address 16 | -i <iface> : capture packets from this interface 17 | -w <key> : use this WEP key to en-/decrypt packets 18 | -h <MAC> : source mac for MITM mode 19 | -f <disallow> : disallow specified client MACs (default: allow) 20 | -W 0|1 : [don't] set WEP flag in beacons 0|1 (default: auto) 21 | -q : quiet (do not print statistics) 22 | -v : verbose (print more messages) 23 | -A : Ad-Hoc Mode (allows other clients to peer) 24 | -Y <proc> : (in|out|both) external packet processing 25 | -c <channel> : sets the channel the AP is running on 26 | -X : hidden ESSID 27 | -s : force shared key authentication (default: auto) 28 | -S : set shared key challenge length (default: 128) 29 | -L : Caffe-Latte WEP attack 30 | -N : cfrag WEP attack (recommended) 31 | -x <nbpps> : number of packets per second (default: 100) 32 | -y : disables responses to broadcast probes 33 | -0 : set all WPA,WEP,open tags. 34 | -z <type> : sets WPA1 tags. 35 | -Z <type> : same as -z, but for WPA2 36 | -V <type> : fake EAPOL 1=MD5 2=SHA1 3=auto 37 | -F <prefix> : write all sent and received frames into pcap file 38 | -P : respond to all probes, even when specifying ESSIDs 39 | -I <interval> : sets the beacon interval value in ms 40 | -C <seconds> : enables beaconing of probed ESSID values 41 | -n <hex> : User specified ANonce when doing the 4-way handshake 42 | --bssid <MAC> : BSSID to filter/use 43 | --bssids <file> : read a list of BSSIDs out of that file 44 | --client <MAC> : MAC of client to filter 45 | --clients <file> : read a list of MACs out of that file 46 | --essid <ESSID> : specify a single ESSID (default: default) 47 | --essids <file> : read a list of ESSIDs out of that file 48 | --help : Displays this usage screen 49 | """ 50 | 51 | command = "airbase-ng" 52 | requires_tempfile = False 53 | requires_tempdir = False 54 | requires_root = True 55 | -------------------------------------------------------------------------------- /tests/unit/test_executor.py: -------------------------------------------------------------------------------- 1 | """Test executor class.""" 2 | 3 | import pytest 4 | 5 | 6 | def test_helpstr_extraction(): 7 | """Test helpstr extraction. 8 | 9 | This tests helpstr(), wich should try to execute a command, and echo 10 | to ensure output 0. 11 | """ 12 | from unittest.mock import patch 13 | 14 | with patch("subprocess.check_output", side_effect=(b"test",)) as mock: 15 | from pyrcrack.executor import ExecutorHelper 16 | 17 | class FakeExecutor(ExecutorHelper): 18 | command = "foobar" 19 | requires_tempfile = False 20 | requires_tempdir = False 21 | requires_root = False 22 | 23 | assert FakeExecutor().helpstr == "test" 24 | mock.assert_called_with("foobar 2>&1; echo", shell=True) 25 | 26 | 27 | def test_usage(): 28 | """Test extract usage from a specified command.""" 29 | # opt = docopt.parse_defaults(self.helpstr) 30 | # return dict({a.short or a.long: bool(a.argcount) for a in opt}) 31 | from unittest.mock import patch 32 | 33 | with patch("subprocess.check_output", side_effect=(b"test",)): 34 | from pyrcrack.executor import ExecutorHelper 35 | 36 | class FakeExecutor(ExecutorHelper): 37 | """Fake command. 38 | Usage: fake [options] 39 | 40 | Options: 41 | -f 42 | -y=<foo> 43 | """ 44 | 45 | command = "foobar" 46 | requires_tempfile = False 47 | requires_tempdir = False 48 | requires_root = False 49 | 50 | assert FakeExecutor().usage == {"-f": False, "-y": True} 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_run_async(): 55 | """Check command usage.""" 56 | import asynctest 57 | 58 | with asynctest.mock.patch("asyncio.create_subprocess_exec") as runmock: 59 | from pyrcrack.executor import ExecutorHelper 60 | 61 | class FakeExecutor(ExecutorHelper): 62 | """Fake command. 63 | Usage: fake [options] 64 | 65 | Options: 66 | -f=<bar> 67 | -y 68 | """ 69 | 70 | command = "foobar" 71 | requires_tempfile = False 72 | requires_tempdir = False 73 | requires_root = False 74 | 75 | await FakeExecutor().run(f="foo", y=True) 76 | 77 | try: 78 | runmock.assert_called_with( 79 | *["foobar", "-f", "foo", "-y"], stderr=-1, stdin=-1, stdout=-1 80 | ) 81 | except AssertionError: 82 | runmock.assert_called_with( 83 | *["foobar", "-y", "-f", "foo"], stderr=-1, stdin=-1, stdout=-1 84 | ) 85 | 86 | 87 | @pytest.mark.parametrize( 88 | "in_,out", (("aircrack-ng", "AircrackNg"), ("airodump-ng", "AirodumpNg")) 89 | ) 90 | def test_stc(in_, out): 91 | """Convert snake case to camelcase in class format.""" 92 | from pyrcrack.executor import stc 93 | 94 | assert stc(in_) == out 95 | -------------------------------------------------------------------------------- /pyrcrack/aircrack.py: -------------------------------------------------------------------------------- 1 | """Aircrack-ng.""" 2 | from .executor import ExecutorHelper 3 | 4 | 5 | class AircrackNg(ExecutorHelper): 6 | """Aircrack-ng 1.6 - (C) 2006-2020 Thomas d'Otreppe 7 | https://www.aircrack-ng.org 8 | 9 | Usage: aircrack-ng [options] <file>... 10 | 11 | Options: 12 | 13 | -a <amode> : force attack mode (1/WEP, 2/WPA-PSK) 14 | -e <essid> : target selection: network identifier 15 | -b <bssid> : target selection: access point's MAC 16 | -p <nbcpu> : # of CPU to use (default: all CPUs) 17 | -q : enable quiet mode (no status output) 18 | -C <macs> : merge the given APs to a virtual one 19 | -l <file> : write key to file. Overwrites file. 20 | -c : search alpha-numeric characters only 21 | -t : search binary coded decimal chr only 22 | -h : search the numeric key for Fritz!BOX 23 | -d <mask> : use masking of the key (A1:XX:CF:YY) 24 | -m <maddr> : MAC address to filter usable packets 25 | -n <nbits> : WEP key length : 64/128/152/256/512 26 | -i <index> : WEP key index (1 to 4), default: any 27 | -f <fudge> : bruteforce fudge factor, default: 2 28 | -k <korek> : disable one attack method (1 to 17) 29 | -x or -x0 : disable bruteforce for last keybytes 30 | -x1 : last keybyte bruteforcing (default) 31 | -x2 : enable last 2 keybytes bruteforcing 32 | -X : disable bruteforce multithreading 33 | -y : experimental single bruteforce mode 34 | -K : use only old KoreK attacks (pre-PTW) 35 | -s : show the key in ASCII while cracking 36 | -M <num> : specify maximum number of IVs to use 37 | -D : WEP decloak, skips broken keystreams 38 | -P <num> : PTW debug: 1: disable Klein, 2: PTW 39 | -1 : run only 1 try to crack key with PTW 40 | -V : run in visual inspection mode 41 | -w <words> : path to wordlist(s) filename(s) 42 | -N <file> : path to new session filename 43 | -R <file> : path to existing session filename 44 | -E <file> : create EWSA Project file v3 45 | -I <str> : PMKID string (hashcat -m 16800) 46 | -j <file> : create Hashcat v3.6+ file (HCCAPX) 47 | -J <file> : create Hashcat file (HCCAP) 48 | -S : WPA cracking speed test 49 | -Z <sec> : WPA cracking speed test length of 50 | execution. 51 | -r <DB> : path to airolib-ng database 52 | --simd-list : Show a list of the available 53 | --simd <option> : Use specific SIMD architecture. 54 | -u : Displays # of CPUs & SIMD support 55 | --help : Displays this usage screen 56 | """ 57 | 58 | command = "aircrack-ng" 59 | requires_tempfile = True 60 | requires_tempdir = False 61 | requires_root = False 62 | 63 | async def run(self, *args, **kwargs): 64 | if self.tempfile: 65 | kwargs["l"] = self.tempfile.name 66 | return await super().run(*args, **kwargs) 67 | 68 | async def get_result(self): 69 | """Read tempfile (write key result)""" 70 | if self.proc: 71 | await self.proc.wait() 72 | if self.tempfile: 73 | return self.tempfile.read() 74 | -------------------------------------------------------------------------------- /pyrcrack/airmon.py: -------------------------------------------------------------------------------- 1 | """Airmon-ng.""" 2 | import asyncio 3 | import re 4 | 5 | from .executor import ExecutorHelper 6 | from .models import Interfaces 7 | 8 | 9 | class AirmonNg(ExecutorHelper): 10 | """Airmon-NG 11 | Usage: airmon-ng <start|stop|check> <interface> [channel or frequency] 12 | """ 13 | 14 | command = "airmon-ng" 15 | requires_tempfile = False 16 | requires_tempdir = False 17 | requires_root = True 18 | 19 | def __init__(self, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.dirty = False 22 | self.monitor_token = None 23 | self.monitor_enabled = [] 24 | 25 | async def run(self, *args, **kwargs): 26 | """Check argument position. 27 | 28 | Forced for this one. 29 | """ 30 | self.dirty = True 31 | if args: 32 | if not any(a in args[0] for a in ("start", "stop", "check")): 33 | raise Exception("First argument for airmon must" 34 | f" be start, stop or check not {args[0]}") 35 | 36 | return await super().run(*args, **kwargs) 37 | 38 | async def __aenter__(self): 39 | """Put selected interface in monitor mode.""" 40 | if not self.run_args[0][0]: 41 | raise RuntimeError("Should be called (airmon()) first.") 42 | ifaces = await self.interfaces 43 | if not any(a.interface == self.run_args[0][0] for a in ifaces): 44 | if not any(a == self.run_args[0][0] for a in ifaces): 45 | raise ValueError("Invalid interface selected") 46 | await self.run("start", self.run_args[0][0]) 47 | # Save interface data while we're on the async cm. 48 | self._interface_data = await self.interfaces 49 | from . import MONITOR 50 | 51 | self.monitor_token = MONITOR.set(self.monitor_interface) 52 | return self 53 | 54 | async def select_interface(self, regex): 55 | ifaces = await self.interfaces 56 | reg = re.compile(regex) 57 | if interface := next((a for a in ifaces if reg.match(str(a))), None): 58 | return interface 59 | raise Exception(f"No interface matching regex {regex}") 60 | 61 | async def __aexit__(self, *args, **kwargs): 62 | """Set monitor-enabled interfaces back to normal.""" 63 | await self.run("stop", self.monitor_interface) 64 | self.dirty = False 65 | await asyncio.sleep(1) 66 | if self.monitor_token: 67 | from . import MONITOR 68 | 69 | MONITOR.reset(self.monitor_token) 70 | await self.interfaces 71 | 72 | @property 73 | def monitor_interface(self): 74 | iface = next(a for a in self._interface_data 75 | if a.interface == self.run_args[0][0]) 76 | return iface.monitor 77 | 78 | @property 79 | async def interfaces(self): 80 | """List of currently available interfaces as reported by airmon-ng. 81 | 82 | This is an awaitable property, use it as in:: 83 | 84 | async with AirmonNg() as airmon: 85 | await airmon.interfaces 86 | 87 | Returns: None 88 | """ 89 | if not self.dirty: 90 | await self.run() 91 | self.dirty = False 92 | data = await self.readlines() 93 | return Interfaces(data) 94 | 95 | def __str__(self): 96 | return self.monitor_interface 97 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 3 | build-backend = "hatchling.build" 4 | 5 | 6 | [project] 7 | name = "pyrcrack" 8 | description = "Pythonic aircrack-ng bindings" 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | license = "GPL-3.0" 12 | keywords = [] 13 | authors = [{ name = "David Francos", email = "me@davidfrancos.net" }] 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 3.7", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: Implementation :: CPython", 23 | "Programming Language :: Python :: Implementation :: PyPy", 24 | ] 25 | dependencies = [ 26 | "stringcase>=1", 27 | "docopt>=0.6", 28 | "async-timeout>=3", 29 | "parse>=1.12", 30 | "pytest-asyncio>=0.14", 31 | "geopy>=2", 32 | "mac-vendor-lookup>=0.1", 33 | "pyshark>=0.4", 34 | "xmltodict>=0.12", 35 | "dotmap>=1", 36 | "rich>=7", 37 | ] 38 | dynamic = ["version"] 39 | 40 | [project.urls] 41 | Documentation = "https://github.com/XayOn/pyrcrack#readme" 42 | Issues = "https://github.com/XayOn/pyrcrack/issues" 43 | Source = "https://github.com/XayOn/pyrcrack" 44 | 45 | [tool.hatch.version] 46 | path = "pyrcrack/__init__.py" 47 | 48 | [tool.hatch.envs.default] 49 | dependencies = [ 50 | "pymdown-extensions", 51 | "pytest", 52 | "mkdocs-include-markdown-plugin", 53 | "asynctest", 54 | "pytest-cov", 55 | "pydocstyle", 56 | "behave", 57 | "pytest-asyncio", 58 | "mkdocs", 59 | "mkdocs-material", 60 | "ruff", 61 | "pillow", 62 | "pygments", 63 | "mkdocs-jupyter", 64 | "cairosvg", 65 | "mkautodoc", 66 | "mkdocs-literate-nav" 67 | ] 68 | 69 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 70 | content-type = "text/markdown" 71 | fragments = [{ path = "README.md" }, { path = "CHANGELOG.md" }] 72 | 73 | [tool.hatch.envs.default.scripts] 74 | cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=pyrcrack --cov=tests {args}" 75 | no-cov = "cov --no-cov {args}" 76 | 77 | [[tool.hatch.envs.test.matrix]] 78 | python = ["38", "39", "310"] 79 | 80 | [tool.coverage.run] 81 | branch = true 82 | parallel = true 83 | omit = ["pyrcrack/__about__.py"] 84 | 85 | [tool.coverage.report] 86 | exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] 87 | 88 | [tool.hatch.build.targets.sdist] 89 | exclude = [ 90 | "/.github", 91 | "/.pytest_cache", 92 | "/.ruff_cache", 93 | "./.gitignore", 94 | "./mkdocs.yml", 95 | "./tests", 96 | ] 97 | 98 | 99 | [tool.ruff.isort] 100 | known-first-party = ["pyrcrack"] 101 | 102 | [tool.ruff.flake8-quotes] 103 | inline-quotes = "double" 104 | 105 | [tool.ruff] 106 | target-version = "py38" 107 | line-length = 80 108 | select = [ 109 | "A", 110 | "B", 111 | "C", 112 | "E", 113 | "F", 114 | "FBT", 115 | "I", 116 | "M", 117 | "N", 118 | "Q", 119 | "RUF", 120 | "S", 121 | "T", 122 | "U", 123 | "W", 124 | "YTT", 125 | ] 126 | ignore = [] 127 | 128 | [tool.ruff.flake8-tidy-imports] 129 | ban-relative-imports = "parents" 130 | 131 | [tool.ruff.per-file-ignores] 132 | "tests/**/*" = ["I252", "S101", "B011"] 133 | 134 | [tool.commitizen] 135 | tag_format = "v$version" 136 | version_files = ["pyrcrack/__init__.py"] 137 | version = "1.2.6" 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | <div align="center"> 3 | <img src="https://github.com/XayOn/pyrcrack/raw/develop/docs/pythonlovesaircrack.png" role=img> 4 | 5 | | | | 6 | | --- | --- | 7 | | CI/CD | [![CI - Test](https://github.com/xayon/pyrcrack/actions/workflows/test.yml/badge.svg)](https://github.com/xayon/pyrcrack/actions/workflows/test.yml) [![CD - Build Package](https://github.com/xayon/pyrcrack/actions/workflows/release.yml/badge.svg)](https://github.com/xayon/pyrcrack/actions/workflows/release.yml) | 8 | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/pyrcrack.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/pyrcrack/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyrcrack.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/pyrcrack/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyrcrack.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/pyrcrack/) | 9 | | Meta | [![code style - yapf](https://img.shields.io/badge/code%20style-yapf-000000.svg)](https://github.com/psf/black) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) [![imports - isort](https://img.shields.io/badge/imports-isort-ef8336.svg)](https://github.com/pycqa/isort) 10 | 11 | </div> 12 | 13 | **Python aircrack-ng bindings** 14 | 15 | PyrCrack is a Python API exposing a common aircrack-ng API. As AircrackNg will 16 | run in background processes, and produce parseable output both in files and 17 | stdout, the most pythonical approach are context managers, cleaning up after 18 | 19 | This library exports a basic aircrack-ng API aiming to keep always a small 20 | readable codebase. 21 | 22 | This has led to a simple library that executes each of the aircrack-ng's suite commands 23 | and auto-detects its usage instructions. Based on that, it dinamically builds 24 | classes inheriting that usage as docstring and a run() method that accepts 25 | keyword parameters and arguments, and checks them BEFORE trying to run them. 26 | 27 | Some classes expose themselves as async iterators, as airodump-ng's wich 28 | returns access points with its associated clients. 29 | 30 | ## Documentation 31 | 32 | The [documentation](https://davidfrancos.net/pyrcrack) is made with [Material 33 | for MkDocs](https://github.com/squidfunk/mkdocs-material) and is hosted by 34 | [GitHub Pages](https://docs.github.com/en/pages). 35 | 36 | ### Examples 37 | 38 | Be sure to check the python [notebook example](./docs/examples/example.ipynb) 39 | 40 | You can have also have a look at the examples/ folder for some usage examples, 41 | such as the basic "scan for targets", that will list available interfaces, let 42 | you choose one, put it in monitor mode, and scan for targets updating results 43 | each 2 seconds. 44 | 45 | ```python 46 | import asyncio 47 | 48 | import pyrcrack 49 | 50 | from rich.console import Console 51 | from rich.prompt import Prompt 52 | 53 | 54 | async def scan_for_targets(): 55 | """Scan for targets, return json.""" 56 | console = Console() 57 | console.clear() 58 | console.show_cursor(False) 59 | airmon = pyrcrack.AirmonNg() 60 | 61 | interface = Prompt.ask( 62 | 'Select an interface', 63 | choices=[a['interface'] for a in await airmon.interfaces]) 64 | 65 | async with airmon(interface) as mon: 66 | async with pyrcrack.AirodumpNg() as pdump: 67 | async for result in pdump(mon.monitor_interface): 68 | console.clear() 69 | console.print(result.table) 70 | await asyncio.sleep(2) 71 | 72 | 73 | asyncio.run(scan_for_targets()) 74 | ``` 75 | 76 | This snippet of code will produce the following results: 77 | 78 | ![scan](https://raw.githubusercontent.com/XayOn/pyrcrack/master/docs/scan.png) 79 | 80 | 81 | 82 | # Contributors 83 | 84 | ![contributors](https://contrib.rocks/image?repo=xayon/pyrcrack) 85 | 86 | 87 | ## License 88 | 89 | Pyrcrack is distributed under the terms of the [GPL2+](https://spdx.org/licenses/GPL2.html) license. 90 | 91 | -------------------------------------------------------------------------------- /pyrcrack/aireplay.py: -------------------------------------------------------------------------------- 1 | """Aireplay-ng.""" 2 | import asyncio 3 | 4 | from .executor import ExecutorHelper 5 | from .models import AireplayResults 6 | 7 | 8 | class AireplayNg(ExecutorHelper): 9 | """Aireplay-ng 1.6 - (C) 2006-2020 Thomas d'Otreppe 10 | https://www.aircrack-ng.org 11 | 12 | Usage: aireplay-ng <options> <replay interface> 13 | 14 | Options: 15 | 16 | -b bssid : MAC address, Access Point 17 | -d dmac : MAC address, Destination 18 | -s smac : MAC address, Source 19 | -m len : minimum packet length 20 | -n len : maximum packet length 21 | -u type : frame control, type field 22 | -v subt : frame control, subtype field 23 | -t tods : frame control, To DS bit 24 | -f fromds : frame control, From DS bit 25 | -w iswep : frame control, WEP bit 26 | -D : disable AP detection 27 | -x nbpps : number of packets per second 28 | -p fctrl : set frame control word (hex) 29 | -a bssid : set Access Point MAC address 30 | -c dmac : set Destination MAC address 31 | -h smac : set Source MAC address 32 | -g value : change ring buffer size (default: 8) 33 | -F : choose first matching packet 34 | -e essid : set target AP SSID 35 | -o npckts : number of packets per burst (0=auto, default: 1) 36 | -q sec : seconds between keep-alives 37 | -Q : send reassociation requests 38 | -y prga : keystream for shared key auth 39 | -T n : exit after retry fake auth request n time 40 | -j : inject FromDS packets 41 | -k IP : set destination IP in fragments 42 | -l IP : set source IP in fragments 43 | -B : activates the bitrate test 44 | -i iface : capture packets from this interface 45 | -r file : extract packets from this pcap file 46 | -R : disable /dev/rtc usage 47 | --ignore-negative-one : if the interface's channel can't be determined 48 | --deauth-rc <rc> : Deauthentication reason code [0-254] 49 | --deauth <count> : deauthenticate 1 or all stations (-0) 50 | --fakeauth <delay> : fake authentication with AP (-1) 51 | --interactive : interactive frame selection (-2) 52 | --arpreplay : standard ARP-request replay (-3) 53 | --chopchop : decrypt/chopchop WEP packet (-4) 54 | --fragment : generates valid keystream (-5) 55 | --caffe-latte : query a client for new IVs (-6) 56 | --cfrag : fragments against a client (-7) 57 | --migmode : attacks WPA migration mode (-8) 58 | --test : tests injection and quality (-9) 59 | --help : Displays this usage screen 60 | """ 61 | 62 | command = "aireplay-ng" 63 | requires_tempfile = False 64 | requires_tempdir = False 65 | requires_root = True 66 | 67 | async def run(self, *args, **kwargs): 68 | """Run async, with prefix stablished as tempdir.""" 69 | asyncio.create_task(self.result_updater()) 70 | return await super().run(*args, **kwargs) 71 | 72 | @property 73 | async def results(self): 74 | return self.meta.get("result", AireplayResults("")) 75 | 76 | async def result_updater(self): 77 | """Set result on local object.""" 78 | # TODO: There might be a situation here where proc is never != None. 79 | # This should have some timeout logic as airodump has 80 | while not self.proc: 81 | await asyncio.sleep(1) 82 | 83 | while self.proc.returncode is None: 84 | self.meta["result"] = await self.get_results() 85 | await asyncio.sleep(2) 86 | 87 | async def get_results(self): 88 | """Get results list.""" 89 | if not self.proc: 90 | return AireplayResults("") 91 | return AireplayResults((await self.proc.communicate())[0].decode()) 92 | -------------------------------------------------------------------------------- /tests/unit/test_airmon.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | async def test_run_airmon(): 6 | """Test main run method.""" 7 | import asynctest 8 | 9 | from pyrcrack import AirmonNg 10 | 11 | with asynctest.mock.patch("asyncio.create_subprocess_exec") as runmock: 12 | with pytest.raises(Exception): 13 | await AirmonNg().run("foo") 14 | await AirmonNg().run("start") 15 | 16 | runmock.assert_called_with( 17 | "airmon-ng", "start", stderr=-1, stdout=-1, stdin=-1) 18 | 19 | with asynctest.mock.patch("asyncio.create_subprocess_exec") as runmock: 20 | await AirmonNg().run() 21 | runmock.assert_called_with("airmon-ng", stderr=-1, stdout=-1, stdin=-1) 22 | 23 | 24 | def test_run_model(): 25 | from pyrcrack.models import Interfaces 26 | 27 | data = [ 28 | b"Found 6 processes that could cause trouble.", 29 | b"Kill them using 'airmon-ng check kill' before putting", 30 | b"the card in monitor mode, they will interfere by changing channels", 31 | b"and sometimes putting the interface back in managed mode", 32 | b" PID Name", 33 | b" 239 avahi-daemon", 34 | b" 241 wpa_supplicant", 35 | b" 243 avahi-daemon", 36 | b" 356 wpa_supplicant", 37 | b" 381 wpa_supplicant", 38 | b" 481 dhcpcd", 39 | b"PHY\tInterface\tDriver\t\tChipset", 40 | b"phy0\twlan0\t\tbrcmfmac\tBroadcom 43430", 41 | ( 42 | b"\t\t(mac80211 monitor mode vif enabled for [phy0]wlan0" 43 | b" on [phy0]wlan0mon)" 44 | ), 45 | b"\t\t(mac80211 station mode vif disabled for [phy0]wlan0)", 46 | b"phy1\twlxe84e066b5386\tmt7601u\t\tRalink Technology, Corp. MT7601U", 47 | ] 48 | assert [a.__dict__ for a in Interfaces(data)] == [ 49 | { 50 | "data": { 51 | "phy": "phy0", 52 | "interface": "wlan0", 53 | "driver": "brcmfmac", 54 | "chipset": "Broadcom 43430", 55 | "monitor": { 56 | "driver": "mac80211", 57 | "interface": "wlan0mon", 58 | "mode": "monitor", 59 | "original_interface": "wlan0", 60 | "status": "enabled", 61 | }, 62 | } 63 | }, 64 | { 65 | "data": { 66 | "phy": "phy1", 67 | "interface": "wlxe84e066b5386", 68 | "driver": "mt7601u", 69 | "chipset": "Ralink Technology, Corp. MT7601U", 70 | } 71 | }, 72 | ] 73 | 74 | data = b"""Found 7 processes that could cause trouble. 75 | Kill them using 'airmon-ng check kill' before putting 76 | the card in monitor mode, they will interfere by changing channels 77 | and sometimes putting the interface back in managed mode 78 | 79 | PID Name 80 | 677 wpa_supplicant 81 | 2209 dhcpcd 82 | 2210 dhcpcd 83 | 2211 dhcpcd 84 | 107797 dhcpcd 85 | 107805 dhcpcd 86 | 107853 dhcpcd 87 | 88 | PHY Interface Driver Chipset 89 | 90 | phy0 wlp3s0 iwlwifi Intel Corporation Wireless 7260 (rev 83) 91 | 92 | \t\t(mac80211 monitor mode vif enabled for [phy0]wlp3s0 on [phy0]wlp3s0mon) 93 | \t\t(mac80211 station mode vif disabled for [phy0]wlp3s0) 94 | """.split( 95 | b"\n" 96 | ) 97 | 98 | assert [a.__dict__ for a in Interfaces(data)] == [ 99 | { 100 | "data": { 101 | "phy": "phy0", 102 | "interface": "wlp3s0", 103 | "driver": "iwlwifi", 104 | "chipset": "Intel Corporation Wireless 7260 (rev 83)", 105 | "monitor": { 106 | "driver": "mac80211", 107 | "interface": "wlp3s0mon", 108 | "mode": "monitor", 109 | "original_interface": "wlp3s0", 110 | "status": "enabled", 111 | }, 112 | } 113 | } 114 | ] 115 | -------------------------------------------------------------------------------- /docs/examples/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Pyrcrack on a python notebook\n", 8 | "\n", 9 | "This is a simple demonstration of pyrcrack being used in a python notebook. \n", 10 | "\n", 11 | "This allows for a powerful web interface, with saved states and extensible, useful for everyday penetration testing tasks.\n", 12 | "\n", 13 | "### Extracting available wifi interfaces\n", 14 | "\n", 15 | "First, we're going to use airmon to get the available wireless interfaces and its drivers." 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 36, 21 | "metadata": {}, 22 | "outputs": [ 23 | { 24 | "data": { 25 | "text/plain": [ 26 | "[{'phy': 'phy1',\n", 27 | " 'interface': 'wlp3s0',\n", 28 | " 'driver': 'iwlwifi',\n", 29 | " 'chipset': 'Intel Corporation Wireless 7260 (rev 83)'}]" 30 | ] 31 | }, 32 | "execution_count": 36, 33 | "metadata": {}, 34 | "output_type": "execute_result" 35 | } 36 | ], 37 | "source": [ 38 | "import pyrcrack\n", 39 | "\n", 40 | "airmon = pyrcrack.AirmonNg()\n", 41 | "\n", 42 | "[a.asdict() for a in await airmon.interfaces]" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "### Scanning\n", 50 | "\n", 51 | "Now we'll use airmon-ng, without bounding it to any access point or channel so it scans freely" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 26, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "data": { 61 | "text/plain": [ 62 | "[TESTNET - (FF:FF:FF:FF:FF:FF)]" 63 | ] 64 | }, 65 | "execution_count": 26, 66 | "metadata": {}, 67 | "output_type": "execute_result" 68 | } 69 | ], 70 | "source": [ 71 | "async with airmon(\"wlp3s0\") as mon:\n", 72 | " async with pyrcrack.AirodumpNg() as pdump:\n", 73 | " async for aps in pdump(mon.monitor_interface):\n", 74 | " break # So notebook execution doesn't get stuck here \n", 75 | "aps" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "### Now, select just THAT specific AP\n", 83 | "\n", 84 | "The airodump property of any ap will return the required parameters for airodump-ng to select that client." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 23, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "data": { 94 | "text/plain": [ 95 | "{'channel': '11', 'bssid': 'FF:FF:FF:FF:FF:FF'}" 96 | ] 97 | }, 98 | "execution_count": 23, 99 | "metadata": {}, 100 | "output_type": "execute_result" 101 | } 102 | ], 103 | "source": [ 104 | "aps[0].airodump" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 37, 110 | "metadata": {}, 111 | "outputs": [ 112 | { 113 | "data": { 114 | "text/plain": [ 115 | "[{'essid': 'TESTNET',\n", 116 | " 'bssid': 'FF:FF:FF:FF:FF:FF',\n", 117 | " 'packets': '2',\n", 118 | " 'dbm': '-74',\n", 119 | " 'score': '76',\n", 120 | " 'channel': '1',\n", 121 | " 'encryption': 'WPA+PSK/WPA+AES-CCM'}]" 122 | ] 123 | }, 124 | "execution_count": 37, 125 | "metadata": {}, 126 | "output_type": "execute_result" 127 | } 128 | ], 129 | "source": [ 130 | "async with airmon(\"wlp3s0\") as mon:\n", 131 | " # Re-set the interface in monitor mode as in \n", 132 | " # previous execution it would have been cleaned up\n", 133 | " async with pyrcrack.AirodumpNg() as pdump:\n", 134 | " async for result in pdump(mon.monitor_interface, **aps[0].airodump):\n", 135 | " break\n", 136 | " \n", 137 | "[a.asdict() for a in result]" 138 | ] 139 | } 140 | ], 141 | "metadata": { 142 | "kernelspec": { 143 | "display_name": "Python 3", 144 | "language": "python", 145 | "name": "python3" 146 | }, 147 | "language_info": { 148 | "codemirror_mode": { 149 | "name": "ipython", 150 | "version": 3 151 | }, 152 | "file_extension": ".py", 153 | "mimetype": "text/x-python", 154 | "name": "python", 155 | "nbconvert_exporter": "python", 156 | "pygments_lexer": "ipython3", 157 | "version": "3.8.5" 158 | } 159 | }, 160 | "nbformat": 4, 161 | "nbformat_minor": 4 162 | } 163 | -------------------------------------------------------------------------------- /pyrcrack/airodump.py: -------------------------------------------------------------------------------- 1 | """Airodump.""" 2 | 3 | import asyncio 4 | import os 5 | import xml 6 | from contextlib import suppress 7 | 8 | import dotmap 9 | import xmltodict 10 | from async_timeout import timeout 11 | 12 | from .executor import ExecutorHelper 13 | from .models import AccessPoint, Result 14 | 15 | 16 | class AirodumpNg(ExecutorHelper): 17 | """Airodump-ng 1.6 - (C) 2006-2020 Thomas d'Otreppe 18 | https://www.aircrack-ng.org 19 | 20 | usage: airodump-ng <options> <interface>[,<interface>,...] 21 | 22 | Options: 23 | --ivs : Save only captured IVs 24 | --gpsd : Use GPSd 25 | --write <prefix> : Dump file prefix 26 | --beacons : Record all beacons in dump file 27 | --update <secs> : Display update delay in seconds 28 | --showack : Prints ack/cts/rts statistics 29 | -h : Hides known stations for --showack 30 | -f <msecs> : Time in ms between hopping channels 31 | --berlin <secs> : Time before removing the AP/client 32 | from the screen when no more packets 33 | are received (Default: 120 seconds) 34 | -r <file> : Read packets from that file 35 | -T : While reading packets from a file, 36 | simulate the arrival rate of them 37 | as if they were "live". 38 | -x <msecs> : Active Scanning Simulation 39 | --manufacturer : Display manufacturer from IEEE OUI list 40 | --uptime : Display AP Uptime from Beacon Timestamp 41 | --wps : Display WPS information (if any) 42 | --output-format <formats> : Output format. Possible values: 43 | pcap, ivs, csv, gps, kismet, netxml, 44 | logcsv 45 | --ignore-negative-one : Removes the message that says 46 | fixed channel <interface>: -1 47 | --write-interval <seconds> : Output file(s) write interval in 48 | seconds 49 | --background <enable> : Override background detection. 50 | -n <int> : Minimum AP packets recv'd before for 51 | displaying it 52 | --encrypt <suite> : Filter APs by cipher suite 53 | --netmask <netmask> : Filter APs by mask 54 | --bssid <bssid> : Filter APs by BSSID 55 | --essid <essid> : Filter APs by ESSID 56 | --essid-regex <regex> : Filter APs by ESSID using a regular 57 | expression 58 | -a : Filter unassociated clients 59 | --ht20 : Set channel to HT20 (802.11n) 60 | --ht40- : Set channel to HT40- (802.11n) 61 | --ht40+ : Set channel to HT40+ (802.11n) 62 | --channel <channels> : Capture on specific channels 63 | --band <abg> : Band on which airodump-ng should hop 64 | -C <frequencies> : Uses these frequencies in MHz to hop 65 | --cswitch <method> : Set channel switching method 66 | -s : same as --cswitch 67 | --help : Displays this usage screen 68 | """ 69 | 70 | requires_tempfile = False 71 | requires_tempdir = True 72 | requires_root = True 73 | command = "airodump-ng" 74 | 75 | async def run(self, *args, **kwargs): 76 | """Run async, with prefix stablished as tempdir.""" 77 | self.execn += 1 78 | 79 | kwargs = { 80 | "background": 1, 81 | "write": self.tempdir.name + "/" + self.uuid, 82 | "write-interval": 1, 83 | "output-format": "netxml,logcsv", 84 | **kwargs, 85 | } 86 | 87 | if "kismet" not in kwargs.get("output-format", ""): 88 | kwargs["output-format"] += ",netxml" 89 | 90 | return await super().run(*args, **kwargs) 91 | 92 | def get_file(self, file_format) -> str: 93 | """Return csv file, not kismet one. 94 | 95 | Arguments: 96 | 97 | file_format: File extension to retrieve (kismet.csv kismet.xml csv 98 | log.csv or pcap) 99 | 100 | Returns: full filename 101 | """ 102 | return f"{self.tempdir.name}/{self.uuid}-{self.execn:02}.{file_format}" 103 | 104 | @property 105 | async def results(self) -> list: 106 | """Return a list of currently detected access points. 107 | 108 | Returns: List of AccessPoint instances 109 | """ 110 | file = self.get_file("kismet.netxml") 111 | try: 112 | # Wait for a sensible 3 seconds for netxml file to be generated and 113 | # process to be running 114 | async with timeout(3): 115 | while not os.path.exists(file): 116 | await asyncio.sleep(1) 117 | 118 | while not self.proc: 119 | # Check if airodump is running, otherwise wait more. 120 | await asyncio.sleep(1) 121 | except asyncio.exceptions.TimeoutError as err: 122 | # No file had been generated or process hadn't started in 3 123 | # seconds. 124 | res = "Unknown" 125 | if self.proc: 126 | res = await self.proc.communicate() 127 | raise Exception("\n".join([a.decode() for a in res])) from err 128 | 129 | while self.running: 130 | # Avoid crashing on file creation 131 | with suppress(ValueError, xml.parsers.expat.ExpatError): 132 | xmla = xmltodict.parse(open(file).read()) 133 | dotmap_data = dotmap.DotMap(xmla) 134 | results = dotmap_data["detection-run"]["wireless-network"] 135 | if results: 136 | if isinstance(results, list): 137 | breakpoint() 138 | return Result( 139 | sorted( 140 | [AccessPoint(ap) for ap in results], 141 | key=lambda x: x.score, 142 | reverse=True, 143 | )) 144 | else: 145 | return Result([AccessPoint(results)]) 146 | return Result([]) 147 | 148 | await asyncio.sleep(1) 149 | return Result([]) 150 | -------------------------------------------------------------------------------- /pyrcrack/models.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import io 3 | import re 4 | 5 | from parse import parse 6 | from rich.table import Table 7 | 8 | DICTS = ("WLAN_", "JAZZTEL_", "MOVISTAR_") 9 | APL_FMT = "{date} Sending {attack} (code {num}) to {dest} -- BSSID: [{bssid}]" 10 | MONITOR_RE = re.compile( 11 | r"\t\t\((\w+) (\w+) mode vif (\w+) for \[\w+\](\w+) on \[\w+\](\w+)\)") 12 | 13 | 14 | class Result(list): 15 | 16 | @property 17 | def table(self): 18 | """Return a nicely formatted table with results.""" 19 | table = Table(show_header=True, 20 | header_style="bold magenta", 21 | show_footer=False) 22 | if self: 23 | keys = self[0].asdict().keys() 24 | for key in keys: 25 | table.add_column(key.capitalize()) 26 | 27 | for res in self: 28 | res = res.asdict() 29 | table.add_row(*[res[k] for k in keys]) 30 | return table 31 | 32 | 33 | class Interface: 34 | 35 | def __init__(self, data, monitor_data): 36 | self.data = data 37 | for data in monitor_data: 38 | if data["original_interface"] == self.data["interface"]: 39 | self.data[data["mode"]] = data 40 | 41 | @property 42 | def interface(self): 43 | return self.data["interface"] 44 | 45 | def __eq__(self, other): 46 | if isinstance(other, Interface): 47 | return other.interface == self.interface 48 | return self.interface == other 49 | 50 | @property 51 | def monitor(self): 52 | return self.data.get("monitor", {}).get("interface", 53 | self.data["interface"]) 54 | 55 | def asdict(self): 56 | return self.data 57 | 58 | def __str__(self): 59 | return self.interface 60 | 61 | 62 | class Interfaces(Result): 63 | 64 | def __init__(self, data): 65 | if data == [b"Run it as root"]: 66 | raise Exception("Pyrcrack must be run as root") 67 | pos = data.index(b"PHY Interface Driver Chipset") 68 | ifaces_data = self.parse(b"\n".join( 69 | [a for a in data[pos:] if a and not a.startswith(b"\t\t")])) 70 | monitor_data = filter(lambda x: MONITOR_RE.match(x.decode()), 71 | data[pos + len(ifaces_data):]) 72 | 73 | def groups(data): 74 | return MONITOR_RE.match(data.decode()).groups() 75 | 76 | keys = ["driver", "mode", "status", "original_interface", "interface"] 77 | monitor_data = [dict(zip(keys, groups(a))) for a in monitor_data] 78 | self.extend([Interface(a, monitor_data) for a in ifaces_data]) 79 | 80 | @staticmethod 81 | def parse(res): 82 | """Parse csv results.""" 83 | with io.StringIO() as fileo: 84 | fileo.write(res.decode().strip().replace("\t\t", "\t")) 85 | fileo.seek(0) 86 | reader = csv.DictReader(fileo, dialect="excel-tab") 87 | return [{a.lower(): b for a, b in row.items()} for row in reader] 88 | 89 | 90 | class Client: 91 | 92 | def __init__(self, data): 93 | self.data = data 94 | 95 | @property 96 | def bssid(self): 97 | return self.data["client-mac"] 98 | 99 | @property 100 | def packets(self): 101 | return getattr(self.data.packets, "total", 0) 102 | 103 | @property 104 | def dbm(self): 105 | return self.data["snr-info"].last_signal_dbm 106 | 107 | 108 | class AccessPoint(dict): 109 | """Represents an access point. 110 | 111 | Stores internal data in "data" property 112 | """ 113 | 114 | def __init__(self, data): 115 | """Initialize an access point. 116 | 117 | Arguments: 118 | 119 | data: Dot data structure from kismet xml file. 120 | """ 121 | self.data = data 122 | self.update(self.asdict()) 123 | 124 | def __repr__(self): 125 | return f"{self.essid} - ({self.bssid})" 126 | 127 | @property 128 | def airodump(self): 129 | return {"channel": self.channel, "bssid": self.bssid} 130 | 131 | @property 132 | def channel(self): 133 | return self.data.channel 134 | 135 | @property 136 | def encryption(self): 137 | return self.data.SSID.encryption 138 | 139 | @property 140 | def clients(self): 141 | """List of connected clients. 142 | 143 | Returns: 144 | 145 | List of Client instance. 146 | """ 147 | if isinstance(self.data["wireless-client"], list): 148 | return [Client(d) for d in self.data["wireless-client"]] 149 | else: 150 | return [Client(self.data["wireless-client"])] 151 | 152 | @property 153 | def total_packets(self): 154 | return getattr(self.packets, "total", 0) 155 | 156 | def asdict(self): 157 | return { 158 | "essid": self.essid, 159 | "bssid": self.bssid, 160 | "packets": str(self.total_packets), 161 | "dbm": str(self.dbm), 162 | "score": str(self.score), 163 | "channel": str(self.channel), 164 | "encryption": "/".join(self.encryption), 165 | } 166 | 167 | @property 168 | def essid(self): 169 | """Essid.""" 170 | return self.data.SSID.essid.get("#text", "") 171 | 172 | @property 173 | def bssid(self): 174 | """Mac (BSSID)""" 175 | return self.data.BSSID 176 | 177 | @property 178 | def score(self): 179 | """Score, used to sort networks. 180 | 181 | Score will take in account the total packets received, the dbm 182 | and if a ssid is susceptible to have dictionaries. 183 | """ 184 | packet_score = int(self.total_packets) 185 | dbm_score = -int(self.dbm) 186 | dict_score = bool(any(self.essid.startswith(a) for a in DICTS)) 187 | name_score = -1000 if not self.essid else 0 188 | enc_score = 1000 if "WEP" in self.encryption else 0 189 | return packet_score + dbm_score + dict_score + name_score + enc_score 190 | 191 | @property 192 | def packets(self): 193 | """Return list of packets.""" 194 | return self.data.packets 195 | 196 | @property 197 | def dbm(self): 198 | """Return dbm info.""" 199 | return self.data["snr-info"].last_signal_dbm 200 | 201 | def __lt__(self, other): 202 | """Compare with score.""" 203 | return self.score 204 | 205 | 206 | class AireplayResult(dict): 207 | """Single aiplay result line.""" 208 | 209 | def __init__(self, *args, **kwargs): 210 | data = parse(APL_FMT, kwargs.pop("_data")) 211 | if data: 212 | self.update(data.named) 213 | super().__init__(*args, **kwargs) 214 | 215 | def asdict(self): 216 | return self 217 | 218 | 219 | class AireplayResults(Result): 220 | """Aireplay results.""" 221 | 222 | def __init__(self, data): 223 | self.extend(AireplayResult(_data=a) for a in data.split("\n") 224 | if "BSSID" in a) 225 | -------------------------------------------------------------------------------- /pyrcrack/executor.py: -------------------------------------------------------------------------------- 1 | """Pyrcrack-ng Executor helper.""" 2 | import asyncio 3 | import functools 4 | import itertools 5 | import logging 6 | import os 7 | import subprocess 8 | import tempfile 9 | import uuid 10 | import warnings 11 | from contextlib import suppress 12 | from contextvars import ContextVar 13 | 14 | import docopt 15 | import stringcase 16 | 17 | logging.basicConfig(level=logging.INFO) 18 | 19 | 20 | class Option: 21 | """Represents a single option (e.g, -e).""" 22 | 23 | def __init__(self, usage, word=None, value=None, logger=None): 24 | """Set option parameters.""" 25 | self.usage = usage 26 | self.word = word 27 | self.logger = logger 28 | self.value = value 29 | keys = usage.keys() 30 | self.is_short = Option.short(word) in keys 31 | self.is_long = Option.long(word) in keys 32 | self.expects_args = bool(usage[self.formatted]) 33 | self.logger.debug("Parsing option %s:%s", self.word, self.value) 34 | 35 | @property 36 | @functools.lru_cache # noqa: B019 37 | def formatted(self): 38 | """Format given option acording to definition.""" 39 | result = Option.short(self.word) if self.is_short else Option.long( 40 | self.word) 41 | 42 | if self.usage.get(result): 43 | return result 44 | 45 | sword = self.word.replace("_", "-") 46 | return Option.short(sword) if self.is_short else Option.long(sword) 47 | 48 | @staticmethod 49 | def long(word): 50 | """Extract long format option.""" 51 | return f"--{word}" 52 | 53 | @staticmethod 54 | def short(word): 55 | """Extract short format option.""" 56 | return f"-{word}" 57 | 58 | @property 59 | def parsed(self): 60 | """Returns key, value if value is required.""" 61 | if self.expects_args: 62 | return (self.formatted, str(self.value)) 63 | return (self.formatted, ) 64 | 65 | def __repr__(self): 66 | return f"Option(<{self.parsed}>, {self.is_short}, {self.expects_args})" 67 | 68 | 69 | def check(): 70 | """Check if aircrack-ng is compatible.""" 71 | ver_check = subprocess.check_output(["aircrack-ng", "--help"]) 72 | if b"Aircrack-ng 1.6" not in ver_check: 73 | if b"Aircrack-ng 1.7" not in ver_check: 74 | raise Exception("Unsupported Aircrack-ng detected") 75 | else: 76 | warnings.warn( 77 | "Aircrack-ng 1.7 detected, some features may not work") 78 | 79 | 80 | class ExecutorHelper: 81 | """Abstract class interface to a shell command.""" 82 | 83 | requires_tempfile = False 84 | requires_tempdir = False 85 | requires_root = True 86 | command = "" 87 | 88 | def __init__(self): 89 | """Set docstring.""" 90 | if not self.__doc__: 91 | self.__doc__ = self.helpstr 92 | self.uuid = uuid.uuid4().hex 93 | self.called = False 94 | self.execn = 0 95 | self.logger = logging.getLogger(self.__class__.__name__) 96 | self.proc = None 97 | self.meta = {} 98 | self.debug = os.getenv("PYRCRACK_DEBUG", "") == 1 99 | self.tempfile = None 100 | self.tempdir = None 101 | if self.requires_tempfile: 102 | self.tempfile = tempfile.NamedTemporaryFile() 103 | elif self.requires_tempdir: 104 | self.tempdir = tempfile.TemporaryDirectory() 105 | if self.requires_root and not os.getenv("SKIP_ROOT_CHECK"): 106 | if os.geteuid() != 0: 107 | raise Exception("Must be run as root") 108 | if not os.getenv("SKIP_VERSION_CHECK"): 109 | check() 110 | 111 | @property 112 | @functools.lru_cache # noqa: B019 113 | def helpstr(self): 114 | """Extract help string for current command.""" 115 | helpcmd = f"{self.command} 2>&1; echo" 116 | return subprocess.check_output(helpcmd, shell=True).decode() 117 | 118 | @property 119 | @functools.lru_cache # noqa: B019 120 | def usage(self): 121 | """Extract usage from a specified command. 122 | 123 | This is useful if usage not defined in subclass, but it is 124 | recommended to define them there. 125 | """ 126 | opt = docopt.parse_defaults(self.__doc__) 127 | return dict({a.short or a.long: bool(a.argcount) for a in opt}) 128 | 129 | def _run(self, *args, **kwargs): 130 | """Check command usage and execute it. 131 | 132 | If self.sync is defined, it will return process call output, 133 | and launch it blockingly. 134 | 135 | Otherwise it will call asyncio.create_subprocess_exec() 136 | """ 137 | if not self.command: 138 | raise Exception("Subclassing error, please specify a base cmd") 139 | 140 | self.logger.debug("Parsing options: %s", kwargs) 141 | options = [ 142 | Option(self.usage, a, v, self.logger) for a, v in kwargs.items() 143 | ] 144 | self.logger.debug("Got options: %s", options) 145 | 146 | opts = ([self.command, *list(args), *list(itertools.chain(*(o.parsed for o in options)))]) 147 | 148 | self.logger.debug("Running command: %s", opts) 149 | return opts 150 | 151 | @staticmethod 152 | def resolve(val): 153 | """Force string conversion. 154 | 155 | - In case an Interface object comes on args 156 | - In case a contextvar comes, retrieve its value 157 | """ 158 | # I'm a little conflicted on this one, as it seems dirty to cast 159 | # everything to string 160 | # Yet, I'm letting it go like this because in the end, this is a 161 | # subprocess call that requires all args to be strings or bytes... 162 | # This will also allow us to use an AP object in a future. 163 | if isinstance(val, ContextVar): 164 | return str(val.get()) 165 | return str(val) 166 | 167 | async def run(self, *args, **kwargs): 168 | """Run asynchronously.""" 169 | opts = self._run(*args, **kwargs) 170 | self.proc = await asyncio.create_subprocess_exec( 171 | *[self.resolve(a) for a in opts], 172 | stdin=subprocess.PIPE, 173 | stdout=subprocess.PIPE, 174 | stderr=subprocess.PIPE, 175 | ) 176 | return self.proc 177 | 178 | def __call__(self, *args, **kwargs): 179 | self.run_args = args, kwargs 180 | return self 181 | 182 | def __aiter__(self): 183 | """Defines us as an async iterator.""" 184 | return self 185 | 186 | async def __anext__(self): 187 | """Get the next result batch.""" 188 | if not self.called: 189 | self.called = True 190 | self.proc = await self.run(*self.run_args[0], **self.run_args[1]) 191 | 192 | if not asyncio.iscoroutine(self.results): 193 | raise RuntimeError("Results must be a coroutine") 194 | 195 | current_results = await self.results 196 | 197 | if not self.running: 198 | # If we haven't sent the latests results, send them now. 199 | if self.last_results != current_results: 200 | self.last_results = current_results 201 | return current_results 202 | raise StopAsyncIteration 203 | 204 | self.last_results = current_results 205 | return self.last_results 206 | 207 | @property 208 | def running(self): 209 | if not self.proc: 210 | return False 211 | return self.proc.returncode is None 212 | 213 | async def readlines(self): 214 | """Return lines as per proc.communicate, non-empty ones.""" 215 | if not self.proc: 216 | return [] 217 | com = await self.proc.communicate() 218 | return [a for a in com[0].split(b"\n") if a] 219 | 220 | @property 221 | async def results(self): 222 | return [self.proc] 223 | 224 | async def __aexit__(self, exc_type, exc_val, exc_tb): 225 | """Clean up conext manager.""" 226 | if self.debug and (self.requires_tempfile or self.requires_tempdir): 227 | self.logger.error(f"Not deleting {self.tempfile}, {self.tempdir}") 228 | 229 | if self.tempfile: 230 | self.tempfile.__exit__(exc_type, exc_val, exc_tb) 231 | 232 | if self.tempdir: 233 | self.tempdir.__exit__(exc_type, exc_val, exc_tb) 234 | 235 | if exc_type in (ProcessLookupError, subprocess.CalledProcessError): 236 | return True 237 | 238 | if self.proc: 239 | with suppress(Exception): 240 | self.proc.kill() 241 | 242 | async def __aenter__(self): 243 | """Create temporary directories and files if required.""" 244 | if self.requires_tempfile and self.tempfile: 245 | self.tempfile.__enter__() 246 | elif self.requires_tempdir and self.tempdir: 247 | self.tempdir.__enter__() 248 | return self 249 | 250 | 251 | def stc(command): 252 | """Convert snake case to camelcase in class format.""" 253 | return stringcase.pascalcase(command.replace("-", "_")) 254 | -------------------------------------------------------------------------------- /docs/icon.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg:svg 5 | version="1.0" 6 | id="svg2" 7 | sodipodi:version="0.32" 8 | inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" 9 | sodipodi:docname="pyrcrack.svg" 10 | width="72.823753pt" 11 | height="72.986252pt" 12 | inkscape:export-filename="python-logo-only.png" 13 | inkscape:export-xdpi="232.44" 14 | inkscape:export-ydpi="232.44" 15 | native-dark-active="" 16 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 17 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 18 | xmlns:xlink="http://www.w3.org/1999/xlink" 19 | xmlns:svg="http://www.w3.org/2000/svg" 20 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 21 | xmlns:cc="http://creativecommons.org/ns#" 22 | xmlns:dc="http://purl.org/dc/elements/1.1/"> 23 | <link 24 | type="text/css" 25 | rel="stylesheet" 26 | id="dark-mode-custom-link" /> 27 | <link 28 | type="text/css" 29 | rel="stylesheet" 30 | id="dark-mode-general-link" /> 31 | <style 32 | lang="en" 33 | type="text/css" 34 | id="dark-mode-custom-style" /> 35 | <style 36 | lang="en" 37 | type="text/css" 38 | id="dark-mode-native-style">:root, ::after, ::before, ::backdrop { 39 | --native-dark-opacity: 0.85; 40 | --native-dark-bg-color: #292929; 41 | --native-dark-text-shadow: none; 42 | --native-dark-font-color: #dcdcdc; 43 | --native-dark-link-color: #8db2e5; 44 | --native-dark-cite-color: #92de92; 45 | --native-dark-fill-color: #7d7d7d; 46 | --native-dark-border-color: #555555; 47 | --native-dark-visited-link-color: #c76ed7; 48 | --native-dark-transparent-color: transparent; 49 | --native-dark-bg-image-color: rgba(0, 0, 0, 0.25); 50 | --native-dark-box-shadow: 0 0 0 1px rgb(255 255 255 / 10%); 51 | --native-dark-bg-image-filter: brightness(50%) contrast(200%) 52 | } 53 | 54 | :root { 55 | color-scheme: dark; 56 | } 57 | 58 | html a:visited, 59 | html a:visited > * { 60 | color: var(--native-dark-visited-link-color) !important; 61 | } 62 | 63 | a[ping]:link, 64 | a[ping]:link > *, 65 | :link:not(cite) { 66 | color: var(--native-dark-link-color) !important; 67 | } 68 | 69 | html cite, 70 | html cite a:link, 71 | html cite a:visited { 72 | color: var(--native-dark-cite-color) !important; 73 | } 74 | 75 | img, 76 | image, 77 | figure:empty { 78 | opacity: var(--native-dark-opacity) !important; 79 | } 80 | </style> 81 | <style 82 | lang="en" 83 | type="text/css" 84 | id="dark-mode-native-sheet" /> 85 | <svg:metadata 86 | id="metadata371"> 87 | <rdf:RDF> 88 | <cc:Work 89 | rdf:about=""> 90 | <dc:format>image/svg+xml</dc:format> 91 | <dc:type 92 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 93 | </cc:Work> 94 | </rdf:RDF> 95 | </svg:metadata> 96 | <sodipodi:namedview 97 | inkscape:window-height="720" 98 | inkscape:window-width="1280" 99 | inkscape:pageshadow="2" 100 | inkscape:pageopacity="0.0" 101 | guidetolerance="10.0" 102 | gridtolerance="10.0" 103 | objecttolerance="10.0" 104 | borderopacity="1.0" 105 | bordercolor="#666666" 106 | pagecolor="#ffffff" 107 | id="base" 108 | inkscape:zoom="3.0351345" 109 | inkscape:cx="55.84596" 110 | inkscape:cy="67.707048" 111 | inkscape:window-x="0" 112 | inkscape:window-y="0" 113 | inkscape:current-layer="g1374-3" 114 | width="210mm" 115 | height="40mm" 116 | units="mm" 117 | inkscape:showpageshadow="2" 118 | inkscape:pagecheckerboard="0" 119 | inkscape:deskcolor="#d1d1d1" 120 | inkscape:document-units="pt" 121 | showgrid="false" 122 | inkscape:window-maximized="1" /> 123 | <svg:defs 124 | id="defs4"> 125 | <svg:linearGradient 126 | id="linearGradient4671"> 127 | <svg:stop 128 | style="stop-color:#cfcfcf;stop-opacity:1" 129 | offset="0" 130 | id="stop4673" /> 131 | <svg:stop 132 | style="stop-color:#e2e2e2;stop-opacity:1" 133 | offset="1" 134 | id="stop4675" /> 135 | </svg:linearGradient> 136 | <svg:linearGradient 137 | id="linearGradient4689"> 138 | <svg:stop 139 | style="stop-color:#909090;stop-opacity:1" 140 | offset="0" 141 | id="stop4691" /> 142 | <svg:stop 143 | style="stop-color:#5d5d5d;stop-opacity:1" 144 | offset="1" 145 | id="stop4693" /> 146 | </svg:linearGradient> 147 | <svg:linearGradient 148 | inkscape:collect="always" 149 | xlink:href="#linearGradient4671" 150 | id="linearGradient1475" 151 | gradientUnits="userSpaceOnUse" 152 | gradientTransform="matrix(0.23645098,0,0,0.23873377,-6.3011674,-4.9188185)" 153 | x1="150.96111" 154 | y1="192.35176" 155 | x2="112.03144" 156 | y2="137.27299" /> 157 | <svg:linearGradient 158 | inkscape:collect="always" 159 | xlink:href="#linearGradient4689" 160 | id="linearGradient1478" 161 | gradientUnits="userSpaceOnUse" 162 | gradientTransform="matrix(0.23645098,0,0,0.23873377,-6.3011674,-4.9188185)" 163 | x1="26.648937" 164 | y1="20.603781" 165 | x2="135.66525" 166 | y2="114.39767" /> 167 | </svg:defs> 168 | <svg:g 169 | id="g355" 170 | transform="matrix(0.77161714,0,0,0.78496295,30.275203,31.987747)"> 171 | <svg:path 172 | style="fill:url(#linearGradient1478);fill-opacity:1;stroke-width:0.420327" 173 | d="M 23.08383,3.8639544e-4 C 21.157198,0.00933851 19.317308,0.17365248 17.698394,0.46011869 12.929277,1.3026651 12.06339,3.0661869 12.06339,6.3184214 v 4.2952126 h 11.270009 v 1.431738 H 12.06339 7.8338529 c -3.2753783,0 -6.1433931,1.968691 -7.04047153,5.713816 -1.03476858,4.292782 -1.08066619,6.971547 0,11.453901 0.80111243,3.336491 2.71427753,5.713815 5.98965483,5.713816 h 3.8748868 v -5.149002 c 0,-3.71985 3.218501,-7.001065 7.040471,-7.001066 h 11.256874 c 3.133514,0 5.635005,-2.580034 5.635004,-5.726951 V 6.3184214 c 0,-3.0542358 -2.576594,-5.34856307 -5.635004,-5.85830271 C 27.019248,0.13784359 25.010461,-0.00856585 23.08383,3.8639544e-4 Z M 16.989093,3.4549462 c 1.164114,0 2.114768,0.966182 2.114768,2.154174 0,1.1837811 -0.950654,2.1410389 -2.114768,2.1410389 -1.168288,-4e-7 -2.114769,-0.9572582 -2.114769,-2.1410389 0,-1.1879916 0.946481,-2.154174 2.114769,-2.154174 z" 174 | id="path1948" /> 175 | <svg:path 176 | style="fill:url(#linearGradient1475);fill-opacity:1;stroke-width:0.420327" 177 | d="m 35.995739,12.045372 v 5.004514 c 0,3.879933 -3.289432,7.145553 -7.040471,7.145554 H 17.698394 c -3.083446,0 -5.635004,2.639014 -5.635004,5.72695 v 10.731464 c 0,3.054238 2.655871,4.850706 5.635004,5.726953 3.567452,1.048971 6.988451,1.238547 11.256874,0 2.83727,-0.821482 5.635005,-2.47472 5.635004,-5.726953 V 36.358643 H 23.333399 v -1.431738 h 11.256873 5.635006 c 3.275379,0 4.495919,-2.284647 5.635,-5.713816 1.176633,-3.530275 1.126568,-6.925208 0,-11.453901 -0.809456,-3.26066 -2.355456,-5.713816 -5.635,-5.713816 z m -6.33117,27.176746 c 1.168289,10e-7 2.114768,0.957259 2.114768,2.141038 0,1.187991 -0.946481,2.154175 -2.114768,2.154175 -1.164116,0 -2.114769,-0.966184 -2.114769,-2.154175 10e-7,-1.183779 0.950652,-2.141038 2.114769,-2.141038 z" 178 | id="path1950" /> 179 | </svg:g> 180 | <svg:g 181 | id="g1374" 182 | transform="matrix(0.48086882,0.57883703,-0.56662118,0.49123593,66.847641,4.7376914)"> 183 | <svg:g 184 | id="g1379" 185 | transform="translate(-75.537976,-17.579496)"> 186 | <svg:path 187 | id="path14351" 188 | d="m 68.695154,67.181983 c 0,0 -3.317076,3.39858 -3.317076,9.861585 m 3.317076,9.820835 c 0,0 -3.317076,-3.39858 -3.317076,-9.861585" 189 | fill-rule="evenodd" 190 | stroke="#3d7ca6" 191 | stroke-linecap="round" 192 | stroke-width="5.19176" 193 | style="fill:#2b0000;stroke:#6e6e6e" /> 194 | <svg:path 195 | id="path14353" 196 | d="m 57.725159,57.336699 c 0,0 -6.642304,6.805308 -6.642304,19.731319 m 6.642304,19.641669 c 0,0 -6.642304,-6.797158 -6.642304,-19.723169" 197 | fill-rule="evenodd" 198 | stroke="#3d7ca6" 199 | stroke-linecap="round" 200 | stroke-width="5.19176" 201 | style="fill:#2b0000;stroke:#393939;stroke-opacity:1" /> 202 | <svg:path 203 | id="path14355" 204 | d="m 45.377806,47.499564 c 0,0 -9.951236,10.195738 -9.951236,29.584755 m 9.951236,29.470651 c 0,0 -9.951236,-10.203886 -9.951236,-29.592903" 205 | fill-rule="evenodd" 206 | stroke="#3d7ca6" 207 | stroke-linecap="round" 208 | stroke-width="5.19176" 209 | style="fill:#2b0000;stroke:#000000;stroke-opacity:1" /> 210 | </svg:g> 211 | </svg:g> 212 | <svg:g 213 | id="g1374-3" 214 | transform="matrix(-0.6033308,-0.44975446,0.43544602,-0.61053998,39.815189,104.34968)"> 215 | <svg:g 216 | id="g1379-6" 217 | transform="matrix(0.98471148,0.15523772,-0.15416454,0.99122217,-56.783925,-25.069772)"> 218 | <svg:path 219 | id="path14351-7" 220 | d="m 68.695154,67.181983 c 0,0 -3.317076,3.39858 -3.317076,9.861585 m 3.317076,9.820835 c 0,0 -3.317076,-3.39858 -3.317076,-9.861585" 221 | fill-rule="evenodd" 222 | stroke="#3d7ca6" 223 | stroke-linecap="round" 224 | stroke-width="5.19176" 225 | style="fill:#2b0000;stroke:#6e6e6e" /> 226 | <svg:path 227 | id="path14353-5" 228 | d="m 57.725159,57.336699 c 0,0 -6.642304,6.805308 -6.642304,19.731319 m 6.642304,19.641669 c 0,0 -6.642304,-6.797158 -6.642304,-19.723169" 229 | fill-rule="evenodd" 230 | stroke="#3d7ca6" 231 | stroke-linecap="round" 232 | stroke-width="5.19176" 233 | style="fill:#2b0000;stroke:#393939;stroke-opacity:1" /> 234 | <svg:path 235 | id="path14355-3" 236 | d="m 45.377806,47.499564 c 0,0 -9.951236,10.195738 -9.951236,29.584755 m 9.951236,29.470651 c 0,0 -9.951236,-10.203886 -9.951236,-29.592903" 237 | fill-rule="evenodd" 238 | stroke="#3d7ca6" 239 | stroke-linecap="round" 240 | stroke-width="5.19176" 241 | style="fill:#2b0000;stroke:#000000;stroke-opacity:1" /> 242 | </svg:g> 243 | </svg:g> 244 | </svg:svg> 245 | --------------------------------------------------------------------------------