├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── dev │ ├── api.rst │ ├── attacks.rst │ ├── flows.rst │ ├── operations.rst │ ├── plugins.rst │ └── special_variables.rst ├── diagrams │ ├── detailed_authentication.uml │ ├── high_level_authentication.uml │ └── raider_flows.uml ├── index.rst ├── make.bat ├── requirements.txt └── user │ ├── architecture.rst │ ├── definitions.rst │ ├── faq.rst │ ├── install.rst │ └── tutorials.rst ├── examples ├── app1 │ ├── 01_main.hy │ └── 02_users.hy ├── app2 │ ├── 01_main.hy │ ├── 02_authentication.hy │ ├── 03_functions.hy │ └── 09_users.hy ├── app3 │ ├── 01_main.hy │ ├── 02_authentication.hy │ ├── 03_functions.hy │ └── 09_users.hy └── app4 │ ├── 01_main.hy │ ├── 02_authentication.hy │ ├── 03_functions.hy │ └── 09_users.hy ├── ext └── logo.png ├── poetry.lock ├── pyproject.toml ├── raider ├── __init__.py ├── __version__.py ├── application.py ├── attacks.py ├── authentication.py ├── config.py ├── flow.py ├── functions.py ├── operations.py ├── plugins │ ├── basic.py │ ├── common.py │ ├── modifiers.py │ └── parsers.py ├── raider.py ├── request.py ├── structures.py ├── user.py └── utils.py ├── scripts ├── authenticate_and_save_session.py ├── fuzz_authenticated_function.py ├── fuzz_authentication_step.py ├── load_session_and_fuzz_function.py └── run_authenticated_function.py └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | ## Python 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Distribution / packaging 9 | build/ 10 | dist/ 11 | *.egg-info/ 12 | *.egg 13 | 14 | # Unit test / coverage reports 15 | .tox/ 16 | .pytest_cache/ 17 | 18 | # mypy 19 | .mypy_cache 20 | 21 | # Emacs 22 | *~ 23 | \#*\# 24 | .\#* 25 | 26 | # Sphinx build files 27 | docs/_build 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | exclude: ^docs/|^scripts/ 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.0.1 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: end-of-file-fixer 10 | - id: check-yaml 11 | - repo: https://github.com/psf/black 12 | rev: 21.6b0 13 | hooks: 14 | - id: black 15 | - repo: https://github.com/PyCQA/isort 16 | rev: 5.4.2 17 | hooks: 18 | - id: isort 19 | - repo: https://github.com/PyCQA/flake8 20 | rev: 3.9.2 21 | hooks: 22 | - id: flake8 23 | - repo: https://github.com/pycqa/pylint 24 | rev: v2.9.3 25 | hooks: 26 | - id: pylint 27 | - repo: https://github.com/pre-commit/mirrors-mypy 28 | rev: v0.910 29 | hooks: 30 | - id: mypy 31 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.5 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | python: 7 | version: 3.8 8 | install: 9 | - requirements: docs/requirements.txt 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release history 2 | 3 | ### 0.2.2 - alpha3 (2021-08-23) 4 | 5 | * Split plugins into common, basic, modifiers and parsers. 6 | * Add Combine modifier. 7 | * Add UrlParser plugin. 8 | * Update documentation with new plugin structure. 9 | 10 | ### 0.2.1 - alpha2 (2021-08-03) 11 | 12 | * Improved the fuzzing module. 13 | * Added request templates. 14 | * Added Combine and Empty plugins. 15 | * Fixed many bugs. 16 | 17 | ### 0.2.0 - alpha1 (2021-08-01) 18 | 19 | * Added new operations and plugins. 20 | * Improved existing operations and plugins. 21 | * Implemented sessions, allowing users to save and load authentication data. 22 | * Implemented basic fuzzing. 23 | * Multiple bug fixes. 24 | * Project directory changed from ``~/.config/raider/apps`` to 25 | ``~/.config/raider/projects``. 26 | * Updated documentation. 27 | 28 | 29 | ### 0.1.3 - prototype (2021-07-20) 30 | 31 | * Raider became open source. 32 | * Package published on PyPi. 33 | * Documentation published on readthedocs. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Raider logo](./ext/logo.png) 2 | 3 | # DEPRECATED Repository 4 | 5 | Raider has become an OWASP project, so the development will move [to their Github account](https://github.com/OWASP/raider). 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | import raider 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "raider" 23 | copyright = "2021, DigeeX" 24 | author = "Daniel Neagaru" 25 | version = raider.__version__ 26 | release = raider.__version__ 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.napoleon", 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.coverage", 38 | "sphinx.ext.viewcode", 39 | "sphinx_autodoc_typehints", 40 | "sphinxcontrib.needs", 41 | "sphinxcontrib.plantuml", 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ["_templates"] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ["_build"] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = "sphinx_rtd_theme" 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ["_static"] 64 | 65 | add_module_names = False 66 | 67 | 68 | # Napoleon settings 69 | napoleon_google_docstring = True 70 | napoleon_include_init_with_doc = False 71 | napoleon_include_private_with_doc = False 72 | napoleon_include_special_with_doc = True 73 | napoleon_use_ivar = False 74 | napoleon_use_param = True 75 | napoleon_use_rtype = True 76 | napoleon_preprocess_types = False 77 | napoleon_type_aliases = None 78 | napoleon_attr_annotations = True 79 | -------------------------------------------------------------------------------- /docs/dev/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | User API reference 4 | ================== 5 | .. module:: raider 6 | 7 | Main Raider class 8 | ----------------- 9 | 10 | .. autoclass:: Raider 11 | :members: 12 | 13 | Config 14 | ------ 15 | 16 | .. automodule:: raider.config 17 | :members: 18 | 19 | Application 20 | ----------- 21 | 22 | .. automodule:: raider.application 23 | :members: 24 | 25 | Authentication 26 | -------------- 27 | 28 | .. automodule:: raider.authentication 29 | :members: 30 | 31 | 32 | Functions 33 | --------- 34 | 35 | .. automodule:: raider.functions 36 | :members: 37 | 38 | 39 | Internal API reference 40 | ====================== 41 | 42 | Request 43 | -------- 44 | 45 | .. automodule:: raider.request 46 | :members: 47 | 48 | 49 | Structures 50 | ---------- 51 | 52 | .. automodule:: raider.structures 53 | :members: 54 | 55 | 56 | User 57 | ---- 58 | 59 | .. automodule:: raider.user 60 | :members: 61 | 62 | 63 | utils 64 | ----- 65 | 66 | .. automodule:: raider.utils 67 | :members: 68 | -------------------------------------------------------------------------------- /docs/dev/attacks.rst: -------------------------------------------------------------------------------- 1 | .. _attacks: 2 | 3 | .. module:: raider.attacks 4 | 5 | Fuzzing 6 | ------- 7 | 8 | .. autoclass:: Fuzz 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/dev/flows.rst: -------------------------------------------------------------------------------- 1 | .. _flows: 2 | 3 | Flows 4 | ===== 5 | 6 | :term:`Flows ` are the main concept in **Raider**, used to 7 | define the HTTP information exchange. Each :term:`request` you want to 8 | send needs its own Flow object. Inside the ``request`` attribute of 9 | the object, needs to be a :class:`Request ` 10 | object containing the definition of the request. This definition can 11 | contain :class:`Plugins ` whose value will be 12 | used when sending the HTTP request. 13 | 14 | .. automodule:: raider.flow 15 | :members: 16 | 17 | 18 | Examples 19 | -------- 20 | 21 | Create the variable ``initialization`` with the Flow. It'll send a 22 | request to the :ref:`_base_url ` using the path 23 | ``admin/``. If the HTTP response code is 200 go to next stage 24 | ``login``. 25 | 26 | .. code-block:: hylang 27 | 28 | (setv initialization 29 | (Flow 30 | :name "initialization" 31 | :request (Request 32 | :method "GET" 33 | :path "admin/") 34 | :operations [(Http 35 | :status 200 36 | :action (NextStage "login"))])) 37 | 38 | 39 | Define Flow ``login``. It will send a POST request to 40 | ``https://www.example.com/admin/login`` with the username and the 41 | password in the body. Extract the cookie ``PHPSESSID`` and store it in 42 | the ``session_id`` plugin. If server responds with HTTP 200 OK, print 43 | ``login successfully``, otherwise quit with the error message ``login 44 | error``. 45 | 46 | .. code-block:: hylang 47 | 48 | (setv username (Variable "username")) 49 | (setv password (Variable "password")) 50 | (setv session_id (Cookie "PHPSESSID")) 51 | 52 | (setv login 53 | (Flow 54 | :name "login" 55 | :request (Request 56 | :method "POST" 57 | :url "https://www.example.com/admin/login" 58 | :data 59 | {"password" password 60 | "username" username}) 61 | :outputs [session_id] 62 | :operations [(Http 63 | :status 200 64 | :action (Print "login successfully") 65 | :otherwise (Error "login error"))])) 66 | 67 | 68 | 69 | Define another ``login`` Flow. Here what's different is the 70 | ``csrf_name`` and ``csrf_value`` plugins. In this application both the 71 | name and the value of the token needs to be extracted, since they change 72 | all the time. They were defined as :class:`Html ` 73 | objects. Later they're being used in the body of the :class:`Request 74 | `. 75 | 76 | If the HTTP response code is 200 means the :term:`MFA ` was enabled and the ``multi_factor`` :term:`stage` 78 | needs to run next. Otherwise, try to log in again. Here the password 79 | is asked from the user by a :class:`Prompt `. 80 | 81 | .. code-block:: hylang 82 | 83 | (setv username (Variable "username")) 84 | (setv password (Prompt "password")) 85 | (setv session_id (Cookie "PHPSESSID")) 86 | 87 | (setv csrf_name 88 | (Html 89 | :name "csrf_name" 90 | :tag "input" 91 | :attributes 92 | {:name "^[0-9A-Fa-f]{10}$" 93 | :value "^[0-9A-Fa-f]{64}$" 94 | :type "hidden"} 95 | :extract "name")) 96 | 97 | (setv csrf_value 98 | (Html 99 | :name "csrf_value" 100 | :tag "input" 101 | :attributes 102 | {:name "^[0-9A-Fa-f]{10}$" 103 | :value "^[0-9A-Fa-f]{64}$" 104 | :type "hidden"} 105 | :extract "value")) 106 | 107 | 108 | (setv login 109 | (Flow 110 | :name "login" 111 | :request (Request 112 | :method "POST" 113 | :path "/login.php" 114 | :cookies [session_id] 115 | :data 116 | {"open" "login" 117 | "action" "customerlogin" 118 | "password" password 119 | "username" username 120 | "redirect" "myaccount" 121 | csrf_name csrf_value}) 122 | :outputs [csrf_name csrf_value] 123 | :operations [(Http 124 | :status 200 125 | :action (NextStage "multi_factor") 126 | :otherwise (NextStage "login"))])) 127 | 128 | -------------------------------------------------------------------------------- /docs/dev/operations.rst: -------------------------------------------------------------------------------- 1 | .. _operations: 2 | 3 | .. module:: raider.operations 4 | 5 | Operations 6 | ========== 7 | 8 | *Raider* operations are pieces of code that will be executed when the 9 | HTTP response is received. The most important one is **NextStage** 10 | which controls the authentication flow. But anything can be done with 11 | the operations, and *Raider* allows writing custom ones in hylang to 12 | enable users to add functionality that isn't supported by the main 13 | code. 14 | 15 | .. _operations_nextstage: 16 | 17 | NextStage 18 | --------- 19 | 20 | Inside the Authentication object NextStage is used to define the 21 | next step of the authentication process. It can also be used inside 22 | "action" attributes of the other Operations to allow conditional 23 | decision making. 24 | 25 | .. code-block:: hylang 26 | 27 | (NextStage "login") 28 | 29 | .. autoclass:: NextStage 30 | :members: 31 | 32 | .. _operations_print: 33 | 34 | Print 35 | ----- 36 | 37 | When this Operation is executed, it will print each of its elements 38 | in a new line. 39 | 40 | .. code-block:: hylang 41 | 42 | (Print 43 | "This will be printed first" 44 | access_token 45 | "This will be printed on the third line") 46 | 47 | (Print.body) 48 | 49 | (Print.headers) 50 | (Print.headers "User-agent") 51 | 52 | (Print.cookies) 53 | (Print.cookies "PHPSESSID") 54 | 55 | 56 | .. autoclass:: Print 57 | :members: 58 | 59 | 60 | .. _operations_save: 61 | 62 | Save 63 | ---- 64 | 65 | When this Operation is executed, it will save its elements 66 | in a file. 67 | 68 | .. code-block:: hylang 69 | 70 | (Save "/tmp/access_token" access_token) 71 | (Save "/tmp/session" session_id :append True) 72 | (Save.body "/tmp/body") 73 | 74 | .. autoclass:: Save 75 | :members: 76 | 77 | 78 | .. _operations_error: 79 | 80 | Error 81 | ----- 82 | 83 | Operation that will exit Raider and print the error message. 84 | 85 | .. code-block:: hylang 86 | 87 | (Error "Login failed.") 88 | 89 | .. autoclass:: Error 90 | :members: 91 | 92 | 93 | .. _operations_http: 94 | 95 | Http 96 | ---- 97 | 98 | .. code-block:: hylang 99 | 100 | (Http 101 | :status 200 102 | :action 103 | (NextStage "login") 104 | :otherwise 105 | (NextStage "multi_factor")) 106 | 107 | .. autoclass:: Http 108 | :members: 109 | 110 | .. _operations_grep: 111 | 112 | Grep 113 | ---- 114 | 115 | .. code-block:: hylang 116 | 117 | (Grep 118 | :regex "TWO_FA_REQUIRED" 119 | :action 120 | (NextStage "multi_factor") 121 | :otherwise 122 | (Print "Logged in successfully")) 123 | 124 | .. autoclass:: Grep 125 | :members: 126 | 127 | .. _operations_api: 128 | 129 | 130 | 131 | Writing custom operations 132 | ------------------------- 133 | 134 | In case the existing operations are not enough, the user can write 135 | their own to add the new functionality. Those new operations should be 136 | written in the project's configuration directory in a ".hy" file. To 137 | do this, a new class has to be defined, which will inherit from 138 | *Raider*'s Operation class: 139 | 140 | .. autoclass:: Operation 141 | :members: 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/dev/plugins.rst: -------------------------------------------------------------------------------- 1 | .. _plugins: 2 | .. module:: raider.plugins.common 3 | 4 | Plugins 5 | ======= 6 | 7 | Plugins in **Raider** are pieces of code that are used to get inputs 8 | from, and put them in the HTTP request, and/or to extract some values 9 | from the response. This is used to facilitate the information exchange 10 | between :ref:`Flows `. Below there's a list of predefined 11 | Plugins. The users are also encouraged to write their own plugins. 12 | 13 | 14 | Common 15 | ------ 16 | 17 | Plugin 18 | ++++++ 19 | 20 | .. autoclass:: Plugin 21 | 22 | Parser 23 | ++++++ 24 | 25 | .. autoclass:: Parser 26 | 27 | Empty 28 | +++++ 29 | 30 | .. autoclass:: Empty 31 | 32 | 33 | .. module:: raider.plugins.basic 34 | 35 | Basic 36 | ----- 37 | 38 | .. _plugin_variable: 39 | 40 | Variable 41 | ++++++++ 42 | 43 | Use this when the value of the plugin should be extracted from the 44 | user data. At the moment only ``username`` and ``password`` are 45 | working. Future versions will allow adding and accessing arbitrary 46 | data from the users. 47 | 48 | Example: 49 | 50 | .. code-block:: hylang 51 | 52 | (setv username (Variable "username")) 53 | 54 | .. autoclass:: Variable 55 | :members: 56 | 57 | .. _plugin_prompt: 58 | 59 | Prompt 60 | ++++++ 61 | 62 | Prompt plugin should be used when the information is not known in 63 | advance, for example when receiving the SMS code. 64 | 65 | Example: 66 | 67 | .. code-block:: hylang 68 | 69 | (setv mfa_code (Prompt "Input code here:")) 70 | 71 | .. autoclass:: Prompt 72 | :members: 73 | 74 | .. _plugin_command: 75 | 76 | .. _plugin_cookie: 77 | 78 | Cookie 79 | ++++++ 80 | 81 | Use Cookie plugin to extract and set new cookies: 82 | 83 | Example: 84 | 85 | .. code-block:: hylang 86 | 87 | (setv session_cookie (Cookie "PHPSESSID")) 88 | 89 | .. autoclass:: Cookie 90 | :members: 91 | 92 | 93 | .. _plugin_header: 94 | 95 | Header 96 | ++++++ 97 | 98 | Use Header plugin to extract and set new headers. It also allows 99 | easier setup for basic and bearer authentication using the provided 100 | classmethods. 101 | 102 | Example: 103 | 104 | .. code-block:: hylang 105 | 106 | (setv x-header (Header "x-header")) 107 | (setv y-header (Header "y-header" "y-value")) 108 | 109 | (setv z-header (Header.basicauth "username" "password")) 110 | 111 | 112 | (setv access_token 113 | (Regex 114 | :name "access_token" 115 | :regex "\"accessToken\":\"([^\"]+)\"")) 116 | 117 | (setv z-header (Header.bearerauth access_token)) 118 | 119 | .. autoclass:: Header 120 | :members: 121 | 122 | 123 | Command 124 | +++++++ 125 | 126 | Use Command plugin if you want to extract information using a shell 127 | command. 128 | 129 | Example: 130 | 131 | .. code-block:: hylang 132 | 133 | (setv mfa_code (Command 134 | :name "otp" 135 | :command "pass otp personal/app1")) 136 | 137 | .. autoclass:: Command 138 | :members: 139 | 140 | 141 | .. _plugin_regex: 142 | 143 | Regex 144 | +++++ 145 | 146 | Use Regex plugin if the data you want extracted can be easily 147 | identified with a regular expression. The string matched in between 148 | ``(`` and ``)`` will be stored as the plugin's value. 149 | 150 | Example: 151 | 152 | .. code-block:: hylang 153 | 154 | (setv access_token 155 | (Regex 156 | :name "access_token" 157 | :regex "\"accessToken\":\"([^\"]+)\"")) 158 | 159 | 160 | .. autoclass:: Regex 161 | :members: 162 | 163 | 164 | .. _plugin_html: 165 | 166 | Html 167 | ++++ 168 | 169 | Use the Html plugin when the data you want can be easily extracted by 170 | parsing HTML tags. Create a new plugin by giving it a name, the tag 171 | where the information is located, some attributes to identify whether 172 | the tag is the right one, and the name of the tag attribute you want 173 | to extract. The attributes are created as a dictionary, and its values 174 | can be regular expressions. 175 | 176 | Example: 177 | 178 | .. code-block:: hylang 179 | 180 | (setv csrf_token 181 | (Html 182 | :name "csrf_token" 183 | :tag "input" 184 | :attributes 185 | {:name "csrf_token" 186 | :value "^[0-9a-f]{40}$" 187 | :type "hidden"} 188 | :extract "value")) 189 | 190 | 191 | .. autoclass:: Html 192 | :members: 193 | 194 | .. _plugin_json: 195 | 196 | Json 197 | ++++ 198 | 199 | .. autoclass:: Json 200 | :members: 201 | 202 | .. module:: raider.plugins.modifiers 203 | 204 | 205 | Modifiers 206 | --------- 207 | 208 | Alter 209 | +++++ 210 | 211 | .. autoclass:: Alter 212 | :members: 213 | 214 | Combine 215 | +++++++ 216 | 217 | .. autoclass:: Combine 218 | :members: 219 | 220 | 221 | 222 | .. module:: raider.plugins.parsers 223 | 224 | Parsers 225 | ------- 226 | 227 | UrlParser 228 | +++++++++ 229 | 230 | .. autoclass:: UrlParser 231 | :members: 232 | 233 | 234 | 235 | .. _plugin_api: 236 | 237 | Writing custom plugins 238 | ---------------------- 239 | 240 | 241 | In case the existing plugins are not enough, the user can write 242 | their own to add the new functionality. Those new plugins should be 243 | written in the project's configuration directory in a ".hy" file. To 244 | do this, a new class has to be defined, which will inherit from 245 | *Raider*'s Plugin class: 246 | 247 | 248 | Let's assume we want a new plugin that will use `unix password store 249 | `_ to extract the OTP from our website. 250 | 251 | 252 | .. code-block:: hylang 253 | 254 | 255 | (defclass PasswordStore [Plugin] 256 | ;; Define class PasswordStore which inherits from Plugin 257 | 258 | (defn __init__ [self path] 259 | ;; Initiatialize the object given the path 260 | 261 | (.__init__ (super) 262 | :name path 263 | :function (. self run_command))) 264 | ;; Call the super() class, i.e. Plugin, and give it the 265 | ;; path as the name identifier, and the function 266 | ;; self.run_command() as a function to get the value. 267 | ;; 268 | ;; We don't need the response nor the user data to use 269 | ;; this plugin, so no flags will be set. 270 | 271 | (defn run_command [self] 272 | (import os) 273 | ;; We need os.popen() to run the command 274 | 275 | (setv self.value 276 | ((. ((. (os.popen 277 | (+ "pass otp " self.path)) 278 | read)) 279 | strip))) 280 | ;; set self.value to the output from "pass otp", 281 | ;; with the newline stripped. 282 | 283 | (return self.value))) 284 | 285 | 286 | And we can create a new variable that will use this class: 287 | 288 | .. code-block:: hylang 289 | 290 | (setv mfa_code (PasswordStore "personal/reddit")) 291 | 292 | 293 | Now whenever we use the ``mfa_code`` in our requests, its value will 294 | be extracted from the password store. 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /docs/dev/special_variables.rst: -------------------------------------------------------------------------------- 1 | Special variables 2 | ================= 3 | 4 | 5 | .. _var_users: 6 | 7 | _users 8 | ------ 9 | 10 | Setting this variable *is required* for **Raider** to run. 11 | 12 | It should contain a list of dictionaries with the user credentials. For 13 | now only usernames and passwords are evaluated, but in future it will be 14 | used for other arbitrary user related information. This data gets 15 | converted into a :class:`UserStore ` object which 16 | provides a dictionary-like structure with :class:`User 17 | ` objects inside. 18 | 19 | Example: 20 | 21 | .. code-block:: hylang 22 | 23 | (setv _users 24 | [{:username "user1" 25 | :password "s3cr3tP4ssWrd1"} 26 | {:username "user2" 27 | :password "s3cr3tP4ssWrd2"}]) 28 | 29 | .. _var_authentication: 30 | 31 | _authentication 32 | --------------- 33 | 34 | This variable *is required* for **Raider** to run. 35 | 36 | It should contain all of the authentication stages in Flow 37 | objects. You can define those stages separately as variables like in 38 | the :ref:`tutorial `, and include them all at the end in the 39 | ``_authentication`` variable. 40 | 41 | Example: 42 | 43 | .. code-block:: hylang 44 | 45 | (setv _authentication 46 | [initialization 47 | login 48 | multi_factor 49 | #_ /]) 50 | 51 | 52 | Where each item in the list is a :class:`Flow ` object, and 53 | might look like this: 54 | 55 | .. code-block:: hylang 56 | 57 | (setv initialization 58 | (Flow 59 | :name "initialization" 60 | :request (Request 61 | :method "GET" 62 | :path "about") 63 | :outputs [csrf_token session_id] 64 | :operations [(Print csrf_token session_id) 65 | (Http 66 | :status 200 67 | :action 68 | (NextStage "login") 69 | :otherwise 70 | (Error "Cannot initialize session"))])) 71 | 72 | .. _var_base_url: 73 | 74 | _base_url 75 | --------- 76 | 77 | This variable *is optional*. 78 | 79 | Setting ``base_url`` will enable a shortcut for writing new 80 | :class:`Request ` objects. When enabled, the 81 | Requests can be created using ``:path`` instead of ``:url`` 82 | 83 | 84 | .. _var_functions: 85 | 86 | _functions 87 | ---------- 88 | 89 | This variable *is optional*. 90 | 91 | It works similarly to the :ref:`_authentication ` 92 | variable, but it includes only the Flows which don't affect the 93 | authentication process. 94 | -------------------------------------------------------------------------------- /docs/diagrams/detailed_authentication.uml: -------------------------------------------------------------------------------- 1 | state User { 2 | User: Username 3 | User: Password 4 | } 5 | 6 | state Factors { 7 | Factors: SMS code 8 | Factors: E-mail confirmation 9 | Factors: TOTP 10 | Factors: Biometrics 11 | } 12 | 13 | 14 | 15 | state Initialization { 16 | state Request0 17 | state Response0 18 | Request0 -[#orange]-> Response0 19 | Response0 --> outputs0 20 | state outputs0 <> 21 | Response0 : CSRF Token 22 | Response0 : Session cookie 23 | } 24 | 25 | 26 | state Login { 27 | state inputs1 <> 28 | state outputs1 <> 29 | state Request1 30 | state Response1 31 | inputs1 --> Request1 32 | outputs0 --> inputs1 33 | Request1 -[#orange]-> Response1 34 | Request1: CSRF Token 35 | Request1: Session cookie 36 | Request1: Username 37 | Request1: Password 38 | Response1 --> outputs1 39 | Response1: CSRF Token 40 | Response1: Session cookie 41 | Response1: User cookie 42 | } 43 | 44 | User -[#blue]-> inputs1 45 | 46 | 47 | state "Multi-factor authentication" as MFA { 48 | state inputs2 <> 49 | state outputs2 <> 50 | state Request2 51 | Request2: CSRF Token 52 | Request2: Session cookie 53 | Request2: User cookie 54 | state Response2 55 | Response2: Session cookie 56 | Response2: User cookie 57 | Response2: MFA passed cookie 58 | Request2 -[#orange]-> Response2 59 | outputs1 --> inputs2 60 | inputs2 --> Request2 61 | Response2 --> outputs2 62 | 63 | } 64 | 65 | [*] --> Initialization 66 | 67 | Initialization -[#green]-> Login 68 | 69 | 70 | Factors -[#blue]-> inputs2 71 | 72 | Login -[#green]> MFA : MFA enabled 73 | Login -[#green]-> Authenticated : MFA disabled 74 | 75 | state Authenticated 76 | state "Login failed" as login_failed 77 | state "MFA failed" as MFA_failed 78 | 79 | Login -left[#green]-> login_failed : Bad credentials 80 | 81 | MFA -[#green]-> Authenticated : MFA passed 82 | MFA -left[#green]-> MFA_failed 83 | 84 | Authenticated --> [*] 85 | -------------------------------------------------------------------------------- /docs/diagrams/high_level_authentication.uml: -------------------------------------------------------------------------------- 1 | [*] --> Unauthenticated : Open login page 2 | 3 | state Unauthenticated 4 | state "MFA required" as MFA_required 5 | state "Login failed" as login_failed 6 | state Authenticated 7 | 8 | Authenticated --> [*] 9 | 10 | state "Login check" as login <> 11 | state "Is MFA enabled?" as mfa_enabled <> 12 | state "Is MFA code correct?" as mfa_correct <> 13 | 14 | state login_check <> 15 | state mfa_enabled_check <> 16 | state mfa_correct_check <> 17 | 18 | 19 | Unauthenticated --> login : Log in 20 | login --> login_check 21 | login_check --> mfa_enabled : Correct credentials 22 | login_check --> login_failed : Bad credentials 23 | 24 | 25 | mfa_enabled --> mfa_enabled_check 26 | mfa_enabled_check --> MFA_required : MFA enabled 27 | mfa_enabled_check --> Authenticated : MFA disabled 28 | 29 | MFA_required --> mfa_correct 30 | mfa_correct --> mfa_correct_check 31 | mfa_correct_check --> login_failed : Failed MFA 32 | mfa_correct_check --> Authenticated : Passed MFA 33 | -------------------------------------------------------------------------------- /docs/diagrams/raider_flows.uml: -------------------------------------------------------------------------------- 1 | skinparam componentStyle rectangle 2 | 3 | package "Flow0" { 4 | frame "Stage0" { 5 | [Request0] -down- [Response0] 6 | } 7 | 8 | component outputs0 [ 9 | output_0 10 | output_1 11 | ... 12 | output_n 13 | ] 14 | 15 | component operations0 [ 16 | operation_0 17 | operation_1 18 | ... 19 | operation_n 20 | ] 21 | 22 | Response0 --> outputs0 23 | note top of outputs0 : Outputs 24 | 25 | outputs0 -right-> operations0 26 | note top of operations0 : Operations 27 | 28 | } 29 | 30 | 31 | package "Flow1" { 32 | frame "Stage1" { 33 | [Request1] -down- [Response1] 34 | } 35 | 36 | component outputs1 [ 37 | output_0 38 | output_1 39 | ... 40 | output_n 41 | ] 42 | 43 | component inputs1 [ 44 | input_0 45 | input_1 46 | ... 47 | input_n 48 | ] 49 | 50 | component operations1 [ 51 | operation_0 52 | operation_1 53 | ... 54 | operation_n 55 | ] 56 | 57 | Response1 --> outputs1 58 | note top of outputs1 : Outputs 59 | note left of inputs1 : Inputs 60 | 61 | outputs1 -right-> operations1 62 | note top of operations1 : Operations 63 | 64 | } 65 | 66 | operations0 -> Flow1 67 | outputs0 -> inputs1 68 | inputs1 -> Request1 69 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Raider's documentation! 2 | ================================== 3 | 4 | .. note:: 5 | This documentation and the entire Raider framework is still work in 6 | progress. Many things are not finished, stuff is missing, other stuff 7 | is not working as expected, and so on... Meanwhile, `read the source 8 | code `_ to understand better how 9 | Raider works, `open Github issues 10 | `_ if you find some 11 | mistakes, or come `talk to us in the community forum 12 | `_. 13 | 14 | 15 | .. image:: ../ext/logo.png 16 | 17 | **Raider** is a framework designed to test :term:`authentication` for 18 | web applications. While web proxies like `ZAProxy 19 | `_ and `Burpsuite 20 | `_ allow authenticated tests, they don't 21 | provide features to test the authentication process itself, 22 | i.e. manipulating the relevant input fields to identify broken 23 | authentication. Most authentication bugs in the wild have been found 24 | by manually testing it or writing custom scripts that replicate the 25 | behaviour. **Raider** aims to make testing easier, by providing the 26 | interface to interact with all important elements found in modern 27 | authentication systems. 28 | 29 | 30 | How does Raider work? 31 | --------------------- 32 | 33 | **Raider** treats the authentication as a :term:`finite state 34 | machine`. Each authentication step is a different :term:`stage`, with 35 | its own inputs and outputs. Those can be cookies, headers, CSRF 36 | tokens, or other pieces of information. 37 | 38 | Each application needs its own configuration directory for **Raider** 39 | to work. The configuration is written in `Hylang 40 | `_. The language choice was done for 41 | multiple reasons, mainly because it's a Lisp dialect embedded in 42 | Python. 43 | 44 | :ref:`Using Lisp was necessarily ` since sometimes the 45 | authentication can get quite complex, and using a static configuration 46 | file would've not been enough to cover all the details. Lisp makes it 47 | easy to combine code and data, which is exactly what was needed here. 48 | 49 | By using a real programming language as a configuration file gives 50 | **Raider** a lot of power, and :ref:`with great power comes great 51 | responsibility `. Theoretically one can write entire malware inside the 52 | application configuration file, which means you should be careful 53 | what's being executed, and **not to use configuration files from 54 | sources you don't trust**. **Raider** will evaluate everything inside 55 | the ``.hy`` files, which means if you're not careful you could shoot 56 | yourself in the foot and break something on your system. 57 | 58 | 59 | Features 60 | -------- 61 | 62 | **Raider** has the goal to support most of the modern authentication 63 | systems, and here are some features that other tools don't offer: 64 | 65 | * Unlimited authentication steps 66 | * Unlimited inputs/outputs for each step 67 | * Ability to conditionally decide the next step 68 | * Running arbitrary operations when receiving the response 69 | * Easy to write custom operations and plugins 70 | 71 | 72 | Raider's philosophy 73 | ------------------- 74 | 75 | **Raider** was developed with the following goals: 76 | 77 | * To abstract authentication concepts using Python objects. 78 | * To support most modern web authentication features. 79 | * To make it easy to add new features for users. 80 | 81 | 82 | And if you're looking at the code and willing to contribute, keep 83 | those in mind: 84 | 85 | * The simpler and cleaner the code, the better. 86 | * New features should be implemented as :term:`Plugins ` and 87 | :term:`Operations ` if possible. 88 | * The :term:`hyfiles` should stay as minimal as possible, while still 89 | allowing the user to get creative. In the future parts of this code 90 | could be autogenerated. 91 | 92 | 93 | User guide 94 | ---------- 95 | 96 | .. toctree:: 97 | :maxdepth: 2 98 | :caption: Getting started 99 | 100 | user/install 101 | user/architecture 102 | user/tutorials 103 | user/definitions 104 | user/faq 105 | 106 | 107 | .. toctree:: 108 | :maxdepth: 2 109 | :caption: Configuration 110 | 111 | dev/special_variables 112 | dev/flows 113 | dev/attacks 114 | dev/plugins 115 | dev/operations 116 | 117 | 118 | .. toctree:: 119 | :maxdepth: 2 120 | :caption: API reference 121 | 122 | dev/api 123 | 124 | 125 | Indices and tables 126 | ================== 127 | 128 | * :ref:`genindex` 129 | * :ref:`modindex` 130 | * :ref:`search` 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | importlib-metadata == 4.6.1 2 | raider == 0.1.3 3 | sphinx_autodoc_typehints == 1.12.0 4 | sphinx-rtd-theme == 0.5.2 5 | sphinxcontrib-plantuml == 0.21 6 | sphinxcontrib-needs == 0.7.0 7 | -------------------------------------------------------------------------------- /docs/user/architecture.rst: -------------------------------------------------------------------------------- 1 | .. _architecture: 2 | 3 | Architecture 4 | ============ 5 | 6 | 7 | Abstracting the authentication process 8 | -------------------------------------- 9 | 10 | First let's start by taking a closer look at how web authentication 11 | works. Every :term:`authentication process ` can be 12 | abstracted as a :term:`Finite State Machine `. 13 | 14 | On a high level, we start in the unauthenticated state, the user sends 15 | the application their credentials, optionally the :term:`multi-factor 16 | authentication (MFA)` code, and if both checks pass, we reach the 17 | authenticated state. A typical modern web application will looks like 18 | the following in a diagram: 19 | 20 | .. uml:: ../diagrams/high_level_authentication.uml 21 | 22 | 23 | Basic concepts in Raider 24 | ------------------------ 25 | 26 | Now let's zoom in and look at the details. Instead of dealing with the 27 | states (*Unauthenticated*, *Login failed*, *MFA required*, and 28 | *Authenticated*), we define the concept of :term:`stages 29 | `, which describes the information exchange between 30 | the client and the server containing one request and the respective 31 | response. 32 | 33 | The example below shows a closer look of the authentication process 34 | for an imaginary web application: 35 | 36 | .. uml:: ../diagrams/detailed_authentication.uml 37 | 38 | 39 | To describe the authentication process from the example defined above, 40 | we need three **stages**. The first one, *Initialization*, doesn't 41 | have any inputs, but creates the *Session cookie* and the *CSRF token* 42 | as outputs. 43 | 44 | Those outputs are passed to the next **stage**, *Login*, together with 45 | user credentials. A request is built with those pieces of information, 46 | and the new outputs are generated. In this case we have the new *CSRF 47 | token*, an updated *session cookie*, and a new cookie identifying the 48 | user: *user cookie*. 49 | 50 | Depending on whether MFA is enabled or not, the third **stage** 51 | *Multi-factor authentication* might be skipped or executed. If it's 52 | enabled, the outputs from the previous **stage** get passed as inputs 53 | to this one, the user is asked to input the next :term:`Factor`, and a 54 | new cookie is set proving the user has passed the checks and is 55 | properly authenticated. 56 | 57 | In **Raider**, stages are implemented using :term:`Flow 58 | ` objects. The authentication process consists of a 59 | series of Flows connected to each other. Each one accepts inputs and 60 | generates outputs. In addition to that, Flow objects implement 61 | :term:`Operations ` which can be used to run 62 | various actions upon receiving the response, but most importantly 63 | they're used to control the authentication process by conditionally or 64 | unconditionally defining the next stage. So for example one can jump 65 | to stage X if the HTTP response code is 200 or to stage Y if it's 403. 66 | 67 | 68 | .. uml:: ../diagrams/raider_flows.uml 69 | 70 | 71 | Inputs and outputs are often the same object, and you may want to 72 | update its value from one Flow to the next (for example the CSRF token 73 | changes for every stage). This was implemented in Raider using 74 | :term:`Plugins `. 75 | 76 | Plugins are pieces of code that can act as inputs for the HTTP requests 77 | to be sent, and/or as outputs from the HTTP responses. They are used to 78 | facilitate the information exchange between Flows. **Raider** provides 79 | the user the option to :ref:`write new plugins ` with a 80 | small piece of hylang code. 81 | 82 | 83 | Once the response is received, the :term:`Operations ` will 84 | be executed. The primary function of operations is to define which Flow 85 | comes next. But they can do anything, and *Raider* :ref:`makes it easy 86 | to write new operations `. 87 | -------------------------------------------------------------------------------- /docs/user/definitions.rst: -------------------------------------------------------------------------------- 1 | Definitions 2 | =========== 3 | 4 | 5 | .. glossary:: 6 | Authentication 7 | The act of proving the identity of a computer system 8 | user. Authentication in the context of web applications is 9 | *usually* performed by submitting a username or ID and a piece of 10 | private information (:term:`factor `) such as a 11 | password. 12 | 13 | In **Raider** the authentication process is defined by a series of 14 | :term:`Flow objects `. Those are extracted from the 15 | :ref:`_authentication` variable in the 16 | :term:`hyfiles `, and stored inside an 17 | :class:`Authentication ` 18 | object. It's also accessible from the :class:`Raider ` 19 | directly: 20 | 21 | .. code-block:: python 22 | 23 | >>> import raider 24 | >>> app = raider.Raider("my_app") 25 | >>> app.authentication 26 | 27 | 28 | 29 | Factor 30 | A factor can be *something the user knows* (passwords, security 31 | questions, etc...), *something they have* (bank card, USB security 32 | key, etc...), *something they are* (fingerprint, eye iris, etc..) or 33 | *somewhere they are* (GPS location, known WiFi connection, etc...). 34 | 35 | Finite state machine 36 | A mathematical model of computation abstracting a process that can 37 | be only in one of a finite number of *states* at any given 38 | time. Check the `Wikipedia article 39 | `_ for more 40 | information, since it explains this better than me anyways. 41 | 42 | Flow 43 | A **Raider** class implementing :term:`stages `. To create a 44 | :class:`Flow ` object, you need to give it a name, 45 | a :class:`Request ` object, and optionally 46 | outputs and :term:`operations `. Check the :ref:`Flow 47 | configuration page ` for more information. 48 | 49 | Functions 50 | A **Raider** class containing all :term:`Flows ` objects 51 | that don't affect the :term:`authentication ` 52 | process. The :class:`Functions ` 53 | object is extracted from the :ref:`_functions ` 54 | variable. 55 | 56 | hyfiles 57 | The documentation uses the term **hyfiles** to refer to any 58 | ``*.hy`` file inside the project's configuration directory. Each 59 | will be evaluated in *alphabetical order* by **Raider**. 60 | 61 | The objects created in previous files are all available in the next 62 | file, since all the ``locals()`` get preserved and loaded again when 63 | reading the next file. A common practice is to prepend the file 64 | names with two digits and an underscore, for example 65 | ``03_authentication.hy`` and ``09_users.hy``. 66 | 67 | Multi-factor authentication (MFA) 68 | An :term:`authentication ` method in which the 69 | user is granted access only after successfully presenting two or 70 | more pieces of evidence (:term:`factors `). 71 | 72 | Operation 73 | A piece of code that will be run after the HTTP :term:`response` is 74 | received. All Operations inherit from :class:`Operation 75 | ` class. 76 | 77 | All defined Operations inside the :term:`Flow ` object will 78 | stop running when the first :class:`NextStage 79 | ` Operation is encountered. 80 | 81 | **Raider** comes with :ref:`some standard operations `, 82 | but it also gives the user the flexibility to :ref:`write their own 83 | Operations easily `. 84 | 85 | Plugin 86 | 87 | A piece of code that can be used to generate inputs for outgoing 88 | HTTP :term:`Requests `, and/or extract outputs from 89 | incoming term:`Responses `. All plugins inherit from 90 | :class:`Plugin ` class. 91 | 92 | When used inside a :term:`Request `, Plugins acts as input 93 | and replace themselves with the actual value. 94 | 95 | When used inside the :term:`Flow's ` ``:output`` parameter, 96 | Plugins act as outputs from the HTTP response, and store the 97 | extracted value for later use. 98 | 99 | **Raider** comes with :ref:`some standard plugins `, but it 100 | also gives the user the flexibility to :ref:`write their own 101 | Plugins easily `. 102 | 103 | Project 104 | To avoid confusion with the :class:`Application 105 | ` class, **Raider** uses the term 106 | Project to refer to an application, with existing :term:`hyfiles`. 107 | 108 | Request 109 | A HTTP request with the defined inputs. In **Raider** it's 110 | implemented as a separate class :class:`Request 111 | `. This however is not used directly most of 112 | the times, but as an argument when creating the :term:`Flow ` 113 | object in :term:`hyfiles `. 114 | 115 | When used inside a Request, a :term:`Plugin ` will replace 116 | itself with its actual value during runtime. 117 | 118 | Response 119 | A HTTP response from which the outputs are extracted and stored 120 | inside the :term:`Plugins `. 121 | 122 | When the :term:`Flow ` object containing this response is 123 | received and processed, the :term:`Operations ` are 124 | executed. 125 | 126 | Stage 127 | A **Raider** concept describing the information exchange between 128 | the client and server, containing one :term:`request ` 129 | and the respective response. 130 | -------------------------------------------------------------------------------- /docs/user/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently asked questions 2 | ========================== 3 | 4 | 5 | What's this and why should I care? 6 | ---------------------------------- 7 | 8 | **Raider** was developed with the goal to improve web 9 | :term:`authentication` testing. It feels like everyone is writing 10 | their own custom tools which work only on their own system, and this 11 | project aims to fill that gap by becoming a universal authentication 12 | testing framework that works on all modern systems. 13 | 14 | How does it work? 15 | ----------------- 16 | 17 | **Raider** treats the :term:`authentication` process as a 18 | :term:`finite state machine`. Each authentication step has to be 19 | configured separately, together with all pieces of information needed 20 | and how to extract them. 21 | 22 | **Raider** uses a configuration directory containing a set of ``.hy`` 23 | files for each new project. Those files contain information describing 24 | the authentication process. **Raider** evaluates them, and gives you 25 | back a Python object to interact with the application. 26 | 27 | Read the :ref:`Architecture ` and `Tutorials 28 | `_ for more information and 29 | examples. 30 | 31 | .. _faq_eval: 32 | 33 | You're telling me it'll evaluate all user input? Isn't that unsafe? 34 | ------------------------------------------------------------------- 35 | 36 | Yes, by making the decision to run real code inside configuration 37 | files I made it possible to run malicious code. Which is why you 38 | should **always write your own configuration**, and not copy it from 39 | untrusted sources. **Raider** assumes you are acting like a 40 | responsible adult if you're using this project. If the user wants to 41 | write an Operation that will ``rm -rf`` something on their machine 42 | when a HTTP response is received, who am I to judge? With that said, I 43 | don't take any responsibility if using **Raider** makes your computer 44 | catch fire, your company bankrupt, starts the third world war, leads 45 | to AI taking over humanity, or anything else in between. 46 | 47 | 48 | 49 | How do I run this? 50 | ------------------ 51 | 52 | A CLI is planned to be done soon. For now, you can only run it by 53 | writing a short Python script: 54 | 55 | .. code-block:: python 56 | 57 | import raider 58 | 59 | session = raider.Raider("app_name") 60 | # Create a Raider() object for application "app_name" 61 | 62 | session.config.proxy = "http://localhost:8080" 63 | # Run traffic through the local web proxy 64 | 65 | session.authenticate() 66 | # Run authentication stages one by one 67 | 68 | session.run_function("get_nickname") 69 | # Run the defined "get_nickname" function 70 | 71 | 72 | 73 | Do I need to know Python and Hylang in order to use **Raider**? 74 | --------------------------------------------------------------- 75 | 76 | Yes, **Raider** documentation already assumes you know the basic 77 | concepts in both Python and Hylang. You don't have to know a lot. If 78 | it's your first time with Python, just get yourself familiar with it, 79 | and when you're ready move on to learning Hylang, which is basically 80 | just Python code surrounded by Lisp parentheses. 81 | 82 | 83 | .. _why_lisp: 84 | 85 | Why Lisp? 86 | --------- 87 | 88 | Because in Lisp, code is data, and data is code. First iterations 89 | through planning this project were done with a static configuration 90 | file, experimenting with different formats. However, it turns out all 91 | of those static formats had problems. They can't easily execute code, 92 | they can't hold data structures, etc... Changing this to a Lisp file, 93 | all those problems vanished away, and it gives the user the power to 94 | add features easily without messing with the main code. 95 | 96 | 97 | 98 | Why is Raider using Hylang? 99 | --------------------------- 100 | 101 | Because the main code is written in Python. After deciding to choose 102 | Lisp for the new configuration format, I obviously googled "python 103 | lisp", and found this project. Looking through the documentation 104 | I realized it turns out to be the perfect fit for my needs. 105 | 106 | 107 | 108 | 109 | Does it work on Windows? 110 | ------------------------ 111 | 112 | Probably not. I don't have enough time to test it on other platforms. 113 | 114 | 115 | What about macOS? BSD? etc? 116 | --------------------------- 117 | 118 | I didn't test it, but should probably work as long as it's unix-like. 119 | 120 | 121 | How can I contribute? 122 | --------------------- 123 | 124 | If you're interested in contributing, you can do so. After you managed 125 | to set up your first application, figure out what could have been made 126 | easier or better. 127 | 128 | Then start writing new Plugins and Operations and share them either on 129 | `Github`_ or `privately with me`_. 130 | 131 | Once you're familiar with the structure of the project, you can start 132 | by fixing bugs and writing new features. 133 | 134 | .. _privately with me: raider@digeex.de 135 | .. _Github: https://github.com/DigeeX/raider 136 | -------------------------------------------------------------------------------- /docs/user/install.rst: -------------------------------------------------------------------------------- 1 | Installing Raider 2 | ================= 3 | 4 | The package is available in the `Python Package Index 5 | `_, so to install the latest stable release of 6 | *Raider* just use the command ``pip3 install --user raider`` 7 | 8 | .. WARNING:: *Raider* was developed on Python 3.9 and it wasn't tested 9 | yet on older versions, so it might have incompatibility 10 | issues. 11 | 12 | If you feel adventurous and want to build *Raider* from source, you 13 | can do so. You will need to do that anyways if you want to contribute 14 | to the development. 15 | 16 | First start by clonning the repository with ``git clone 17 | https://github.com/DigeeX/raider``. 18 | 19 | Using a python virtual environment is recommended to avoid weird 20 | issues with python incompatibilities when working on the code. However 21 | you can still use ``pip3 install .`` in the project's directory to 22 | install the package locally. 23 | 24 | If you choose to use the virtual environment, `install poetry 25 | `_ since that's how 26 | *Raider* was developed. 27 | 28 | Once poetry is installed, you can prepare the virtual environment and 29 | switch to it to work with *Raider*: 30 | 31 | .. code-block:: bash 32 | 33 | cd raider 34 | poetry install 35 | poetry shell 36 | 37 | 38 | And now you're working inside the virtual environment, and *Raider* 39 | should be available here. 40 | -------------------------------------------------------------------------------- /docs/user/tutorials.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials: 2 | 3 | Tutorials 4 | ========= 5 | 6 | This page has been moved to the `community forum 7 | `_ to make it possible for 8 | anyone to write more tutorials. 9 | -------------------------------------------------------------------------------- /examples/app1/01_main.hy: -------------------------------------------------------------------------------- 1 | (print "App1") 2 | (setv _base_url "https://app1.de/") 3 | 4 | 5 | (setv username (Variable "username")) 6 | (setv password (Variable "password")) 7 | 8 | (setv session_id 9 | (Cookie "admin-session")) 10 | 11 | (setv initialization 12 | (Flow 13 | :name "initialization" 14 | :request (Request 15 | :method "GET" 16 | :url "https://auth.app1.de/login") 17 | :operations [(Http 18 | :status 200 19 | :action (NextStage "login"))])) 20 | 21 | (setv login 22 | (Flow 23 | :name "login" 24 | :request (Request 25 | :method "POST" 26 | :path "/login/session" 27 | :data 28 | {"password" password 29 | "username" username}) 30 | :outputs [session_id] 31 | :operations [(Http 32 | :status 403 33 | :action (Error "login error"))])) 34 | 35 | 36 | 37 | (setv _authentication 38 | [initialization 39 | login]) 40 | -------------------------------------------------------------------------------- /examples/app1/02_users.hy: -------------------------------------------------------------------------------- 1 | (setv _users [{:username "username@domain.com" 2 | :password "mySecretPassword"} 3 | {:username "admin@domain.com" 4 | :password "AnotherPassword"}]) 5 | -------------------------------------------------------------------------------- /examples/app2/01_main.hy: -------------------------------------------------------------------------------- 1 | (print "App2") 2 | (setv _base_url "https://www.app2.de") 3 | 4 | 5 | (setv username (Variable "username")) 6 | (setv password (Variable "password")) 7 | (setv mfa_code (Prompt "MFA")) 8 | 9 | 10 | 11 | (setv csrf_name 12 | (Html 13 | :name "csrf_name" 14 | :tag "input" 15 | :attributes 16 | {:name "^[0-9A-Fa-f]{10}$" 17 | :value "^[0-9A-Fa-f]{64}$" 18 | :type "hidden"} 19 | :extract "name")) 20 | 21 | (setv csrf_value 22 | (Html 23 | :name "csrf_value" 24 | :tag "input" 25 | :attributes 26 | {:name "^[0-9A-Fa-f]{10}$" 27 | :value "^[0-9A-Fa-f]{64}$" 28 | :type "hidden"} 29 | :extract "value")) 30 | 31 | 32 | 33 | (setv session_id 34 | (Cookie "PHPSESSID")) 35 | (setv init_vector 36 | (Cookie "iv")) 37 | (setv verify 38 | (Cookie "verify")) 39 | -------------------------------------------------------------------------------- /examples/app2/02_authentication.hy: -------------------------------------------------------------------------------- 1 | (setv initialization 2 | (Flow 3 | :name "initialization" 4 | :request (Request 5 | :method "POST" 6 | :path "/auth/login.php" 7 | :data {"open" "login"}) 8 | :outputs [csrf_name csrf_value session_id] 9 | :operations [(Http 10 | :status 200 11 | :action (NextStage "login"))])) 12 | 13 | 14 | (setv login 15 | (Flow 16 | :name "login" 17 | :request (Request 18 | :method "POST" 19 | :path "/auth/login.php" 20 | :cookies [session_id] 21 | :data 22 | {"open" "login" 23 | "action" "customerlogin" 24 | "password" password 25 | "username" username 26 | "redirect" "myaccount" 27 | csrf_name csrf_value}) 28 | :outputs [csrf_name csrf_value] 29 | :operations [(Http 30 | :status 200 31 | :action (NextStage "multi_factor") 32 | :otherwise (NextStage "login"))])) 33 | 34 | 35 | (setv multi_factor 36 | (Flow 37 | :name "multi_factor" 38 | :request (Request 39 | :method "POST" 40 | :path "/auth/login.php" 41 | :cookies [session_id] 42 | :data 43 | {"open" "login" 44 | "action" "customerlogin" 45 | "redirect" "myaccount" 46 | "twofactorcode" mfa_code 47 | csrf_name csrf_value}) 48 | :outputs [init_vector verify] 49 | :operations [(Http 50 | :status 200 51 | :action (NextStage None)) 52 | (Http 53 | :status 403 54 | :action (NextStage "initialization"))])) 55 | 56 | 57 | 58 | 59 | (setv _authentication 60 | [initialization 61 | login 62 | multi_factor]) 63 | -------------------------------------------------------------------------------- /examples/app2/03_functions.hy: -------------------------------------------------------------------------------- 1 | (setv nickname 2 | (Html 3 | :name "nickname" 4 | :tag "input" 5 | :attributes 6 | {:type "text" 7 | :name "name"} 8 | :extract "value")) 9 | 10 | (setv get_nickname 11 | (Flow 12 | :name "get_nickname" 13 | :request (Request 14 | :method "GET" 15 | :cookies [session_id init_vector verify] 16 | :path "/my-details") 17 | :outputs [nickname] 18 | :operations [(Print nickname)])) 19 | 20 | (setv _functions [get_nickname]) 21 | -------------------------------------------------------------------------------- /examples/app2/09_users.hy: -------------------------------------------------------------------------------- 1 | (setv _users [{:username "account1@domain.com" 2 | :password "password1"} 3 | {:username "account2@domain.com" 4 | :password "password2"}]) 5 | -------------------------------------------------------------------------------- /examples/app3/01_main.hy: -------------------------------------------------------------------------------- 1 | (print "App3") 2 | (setv _base_url "https://app3.org") 3 | 4 | (setv username (Variable "username")) 5 | (setv password (Variable "password")) 6 | (setv mfa_code (Prompt "MFA")) 7 | 8 | (setv csrf_token 9 | (Html 10 | :name "csrf_token" 11 | :tag "meta" 12 | :attributes 13 | {:content "^[A-Za-z0-9+/=]+$" 14 | :name "csrf-token"} 15 | :extract "content")) 16 | 17 | (setv session_id (Cookie "session_id")) 18 | (setv remember_user (Cookie "remember_user_token")) 19 | -------------------------------------------------------------------------------- /examples/app3/02_authentication.hy: -------------------------------------------------------------------------------- 1 | (setv initialization 2 | (Flow 3 | :name "initialization" 4 | :request (Request 5 | :method "GET" 6 | :path "about") 7 | :outputs [csrf_token session_id] 8 | :operations [(Print csrf_token session_id) 9 | (Http 10 | :status 200 11 | :action 12 | (NextStage "login") 13 | :otherwise 14 | (Error "Cannot initialize session"))])) 15 | 16 | (setv login 17 | (Flow 18 | :name "login" 19 | :request 20 | (Request 21 | :method "POST" 22 | :path "auth/sign_in" 23 | :cookies [session_id] 24 | :data 25 | {"csrf_token" csrf_token 26 | "email" username 27 | "password" password}) 28 | :outputs [session_id csrf_token remember_user] 29 | :operations [(Grep 30 | :regex "Invalid Email or password" 31 | :action 32 | (Error "Invalid credentials")) 33 | (Grep 34 | :regex "Enter the two-factor code" 35 | :action 36 | (NextStage "multi_factor")) 37 | (Http 38 | :status 302 39 | :action 40 | (Print "Authentication successful"))])) 41 | 42 | 43 | 44 | (setv multi_factor 45 | (Flow 46 | :name "multi_factor" 47 | :request 48 | (Request 49 | :method "POST" 50 | :path "auth/sign_in" 51 | :cookies [session_id remember_user] 52 | :data 53 | {"csrf_token" csrf_token 54 | "otp_attempt" mfa_code}) 55 | :outputs [csrf_token session_id remember_user] 56 | :operations [(Grep 57 | :regex "Invalid two-factor code" 58 | :action 59 | (NextStage "multi_factor") 60 | :otherwise 61 | (Print "Authenticated successfully"))])) 62 | 63 | 64 | (setv _authentication [initialization 65 | login 66 | multi_factor]) 67 | -------------------------------------------------------------------------------- /examples/app3/03_functions.hy: -------------------------------------------------------------------------------- 1 | (setv nickname 2 | (Html 3 | :name "nickname" 4 | :tag "input" 5 | :attributes 6 | {:id "display_name"} 7 | :extract "data")) 8 | 9 | (setv get_nickname 10 | (Flow 11 | :name "get_nickname" 12 | :request (Request 13 | :method "GET" 14 | :cookies [session_id remember_user] 15 | :path "/settings/profile") 16 | :outputs [nickname] 17 | :operations [(Print nickname)])) 18 | 19 | 20 | (setv _functions [get_nickname]) 21 | -------------------------------------------------------------------------------- /examples/app3/09_users.hy: -------------------------------------------------------------------------------- 1 | (setv _users 2 | [{:username "username1" 3 | :password "password1"} 4 | {:username "username2" 5 | :password "password2"}]) 6 | -------------------------------------------------------------------------------- /examples/app4/01_main.hy: -------------------------------------------------------------------------------- 1 | (print "App4") 2 | (setv _base_url "https://www.app4.com/") 3 | 4 | 5 | ;; Only inputs 6 | (setv username (Variable "username")) 7 | (setv password (Variable "password")) 8 | (setv mfa_code (Prompt "MFA")) 9 | 10 | ;; both inputs/outputs 11 | (setv csrf_token 12 | (Html 13 | :name "csrf_token" 14 | :tag "input" 15 | :attributes 16 | {:name "csrf_token" 17 | :value "^[0-9a-f]{40}$" 18 | :type "hidden"} 19 | :extract "value")) 20 | 21 | (setv access_token 22 | (Regex 23 | :name "access_token" 24 | :regex "\"accessToken\":\"([^\"]+)\"")) 25 | 26 | (setv session_id (Cookie "session")) 27 | (setv app_session (Cookie "app_session")) 28 | -------------------------------------------------------------------------------- /examples/app4/02_authentication.hy: -------------------------------------------------------------------------------- 1 | (setv initialization 2 | (Flow 3 | :name "initialization" 4 | :request (Request 5 | :method "GET" 6 | :path "login/") 7 | :outputs [csrf_token session_id] 8 | :operations [(Print session_id csrf_token) 9 | (NextStage "login")])) 10 | 11 | (setv login 12 | (Flow 13 | :name "login" 14 | :request (Request 15 | :method "POST" 16 | :path "login" 17 | :cookies [session_id] 18 | :data 19 | {"password" password 20 | "username" username 21 | "csrf_token" csrf_token}) 22 | :outputs [session_id app_session] 23 | :operations [(Print session_id app_session) 24 | (Http 25 | :status 200 26 | :action 27 | (Grep 28 | :regex "TWO_FA_REQUIRED" 29 | :action 30 | (NextStage "multi_factor") 31 | :otherwise 32 | (NextStage "get_access_token")) 33 | :otherwise 34 | (Error "Login error"))])) 35 | 36 | (setv multi_factor 37 | (Flow 38 | :name "multi_factor" 39 | :request (Request 40 | :method "POST" 41 | :path "login" 42 | :cookies [session_id] 43 | :data 44 | {"password" password 45 | "username" username 46 | "csrf_token" csrf_token 47 | "otp" mfa_code}) 48 | :outputs [app_session] 49 | :operations [(Print app_session csrf_token) 50 | (Http 51 | :status 200 52 | :action 53 | (NextStage "get_access_token")) 54 | (Http 55 | :status 400 56 | :action 57 | (Grep 58 | :regex "WRONG_OTP" 59 | :action 60 | (NextStage "initialization") 61 | :otherwise 62 | (Error "Bad CSRF")))])) 63 | 64 | 65 | 66 | (setv get_access_token 67 | (Flow 68 | :name "get_access_token" 69 | :request (Request 70 | :method "GET" 71 | :path "/" 72 | :cookies [app_session]) 73 | :outputs [access_token] 74 | :operations [(Print access_token)])) 75 | 76 | 77 | 78 | (setv _authentication 79 | [initialization 80 | login 81 | multi_factor 82 | get_access_token]) 83 | -------------------------------------------------------------------------------- /examples/app4/03_functions.hy: -------------------------------------------------------------------------------- 1 | (setv nickname 2 | (Regex 3 | :name "nickname" 4 | :regex "href=\"/user/([^\"]+)")) 5 | 6 | (setv get_unread_messages 7 | (Flow 8 | :name "get_unread_messages" 9 | :request (Request 10 | :method "GET" 11 | :headers [(Header.bearerauth access_token)] 12 | :url "https://www.app4.com/unread_message_count"))) 13 | 14 | (setv get_nickname 15 | (Flow 16 | :name "get_nickname" 17 | :request (Request 18 | :method "GET" 19 | :cookies [session_id app_session] 20 | :path "/") 21 | :outputs [nickname] 22 | :operations [(Print nickname)])) 23 | 24 | (setv _functions [get_unread_messages 25 | get_nickname]) 26 | -------------------------------------------------------------------------------- /examples/app4/09_users.hy: -------------------------------------------------------------------------------- 1 | (setv _users 2 | [{:username "myusername1" 3 | :password "password1"} 4 | {:username "myusername2" 5 | :password "password2"}]) 6 | -------------------------------------------------------------------------------- /ext/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigeeX/raider/6853f4b6b29150d698b558806a14ea83d2e655d5/ext/logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "raider" 3 | version = "0.2.2" 4 | description = "Web authentication testing framework" 5 | authors = ["Daniel Neagaru "] 6 | packages = [ 7 | { include = "raider" } 8 | ] 9 | license = "GPL-3.0-or-later" 10 | readme = "README.md" 11 | homepage = "https://github.com/DigeeX/raider" 12 | repository = "https://github.com/DigeeX/raider" 13 | documentation = "https://raider.readthedocs.io/en/latest/" 14 | include = [ 15 | "LICENSE", 16 | "CHANGELOG.md", 17 | "docs/**/*", 18 | "examples", 19 | "scripts" 20 | ] 21 | keywords = ["authentication", "security", "raider", "digeex", "hy"] 22 | classifiers = [ 23 | "Development Status :: 3 - Alpha", 24 | "Environment :: Console", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: System Administrators", 27 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 28 | "Natural Language :: English", 29 | "Operating System :: POSIX :: Linux", 30 | "Programming Language :: Lisp", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Topic :: Internet :: WWW/HTTP", 34 | "Topic :: Security", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | "Topic :: Software Development :: Testing", 37 | ] 38 | 39 | # [tool.poetry.scripts] 40 | # raider = "raider.cli:main" 41 | 42 | [tool.poetry.dependencies] 43 | python = "^3.8" 44 | hy = "^0.20.0" 45 | requests = "^2.25.1" 46 | importlib-metadata = "^4.6.1" 47 | bs4 = "^0.0.1" 48 | 49 | [tool.poetry.dev-dependencies] 50 | pytest = "^5.2" 51 | pytest-cov = "^2.12.1" 52 | pre-commit = "^2.13.0" 53 | flake8 = "^3.9.2" 54 | pylint = "^2.9.3" 55 | black = {version = "^21.6b0", allow-prereleases = true} 56 | isort = "^5.9.1" 57 | sphinxcontrib-needs = "^0.7.0" 58 | sphinx-autodoc-typehints = "^1.12.0" 59 | sphinx-rtd-theme = "^0.5.2" 60 | 61 | [build-system] 62 | requires = ["poetry-core>=1.0.0"] 63 | build-backend = "poetry.core.masonry.api" 64 | 65 | [tool.black] 66 | line-length = 79 67 | target-version = ['py39'] 68 | include = '\.pyi?$' 69 | exclude = ''' 70 | ( 71 | /( 72 | | \.git 73 | | \.tox 74 | | dist 75 | | docs 76 | | scripts 77 | )/ 78 | ) 79 | ''' 80 | 81 | [tool.isort] 82 | multi_line_output = 3 83 | include_trailing_comma = true 84 | force_grid_wrap = 0 85 | use_parentheses = true 86 | line_length = 79 87 | 88 | 89 | [tool.pylint.master] 90 | ignore-patterns = ''' 91 | \.git 92 | |conf.py 93 | |scripts/* 94 | ''' 95 | 96 | [tool.pylint.message_control] 97 | disable = ''' 98 | import-error, 99 | ''' 100 | 101 | 102 | [tool.mypy] 103 | python_version = "3.9" 104 | follow_imports = "silent" 105 | strict_optional = true 106 | warn_redundant_casts = true 107 | warn_unused_ignores = true 108 | disallow_any_generics = true 109 | check_untyped_defs = true 110 | no_implicit_reexport = true 111 | disallow_untyped_defs = true 112 | exclude = "scripts" 113 | overrides = [ 114 | 115 | # TODO 116 | # something doesn't work with requests types in the virtual environment 117 | { module = "requests.*", ignore_missing_imports = true }, 118 | 119 | ] 120 | -------------------------------------------------------------------------------- /raider/__init__.py: -------------------------------------------------------------------------------- 1 | """Import stuff for external access. 2 | """ 3 | 4 | from raider.__version__ import __version__ 5 | from raider.application import Application 6 | from raider.authentication import Authentication 7 | from raider.config import Config 8 | from raider.flow import Flow 9 | from raider.functions import Functions 10 | from raider.raider import Raider 11 | from raider.request import Request 12 | from raider.user import User 13 | -------------------------------------------------------------------------------- /raider/__version__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Raider version information. 17 | """ 18 | 19 | from importlib_metadata import version 20 | 21 | __title__ = "raider" 22 | __description__ = "Authentication testing tool" 23 | __version__ = version("raider") 24 | __author__ = "Daniel Neagaru" 25 | __author_email__ = "daniel@digeex.de" 26 | __license__ = "GPLv3+" 27 | __copyright__ = "Copyright 2021 Daniel Neagaru" 28 | -------------------------------------------------------------------------------- /raider/application.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Application class holding project configuration. 17 | """ 18 | 19 | import logging 20 | 21 | from raider.authentication import Authentication 22 | from raider.config import Config 23 | from raider.functions import Functions 24 | from raider.user import UserStore 25 | from raider.utils import create_hy_expression, eval_file, get_project_file 26 | 27 | 28 | class Application: 29 | """Class holding all the project related data. 30 | 31 | This class isn't supposed to be used directly by the user, instead 32 | the Raider class should be used, which will deal with the 33 | Application class internally. 34 | 35 | Attributes: 36 | name: 37 | A string with the name of the application. 38 | base_url: 39 | A string with the base URL of the application. 40 | config: 41 | A Config object with Raider global configuration plus the 42 | variables defined in hy configuration files related to the 43 | Application. 44 | users: 45 | A UserStore object generated from the "_users" variable set in 46 | the hy configuration files for the project. 47 | active_user: 48 | A User object pointing to the active user inside the "users" 49 | object. 50 | authentication: 51 | An Authentication object containing all the Flows relevant to 52 | the authentication process. It's created out of the 53 | "_authentication" variable from the hy configuration files. 54 | functions: 55 | A Functions object with all Flows that don't affect the 56 | authentication process. This object is being created out of the 57 | "_functions" variable from the hy configuration files. 58 | 59 | """ 60 | 61 | def __init__(self, project: str = None) -> None: 62 | """Initializes the Application object. 63 | 64 | Sets up the environment necessary to test the specified 65 | application. 66 | 67 | Args: 68 | project: 69 | A string with the name of the application to be 70 | initialized. If not supplied, the last used application will 71 | be selected 72 | 73 | """ 74 | self.config = Config() 75 | 76 | if project: 77 | self.config.active_project = project 78 | self.config.write_config_file() 79 | self.project = project 80 | else: 81 | self.project = self.config.active_project 82 | 83 | output = self.config.load_project(project) 84 | self.users = UserStore(output["_users"]) 85 | active_user = output.get("_active_user") 86 | if active_user and active_user in self.users: 87 | self.active_user = self.users[active_user] 88 | else: 89 | self.active_user = self.users.active 90 | 91 | self.authentication = Authentication(output["_authentication"]) 92 | functions = output.get("_functions") 93 | if functions: 94 | self.functions = Functions(functions) 95 | self.base_url = output.get("_base_url") 96 | 97 | def authenticate(self, username: str = None) -> None: 98 | """Authenticates the user. 99 | 100 | Runs all the steps of the authentication process defined in the 101 | hy config files for the application. 102 | 103 | Args: 104 | username: 105 | A string with the user to be authenticated. If not supplied, 106 | the last used username will be selected. 107 | 108 | """ 109 | if username: 110 | self.active_user = self.users[username] 111 | self.authentication.run_all(self.active_user, self.config) 112 | self.write_project_file() 113 | 114 | def write_session_file(self) -> None: 115 | """Saves session data. 116 | 117 | Saves user related session data in a file for later use. This 118 | includes cookies, headers, and other data extracted using 119 | Plugins. 120 | 121 | """ 122 | filename = get_project_file(self.project, "_userdata.hy") 123 | value = "" 124 | cookies = {} 125 | headers = {} 126 | data = {} 127 | with open(filename, "w") as sess_file: 128 | for username in self.users: 129 | user = self.users[username] 130 | cookies.update({username: user.cookies.to_dict()}) 131 | headers.update({username: user.headers.to_dict()}) 132 | data.update({username: user.data.to_dict()}) 133 | 134 | value += create_hy_expression("_cookies", cookies) 135 | value += create_hy_expression("_headers", headers) 136 | value += create_hy_expression("_data", data) 137 | logging.debug("Writing to session file %s", filename) 138 | logging.debug("value = %s", str(value)) 139 | sess_file.write(value) 140 | 141 | def load_session_file(self) -> None: 142 | """Loads session data. 143 | 144 | If session data was saved with write_session_file() this 145 | function will load this data into existing :class:`User 146 | ` objects. 147 | 148 | """ 149 | filename = get_project_file(self.project, "_userdata.hy") 150 | output = eval_file(filename) 151 | cookies = output.get("_cookies") 152 | headers = output.get("_headers") 153 | data = output.get("_data") 154 | 155 | if cookies: 156 | for username in cookies: 157 | self.users[username].set_cookies_from_dict(cookies[username]) 158 | 159 | if headers: 160 | for username in headers: 161 | self.users[username].set_headers_from_dict(headers[username]) 162 | 163 | if data: 164 | for username in data: 165 | self.users[username].set_data_from_dict(data[username]) 166 | 167 | def write_project_file(self) -> None: 168 | """Writes the project settings. 169 | 170 | For now only the active user is saved, so that the next time the 171 | project is used, there's no need to specify the user manually. 172 | 173 | """ 174 | filename = get_project_file(self.project, "_project.hy") 175 | value = "" 176 | with open(filename, "w") as proj_file: 177 | value += create_hy_expression( 178 | "_active_user", self.active_user.username 179 | ) 180 | logging.debug("Writing to session file %s", filename) 181 | logging.debug("value = %s", str(value)) 182 | proj_file.write(value) 183 | -------------------------------------------------------------------------------- /raider/attacks.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Attacks to be run on Flows. 17 | """ 18 | 19 | import logging 20 | import sys 21 | from copy import deepcopy 22 | from functools import partial 23 | from typing import Callable, List, Optional 24 | 25 | from raider.application import Application 26 | from raider.flow import Flow 27 | from raider.plugins.common import Plugin 28 | 29 | 30 | class Fuzz: 31 | """Fuzz an input.""" 32 | 33 | # Fuzzing flags 34 | IS_AUTHENTICATION = 0x01 35 | 36 | def __init__( 37 | self, 38 | application: Application, 39 | flow: Flow, 40 | fuzzing_point: str, 41 | flags: int = 0, 42 | ) -> None: 43 | """Initialize the Fuzz object. 44 | 45 | Given a :class:`Flow `, a fuzzing point (in 46 | case of Raider this should be a :class:`Plugin 47 | `), and a function, run the attack. The 48 | function is used to generate the strings to be used for 49 | fuzzing. The ``fuzzing_point`` attribute should contain the name 50 | of the plugin. 51 | 52 | Args: 53 | application: 54 | An :class:`Application ` 55 | object. 56 | flow: 57 | A :class:`Flow ` object which needs to be 58 | fuzzed. 59 | fuzzing_point: 60 | The name given to the :class:`Plugin 61 | ` which should be fuzzed. 62 | fuzzing_generator: 63 | A function which returns a Python generator, that will 64 | create the strings that will be used for fuzzing. The 65 | function should accept one argument. This will be the value 66 | of the plugin before fuzzing. It can be considered when 67 | building the fuzzing list, or ignored. 68 | 69 | """ 70 | 71 | self.application = application 72 | self.flow = flow 73 | self.fuzzing_point = fuzzing_point 74 | self.flags = flags 75 | 76 | self.generator: Optional[Callable[..., List[str]]] = None 77 | self.processor: Callable[[str], str] = lambda value: value 78 | 79 | def run(self) -> None: 80 | """Runs the fuzzer.""" 81 | if self.is_authentication: 82 | self.attack_authentication() 83 | else: 84 | self.attack_function() 85 | 86 | def set_input_file( 87 | self, filename: str, prepend: bool = False, append: bool = False 88 | ) -> None: 89 | """Sets the input file for the fuzzer. 90 | 91 | Uses the input file to generate fuzzing strings, and sets the 92 | generator function to return those values. 93 | 94 | """ 95 | 96 | def fuzzing_generator( 97 | value: str, filename: str, prepend: bool, append: bool 98 | ) -> List[str]: 99 | """Generate a list of strings to use for fuzzing. 100 | 101 | Args: 102 | value: 103 | The original value of the field. 104 | filename: 105 | The filename with the inputs. 106 | prepend: 107 | A boolean flag meaning the original value will be 108 | prepended with the fuzzing string. 109 | append: 110 | A boolean flag meaning the original value will be 111 | appended with the fuzzing string. 112 | 113 | Returns: 114 | A list of final strings to be fuzzed. 115 | """ 116 | fuzzstrings = [] 117 | with open(filename) as contents: 118 | for item in contents.readlines(): 119 | if prepend: 120 | fuzzstrings.append(item.strip() + value) 121 | elif append: 122 | fuzzstrings.append(value + item.strip()) 123 | else: 124 | fuzzstrings.append(item.strip()) 125 | 126 | return fuzzstrings 127 | 128 | self.generator = partial( 129 | fuzzing_generator, 130 | filename=filename, 131 | prepend=prepend, 132 | append=append, 133 | ) 134 | 135 | def get_fuzzing_input(self, flow: Flow) -> Plugin: 136 | """Returns the Plugin associated with the fuzzing input. 137 | 138 | Args: 139 | flow: 140 | The flow object with the plugin to be returned. 141 | 142 | Returns: 143 | The plugin object to be fuzzed. 144 | """ 145 | flow_inputs = flow.request.list_inputs() 146 | if flow_inputs: 147 | fuzzing_plugin = flow_inputs.get(self.fuzzing_point) 148 | if not fuzzing_plugin: 149 | logging.critical( 150 | "Fuzzing point %s not found", self.fuzzing_point 151 | ) 152 | sys.exit() 153 | else: 154 | logging.critical("Flow %s has no inputs", flow.name) 155 | sys.exit() 156 | 157 | return fuzzing_plugin 158 | 159 | def attack_function(self) -> None: 160 | """Attacks a flow defined in ``_functions``. 161 | 162 | Fuzz blindly the Flow object. It doesn't take into account the 163 | authentication process, so this function is useful for fuzzing 164 | stuff as an already authenticated user. 165 | 166 | Args: 167 | user: 168 | A :class:`User ` object with the user 169 | specific information. 170 | config: 171 | A :class:`Config ` object with global 172 | **Raider** configuration. 173 | 174 | """ 175 | user = self.application.active_user 176 | config = self.application.config 177 | flow = deepcopy(self.flow) 178 | fuzzing_plugin = self.get_fuzzing_input(flow) 179 | flow.get_plugin_values(user) 180 | 181 | # Reset plugin flags because it doesn't need userdata nor 182 | # the HTTP response anymore when fuzzing 183 | fuzzing_plugin.flags = 0 184 | 185 | if not self.generator: 186 | logging.critical( 187 | "Cannot run fuzzing without configuring the generator." 188 | ) 189 | sys.exit() 190 | 191 | for item in self.generator(fuzzing_plugin.value): 192 | fuzzing_plugin.value = self.processor(item) 193 | fuzzing_plugin.function = fuzzing_plugin.return_value 194 | flow.execute(user, config) 195 | flow.run_operations() 196 | 197 | def attack_authentication(self) -> None: 198 | """Attacks a Flow defined in ``_authentication``. 199 | 200 | Unlike ``attack_function``, this will take into account the 201 | finite state machine defined in the hyfiles. This should be used 202 | when the authentication process can be altered by the fuzzing, 203 | for example if some token needs to be extracted again from a 204 | previous authentication step for fuzzing to work. 205 | 206 | It will first follow the authentication process until reaching 207 | the desired state, then it will try fuzzing it, and if a 208 | :class:`NextStage ` operation is 209 | encountered, it will follow the instruction and move to this 210 | stage, then continue fuzzing. 211 | 212 | """ 213 | authentication = self.application.authentication 214 | user = self.application.active_user 215 | config = self.application.config 216 | 217 | while authentication.current_stage_name != self.flow.name: 218 | authentication.run_current_stage(user, config) 219 | 220 | flow = deepcopy(self.flow) 221 | fuzzing_plugin = self.get_fuzzing_input(flow) 222 | 223 | # Reset plugin flags because it doesn't need userdata nor 224 | # the HTTP response anymore when fuzzing 225 | fuzzing_plugin.flags = 0 226 | 227 | if not self.generator: 228 | logging.critical( 229 | "Cannot run fuzzing without configuring the generator." 230 | ) 231 | sys.exit() 232 | 233 | elements = self.generator(fuzzing_plugin.value) 234 | 235 | for item in elements: 236 | fuzzing_plugin.value = self.processor(item) 237 | fuzzing_plugin.function = fuzzing_plugin.return_value 238 | flow.execute(user, config) 239 | next_stage = flow.run_operations() 240 | if next_stage: 241 | while next_stage != flow.name: 242 | if next_stage: 243 | next_stage = authentication.run_stage( 244 | next_stage, user, config 245 | ) 246 | else: 247 | logging.critical( 248 | ( 249 | "Cannot reach the %s stage. ", 250 | "Make sure you defined NextStage correctly.", 251 | ), 252 | flow.name, 253 | ) 254 | 255 | @property 256 | def is_authentication(self) -> bool: 257 | """Returns True if the IS_AUTHENTICATION flag is set.""" 258 | return bool(self.flags & self.IS_AUTHENTICATION) 259 | -------------------------------------------------------------------------------- /raider/authentication.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Authentication class responsible for running the defined stages. 17 | """ 18 | 19 | import logging 20 | import sys 21 | from typing import List, Optional, Union 22 | 23 | from raider.config import Config 24 | from raider.flow import Flow 25 | from raider.plugins.basic import Cookie, Header, Html, Json, Regex 26 | from raider.user import User 27 | 28 | 29 | class Authentication: 30 | """Class holding authentication stages. 31 | 32 | This class holds all the information necessary to authenticate. It 33 | provides functions to run those authentication steps. 34 | 35 | Attributes: 36 | stages: 37 | A list of Flow objects relevant to the authentication process. 38 | 39 | """ 40 | 41 | def __init__(self, stages: List[Flow]) -> None: 42 | """Initializes the Authentication object. 43 | 44 | Creates an object to handle the authentication process. 45 | 46 | Args: 47 | stages: 48 | A list of Flow objects defined inside "_authentication" 49 | variable in hy configuration files. 50 | 51 | """ 52 | self.stages = stages 53 | self._current_stage = 0 54 | 55 | def get_stage_by_name(self, name: str) -> Optional[Flow]: 56 | """Returns the Flow object given the name. 57 | 58 | Args: 59 | name: 60 | A string with the name of the Flow as defined in the hy 61 | configuration files. 62 | 63 | Returns: 64 | A Flow object matching the name supplied to the function, or 65 | None if there are no such object. 66 | 67 | """ 68 | for stage in self.stages: 69 | if stage.name == name: 70 | return stage 71 | return None 72 | 73 | def get_stage_name_by_id(self, stage_id: int) -> str: 74 | """Returns the stage name given its number. 75 | 76 | Each authentication step is given an index based on its position 77 | in the "_authentication" list. This function returns the name of 78 | the Flow based on its position in this list. 79 | 80 | Args: 81 | stage_id: 82 | An integer with the index of the stage. 83 | 84 | Returns: 85 | A string with the name of the Flow in the position "stage_id". 86 | 87 | """ 88 | return self.stages[stage_id].name 89 | 90 | def get_stage_index(self, name: str) -> int: 91 | """Returns the index of the stage given its name. 92 | 93 | 94 | Each authentication step is given an index based on its position 95 | in the "_authentication" list. This function returns the index of 96 | the Flow based on its name. 97 | 98 | Args: 99 | name: 100 | A string with the name of the Flow. 101 | 102 | Returns: 103 | An integer with the index of the Flow with the specified "name". 104 | """ 105 | if not name: 106 | return -1 107 | for stage in self.stages: 108 | if stage.name == name: 109 | return self.stages.index(stage) 110 | 111 | return -1 112 | 113 | def run_all(self, user: User, config: Config) -> None: 114 | """Runs all authentication stages. 115 | 116 | This function will run all authentication stages for the 117 | specified User and will take into account the supplied Config 118 | for things like the user agent and the web proxy to use. 119 | 120 | Args: 121 | user: 122 | A User object containing the credentials and where the user 123 | specific data will be stored. 124 | config: 125 | A Config object with the global Raider settings. 126 | 127 | """ 128 | while self._current_stage >= 0: 129 | logging.info( 130 | "Running stage %s", 131 | self.get_stage_name_by_id(self._current_stage), 132 | ) 133 | self.run_current_stage(user, config) 134 | 135 | def run_current_stage(self, user: User, config: Config) -> None: 136 | """Runs the current stage only. 137 | 138 | Authentication class keeps the index of the current stage in the 139 | "_current_stage" variable. This function runs only one 140 | authentication step indexed by this variable. 141 | 142 | Args: 143 | user: 144 | A User object containing the credentials and where the user 145 | specific data will be stored. 146 | config: 147 | A Config object with the global Raider settings. 148 | 149 | """ 150 | self.run_stage(self._current_stage, user, config) 151 | 152 | def run_stage( 153 | self, stage_id: Union[int, str], user: User, config: Config 154 | ) -> Optional[str]: 155 | """Runs one authentication Stage. 156 | 157 | First, the Flow object of the specified stage is identified, 158 | then the related HTTP request is processed, sent, the response 159 | is received, and the operations are run on the Flow. 160 | 161 | Args: 162 | stage_id: 163 | A string or an integer identifying the authentication stage 164 | to run. If it's a string, it's the name of the Flow, and if 165 | it's an integer, it's the index of the Flow object in the 166 | "_authentication" variable. 167 | user: 168 | A User object containing the credentials and where the user 169 | specific data will be stored. 170 | config: 171 | A Config object with the global Raider settings. 172 | 173 | Returns: 174 | Optionally, this function returns a string with the name of 175 | the next Flow in the authentication process. 176 | 177 | """ 178 | 179 | stage: Optional[Flow] 180 | if isinstance(stage_id, int): 181 | stage = self.stages[stage_id] 182 | elif isinstance(stage_id, str): 183 | stage = self.get_stage_by_name(stage_id) 184 | 185 | if not stage: 186 | logging.critical("Stage %s not defined. Cannot continue", stage_id) 187 | sys.exit() 188 | 189 | stage.execute(user, config) 190 | 191 | if stage.outputs: 192 | for item in stage.outputs: 193 | if isinstance(item, Cookie): 194 | user.set_cookie(item) 195 | elif isinstance(item, Header): 196 | user.set_header(item) 197 | elif isinstance(item, (Regex, Html, Json)): 198 | user.set_data(item) 199 | 200 | next_stage = stage.run_operations() 201 | if next_stage: 202 | self._current_stage = self.get_stage_index(next_stage) 203 | else: 204 | self._current_stage = -1 205 | return next_stage 206 | 207 | @property 208 | def current_stage_name(self) -> str: 209 | """Returns the name of the current stage.""" 210 | return self.get_stage_name_by_id(self._current_stage) 211 | -------------------------------------------------------------------------------- /raider/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Config class holding global Raider configuration. 17 | """ 18 | 19 | import logging 20 | import os 21 | import sys 22 | from typing import Any, Dict 23 | 24 | from raider.utils import ( 25 | create_hy_expression, 26 | default_user_agent, 27 | eval_file, 28 | eval_project_file, 29 | get_config_file, 30 | get_project_dir, 31 | list_projects, 32 | ) 33 | 34 | 35 | class Config: 36 | """Class dealing with global Raider configuration. 37 | 38 | A Config object will contain all the information necessary to run 39 | Raider. It will define global configurations like the web proxy and 40 | the logging level, but also the data defined in the active project 41 | configuration files. 42 | 43 | Attributes: 44 | proxy: 45 | An optional string to define the web proxy to relay the traffic 46 | through. 47 | verify: 48 | A boolean flag which will let the requests library know whether 49 | to check the SSL certificate or ignore it. 50 | loglevel: 51 | A string used by the logging library to define the desired 52 | logging level. 53 | user_agent: 54 | A string which will be used as the user agent in HTTP requests. 55 | active_project: 56 | A string defining the current active project. 57 | project_config: 58 | A dictionary containing all of the local variables defined in 59 | the active project's hy configuration files. 60 | logger: 61 | A logging.RootLogger object used for debugging. 62 | 63 | """ 64 | 65 | def __init__(self) -> None: 66 | """Initializes the Config object. 67 | 68 | Retrieves configuration from "common.hy" file, or populates it 69 | with the default values if it doesn't exist. 70 | 71 | """ 72 | filename = get_config_file("common.hy") 73 | if os.path.isfile(filename): 74 | output = eval_file(filename) 75 | else: 76 | output = {} 77 | 78 | self.proxy = output.get("proxy", None) 79 | self.verify = output.get("verify", False) 80 | self.loglevel = output.get("loglevel", "WARNING") 81 | self.user_agent = output.get("user_agent", default_user_agent()) 82 | self.active_project = output.get("active_project", None) 83 | self.project_config: Dict[str, Any] = {} 84 | 85 | self.logger = logging.getLogger() 86 | self.logger.setLevel(self.loglevel) 87 | 88 | if not list_projects(): 89 | self.logger.critical( 90 | "No application have been configured. Cannot run." 91 | ) 92 | sys.exit() 93 | 94 | def load_project(self, project: str = None) -> Dict[str, Any]: 95 | """Loads project settings. 96 | 97 | Goes through all the ".hy" files in the project directory, 98 | evaluates them all, and returns the created locals, making them 99 | available to the rest of Raider. 100 | 101 | Files are loaded in alphabetical order, and objects created in 102 | one of them will be available to the next one, eliminating the 103 | need to use imports. This allows the user to split the 104 | configuration files however it makes sense, and Raider doesn't 105 | impose any restrictions on those files. 106 | 107 | All ".hy" files in the project directory are evaluated, which 108 | could be considered unsafe and could cause all kinds of security 109 | issues, but Raider assumes the user knows what they're doing and 110 | will not copy/paste hylang code from untrusted sources. 111 | 112 | Args: 113 | project: 114 | A string with the name of the project. By default the 115 | project is located in "~/.config/raider/". All ".hy" files 116 | from this directory will be executed and the locals that 117 | were created during that will be returned. 118 | Returns: 119 | A dictionary as returned by the locals() function. It contains 120 | all of the locally defined objects in the ".hy" configuration 121 | files. 122 | """ 123 | if not project: 124 | active_project = self.active_project 125 | else: 126 | active_project = project 127 | 128 | hyfiles = sorted(os.listdir(get_project_dir(active_project))) 129 | shared_locals: Dict[str, Any] 130 | shared_locals = {} 131 | for confile in hyfiles: 132 | if confile.endswith(".hy") and not confile.startswith("."): 133 | shared_locals.update( 134 | eval_project_file(active_project, confile, shared_locals) 135 | ) 136 | self.project_config = shared_locals 137 | return shared_locals 138 | 139 | def write_config_file(self) -> None: 140 | """Writes global configuration to common.hy. 141 | 142 | Gets the current configuration from the Config object and writes 143 | them in hylang format in the "common.hy" file. 144 | """ 145 | filename = get_config_file("common.hy") 146 | data = "" 147 | with open(filename, "w") as conf_file: 148 | data += create_hy_expression("proxy", self.proxy) 149 | data += create_hy_expression("user_agent", self.user_agent) 150 | data += create_hy_expression("loglevel", self.loglevel) 151 | data += create_hy_expression("verify", self.verify) 152 | data += create_hy_expression("active_project", self.active_project) 153 | self.logger.debug("Writing to config file %s", filename) 154 | self.logger.debug("data = %s", str(data)) 155 | conf_file.write(data) 156 | 157 | def print_config(self) -> None: 158 | """Prints current configuration.""" 159 | print("proxy: " + self.proxy) 160 | print("verify: " + str(self.verify)) 161 | print("loglevel: " + self.loglevel) 162 | print("user_agent: " + self.user_agent) 163 | print("active_project: " + self.active_project) 164 | -------------------------------------------------------------------------------- /raider/flow.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Flow class holding the information exchanged between server and client. 17 | """ 18 | 19 | import logging 20 | from typing import List, Optional 21 | 22 | import requests 23 | 24 | from raider.config import Config 25 | from raider.operations import Operation 26 | from raider.plugins.common import Plugin 27 | from raider.request import Request 28 | from raider.user import User 29 | 30 | 31 | class Flow: 32 | """Class dealing with the information exchange from HTTP communication. 33 | 34 | A Flow object in Raider defines all the information about one single 35 | HTTP information exchange. It has a ``name``, contains one 36 | ``request``, the ``response``, the ``outputs`` that needs to be 37 | extracted from the response, and a list of ``operations`` to be run 38 | when the exchange is over. 39 | 40 | Flow objects are used as states in the :class:`Authentication 41 | ` class to define the 42 | authentication process as a finite state machine. 43 | 44 | It's also used in the :class:`Functions 45 | ` class to run arbitrary actions when it 46 | doesn't affect the authentication state. 47 | 48 | Attributes: 49 | name: 50 | A string used as a unique identifier for the defined Flow. 51 | request: 52 | A :class:`Request ` object detailing the 53 | HTTP request with its elements. 54 | response: 55 | A :class:`requests.model.Response` object. It's empty until the 56 | request is sent. When the HTTP response arrives, it's stored 57 | here. 58 | outputs: 59 | A list of :class:`Plugin ` objects 60 | detailing the pieces of information to be extracted from the 61 | response. Those will be later available for other Flow objects. 62 | operations: 63 | A list of :class:`Operation ` 64 | objects to be executed after the response is received and 65 | outputs are extracted. Should contain a :class:`NextStage 66 | ` operation if another Flow is 67 | expected. 68 | logger: 69 | A :class:`logging.RootLogger` object used for debugging. 70 | 71 | """ 72 | 73 | def __init__( 74 | self, 75 | name: str, 76 | request: Request, 77 | outputs: List[Plugin] = None, 78 | operations: List[Operation] = None, 79 | ) -> None: 80 | """Initializes the Flow object. 81 | 82 | Creates the Flow object with the associated Request, the outputs 83 | to be extracted, and the operations to be run upon completion. 84 | 85 | Args: 86 | name: 87 | A string with a unique identifier for this Flow. 88 | request: 89 | A Request object associated with this Flow. 90 | outputs: 91 | A list of Plugins to be used for extracting data from the 92 | response. 93 | operations: 94 | A list of Operations to be run after the response is 95 | received. 96 | 97 | """ 98 | self.name = name 99 | 100 | self.outputs = outputs 101 | self.operations = operations 102 | 103 | self.request = request 104 | self.response: requests.models.Response = None 105 | 106 | self.logger = logging.getLogger(self.name) 107 | 108 | def execute(self, user: User, config: Config) -> None: 109 | """Sends the request and extracts the outputs. 110 | 111 | Given the user in context and the global Raider configuration, 112 | sends the HTTP request and extracts the defined outputs. 113 | 114 | Iterates through the defined outputs in the Flow object, and 115 | extracts the data from the HTTP response, saving it in the 116 | respective :class:`Plugin ` object. 117 | 118 | Args: 119 | user: 120 | An object containing all the user specific data relevant for 121 | this action. 122 | config: 123 | The global Raider configuration. 124 | 125 | """ 126 | self.response = self.request.send(user, config) 127 | if self.outputs: 128 | for output in self.outputs: 129 | if output.needs_response: 130 | output.extract_value_from_response(self.response) 131 | if output.name_not_known_in_advance: 132 | output.extract_name_from_response(self.response) 133 | elif output.depends_on_other_plugins: 134 | for item in output.plugins: 135 | item.get_value(user.to_dict()) 136 | output.get_value(user.to_dict()) 137 | 138 | def get_plugin_values(self, user: User) -> None: 139 | """Given a user, get the plugins' values from it. 140 | 141 | Args: 142 | user: 143 | A :class:`User ` object with the userdata. 144 | 145 | """ 146 | flow_inputs = self.request.list_inputs() 147 | if flow_inputs: 148 | for plugin in flow_inputs.values(): 149 | plugin.get_value(user.to_dict()) 150 | 151 | def run_operations(self) -> Optional[str]: 152 | """Runs the defined :class:`operations `. 153 | 154 | Iterates through the defined ``operations`` and executes them 155 | one by one. Iteration stops when the first :class:`NextStage 156 | ` operations is encountered. 157 | 158 | Returns: 159 | A string with the name of the next stage to run or None. 160 | 161 | """ 162 | next_stage = None 163 | 164 | if self.operations: 165 | for item in self.operations: 166 | next_stage = item.run(self.response) 167 | if next_stage: 168 | break 169 | 170 | return next_stage 171 | -------------------------------------------------------------------------------- /raider/functions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Functions class holding all Flows unrelated to authentication. 17 | """ 18 | 19 | 20 | import logging 21 | import sys 22 | from typing import List, Optional 23 | 24 | from raider.config import Config 25 | from raider.flow import Flow 26 | from raider.user import User 27 | 28 | 29 | class Functions: 30 | """Class holding all Flows that don't affect the authentication. 31 | 32 | This class shouldn't be used directly by the user, instead the 33 | Raider class should be used which will deal with Functions 34 | internally. 35 | 36 | Attributes: 37 | functions: 38 | A list of Flow objects with all available functions. 39 | 40 | """ 41 | 42 | def __init__(self, functions: List[Flow]) -> None: 43 | """Initializes the Functions object. 44 | 45 | Args: 46 | functions: 47 | A list of Flow objects to be included in the Functions 48 | object. 49 | 50 | """ 51 | self.functions = functions 52 | 53 | def get_function_by_name(self, name: str) -> Optional[Flow]: 54 | """Gets the function given its name. 55 | 56 | Tries to find the Flow object with the given name, and returns 57 | it if it's found, otherwise returns None. 58 | 59 | Args: 60 | name: 61 | A string with the unique identifier of the function as 62 | defined in the Flow. 63 | 64 | Returns: 65 | A Flow object associated with the name, or None if no such 66 | function has been found. 67 | 68 | """ 69 | for function in self.functions: 70 | if function.name == name: 71 | return function 72 | return None 73 | 74 | def run(self, name: str, user: User, config: Config) -> None: 75 | """Runs a Function. 76 | 77 | Executes the given function, in the context of the specified 78 | user, and applies the global Raider configuration. 79 | 80 | Args: 81 | name: 82 | A string with the name of the function to run. 83 | user: 84 | A User object containing all the data needed to run the 85 | function in this user's context. 86 | config: 87 | A Config object with the global Raider configuration. 88 | 89 | """ 90 | logging.info("Running function %s", name) 91 | function = self.get_function_by_name(name) 92 | if function: 93 | function.execute(user, config) 94 | if function.outputs: 95 | for item in function.outputs: 96 | if item.value: 97 | user.set_data(item) 98 | 99 | function.run_operations() 100 | 101 | else: 102 | logging.critical("Function %s not defined. Cannot continue", name) 103 | sys.exit() 104 | -------------------------------------------------------------------------------- /raider/operations.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Operations performed on Flows after the response is received. 17 | """ 18 | 19 | import logging 20 | import re 21 | import sys 22 | from functools import partial 23 | from typing import Any, Callable, List, Optional, Union 24 | 25 | import requests 26 | 27 | from raider.plugins.common import Plugin 28 | 29 | 30 | def execute_actions( 31 | operations: Union["Operation", List["Operation"]], 32 | response: requests.models.Response, 33 | ) -> Optional[str]: 34 | """Run an Operation or a list of Operations. 35 | 36 | In order to allow multiple Operations to be run inside the "action" 37 | and "otherwise" attributes lists of Operations are accepted. The 38 | execution will stop if one of the Operations returns a string, to 39 | indicate the next stage has been decided. 40 | 41 | Args: 42 | operations: 43 | An Operation object or a list of Operations to be executed. 44 | response: 45 | A requests.models.Response object with the HTTP response that 46 | might be needed to run the Operation. 47 | 48 | Returns: 49 | A string with the name of the next stage to be run, or None. 50 | 51 | """ 52 | if isinstance(operations, Operation): 53 | return operations.run(response) 54 | 55 | if isinstance(operations, list): 56 | for item in operations: 57 | output = item.run(response) 58 | if output: 59 | return output 60 | 61 | return None 62 | 63 | 64 | class Operation: 65 | """Parent class for all operations. 66 | 67 | Each Operation class inherits from here. 68 | 69 | Attributes: 70 | function: 71 | A callable function to be executed when the operation is run. 72 | flags: 73 | An integer with the flags which define the behaviour of the 74 | Operation. For now only two flags are allowed: NEEDS_RESPONSE 75 | and IS_CONDITIONAL. If NEEDS_RESPONSE is set, the HTTP response 76 | will be sent to the "function" for further processing. If 77 | IS_CONDITIONAL is set, the function should return a boolean, and 78 | if the return value is True the Operation inside "action" will 79 | be run next, if it's False, the one from the "otherwise" will be 80 | run. 81 | action: 82 | An Operation object that will be run if the function returns 83 | True. Will only be used if the flag IS_CONDITIONAL is set. 84 | otherwise: 85 | An Operation object that will be run if the function returns 86 | False. Will only be used if the flag IS_CONDITIONAL is set. 87 | 88 | """ 89 | 90 | # Operation flags 91 | 92 | # Operation is conditional. Needs to have an ``action`` defined and 93 | # optionally an ``otherwise``. 94 | IS_CONDITIONAL = 0x01 95 | 96 | # Operation's function needs the HTTP response to run. 97 | NEEDS_RESPONSE = 0x02 98 | 99 | # Operation will append instead of overwrite. Used when dealing with files 100 | # to make sure old data doesn't get overwritten. 101 | WILL_APPEND = 0x04 102 | 103 | def __init__( 104 | self, 105 | function: Callable[..., Any], 106 | flags: int = 0, 107 | action: Optional[Union["Operation", List["Operation"]]] = None, 108 | otherwise: Optional[Union["Operation", List["Operation"]]] = None, 109 | ): 110 | """Initializes the Operation object. 111 | 112 | Args: 113 | function: 114 | A callable function to be executed when the operation is run. 115 | flags: 116 | An integer with the flags that define the behaviour of this 117 | Operation. 118 | action: 119 | An Operation object that will be run when the function 120 | returns True. 121 | otherwise: 122 | An Operation object that will be run when the function 123 | returns False. 124 | 125 | """ 126 | self.function = function 127 | self.flags = flags 128 | self.action = action 129 | self.otherwise = otherwise 130 | 131 | def run(self, response: requests.models.Response) -> Optional[str]: 132 | """Runs the Operation. 133 | 134 | Runs the defined Operation, considering the "flags" set. 135 | 136 | Args: 137 | response: 138 | A requests.models.Response object with the HTTP response to 139 | be passed to the operation's "function". 140 | 141 | Returns: 142 | An optional string with the name of the next stage. 143 | 144 | """ 145 | logging.debug("Running operation %s", str(self)) 146 | if self.is_conditional: 147 | return self.run_conditional(response) 148 | if self.needs_response: 149 | return self.function(response) 150 | return self.function() 151 | 152 | def run_conditional( 153 | self, response: requests.models.Response 154 | ) -> Optional[str]: 155 | """Runs a conditional operation. 156 | 157 | If the IS_CONDITIONAL flag is set, run the Operation's 158 | "function" and if True runs the "action" next, if it's False 159 | runs the "otherwise" Operation instead. 160 | 161 | Args: 162 | response: 163 | A requests.models.Response object with the HTTP response to 164 | be passed to the operation's "function". 165 | 166 | Returns: 167 | An optional string with the name of the next stage. 168 | 169 | """ 170 | if self.needs_response: 171 | check = self.function(response) 172 | else: 173 | check = self.function() 174 | 175 | if check and self.action: 176 | return execute_actions(self.action, response) 177 | if self.otherwise: 178 | return execute_actions(self.otherwise, response) 179 | 180 | return None 181 | 182 | @property 183 | def needs_response(self) -> bool: 184 | """Returns True if the NEEDS_RESPONSE flag is set.""" 185 | return bool(self.flags & self.NEEDS_RESPONSE) 186 | 187 | @property 188 | def is_conditional(self) -> bool: 189 | """Returns True if the IS_CONDITIONAL flag is set.""" 190 | return bool(self.flags & self.IS_CONDITIONAL) 191 | 192 | @property 193 | def will_append(self) -> bool: 194 | """Returns True if the WILL_APPEND flag is set.""" 195 | return bool(self.flags & self.WILL_APPEND) 196 | 197 | 198 | class Http(Operation): 199 | """Operation that runs actions depending on the HTTP status code. 200 | 201 | A Http object will check if the HTTP response status code matches 202 | the code defined in its "status" attribute, and run the Operation 203 | inside "action" if it matches or the one inside "otherwise" if not 204 | matching. 205 | 206 | Attributes: 207 | status: 208 | An integer with the HTTP status code to be checked. 209 | action: 210 | An Operation that will be executed if the status code matches. 211 | otherwise: 212 | An Operation that will be executed if the status code doesn't 213 | match. 214 | 215 | """ 216 | 217 | def __init__( 218 | self, 219 | status: int, 220 | action: Optional[Union[Operation, List[Operation]]], 221 | otherwise: Optional[Union[Operation, List[Operation]]] = None, 222 | ) -> None: 223 | """Initializes the Http Operation. 224 | 225 | Args: 226 | status: 227 | An integer with the HTTP response status code. 228 | action: 229 | An Operation object to be run if the defined status matches 230 | the response status code. 231 | otherwise: 232 | An Operation object to be run if the defined status doesn't 233 | match the response status code. 234 | 235 | """ 236 | self.status = status 237 | super().__init__( 238 | function=self.match_status_code, 239 | action=action, 240 | otherwise=otherwise, 241 | flags=Operation.IS_CONDITIONAL | Operation.NEEDS_RESPONSE, 242 | ) 243 | 244 | def match_status_code(self, response: requests.models.Response) -> bool: 245 | """Check if the defined status matches the response status code.""" 246 | return self.status == response.status_code 247 | 248 | def __str__(self) -> str: 249 | """Returns a string representation of the Operation.""" 250 | return ( 251 | "(Http:" 252 | + str(self.status) 253 | + "=" 254 | + str(self.action) 255 | + "/" 256 | + str(self.otherwise) 257 | + ")" 258 | ) 259 | 260 | 261 | class Grep(Operation): 262 | """Operation that runs actions depending on Regex matches. 263 | 264 | A Grep object will check if the HTTP response body matches the regex 265 | defined in its "regex" attribute, and run the Operation inside 266 | "action" if it matches or the one inside "otherwise" if not 267 | matching. 268 | 269 | Attributes: 270 | regex: 271 | A string with the regular expression to be checked. 272 | action: 273 | An Operation that will be executed if the status code matches. 274 | otherwise: 275 | An Operation that will be executed if the status code doesn't 276 | match. 277 | 278 | """ 279 | 280 | def __init__( 281 | self, 282 | regex: str, 283 | action: Operation, 284 | otherwise: Optional[Operation] = None, 285 | ) -> None: 286 | """Initializes the Grep Operation. 287 | 288 | Args: 289 | regex: 290 | A string with the regular expression to be checked. 291 | action: 292 | An Operation object to be run if the defined regex matches 293 | the response body. 294 | otherwise: 295 | An Operation object to be run if the defined regex doesn't 296 | match the response body. 297 | 298 | """ 299 | self.regex = regex 300 | super().__init__( 301 | function=self.match_response, 302 | action=action, 303 | otherwise=otherwise, 304 | flags=Operation.IS_CONDITIONAL | Operation.NEEDS_RESPONSE, 305 | ) 306 | 307 | def match_response(self, response: requests.models.Response) -> bool: 308 | """Checks if the response body contains the defined regex.""" 309 | return bool(re.search(self.regex, response.text)) 310 | 311 | def __str__(self) -> str: 312 | """Returns a string representation of the Operation.""" 313 | return ( 314 | "(Grep:" 315 | + str(self.regex) 316 | + "=" 317 | + str(self.action) 318 | + "/" 319 | + str(self.otherwise) 320 | + ")" 321 | ) 322 | 323 | 324 | class Save(Operation): 325 | """Operation to save information to files. 326 | 327 | Attributes: 328 | filename: 329 | The path to the file where the data should be saved. 330 | """ 331 | 332 | def __init__( 333 | self, 334 | filename: str, 335 | plugin: Optional[Plugin] = None, 336 | save_function: Callable[..., None] = None, 337 | flags: int = 0, 338 | ) -> None: 339 | """Initializes the Save operation. 340 | 341 | Args: 342 | filename: 343 | The path of the file where data should be saved. 344 | plugin: 345 | If saving Plugin's value, this should contain the plugin. 346 | save_function: 347 | A function to use when writing the file. Use when needing 348 | some more complex saving instructions. 349 | flags: 350 | Operation's flags. No flag is set by default. Set 351 | WILL_APPEND if needed to append to file instead of overwrite. 352 | 353 | """ 354 | self.filename = filename 355 | if not save_function: 356 | if plugin: 357 | super().__init__( 358 | function=partial( 359 | self.save_to_file, 360 | content=plugin, 361 | ), 362 | flags=flags, 363 | ) 364 | else: 365 | super().__init__(function=self.save_to_file, flags=flags) 366 | else: 367 | super().__init__(function=save_function, flags=flags) 368 | 369 | def save_to_file( 370 | self, content: Union[str, Plugin, requests.models.Response] 371 | ) -> None: 372 | """Saves a string or plugin's content to a file. 373 | 374 | Given the content (a string or a plugin), open the file and 375 | write its contents. If WILL_APPEND was set, append to file 376 | instead of overwrite. 377 | 378 | Args: 379 | content: 380 | A string or a Plugin with the data to be written. 381 | 382 | """ 383 | if self.will_append: 384 | mode = "a" 385 | else: 386 | mode = "w" 387 | 388 | with open(self.filename, mode) as outfile: 389 | if isinstance(content, requests.models.Response): 390 | outfile.write(content.text) 391 | elif isinstance(content, Plugin): 392 | outfile.write(content.value) 393 | else: 394 | outfile.write(content) 395 | outfile.write("\n") 396 | 397 | @classmethod 398 | def append(cls, filename: str, plugin: Plugin) -> "Save": 399 | """Append to file instead of overwrite. 400 | 401 | Args: 402 | filename: 403 | Path to the file to append to. 404 | plugin: 405 | The Plugin with the content to write. 406 | 407 | Returns: 408 | A Save object which will append data instead of overwrite. 409 | """ 410 | operation = cls( 411 | filename=filename, plugin=plugin, flags=Operation.WILL_APPEND 412 | ) 413 | return operation 414 | 415 | @classmethod 416 | def body(cls, filename: str, append: bool = False) -> "Save": 417 | """Save the entire HTTP body. 418 | 419 | If you need to save the entire body instead of extracting some 420 | data from it using plugins, use ``Save.body``. Given a filename, 421 | and optionally a boolean ``append``, write the body's contents 422 | into the file. 423 | 424 | Args: 425 | filename: 426 | The path to the file where to write the data. 427 | append: 428 | A boolean which when True, will append to existing file 429 | instead of overwriting it. 430 | 431 | Returns: 432 | A Save object which will save the response body. 433 | 434 | """ 435 | if append: 436 | flags = Operation.NEEDS_RESPONSE | Operation.WILL_APPEND 437 | else: 438 | flags = Operation.NEEDS_RESPONSE 439 | 440 | operation = cls( 441 | filename=filename, 442 | flags=flags, 443 | ) 444 | 445 | return operation 446 | 447 | 448 | class Print(Operation): 449 | """Operation that prints desired information. 450 | 451 | When this Operation is executed, it will print each of its elements 452 | in a new line. 453 | 454 | Attributes: 455 | *args: 456 | A list of Plugins and/or strings. The plugin's extracted values 457 | will be printed. 458 | 459 | """ 460 | 461 | def __init__( 462 | self, 463 | *args: Union[str, Plugin], 464 | flags: int = 0, 465 | function: Callable[..., Any] = None, 466 | ): 467 | """Initializes the Print Operation. 468 | 469 | Args: 470 | *args: 471 | Strings or Plugin objects to be printed. 472 | """ 473 | self.args = args 474 | if function: 475 | super().__init__( 476 | function=function, 477 | flags=flags, 478 | ) 479 | else: 480 | super().__init__( 481 | function=self.print_items, 482 | flags=flags, 483 | ) 484 | 485 | def print_items(self) -> None: 486 | """Prints the defined items.""" 487 | for item in self.args: 488 | if isinstance(item, str): 489 | print(item) 490 | else: 491 | print(item.name + " = " + str(item.value)) 492 | 493 | def __str__(self) -> str: 494 | """Returns a string representation of the Print Operation.""" 495 | return "(Print:" + str(self.args) + ")" 496 | 497 | @classmethod 498 | def body(cls) -> "Print": 499 | """Classmethod to print the HTTP response body.""" 500 | operation = cls( 501 | function=lambda response: print( 502 | "\nHTTP response body:\n" + response.text 503 | ), 504 | flags=Operation.NEEDS_RESPONSE, 505 | ) 506 | return operation 507 | 508 | @classmethod 509 | def headers(cls, headers: List[str] = None) -> "Print": 510 | """Classmethod to print the HTTP response headers. 511 | 512 | Args: 513 | headers: 514 | A list of strings containing the headers that needs to be 515 | printed. 516 | """ 517 | 518 | def print_headers( 519 | response: requests.models.Response, 520 | headers: List[str] = None, 521 | ) -> None: 522 | """Prints headers from the response. 523 | 524 | Given a response, and optionally a list of headers, print 525 | those headers, or all the headers otherwise. 526 | 527 | Args: 528 | response: 529 | The HTTP response received. 530 | headers: 531 | A list of strings with the desired headers. 532 | 533 | """ 534 | 535 | print("HTTP response headers:") 536 | 537 | if headers: 538 | for header in headers: 539 | value = response.headers.get(header) 540 | if value: 541 | print(": ".join([header, value])) 542 | else: 543 | for name, value in response.headers.items(): 544 | print(": ".join([name, value])) 545 | 546 | operation = cls( 547 | function=partial(print_headers, headers=headers), 548 | flags=Operation.NEEDS_RESPONSE, 549 | ) 550 | return operation 551 | 552 | @classmethod 553 | def cookies(cls, cookies: List[str] = None) -> "Print": 554 | """Classmethod to print the HTTP response cookies. 555 | 556 | Args: 557 | cookies: 558 | A list of strings containing the cookies that needs to be 559 | printed. 560 | 561 | """ 562 | 563 | def print_cookies( 564 | response: requests.models.Response, 565 | cookies: List[str] = None, 566 | ) -> None: 567 | """Prints cookies from the response. 568 | 569 | Args: 570 | response: 571 | The HTTP response received. 572 | cookies: 573 | A list of strings with the desired cookies. 574 | 575 | """ 576 | 577 | print("HTTP response cookies:") 578 | 579 | if cookies: 580 | for cookie in cookies: 581 | value = response.cookies.get(cookie) 582 | if value: 583 | print(": ".join([cookie, value])) 584 | else: 585 | for name, value in response.cookies.items(): 586 | print(": ".join([name, value])) 587 | 588 | operation = cls( 589 | function=partial(print_cookies, cookies=cookies), 590 | flags=Operation.NEEDS_RESPONSE, 591 | ) 592 | return operation 593 | 594 | 595 | class Error(Operation): 596 | """Operation that will exit Raider and print the error message. 597 | 598 | Attributes: 599 | message: 600 | A string with the error message to be printed. 601 | """ 602 | 603 | def __init__(self, message: str) -> None: 604 | """Initializes the Error Operation. 605 | 606 | Args: 607 | message: 608 | A string with the error message to be displayed. 609 | """ 610 | self.message = message 611 | super().__init__( 612 | function=lambda: sys.exit(self.message), 613 | ) 614 | 615 | def __str__(self) -> str: 616 | """Returns a string representation of the Operation.""" 617 | return "(Error:" + str(self.message) + ")" 618 | 619 | 620 | class NextStage(Operation): 621 | """Operation defining the next stage. 622 | 623 | Inside the Authentication object NextStage is used to define the 624 | next step of the authentication process. It can also be used inside 625 | "action" attributes of the other Operations to allow conditional 626 | decision making. 627 | 628 | Attributes: 629 | next_stage: 630 | A string with the name of the next stage to be executed. 631 | 632 | """ 633 | 634 | def __init__(self, next_stage: Optional[str]) -> None: 635 | """Initializes the NextStage Operation. 636 | 637 | Args: 638 | next_stage: 639 | A string with the name of the next stage. 640 | """ 641 | self.next_stage = str(next_stage) 642 | super().__init__( 643 | function=lambda: self.next_stage, 644 | ) 645 | 646 | def __str__(self) -> str: 647 | """Returns a string representation of the Operation.""" 648 | return "(NextStage:" + self.next_stage + ")" 649 | -------------------------------------------------------------------------------- /raider/plugins/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | """Common Plugin classes used by other plugins. 16 | """ 17 | 18 | 19 | import logging 20 | from typing import Callable, Dict, List, Optional 21 | 22 | import requests 23 | 24 | 25 | class Plugin: 26 | """Parent class for all plugins. 27 | 28 | Each Plugin class inherits from here. "get_value" function should 29 | be called when extracting the value from the plugin, which will then 30 | be stored in the "value" attribute. 31 | 32 | Attributes: 33 | name: 34 | A string used as an identifier for the Plugin. 35 | function: 36 | A function which will be called to extract the "value" of the 37 | Plugin when used as an input in a Flow. The function should set 38 | self.value and also return it. 39 | value: 40 | A string containing the Plugin's output value to be used as 41 | input in the HTTP request. 42 | flags: 43 | An integer containing the flags that define the Plugin's 44 | behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is 45 | supported. If NEEDS_USERDATA is set, the plugin will get its 46 | value from the user's data, which will be sent to the function 47 | defined here. If NEEDS_RESPONSE is set, the Plugin will extract 48 | its value from the HTTP response instead. 49 | 50 | """ 51 | 52 | # Plugin flags 53 | NEEDS_USERDATA = 0x01 54 | NEEDS_RESPONSE = 0x02 55 | DEPENDS_ON_OTHER_PLUGINS = 0x04 56 | NAME_NOT_KNOWN_IN_ADVANCE = 0x08 57 | 58 | def __init__( 59 | self, 60 | name: str, 61 | function: Optional[Callable[..., Optional[str]]] = None, 62 | flags: int = 0, 63 | value: Optional[str] = None, 64 | ) -> None: 65 | """Initializes a Plugin object. 66 | 67 | Creates a Plugin object, holding a "function" defining how to 68 | extract the "value". 69 | 70 | Args: 71 | name: 72 | A string with the unique identifier of the Plugin. 73 | function: 74 | A Callable function that will be used to extract the 75 | Plugin's value. 76 | value: 77 | A string with the extracted value from the Plugin. 78 | flags: 79 | An integer containing the flags that define the Plugin's 80 | behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is 81 | supported. If NEEDS_USERDATA is set, the plugin will get its 82 | value from the user's data, which will be sent to the function 83 | defined here. If NEEDS_RESPONSE is set, the Plugin will extract 84 | its value from the HTTP response instead. 85 | 86 | """ 87 | self.name = name 88 | self.plugins: List["Plugin"] = [] 89 | self.value: Optional[str] = value 90 | self.flags = flags 91 | 92 | self.function: Callable[..., Optional[str]] 93 | self.name_function: Optional[Callable[..., Optional[str]]] = None 94 | 95 | if (flags & Plugin.NEEDS_USERDATA) and not function: 96 | self.function = self.extract_value_from_userdata 97 | elif not function: 98 | self.function = self.return_value 99 | else: 100 | self.function = function 101 | 102 | def get_value( 103 | self, 104 | userdata: Dict[str, str], 105 | ) -> Optional[str]: 106 | """Gets the value from the Plugin. 107 | 108 | Depending on the Plugin's flags, extract and return its value. 109 | 110 | Args: 111 | userdata: 112 | A dictionary with the user specific data. 113 | """ 114 | if not self.needs_response: 115 | if self.needs_userdata: 116 | self.value = self.function(userdata) 117 | elif self.depends_on_other_plugins: 118 | for item in self.plugins: 119 | item.get_value(userdata) 120 | self.value = self.function() 121 | else: 122 | self.value = self.function() 123 | return self.value 124 | 125 | def extract_value_from_response( 126 | self, 127 | response: Optional[requests.models.Response], 128 | ) -> None: 129 | """Extracts the value of the Plugin from the HTTP response. 130 | 131 | If NEEDS_RESPONSE flag is set, the Plugin will extract its value 132 | upon receiving the HTTP response, and store it inside the "value" 133 | attribute. 134 | 135 | Args: 136 | response: 137 | An requests.models.Response object with the HTTP response. 138 | 139 | """ 140 | output = self.function(response) 141 | if output: 142 | self.value = output 143 | logging.debug( 144 | "Found ouput %s = %s", 145 | self.name, 146 | self.value, 147 | ) 148 | else: 149 | logging.warning("Couldn't extract output: %s", str(self.name)) 150 | 151 | def extract_name_from_response( 152 | self, 153 | response: Optional[requests.models.Response], 154 | ) -> None: 155 | """Extracts the name of the Plugin from the HTTP response. 156 | 157 | If NAME_NOT_KNOWN_IN_ADVANCE flag is set, the Plugin will set 158 | its name after receiving the HTTP response, and store it inside 159 | the "name" attribute. 160 | 161 | Args: 162 | response: 163 | An requests.models.Response object with the HTTP response. 164 | 165 | """ 166 | if callable(self.name_function): 167 | # pylint can't figure out name_function is callable 168 | # pylint: disable=E1102 169 | output = self.name_function(response) 170 | if output: 171 | self.name = output 172 | else: 173 | logging.warning("Couldn't extract name: %s", str(self.name)) 174 | 175 | def extract_value_from_userdata( 176 | self, data: Dict[str, str] = None 177 | ) -> Optional[str]: 178 | """Extracts the plugin value from userdata. 179 | 180 | Given a dictionary with the userdata, return its value with the 181 | same name as the "name" attribute from this Plugin. 182 | 183 | Args: 184 | data: 185 | A dictionary with user specific data. 186 | 187 | Returns: 188 | A string with the value of the variable found. None if no such 189 | variable has been defined. 190 | 191 | """ 192 | if data and self.name in data: 193 | self.value = data[self.name] 194 | return self.value 195 | 196 | def return_value(self) -> Optional[str]: 197 | """Just return plugin's value. 198 | 199 | This is used when needing a function just to return the value. 200 | 201 | """ 202 | return self.value 203 | 204 | @property 205 | def needs_userdata(self) -> bool: 206 | """Returns True if the NEEDS_USERDATA flag is set.""" 207 | return bool(self.flags & self.NEEDS_USERDATA) 208 | 209 | @property 210 | def needs_response(self) -> bool: 211 | """Returns True if the NEEDS_RESPONSE flag is set.""" 212 | return bool(self.flags & self.NEEDS_RESPONSE) 213 | 214 | @property 215 | def depends_on_other_plugins(self) -> bool: 216 | """Returns True if the DEPENDS_ON_OTHER_PLUGINS flag is set.""" 217 | return bool(self.flags & self.DEPENDS_ON_OTHER_PLUGINS) 218 | 219 | @property 220 | def name_not_known_in_advance(self) -> bool: 221 | """Returns True if the NAME_NOT_KNOWN_IN_ADVANCE flag is set.""" 222 | return bool(self.flags & self.NAME_NOT_KNOWN_IN_ADVANCE) 223 | 224 | 225 | class Parser(Plugin): 226 | """Plugins that parse other plugins.""" 227 | 228 | def __init__( 229 | self, 230 | name: str, 231 | function: Callable[[], Optional[str]], 232 | value: str = None, 233 | ) -> None: 234 | """Initializes the Parser plugin.""" 235 | super().__init__( 236 | name=name, 237 | value=value, 238 | function=function, 239 | flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, 240 | ) 241 | 242 | 243 | class Empty(Plugin): 244 | """Empty plugin to use for fuzzing new data.""" 245 | 246 | def __init__(self, name: str): 247 | """Initialize Empty plugin.""" 248 | super().__init__( 249 | name=name, 250 | flags=0, 251 | ) 252 | -------------------------------------------------------------------------------- /raider/plugins/modifiers.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | """Plugins that modify other plugins. 16 | """ 17 | 18 | from functools import partial 19 | from typing import Callable, Optional, Union 20 | 21 | from raider.plugins.common import Plugin 22 | 23 | 24 | class Alter(Plugin): 25 | """Plugin used to alter other plugin's value. 26 | 27 | If the value extracted from other plugins cannot be used in it's raw 28 | form and needs to be somehow processed, Alter plugin can be used to 29 | do that. Initialize it with the original plugin and a function which 30 | will process the string and return the modified value. 31 | 32 | Attributes: 33 | alter_function: 34 | A function which will be given the plugin's value. It should 35 | return a string with the processed value. 36 | 37 | """ 38 | 39 | def __init__( 40 | self, 41 | parent_plugin: Plugin, 42 | alter_function: Optional[Callable[[str], Optional[str]]] = None, 43 | ) -> None: 44 | """Initializes the Alter Plugin. 45 | 46 | Given the original plugin, and a function to alter the data, 47 | initialize the object, and get the modified value. 48 | 49 | Args: 50 | plugin: 51 | The original Plugin where the value is to be found. 52 | alter_function: 53 | The Function with instructions on how to alter the value. 54 | """ 55 | super().__init__( 56 | name=parent_plugin.name, 57 | value=parent_plugin.value, 58 | flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, 59 | function=self.process_value, 60 | ) 61 | self.plugins = [parent_plugin] 62 | self.alter_function = alter_function 63 | 64 | def process_value(self) -> Optional[str]: 65 | """Process the original plugin's value. 66 | 67 | Gives the original plugin's value to ``alter_function``. Return 68 | the processed value and store it in self.value. 69 | 70 | Returns: 71 | A string with the processed value. 72 | 73 | """ 74 | if self.plugins[0].value: 75 | if self.alter_function: 76 | self.value = self.alter_function(self.plugins[0].value) 77 | else: 78 | self.value = None 79 | 80 | return self.value 81 | 82 | @classmethod 83 | def prepend(cls, parent_plugin: Plugin, string: str) -> "Alter": 84 | """Prepend a string to plugin's value.""" 85 | alter = cls( 86 | parent_plugin=parent_plugin, 87 | alter_function=lambda value: string + value, 88 | ) 89 | 90 | return alter 91 | 92 | @classmethod 93 | def append(cls, parent_plugin: Plugin, string: str) -> "Alter": 94 | """Append a string after the plugin's value""" 95 | alter = cls( 96 | parent_plugin=parent_plugin, 97 | alter_function=lambda value: value + string, 98 | ) 99 | 100 | return alter 101 | 102 | @classmethod 103 | def replace( 104 | cls, 105 | parent_plugin: Plugin, 106 | old_value: str, 107 | new_value: Union[str, Plugin], 108 | ) -> "Alter": 109 | """Replace a substring from plugin's value with something else.""" 110 | 111 | def replace_old_value( 112 | value: str, old: str, new: Union[str, Plugin] 113 | ) -> Optional[str]: 114 | """Replaces an old substring with the new one.""" 115 | if isinstance(new, Plugin): 116 | if not new.value: 117 | return None 118 | return value.replace(old, new.value) 119 | return value.replace(old, new) 120 | 121 | alter = cls( 122 | parent_plugin=parent_plugin, 123 | alter_function=partial( 124 | replace_old_value, old=old_value, new=new_value 125 | ), 126 | ) 127 | 128 | if isinstance(new_value, Plugin): 129 | alter.plugins.append(new_value) 130 | 131 | return alter 132 | 133 | 134 | class Combine(Plugin): 135 | """Plugin to combine the values of other plugins.""" 136 | 137 | def __init__(self, *args: Union[str, Plugin]): 138 | """Initialize Combine object.""" 139 | self.args = args 140 | name = str(sum(hash(item) for item in args)) 141 | super().__init__( 142 | name=name, 143 | flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, 144 | function=self.concatenate_values, 145 | ) 146 | self.plugins = [] 147 | for item in args: 148 | if isinstance(item, Plugin): 149 | self.plugins.append(item) 150 | 151 | def concatenate_values(self) -> str: 152 | """Concatenate the provided values. 153 | 154 | This function will concatenate the arguments values. Accepts 155 | both strings and plugins. 156 | 157 | """ 158 | combined = "" 159 | for item in self.args: 160 | if isinstance(item, str): 161 | combined += item 162 | elif item.value: 163 | combined += item.value 164 | return combined 165 | -------------------------------------------------------------------------------- /raider/plugins/parsers.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Plugins used to parse data. 17 | """ 18 | 19 | from typing import Optional, Union 20 | from urllib.parse import parse_qs, urlsplit 21 | 22 | from raider.plugins.common import Parser, Plugin 23 | 24 | 25 | class UrlParser(Parser): 26 | """Parse the URL and extract elements from it. 27 | 28 | Use this when needing to extract some piece of information from 29 | the URL. 30 | 31 | """ 32 | 33 | def __init__(self, parent_plugin: Plugin, element: str) -> None: 34 | super().__init__(name=element, function=self.parse_url) 35 | self.plugins = [parent_plugin] 36 | self.element = element 37 | self.url: Optional[str] = None 38 | 39 | def parse_url(self) -> Optional[str]: 40 | """Parses the URL and returns the string with the desired element.""" 41 | 42 | def get_query(query: Union[str, bytes], element: str) -> str: 43 | """Extracts a parameter from the URL query.""" 44 | key = element.split(".")[1] 45 | return parse_qs(str(query))[key][0] 46 | 47 | value: Optional[str] = None 48 | 49 | self.url = self.plugins[0].value 50 | parsed_url = urlsplit(self.url) 51 | 52 | if self.element.startswith("query"): 53 | value = get_query(parsed_url.query, self.element) 54 | elif self.element.startswith("netloc"): 55 | value = str(parsed_url.netloc) 56 | elif self.element.startswith("path"): 57 | value = str(parsed_url.path) 58 | elif self.element.startswith("scheme"): 59 | value = str(parsed_url.scheme) 60 | elif self.element.startswith("fragment"): 61 | value = str(parsed_url.fragment) 62 | 63 | if value: 64 | return str(value) 65 | return None 66 | -------------------------------------------------------------------------------- /raider/raider.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Main object used to perform common actions. 17 | """ 18 | 19 | import logging 20 | import sys 21 | from typing import Optional 22 | 23 | from raider.application import Application 24 | from raider.attacks import Fuzz 25 | from raider.authentication import Authentication 26 | from raider.config import Config 27 | from raider.plugins.common import Plugin 28 | from raider.user import User 29 | 30 | 31 | class Raider: 32 | """Main class used as the point of entry. 33 | 34 | The Raider class should be used to access everything else inside 35 | Raider. For now it's still not doing much, but for the future this 36 | is where all of the features available to the end user should be. 37 | 38 | Attributes: 39 | application: 40 | An :class:`Application ` object 41 | with the currently active project. 42 | config: 43 | A Config object containing all of the necessary settings. 44 | user: 45 | A User object containing the active user of the active project. 46 | functions: 47 | A Functions object containing the defined functions of the 48 | active project. 49 | 50 | """ 51 | 52 | # Raider flags 53 | # Session was loaded and the information is already in userdata 54 | SESSION_LOADED = 0x01 55 | 56 | def __init__(self, project: Optional[str] = None, flags: int = 0) -> None: 57 | """Initializes the Raider object. 58 | 59 | Initializes the main entry point for Raider. If the name of the 60 | project is supplied, this application will be used, otherwise 61 | the last used application will be chosen. 62 | 63 | Args: 64 | project: 65 | A string with the name of the project. 66 | flags: 67 | An integer with the flags. Only SESSION_LOADED is supported 68 | now. It indicates the authentication was not performed from 69 | the start, but loaded from a previously saved session file, 70 | which means the plugins should get their value from userdata. 71 | 72 | """ 73 | self.application = Application(project) 74 | if hasattr(self.application, "functions"): 75 | self.functions = self.application.functions 76 | 77 | self.flags = flags 78 | 79 | def authenticate(self, username: str = None) -> None: 80 | """Authenticates in the chosen application. 81 | 82 | Runs the authentication process from start to end on the 83 | selected application with the specified user. 84 | 85 | Args: 86 | username: 87 | A string with the username to authenticate. If not 88 | specified, the last used user will be selected. 89 | 90 | """ 91 | self.application.authenticate(username) 92 | 93 | def load_session(self) -> None: 94 | """Loads saved session from ``_userdata.hy``.""" 95 | self.application.load_session_file() 96 | self.flags = self.flags | self.SESSION_LOADED 97 | 98 | def save_session(self) -> None: 99 | """Saves session to ``_userdata.hy``.""" 100 | self.application.write_session_file() 101 | 102 | def run_function(self, function: str) -> None: 103 | """Runs a function in the chosen application. 104 | 105 | With the selected application and user run the function from the 106 | argument. 107 | 108 | Args: 109 | function: 110 | A string with the function identifier as defined in 111 | "_functions" variable. 112 | 113 | """ 114 | if not hasattr(self, "functions"): 115 | logging.critical("No functions defined. Cannot continue.") 116 | sys.exit() 117 | 118 | if self.session_loaded: 119 | self.fix_function_plugins(function) 120 | 121 | self.functions.run(function, self.user, self.config) 122 | 123 | def fuzz( 124 | self, 125 | flow_name: str, 126 | fuzzing_point: str, 127 | ) -> Fuzz: 128 | """Fuzz a function with an authenticated user. 129 | 130 | Given a function name, a starting point for fuzzing, and a 131 | function to generate the fuzzing strings, run the attack. 132 | 133 | Args: 134 | flow_name: 135 | The name of the :class:`Flow ` containing 136 | the :class:`Request ` which will be 137 | fuzzed. 138 | fuzzing_point: 139 | The name given to the :class:`Plugin 140 | ` inside :class:`Request 141 | ` which will be fuzzed. 142 | 143 | """ 144 | is_authentication = False 145 | flow = self.functions.get_function_by_name(flow_name) 146 | if not flow: 147 | flow = self.authentication.get_stage_by_name(flow_name) 148 | is_authentication = True 149 | if flow: 150 | if self.session_loaded: 151 | self.fix_function_plugins(flow_name) 152 | 153 | if is_authentication: 154 | fuzzer = Fuzz( 155 | application=self.application, 156 | flow=flow, 157 | fuzzing_point=fuzzing_point, 158 | flags=Fuzz.IS_AUTHENTICATION, 159 | ) 160 | else: 161 | fuzzer = Fuzz( 162 | application=self.application, 163 | flow=flow, 164 | fuzzing_point=fuzzing_point, 165 | flags=0, 166 | ) 167 | 168 | else: 169 | logging.critical( 170 | "Function %s not defined, cannot fuzz!", flow_name 171 | ) 172 | sys.exit() 173 | 174 | return fuzzer 175 | 176 | def fix_function_plugins(self, function: str) -> None: 177 | """Given a function name, prepare its Flow to be fuzzed. 178 | 179 | For each plugin acting as an input for the defined function, 180 | change its flags and function so it uses the previously 181 | extracted data instead of extracting it again. 182 | 183 | """ 184 | flow = self.functions.get_function_by_name(function) 185 | if not flow: 186 | logging.critical( 187 | "Function %s not found. Cannot continue.", function 188 | ) 189 | sys.exit() 190 | 191 | inputs = flow.request.list_inputs() 192 | 193 | if inputs: 194 | for plugin in inputs.values(): 195 | # Reset plugin flags, and get the values from userdata 196 | plugin.flags = Plugin.NEEDS_USERDATA 197 | plugin.function = plugin.extract_value_from_userdata 198 | 199 | @property 200 | def authentication(self) -> Authentication: 201 | """Returns the Authentication object""" 202 | return self.application.authentication 203 | 204 | @property 205 | def config(self) -> Config: 206 | """Returns the Configuration object""" 207 | return self.application.config 208 | 209 | @property 210 | def user(self) -> User: 211 | """Returns the User object""" 212 | return self.application.active_user 213 | 214 | @property 215 | def session_loaded(self) -> bool: 216 | """Returns True if the SESSION_LOADED flag is set.""" 217 | return bool(self.flags & self.SESSION_LOADED) 218 | -------------------------------------------------------------------------------- /raider/request.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | """Request class used to handle HTTP. 18 | """ 19 | 20 | import logging 21 | import sys 22 | import urllib 23 | from copy import deepcopy 24 | from typing import Any, Dict, List, Optional, Union 25 | from urllib import parse 26 | 27 | import requests 28 | from urllib3.exceptions import InsecureRequestWarning 29 | 30 | from raider.config import Config 31 | from raider.plugins.basic import Cookie, Header 32 | from raider.plugins.common import Plugin 33 | from raider.structures import CookieStore, DataStore, HeaderStore 34 | from raider.user import User 35 | 36 | 37 | class PostBody(DataStore): 38 | """Holds the POST body data. 39 | 40 | This class was created to enable the user to send the POST body in a 41 | different format than the default url encoded. For now only JSON 42 | encoding has been implemented. 43 | 44 | Attributes: 45 | encoding: 46 | A string with the desired encoding. For now only "json" is 47 | supported. If the encoding is skipped, the request will be url 48 | encoded, and the Content-Type will be 49 | ``application/x-www-form-urlencoded``. 50 | 51 | """ 52 | 53 | def __init__(self, data: Dict[Any, Any], encoding: str) -> None: 54 | """Initializes the PostBody object. 55 | 56 | Args: 57 | data: 58 | A dictionary with the data to be sent. 59 | encoding: 60 | A string with the encoding. Only "json" is supported for 61 | now. 62 | 63 | """ 64 | self.encoding = encoding 65 | super().__init__(data) 66 | 67 | 68 | # Request needs many arguments 69 | # pylint: disable=too-many-arguments 70 | class Request: 71 | """Class holding the elements of the HTTP request. 72 | 73 | When a Flow object is created, it defines a Request object with the 74 | information necessary to create a HTTP request. The "method" 75 | attribute is required. One and only one of "url" and "path" is 76 | required too. Everything else is optional. 77 | 78 | The Request object can contain Plugins which will be evaluated and 79 | its value replaced in the HTTP request. 80 | 81 | Attributes: 82 | method: 83 | A string with the HTTP request method. Only GET and POST is 84 | supported for now. 85 | url: 86 | A string with the URL of the HTTP request. Cannot be used if 87 | "path" is used. 88 | path: 89 | A string with the path of the HTTP request. The base URL is 90 | defined in the "_base_url" variable from the hy configuration 91 | files of the project. If "path" is defined "url" cannot be used. 92 | cookies: 93 | A list of Cookie objects to be sent with the HTTP request. 94 | headers: 95 | A list of Header objects to be sent with the HTTP request. 96 | data: 97 | A dictionary of Any objects. Can contain strings and 98 | Plugins. When a key or a value of the dictionary is a Plugin, it 99 | will be evaluated and its value will be used in the HTTP 100 | request. If the "method" is GET those values will be put inside 101 | the URL parameters, and if the "method" is POST they will be 102 | inside the POST request body. 103 | 104 | """ 105 | 106 | def __init__( 107 | self, 108 | method: str, 109 | url: Optional[Union[str, Plugin]] = None, 110 | path: Optional[Union[str, Plugin]] = None, 111 | cookies: Optional[List[Cookie]] = None, 112 | headers: Optional[List[Header]] = None, 113 | data: Optional[Union[Dict[Any, Any], PostBody]] = None, 114 | ) -> None: 115 | """Initializes the Request object. 116 | 117 | Args: 118 | method: 119 | A string with the HTTP method. Can be either "GET" or "POST". 120 | url: 121 | A string with the full URL of the Request. Cannot be defined 122 | together with "path" argument. 123 | path: 124 | A string with the partial path pointing to the endpoint the 125 | Request needs to be sent. This value will be prepended by 126 | the "_base_url" variable set up in hy configuration files to 127 | create the full URL. If "path" is defined, the "url" 128 | argument cannot be defined. 129 | cookies: 130 | A list of Cookie Plugins. Its values will be calculated and 131 | inserted into the HTTP Request on runtime. 132 | headers: 133 | A list of Header Plugins. Its values will be calculated and 134 | inserted into the HTTP Request on runtime. 135 | data: 136 | A dictionary with values to be inserted into the HTTP GET 137 | parameters or POST body. Both keys and values of the 138 | dictionary can be Plugins. Those values will be inserted 139 | into the Request on runtime. 140 | 141 | """ 142 | self.method = method 143 | if not self.method: 144 | logging.critical("Required :method parameter, can't run without") 145 | 146 | if not bool(url) ^ bool(path): 147 | logging.critical( 148 | "One and only one of :path and :url parameters required" 149 | ) 150 | sys.exit() 151 | 152 | self.url = url 153 | self.path = path 154 | 155 | self.headers = HeaderStore(headers) 156 | self.cookies = CookieStore(cookies) 157 | 158 | self.data: Union[PostBody, DataStore] 159 | if isinstance(data, PostBody): 160 | self.data = data 161 | else: 162 | self.data = DataStore(data) 163 | 164 | def list_inputs(self) -> Optional[Dict[str, Plugin]]: 165 | """Returns a list of request's inputs.""" 166 | 167 | def get_children_plugins(plugin: Plugin) -> Dict[str, Plugin]: 168 | """Returns the children plugins. 169 | 170 | If a plugin has the flag DEPENDS_ON_OTHER_PLUGINS set, 171 | return a dictionary with each plugin associated to its name. 172 | 173 | """ 174 | output = {} 175 | if plugin.depends_on_other_plugins: 176 | for item in plugin.plugins: 177 | output.update({item.name: item}) 178 | return output 179 | 180 | inputs = {} 181 | 182 | if isinstance(self.url, Plugin): 183 | inputs.update({self.url.name: self.url}) 184 | inputs.update(get_children_plugins(self.url)) 185 | if isinstance(self.path, Plugin): 186 | inputs.update({self.path.name: self.path}) 187 | inputs.update(get_children_plugins(self.path)) 188 | 189 | for name in self.cookies: 190 | cookie = self.cookies[name] 191 | inputs.update({name: cookie}) 192 | inputs.update(get_children_plugins(cookie)) 193 | 194 | for name in self.headers: 195 | header = self.headers[name] 196 | inputs.update({name: header}) 197 | inputs.update(get_children_plugins(header)) 198 | 199 | for key, value in self.data.items(): 200 | if isinstance(key, Plugin): 201 | inputs.update({key.name: key}) 202 | inputs.update(get_children_plugins(key)) 203 | if isinstance(value, Plugin): 204 | inputs.update({value.name: value}) 205 | inputs.update(get_children_plugins(value)) 206 | 207 | return inputs 208 | 209 | # pylint: disable=W0511 210 | # TODO: Will redesign this function later. 211 | # pylint: disable=R0912 212 | def process_inputs( 213 | self, user: User, config: Config 214 | ) -> Dict[str, Dict[str, str]]: 215 | """Process the Request inputs. 216 | 217 | Uses the supplied user data to replace the Plugins in the inputs 218 | with their actual value. Returns those values. 219 | 220 | Args: 221 | user: 222 | A User object containing the user specific data to be used 223 | when processing the inputs. 224 | config: 225 | A Config object with the global Raider configuration. 226 | 227 | Returns: 228 | A dictionary with the cookies, headers, and other data created 229 | from processing the inputs. 230 | 231 | """ 232 | userdata = user.to_dict() 233 | 234 | cookies = self.cookies.to_dict().copy() 235 | headers = self.headers.to_dict().copy() 236 | httpdata = self.data.to_dict().copy() 237 | 238 | if self.path: 239 | base_url = config.project_config["_base_url"] 240 | if isinstance(self.path, Plugin): 241 | path = self.path.get_value(userdata) 242 | else: 243 | path = self.path 244 | self.url = parse.urljoin(base_url, path) 245 | 246 | if isinstance(self.url, Plugin): 247 | self.url = self.url.get_value(userdata) 248 | 249 | headers.update({"user-agent": config.user_agent}) 250 | 251 | for key in self.cookies: 252 | name = self.cookies[key].name 253 | if self.cookies[key].name_not_known_in_advance: 254 | cookies.pop(key) 255 | value = self.cookies[key].get_value(userdata) 256 | if value: 257 | cookies.update({name: value}) 258 | 259 | for key in self.headers: 260 | name = self.headers[key].name 261 | if self.headers[key].name_not_known_in_advance: 262 | headers.pop(key) 263 | value = self.headers[key].get_value(userdata) 264 | if value: 265 | headers.update({name: value}) 266 | 267 | for key in list(httpdata): 268 | value = httpdata[key] 269 | if isinstance(value, Plugin): 270 | new_value = value.get_value(userdata) 271 | if new_value: 272 | httpdata.update({key: new_value}) 273 | 274 | if isinstance(key, Plugin): 275 | new_value = httpdata.pop(key) 276 | new_key = key.get_value(userdata) 277 | if new_key: 278 | httpdata.update({new_key: new_value}) 279 | 280 | return {"cookies": cookies, "data": httpdata, "headers": headers} 281 | 282 | def send( 283 | self, user: User, config: Config 284 | ) -> Optional[requests.models.Response]: 285 | """Sends the HTTP request. 286 | 287 | With the given user information, replaces the input plugins with 288 | their values, and sends the HTTP request. Returns the response. 289 | 290 | Args: 291 | user: 292 | A User object with the user specific data to be used when 293 | processing inputs. 294 | config: 295 | A Config object with the global Raider configuration. 296 | 297 | Returns: 298 | A requests.models.Response object with the HTTP response 299 | received after sending the generated request. 300 | 301 | """ 302 | verify = config.verify 303 | if not verify: 304 | # False positive 305 | # pylint: disable=no-member 306 | requests.packages.urllib3.disable_warnings( 307 | category=InsecureRequestWarning 308 | ) 309 | 310 | proxies = {"all": config.proxy} 311 | 312 | inputs = self.process_inputs(user, config) 313 | cookies = inputs["cookies"] 314 | headers = inputs["headers"] 315 | data = inputs["data"] 316 | 317 | logging.debug("Sending HTTP request:") 318 | logging.debug("%s %s", self.method, self.url) 319 | logging.debug("Cookies: %s", str(cookies)) 320 | logging.debug("Headers: %s", str(headers)) 321 | logging.debug("Data: %s", str(data)) 322 | 323 | if self.method == "GET": 324 | # Encode special characters. This will replace "+" signs with "%20" 325 | # in URLs. For some reason mypy doesn't like this, so typing will 326 | # be ignored for this line. 327 | params = urllib.parse.urlencode( 328 | data, quote_via=urllib.parse.quote 329 | ) # type: ignore 330 | req = requests.get( 331 | self.url, 332 | params=params, 333 | headers=headers, 334 | cookies=cookies, 335 | proxies=proxies, 336 | verify=verify, 337 | allow_redirects=False, 338 | ) 339 | 340 | return req 341 | 342 | if self.method == "POST": 343 | if ( 344 | isinstance(self.data, PostBody) 345 | and self.data.encoding == "json" 346 | ): 347 | req = requests.post( 348 | self.url, 349 | json=data, 350 | headers=headers, 351 | cookies=cookies, 352 | proxies=proxies, 353 | verify=verify, 354 | allow_redirects=False, 355 | ) 356 | else: 357 | req = requests.post( 358 | self.url, 359 | data=data, 360 | headers=headers, 361 | cookies=cookies, 362 | proxies=proxies, 363 | verify=verify, 364 | allow_redirects=False, 365 | ) 366 | 367 | return req 368 | 369 | logging.critical("Method %s not allowed", self.method) 370 | sys.exit() 371 | return None 372 | 373 | 374 | class Template(Request): 375 | """Template class to hold requests. 376 | 377 | It will initiate itself with a :class:`Request 378 | ` parent, and when called will return a 379 | copy of itself with the modified parameters. 380 | 381 | """ 382 | 383 | def __init__( 384 | self, 385 | method: str, 386 | url: Optional[Union[str, Plugin]] = None, 387 | path: Optional[Union[str, Plugin]] = None, 388 | cookies: Optional[List[Cookie]] = None, 389 | headers: Optional[List[Header]] = None, 390 | data: Optional[Union[Dict[Any, Any], PostBody]] = None, 391 | ) -> None: 392 | """Initializes the template object.""" 393 | super().__init__( 394 | method=method, 395 | url=url, 396 | path=path, 397 | cookies=cookies, 398 | headers=headers, 399 | data=data, 400 | ) 401 | 402 | def __call__( 403 | self, 404 | method: Optional[str] = None, 405 | url: Optional[Union[str, Plugin]] = None, 406 | path: Optional[Union[str, Plugin]] = None, 407 | cookies: Optional[List[Cookie]] = None, 408 | headers: Optional[List[Header]] = None, 409 | data: Optional[Union[Dict[Any, Any], PostBody]] = None, 410 | ) -> "Template": 411 | """Allow the object to be called. 412 | 413 | Accepts the same arguments as the :class:`Request 414 | ` class. When called, will return a copy 415 | of itself with the modified parameters. 416 | 417 | """ 418 | if bool(url) & bool(path): 419 | logging.critical( 420 | "One and only one of :path and :url parameters allowed" 421 | ) 422 | sys.exit() 423 | 424 | template = deepcopy(self) 425 | 426 | if method: 427 | template.method = method 428 | 429 | if url: 430 | template.url = url 431 | 432 | if path: 433 | template.path = path 434 | 435 | if cookies: 436 | template.cookies.merge(CookieStore(cookies)) 437 | 438 | if headers: 439 | template.headers.merge(HeaderStore(headers)) 440 | 441 | if data: 442 | if isinstance(data, PostBody): 443 | template.data.update(data.to_dict()) 444 | else: 445 | template.data.update(data) 446 | 447 | return template 448 | -------------------------------------------------------------------------------- /raider/structures.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Data structures used in Raider. 17 | """ 18 | 19 | from typing import Any, Dict, Iterator, List, Optional, Tuple 20 | 21 | from raider.plugins.basic import Cookie, Header 22 | 23 | 24 | class DataStore: 25 | """Class defining a dictionary-like data structure. 26 | 27 | This class was created to hold information relevant to Raider in a 28 | structure similar to Python dictionaries. 29 | 30 | """ 31 | 32 | def __init__(self, data: Optional[Dict[Any, Any]]) -> None: 33 | """Initializes the DataStore object. 34 | 35 | Given a dictionary with the data, store them in this object. 36 | 37 | Args: 38 | data: 39 | A dictionary with Any elements to be stored. 40 | 41 | """ 42 | self._index = -1 43 | if data: 44 | self._store = data 45 | else: 46 | self._store = {} 47 | 48 | def __getitem__(self, key: Any) -> Any: 49 | """Getter to return an element with the key.""" 50 | if key in self._store: 51 | return self._store[key] 52 | return None 53 | 54 | def __setitem__(self, key: Any, value: Any) -> None: 55 | """Setter to add a new element to DataStore.""" 56 | self._store.update({key: value}) 57 | 58 | def __iter__(self) -> Iterator[Any]: 59 | """Iterator to yield the keys.""" 60 | for key in list(self._store): 61 | yield key 62 | 63 | def __next__(self) -> Any: 64 | """Iterator to get the next element.""" 65 | self._index += 1 66 | if self._index >= len(self._store): 67 | self._index = -1 68 | raise StopIteration 69 | 70 | key = list(self._store)[self._index] 71 | return self._store[key] 72 | 73 | def update(self, data: Dict[Any, Any]) -> None: 74 | """Updates the DataStore with a new element.""" 75 | self._store.update(data) 76 | 77 | def pop(self, name: Any) -> Any: 78 | """Pops an element from the DataStore.""" 79 | return self._store.pop(name) 80 | 81 | def list_keys(self) -> List[Any]: 82 | """Returns a list of the keys in the DataStore.""" 83 | return list(self._store) 84 | 85 | def list_values(self) -> List[Any]: 86 | """Returns a list of the values in the DataStore.""" 87 | data = [] 88 | for key in self._store: 89 | data.append(self._store[key]) 90 | return data 91 | 92 | def to_dict(self) -> Dict[Any, Any]: 93 | """Returns the DataStore elements as a dictionary.""" 94 | return self._store 95 | 96 | def items(self) -> List[Tuple[Any, Any]]: 97 | """Returns a list of tuples containing the keys and values.""" 98 | data = [] 99 | for key in self._store: 100 | data.append((key, self._store[key])) 101 | 102 | return data 103 | 104 | 105 | class HeaderStore(DataStore): 106 | """Class storing the HTTP headers. 107 | 108 | This class inherits from DataStore, and converts the values into 109 | Header objects. 110 | 111 | """ 112 | 113 | def __init__(self, data: Optional[List[Header]]) -> None: 114 | """Initializes the HeaderStore object. 115 | 116 | Creates a HeaderStore object out of the given Header list. 117 | 118 | Args: 119 | data: 120 | A list of Header objects to store. 121 | 122 | """ 123 | values = {} 124 | if data: 125 | for header in data: 126 | values[header.name.lower()] = header 127 | super().__init__(values) 128 | 129 | def set(self, header: Header) -> None: 130 | """Sets the value of a Header. 131 | 132 | Given a Header object, add or update its value in the 133 | HeaderStore. 134 | 135 | Args: 136 | header: 137 | A Header object to be added to the HeaderStore. 138 | 139 | """ 140 | super().update({header.name.lower(): header.value}) 141 | 142 | def merge(self, headerstore: "HeaderStore") -> None: 143 | """Merge HeaderStore object with another one.""" 144 | for item in headerstore: 145 | self._store[item] = headerstore[item] 146 | 147 | @classmethod 148 | def from_dict(cls, data: Optional[Dict[str, str]]) -> "HeaderStore": 149 | """Creates a HeaderStore object from a dictionary. 150 | 151 | Given a dictionary with header values, creates a HeaderStore 152 | object and returns it. 153 | 154 | Args: 155 | data: 156 | A dictionary with header values. Those will be mapped in 157 | Header objects. 158 | 159 | Returns: 160 | A HeaderStore object containing the headers created from the 161 | supplied dictionary. 162 | 163 | """ 164 | headerlist = [] 165 | if data: 166 | for name, value in data.items(): 167 | header = Header(name, value) 168 | headerlist.append(header) 169 | return cls(headerlist) 170 | 171 | 172 | class CookieStore(DataStore): 173 | """Class storing the HTTP cookies. 174 | 175 | This class inherits from DataStore, and converts the values into 176 | Cookie objects. 177 | 178 | """ 179 | 180 | def __init__(self, data: Optional[List[Cookie]]) -> None: 181 | """Initializes a CookieStore object. 182 | 183 | Given a list of Cookie objects, create the CookieStore 184 | containing them. 185 | 186 | Args: 187 | data: 188 | A list of Cookies to be added to the CookieStore. 189 | 190 | """ 191 | values = {} 192 | if data: 193 | for cookie in data: 194 | values[cookie.name] = cookie 195 | super().__init__(values) 196 | 197 | def set(self, cookie: Cookie) -> None: 198 | """Sets the value of a Cookie. 199 | 200 | Given a Cookie object, add or update its value in the 201 | CookieStore. 202 | 203 | Args: 204 | cookie: 205 | A Cookie object to be added to the CookieStore 206 | 207 | """ 208 | super().update({cookie.name: cookie.value}) 209 | 210 | def merge(self, cookiestore: "CookieStore") -> None: 211 | """Merge CookieStore object with another one.""" 212 | for item in cookiestore: 213 | self._store[item] = cookiestore[item] 214 | 215 | @classmethod 216 | def from_dict(cls, data: Optional[Dict[str, str]]) -> "CookieStore": 217 | """Creates a CookieStore object from a dictionary. 218 | 219 | Given a dictionary with cookie values, creates a CookieStore 220 | object and returns it. 221 | 222 | Args: 223 | data: 224 | A dictionary with cookie values. Those will be mapped in 225 | Cookie objects. 226 | 227 | Returns: 228 | A CookieStore object containing the cookies created from the 229 | supplied dictionary. 230 | 231 | """ 232 | cookielist = [] 233 | if data: 234 | for name, value in data.items(): 235 | cookie = Cookie(name, value) 236 | cookielist.append(cookie) 237 | return cls(cookielist) 238 | -------------------------------------------------------------------------------- /raider/user.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Classes used for handling users. 17 | """ 18 | 19 | 20 | from typing import Dict, List 21 | 22 | import hy 23 | 24 | from raider.plugins.basic import Cookie, Header 25 | from raider.plugins.common import Plugin 26 | from raider.structures import CookieStore, DataStore, HeaderStore 27 | from raider.utils import hy_dict_to_python 28 | 29 | 30 | class User: 31 | """Class holding user related information. 32 | 33 | User objects are created inside the UserStore. Each User object 34 | contains at least the username and the password. Every time a Plugin 35 | generates an output, it is saved in the User object. If the Plugin 36 | is a Cookie or a Header, the output will be stored in the the 37 | "cookies" and "headers" attributes respectively. Otherwise they'll 38 | be saved inside "data". 39 | 40 | Attributes: 41 | username: 42 | A string containing the user's email or username used to log in. 43 | password: 44 | A string containing the user's password. 45 | cookies: 46 | A CookieStore object containing all of the collected cookies for 47 | this user. The Cookie plugin only writes here. 48 | headers: 49 | A HeaderStore object containing all of the collected headers for 50 | this user. The Header plugin only writes here. 51 | data: 52 | A DataStore object containing the rest of the data collected 53 | from plugins for this user. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | username: str, 60 | password: str, 61 | **kwargs: Dict[str, str], 62 | ) -> None: 63 | """Initializes a User object. 64 | 65 | Creates an object for easy access to user specific 66 | information. It's used to store the username, password, cookies, 67 | headers, and other data extracted from the Plugin objects. 68 | 69 | Args: 70 | username: 71 | A string with the username used for the login process. 72 | password: 73 | A string with the password used for the login process. 74 | **kwargs: 75 | A dictionary with additional data about the user. 76 | 77 | """ 78 | 79 | self.username = username 80 | self.password = password 81 | 82 | self.cookies = CookieStore.from_dict(kwargs.get("cookies")) 83 | self.headers = HeaderStore.from_dict(kwargs.get("headers")) 84 | self.data = DataStore(kwargs.get("data")) 85 | 86 | def set_cookie(self, cookie: Cookie) -> None: 87 | """Sets the cookie for the user. 88 | 89 | Given a Cookie object, update the user's "cookies" attribute to 90 | include this cookie. 91 | 92 | Args: 93 | cookie: 94 | A Cookie Plugin object with the data to be added. 95 | 96 | """ 97 | if cookie.value: 98 | self.cookies.set(cookie) 99 | 100 | def set_cookies_from_dict(self, data: Dict[str, str]) -> None: 101 | """Set user's cookies from a dictionary. 102 | 103 | Given a dictionary of cookies, convert them to :class:`Cookie 104 | ` objects, and load them in the 105 | :class:`User ` object respectively. 106 | 107 | Args: 108 | data: 109 | A dictionary of strings corresponding to cookie keys and 110 | values. 111 | 112 | """ 113 | cookies = [] 114 | for key, value in data.items(): 115 | cookie = Cookie(key, value) 116 | cookies.append(cookie) 117 | 118 | for item in cookies: 119 | self.set_cookie(item) 120 | 121 | def set_header(self, header: Header) -> None: 122 | """Sets the header for the user. 123 | 124 | Given a Header object, update the user's "headers" attribute to 125 | include this header. 126 | 127 | Args: 128 | header: 129 | A Header Plugin object with the data to be added. 130 | 131 | """ 132 | if header.value: 133 | self.headers.set(header) 134 | 135 | def set_headers_from_dict(self, data: Dict[str, str]) -> None: 136 | """Set user's headers from a dictionary. 137 | 138 | Given a dictionary of headers, convert them to :class:`Header 139 | ` objects, and load them in the 140 | :class:`User ` object respectively. 141 | 142 | Args: 143 | data: 144 | A dictionary of strings corresponding to header keys and 145 | values. 146 | 147 | """ 148 | headers = [] 149 | for key, value in data.items(): 150 | header = Header(key, value) 151 | headers.append(header) 152 | 153 | for item in headers: 154 | self.set_header(item) 155 | 156 | def set_data(self, data: Plugin) -> None: 157 | """Sets the data for the user. 158 | 159 | Given a Plugin, update the user's data attribute to include this 160 | data. 161 | 162 | Args: 163 | data: 164 | A Plugin object with the data to be added. 165 | 166 | """ 167 | if data.value: 168 | self.data.update({data.name: data.value}) 169 | 170 | def set_data_from_dict(self, data: Dict[str, str]) -> None: 171 | """Set user's data from a dictionary. 172 | 173 | Given a dictionary of data items from :class:`Plugins 174 | `, load them in the :class:`User 175 | ` object respectively. 176 | 177 | Args: 178 | data: 179 | A dictionary of strings corresponding to data keys and 180 | values. 181 | 182 | """ 183 | for key, value in data.items(): 184 | self.data.update({key: value}) 185 | 186 | def to_dict(self) -> Dict[str, str]: 187 | """Returns this object's data in a dictionary format.""" 188 | data = {} 189 | data["username"] = self.username 190 | data["password"] = self.password 191 | data.update(self.cookies.to_dict()) 192 | data.update(self.headers.to_dict()) 193 | data.update(self.data.to_dict()) 194 | return data 195 | 196 | 197 | class UserStore(DataStore): 198 | """Class holding all the users of the Application. 199 | 200 | UserStore inherits from DataStore, and contains the users set up in 201 | the "_users" variable from the hy configuration file. Each user is 202 | an User object. The data from a UserStore object can be accessed 203 | same way like from the DataStore. 204 | 205 | If "_active_user" is set up in the configuration file, this will be 206 | the default user. Otherwise, the first user will be the active one. 207 | 208 | Attributes: 209 | active_user: 210 | A string with the currently active user. 211 | 212 | """ 213 | 214 | def __init__( 215 | self, users: List[Dict[hy.HyKeyword, str]], active_user: str = None 216 | ) -> None: 217 | """Initializes the UserStore object. 218 | 219 | Given a list of dictionaries, map them to a User object and 220 | store them in this UserStore object. 221 | 222 | Args: 223 | users: 224 | A list of dictionaries. Dictionary's data is mapped to a 225 | User object. 226 | active_user: 227 | An optional string specifying the active user to be set. 228 | 229 | """ 230 | if active_user: 231 | self.active_user = active_user 232 | else: 233 | self.active_user = hy_dict_to_python(users[0])["username"] 234 | 235 | values = {} 236 | for item in users: 237 | userdata = hy_dict_to_python(item) 238 | username = userdata["username"] 239 | user = User(**userdata) 240 | values[username] = user 241 | 242 | super().__init__(values) 243 | 244 | def to_dict(self) -> Dict[str, str]: 245 | """Returns the UserStore data in dictionary format.""" 246 | data = {} 247 | for username in self: 248 | data[username] = self[username].to_dict() 249 | 250 | return data 251 | 252 | @property 253 | def active(self) -> User: 254 | """Returns the active user as an User object.""" 255 | return self[self.active_user] 256 | -------------------------------------------------------------------------------- /raider/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 DigeeX 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """Functions that are used within Raider. 17 | """ 18 | 19 | import logging 20 | import os 21 | import re 22 | import sys 23 | from typing import Any, Dict, List, Union 24 | 25 | import bs4 26 | import hy 27 | 28 | from raider.__version__ import __version__ 29 | 30 | 31 | def default_user_agent() -> str: 32 | """Gets the default user agent. 33 | 34 | Gets the current version of Raider and creates the user agent 35 | string. 36 | 37 | Returns: 38 | A string with the user agent. 39 | 40 | """ 41 | return "digeex_raider/" + __version__ 42 | 43 | 44 | def get_config_dir() -> str: 45 | """Gets the configuration directory. 46 | 47 | Returns the path of the directory with the Raider configuration 48 | files. 49 | 50 | Returns: 51 | A string with the path of the configuration directory. 52 | 53 | """ 54 | confdir = os.path.expanduser("~/.config") 55 | raider_conf = os.path.join(confdir, "raider") 56 | os.makedirs(raider_conf, exist_ok=True) 57 | return raider_conf 58 | 59 | 60 | def get_config_file(filename: str) -> str: 61 | """Gets the configuration file. 62 | 63 | Given the file name, it returns the path of this file in the Raider 64 | configuration directory. 65 | 66 | Args: 67 | filename: 68 | A string with the name of the file to look up for in the main 69 | configuration directory. 70 | 71 | Returns: 72 | A string with the path of the file. 73 | 74 | """ 75 | confdir = get_config_dir() 76 | file_path = os.path.join(confdir, filename) 77 | return file_path 78 | 79 | 80 | def get_project_dir(project: str) -> str: 81 | """Gets the directory of the project. 82 | 83 | Given the name of the project, returns the path to the directory 84 | containing the configuration files for this project. 85 | 86 | Args: 87 | project: 88 | A string with the name of the project. 89 | 90 | Returns: 91 | A string with the path of the directory where the config files for 92 | the project are located. 93 | 94 | """ 95 | confdir = get_config_dir() 96 | project_conf = os.path.join(confdir, "projects", project) 97 | return project_conf 98 | 99 | 100 | def get_project_file(project: str, filename: str) -> str: 101 | """Gets a file from a project. 102 | 103 | Given the project name and the file name, it returns the path to 104 | that file. 105 | 106 | Args: 107 | project: 108 | A string with the name of the project. 109 | filename: 110 | A string with the file name. 111 | 112 | Returns: 113 | The path of the file in the project directory. 114 | 115 | """ 116 | project_conf = get_project_dir(project) 117 | file_path = os.path.join(project_conf, filename) 118 | return file_path 119 | 120 | 121 | def import_raider_objects() -> Dict[str, Any]: 122 | """Imports Raider objects to use inside hy configuration files. 123 | 124 | To make Raider objects visible inside hy files without using 125 | separate imports, this function does the imports and returns the 126 | locals() which is later used when evaluating hy files. 127 | 128 | Returns: 129 | A dictionary with the locals() containing all the Raider objects 130 | that can be used in hy files. 131 | 132 | """ 133 | hy_imports = { 134 | "plugins.common": ("Empty " "Plugin " "Parser "), 135 | "plugins.basic": ( 136 | "Regex " 137 | "Html " 138 | "Json " 139 | "Variable " 140 | "Command " 141 | "Prompt " 142 | "Cookie " 143 | "Header " 144 | ), 145 | "plugins.modifiers": ("Alter " "Combine "), 146 | "plugins.parsers": ("Parser " "UrlParser "), 147 | "flow": "Flow", 148 | "request": "Request PostBody Template", 149 | "operations": ( 150 | "Http " "Grep " "Print " "Error " "NextStage " "Operation " "Save " 151 | ), 152 | } 153 | 154 | for module, classes in hy_imports.items(): 155 | expr = hy.read_str( 156 | "(import [raider." + module + " [" + classes + "]])" 157 | ) 158 | logging.debug("expr = %s", str(expr).replace("\n", " ")) 159 | hy.eval(expr) 160 | 161 | return locals() 162 | 163 | 164 | def hy_dict_to_python(hy_dict: Dict[hy.HyKeyword, Any]) -> Dict[str, Any]: 165 | """Converts a hy dictionary to a python dictionary. 166 | 167 | When creating dictionaries in hylang using :parameters they become 168 | hy.HyKeyword objects. This function converts them to normal python 169 | dictionaries. 170 | 171 | Args: 172 | hy_dict: 173 | A dictionary created in hy, which uses hy.HyKeyword instead of 174 | simple strings as keys. 175 | 176 | Returns: 177 | A dictionary with the same elements only with hy.HyKeyword keys 178 | converted into normal strings. 179 | 180 | """ 181 | data = {} 182 | for hy_key in hy_dict: 183 | key = hy_key.name 184 | data.update({key: hy_dict[hy_key]}) 185 | 186 | return data 187 | 188 | 189 | def py_dict_to_hy_list( 190 | data: Dict[str, Any] 191 | ) -> List[Union[hy.HyString, hy.HyDict, hy.HySymbol]]: 192 | """Converts a python dictionary to a hylang list. 193 | 194 | In hy, dictionaries are created out of lists, and this function 195 | converts a normal python dictionary to a list made out of hy symbols 196 | that will be later used to create the hy dictionary. 197 | 198 | Args: 199 | data: 200 | A python dictionary with the data to convert. 201 | 202 | Returns: 203 | A list with hy objects that can be used to create a hy dictionary. 204 | 205 | """ 206 | value = [] 207 | for key in data: 208 | if isinstance(key, str): 209 | value.append(hy.HyString(key)) 210 | if isinstance(data[key], dict): 211 | value.append(hy.HyDict(py_dict_to_hy_list(data[key]))) 212 | elif isinstance(data[key], str): 213 | value.append(hy.HyString(data[key])) 214 | else: 215 | value.append(hy.HySymbol(data[key])) 216 | 217 | return value 218 | 219 | 220 | def create_hy_expression( 221 | variable: str, value: Union[str, Dict[Any, Any], List[Any]] 222 | ) -> str: 223 | """Creates a hy expression. 224 | 225 | Raider configuration is saved in hy format, and this function 226 | creates the assignments in this format. 227 | 228 | Args: 229 | variable: 230 | A string with the name of the variable to be created. 231 | value: 232 | The value of the variable. 233 | 234 | Returns: 235 | A string with the valid hy expression. 236 | """ 237 | data = [] 238 | data.append(hy.HySymbol("setv")) 239 | data.append(hy.HySymbol(variable)) 240 | 241 | if isinstance(value, dict): 242 | data.append(hy.HyDict(py_dict_to_hy_list(value))) 243 | elif isinstance(value, list): 244 | data.append(hy.HyList(value)) 245 | elif isinstance(value, str): 246 | data.append(hy.HyString(value)) 247 | else: 248 | data.append(hy.HySymbol(value)) 249 | 250 | return serialize_hy(hy.HyExpression(data)) + "\n" 251 | 252 | 253 | def serialize_hy( 254 | form: Union[ 255 | hy.models.HyExpression, 256 | hy.models.HyDict, 257 | hy.models.HyList, 258 | hy.models.HySymbol, 259 | hy.models.HyInteger, 260 | hy.models.HyKeyword, 261 | hy.models.HyString, 262 | ] 263 | ) -> str: 264 | """Serializes hy expression. 265 | 266 | This function serializes the supplied hy expression and returns it 267 | in a string format, so that it can be later saved in a file. 268 | 269 | Args: 270 | form: 271 | A hy expression to convert to a string. 272 | 273 | Returns: 274 | A string with the serialized form. 275 | 276 | """ 277 | if isinstance(form, hy.models.HyExpression): 278 | hystring = "(" + " ".join([serialize_hy(x) for x in form]) + ")" 279 | elif isinstance(form, hy.models.HyDict): 280 | hystring = "{" + " ".join([serialize_hy(x) for x in form]) + "}" 281 | elif isinstance(form, hy.models.HyList): 282 | hystring = "[" + " ".join([serialize_hy(x) for x in form]) + "]" 283 | elif isinstance(form, hy.models.HySymbol): 284 | hystring = "{}".format(form) 285 | elif isinstance(form, hy.models.HyInteger): 286 | hystring = "{}".format(int(form)) 287 | elif isinstance(form, hy.models.HyKeyword): 288 | hystring = "{}".format(form.name) 289 | elif isinstance(form, hy.models.HyString): 290 | hystring = '"{}"'.format(form) 291 | else: 292 | hystring = "{}".format(form) 293 | 294 | return hystring 295 | 296 | 297 | def eval_file( 298 | filename: str, shared_locals: Dict[str, Any] = None 299 | ) -> Dict[str, Any]: 300 | """Evaluate hy file. 301 | 302 | This function evaluates all the content inside the supplied hy file, 303 | and returns the created locals() so that it can be later used for 304 | other files. 305 | 306 | Args: 307 | filename: 308 | A string with the file name to be evaluated. 309 | shared_locals: 310 | A dictionary with the locals() that will be considered when 311 | evaluating the file. 312 | 313 | Returns: 314 | A dictionary with the updated locals() after evaluating the hy 315 | file. 316 | 317 | """ 318 | if shared_locals: 319 | locals().update(shared_locals) 320 | 321 | logging.debug("Loading %s", filename) 322 | with open(filename) as hyfile: 323 | try: 324 | while True: 325 | expr = hy.read(hyfile) 326 | if expr: 327 | logging.debug("expr = %s", str(expr).replace("\n", " ")) 328 | hy.eval(expr) 329 | except EOFError: 330 | logging.debug("Finished processing %s", filename) 331 | 332 | return locals() 333 | 334 | 335 | def eval_project_file( 336 | project: str, filename: str, shared_locals: Dict[str, Any] 337 | ) -> Dict[str, Any]: 338 | """Evaluate a hy file from a project. 339 | 340 | This function evaluates the specified file inside the project and 341 | returns the locals() which are updated after evaluating the file. 342 | 343 | Args: 344 | project: 345 | A string with the name of the project. 346 | filename: 347 | A string with the file name to be evaluated. 348 | shared_locals: 349 | A dictionary of locals() to be included when evaluating the 350 | file. 351 | 352 | Returns: 353 | A dictionary of locals() updated after evaluating the file. 354 | 355 | """ 356 | raider_objects = import_raider_objects() 357 | locals().update(raider_objects) 358 | if shared_locals: 359 | locals().update(shared_locals) 360 | 361 | file_path = get_project_file(project, filename) 362 | shared_locals = eval_file(file_path, locals()) 363 | return shared_locals 364 | 365 | 366 | def list_projects() -> List[str]: 367 | """List existing projects. 368 | 369 | This function returns the list of projects that have been 370 | configured in Raider. 371 | 372 | Returns: 373 | A list with the strings of the project found in the 374 | configuration directory. 375 | 376 | """ 377 | projects = [] 378 | projectdir = os.path.join(get_config_dir(), "projects") 379 | os.makedirs(projectdir, exist_ok=True) 380 | for filename in os.listdir(projectdir): 381 | if not filename[0] == "_": 382 | projects.append(filename) 383 | return projects 384 | 385 | 386 | def match_tag(html_tag: bs4.element.Tag, attributes: Dict[str, str]) -> bool: 387 | """Tells if a tag matches the search. 388 | 389 | This function checks whether the supplied tag matches the 390 | attributes. The attributes is a dictionary, and the values are 391 | treated as a regular expression, to allow checking for tags that 392 | don't have a static value. 393 | 394 | Args: 395 | html_tag: 396 | A bs4.element.Tag object with the tag to be checked. 397 | attributes: 398 | A dictionary of attributes to check whether they match with the tag. 399 | 400 | Returns: 401 | A boolean saying whether the tag matched with the attributes or not. 402 | 403 | """ 404 | for key, value in attributes.items(): 405 | if not (key in html_tag.attrs) or not ( 406 | re.match(value, html_tag.attrs[key]) 407 | ): 408 | return False 409 | return True 410 | 411 | 412 | def parse_json_filter(raw: str) -> List[str]: 413 | """Parses a raw JSON filter and returns a list with the items. 414 | 415 | Args: 416 | raw: 417 | A string with the expected JSON filter. 418 | 419 | Returns: 420 | A list with all items found in the filter. 421 | """ 422 | splitted = raw.split(".") 423 | 424 | parsed_filter = [] 425 | for item in splitted: 426 | parsed_item = [] 427 | open_delim_index = item.find("[") 428 | 429 | if open_delim_index != -1: 430 | if open_delim_index == 0: 431 | logging.critical( 432 | ( 433 | "Syntax error. '.' should be followed by a key,", 434 | "not an array index.", 435 | ) 436 | ) 437 | sys.exit() 438 | parsed_item.append(item[:open_delim_index].strip('"')) 439 | array_indices = item[open_delim_index:] 440 | open_delim_index = 0 441 | 442 | while array_indices: 443 | close_delim_index = array_indices.find( 444 | "]", open_delim_index + 1 445 | ) 446 | if close_delim_index == -1: 447 | logging.critical("Syntax error. Closing ']' not found.") 448 | sys.exit() 449 | 450 | index = array_indices[open_delim_index + 1 : close_delim_index] 451 | if index.isdecimal(): 452 | parsed_item.append("[" + index + "]") 453 | array_indices = array_indices[close_delim_index + 1 :] 454 | else: 455 | logging.critical( 456 | ( 457 | "Syntax error.", 458 | "The index between '[' and '] is not a decimal.", 459 | ) 460 | ) 461 | sys.exit() 462 | else: 463 | parsed_item.append(item.strip('"')) 464 | 465 | parsed_filter += parsed_item 466 | 467 | return parsed_filter 468 | -------------------------------------------------------------------------------- /scripts/authenticate_and_save_session.py: -------------------------------------------------------------------------------- 1 | from raider import Raider 2 | 3 | raider = Raider("test") 4 | raider.config.proxy = "http://localhost:8080" 5 | raider.authenticate() 6 | raider.run_function("test") 7 | raider.save_session() 8 | -------------------------------------------------------------------------------- /scripts/fuzz_authenticated_function.py: -------------------------------------------------------------------------------- 1 | from raider import Raider 2 | 3 | raider = Raider("my_app") 4 | raider.config.proxy = "http://localhost:8080" 5 | raider.authenticate() 6 | 7 | 8 | def fuzz_inputs(value): 9 | return [value + str(i) for i in range(0, 10)] 10 | 11 | 12 | raider.fuzz_function("my_function", "access_token", fuzz_inputs) 13 | -------------------------------------------------------------------------------- /scripts/fuzz_authentication_step.py: -------------------------------------------------------------------------------- 1 | from raider import Raider 2 | 3 | raider = Raider("my_app") 4 | raider.config.proxy = "http://localhost:8080" 5 | 6 | 7 | def fuzz_inputs(value): 8 | return [value + str(i) for i in range(0, 10)] 9 | 10 | 11 | raider.fuzz_authentication("authentication_stage", "access_token", fuzz_inputs) 12 | -------------------------------------------------------------------------------- /scripts/load_session_and_fuzz_function.py: -------------------------------------------------------------------------------- 1 | from raider import Raider 2 | 3 | raider = Raider("my_app") 4 | raider.config.proxy = "http://localhost:8080" 5 | raider.load_session() 6 | 7 | 8 | def fuzz_inputs(value): 9 | for i in range(0, 10): 10 | yield value + str(i) 11 | 12 | 13 | raider.fuzz_function("my_function", "session_id", fuzz_inputs) 14 | -------------------------------------------------------------------------------- /scripts/run_authenticated_function.py: -------------------------------------------------------------------------------- 1 | from raider import Raider 2 | 3 | raider = Raider("my_app") 4 | raider.config.proxy = "http://localhost:8080" 5 | raider.authenticate() 6 | raider.run_function("my_function") 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE 6 | 7 | [flake8] 8 | exclude = raider/__init__.py,docs/conf.py,scripts 9 | ignore = E203,W503 10 | --------------------------------------------------------------------------------