From 675baddfbbd83def1e7ec1661fbe99aa1cafa440 Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Wed, 4 Aug 2021 18:51:48 +0200 Subject: [PATCH 1/8] Fix bug with Alter Plugin --- raider/flow.py | 19 +++++++------------ raider/plugins.py | 38 +++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/raider/flow.py b/raider/flow.py index 9f8fc30..f61a68f 100644 --- a/raider/flow.py +++ b/raider/flow.py @@ -111,6 +111,10 @@ def execute(self, user: User, config: Config) -> None: Given the user in context and the global Raider configuration, sends the HTTP request and extracts the defined outputs. + Iterates through the defined outputs in the Flow object, and + extracts the data from the HTTP response, saving it in the + respective :class:`Plugin ` object. + Args: user: An object containing all the user specific data relevant for @@ -120,22 +124,13 @@ def execute(self, user: User, config: Config) -> None: """ self.response = self.request.send(user, config) - self.get_outputs() - - def get_outputs(self) -> None: - """Extract the outputs from the HTTP response. - - Iterates through the defined outputs in the Flow object, and - extracts the data from the HTTP response, saving it in the - respective :class:`Plugin ` object. - - """ if self.outputs: for output in self.outputs: if output.needs_response: output.extract_value_from_response(self.response) - elif output.plugins: - output.value = output.function() + elif output.depends_on_other_plugins: + for item in output.plugins: + item.get_value(user.to_dict()) def get_plugin_values(self, user: User) -> None: """Given a user, get the plugins' values from it. diff --git a/raider/plugins.py b/raider/plugins.py index dbc8c7a..734bb94 100644 --- a/raider/plugins.py +++ b/raider/plugins.py @@ -495,14 +495,16 @@ def extract_json_field(self, text: str) -> Optional[str]: return self.value @classmethod - def from_plugin(cls, plugin: Plugin, name: str, extract: str) -> "Json": + def from_plugin( + cls, parent_plugin: Plugin, name: str, extract: str + ) -> "Json": """Extracts the JSON field from another plugin's value.""" json_plugin = cls( name=name, extract=extract, flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, ) - json_plugin.plugins = [plugin] + json_plugin.plugins = [parent_plugin] json_plugin.function = json_plugin.extract_json_field return json_plugin @@ -677,7 +679,7 @@ def __str__(self) -> str: return str({self.name: self.value}) @classmethod - def from_plugin(cls, plugin: Plugin, name: str) -> "Cookie": + def from_plugin(cls, parent_plugin: Plugin, name: str) -> "Cookie": """Creates a Cookie from a Plugin. Given another :class:`plugin `, and a @@ -695,8 +697,8 @@ def from_plugin(cls, plugin: Plugin, name: str) -> "Cookie": """ cookie = cls( name=name, - value=plugin.value, - function=lambda: plugin.value if plugin.value else None, + value=parent_plugin.value, + function=lambda: parent_plugin.value, flags=0, ) return cookie @@ -813,7 +815,7 @@ def bearerauth(cls, access_token: Plugin) -> "Header": return header @classmethod - def from_plugin(cls, plugin: Plugin, name: str) -> "Header": + def from_plugin(cls, parent_plugin: Plugin, name: str) -> "Header": """Creates a Header from a Plugin. Given another :class:`plugin `, and a @@ -832,7 +834,7 @@ def from_plugin(cls, plugin: Plugin, name: str) -> "Header": header = cls( name=name, value=None, - function=lambda: plugin.value if plugin.value else None, + function=lambda: parent_plugin.value, flags=0, ) return header @@ -855,7 +857,7 @@ class Alter(Plugin): def __init__( self, - plugin: Plugin, + parent_plugin: Plugin, alter_function: Callable[[str], Optional[str]], ) -> None: """Initializes the Alter Plugin. @@ -870,12 +872,12 @@ def __init__( The Function with instructions on how to alter the value. """ super().__init__( - name=plugin.name, - value=plugin.value, + name=parent_plugin.name, + value=parent_plugin.value, flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, function=self.process_value, ) - self.plugins = [plugin] + self.plugins = [parent_plugin] self.alter_function = alter_function def process_value(self) -> Optional[str]: @@ -894,16 +896,22 @@ def process_value(self) -> Optional[str]: return self.value @classmethod - def prepend(cls, plugin: Plugin, string: str) -> "Alter": + def prepend(cls, parent_plugin: Plugin, string: str) -> "Alter": """Prepend a string to plugin's value.""" - alter = cls(plugin=plugin, alter_function=lambda value: string + value) + alter = cls( + parent_plugin=parent_plugin, + alter_function=lambda value: string + value, + ) return alter @classmethod - def append(cls, plugin: Plugin, string: str) -> "Alter": + def append(cls, parent_plugin: Plugin, string: str) -> "Alter": """Append a string after the plugin's value""" - alter = cls(plugin=plugin, alter_function=lambda value: value + string) + alter = cls( + parent_plugin=parent_plugin, + alter_function=lambda value: value + string, + ) return alter From 8b3a6f9c4aaadbb923f7c409594f9d141815771c Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Fri, 13 Aug 2021 15:56:18 +0200 Subject: [PATCH 2/8] Add Parsers --- raider/flow.py | 1 + raider/parsers.py | 83 ++++++++++++++++++++++++++++++++++++++++++++ raider/plugins.py | 27 +++++++++----- raider/request.py | 24 +++++++++---- raider/structures.py | 10 ++++++ raider/utils.py | 1 + 6 files changed, 130 insertions(+), 16 deletions(-) create mode 100644 raider/parsers.py diff --git a/raider/flow.py b/raider/flow.py index f61a68f..9e259d9 100644 --- a/raider/flow.py +++ b/raider/flow.py @@ -131,6 +131,7 @@ def execute(self, user: User, config: Config) -> None: elif output.depends_on_other_plugins: for item in output.plugins: item.get_value(user.to_dict()) + output.get_value(user.to_dict()) def get_plugin_values(self, user: User) -> None: """Given a user, get the plugins' values from it. diff --git a/raider/parsers.py b/raider/parsers.py new file mode 100644 index 0000000..5c5acd1 --- /dev/null +++ b/raider/parsers.py @@ -0,0 +1,83 @@ +# Copyright (C) 2021 DigeeX +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Plugins used to parse data. +""" + +from typing import Callable, Optional, Union +from urllib.parse import parse_qs, urlsplit + +from raider.plugins import Plugin + + +class Parser(Plugin): + """Plugins that parse other plugins.""" + + def __init__( + self, + name: str, + function: Callable[[], Optional[str]], + value: str = None, + ) -> None: + """Initializes the Parser plugin.""" + super().__init__( + name=name, + value=value, + function=function, + flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, + ) + + +class UrlParser(Parser): + """Parse the URL and extract elements from it. + + Use this when needing to extract some piece of information from + the URL. + + """ + + def __init__(self, parent_plugin: Plugin, element: str) -> None: + super().__init__(name=element, function=self.parse_url) + self.plugins = [parent_plugin] + self.element = element + self.url: Optional[str] = None + + def parse_url(self) -> Optional[str]: + """Parses the URL and returns the string with the desired element.""" + + def get_query(query: Union[str, bytes], element: str) -> str: + """Extracts a parameter from the URL query.""" + key = element.split(".")[1] + return parse_qs(str(query))[key][0] + + value: Optional[str] = None + + self.url = self.plugins[0].value + parsed_url = urlsplit(self.url) + + if self.element.startswith("query"): + value = get_query(parsed_url.query, self.element) + elif self.element.startswith("netloc"): + value = str(parsed_url.netloc) + elif self.element.startswith("path"): + value = str(parsed_url.path) + elif self.element.startswith("scheme"): + value = str(parsed_url.scheme) + elif self.element.startswith("fragment"): + value = str(parsed_url.fragment) + + if value: + return str(value) + return None diff --git a/raider/plugins.py b/raider/plugins.py index 734bb94..b7bb3cd 100644 --- a/raider/plugins.py +++ b/raider/plugins.py @@ -65,7 +65,7 @@ class Plugin: def __init__( self, name: str, - function: Callable[..., Optional[str]], + function: Optional[Callable[..., Optional[str]]] = None, flags: int = 0, value: Optional[str] = None, ) -> None: @@ -100,6 +100,8 @@ def __init__( if (flags & Plugin.NEEDS_USERDATA) and not function: self.function = self.extract_from_userdata + elif not function: + self.function = self.return_value else: self.function = function @@ -118,6 +120,10 @@ def get_value( if not self.needs_response: if self.needs_userdata: self.value = self.function(userdata) + elif self.depends_on_other_plugins: + for item in self.plugins: + item.get_value(userdata) + self.value = self.function() else: self.value = self.function() return self.value @@ -438,6 +444,12 @@ def extract_json_from_response( """Extracts the json field from a HTTP response.""" return self.extract_json_field(response.text) + def extract_json_from_plugin(self) -> Optional[str]: + """Extracts the json field from a plugin.""" + if self.plugins[0].value: + return self.extract_json_field(self.plugins[0].value) + return None + def extract_json_field(self, text: str) -> Optional[str]: """Extracts the JSON field from the text. @@ -505,7 +517,7 @@ def from_plugin( flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, ) json_plugin.plugins = [parent_plugin] - json_plugin.function = json_plugin.extract_json_field + json_plugin.function = json_plugin.extract_json_from_plugin return json_plugin def __str__(self) -> str: @@ -658,7 +670,6 @@ def __init__( else: super().__init__( name=name, - function=lambda: self.value, value=value, flags=flags, ) @@ -698,7 +709,6 @@ def from_plugin(cls, parent_plugin: Plugin, name: str) -> "Cookie": cookie = cls( name=name, value=parent_plugin.value, - function=lambda: parent_plugin.value, flags=0, ) return cookie @@ -747,7 +757,6 @@ def __init__( else: super().__init__( name=name, - function=lambda: self.value, value=value, flags=flags, ) @@ -833,10 +842,11 @@ def from_plugin(cls, parent_plugin: Plugin, name: str) -> "Header": """ header = cls( name=name, - value=None, - function=lambda: parent_plugin.value, - flags=0, + value=parent_plugin.value, + flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, ) + header.plugins = [parent_plugin] + header.function = lambda: header.plugins[0].value return header @@ -957,5 +967,4 @@ def __init__(self, name: str): super().__init__( name=name, flags=0, - function=self.return_value, ) diff --git a/raider/request.py b/raider/request.py index 781ad22..d3e8537 100644 --- a/raider/request.py +++ b/raider/request.py @@ -205,6 +205,9 @@ def get_children_plugins(plugin: Plugin) -> Dict[str, Plugin]: return inputs + # pylint: disable=W0511 + # TODO: Will redesign this function later. + # pylint: disable=R0912 def process_inputs( self, user: User, config: Config ) -> Dict[str, Dict[str, str]]: @@ -246,17 +249,20 @@ def process_inputs( for key in self.cookies: value = self.cookies[key].get_value(userdata) - cookies.update({key: value}) + if value: + cookies.update({key: value}) for key in self.headers: value = self.headers[key].get_value(userdata) - headers.update({key: value}) + if value: + headers.update({key: value}) for key in list(httpdata): value = httpdata[key] if isinstance(value, Plugin): new_value = value.get_value(userdata) - httpdata.update({key: new_value}) + if new_value: + httpdata.update({key: new_value}) if isinstance(key, Plugin): new_value = httpdata.pop(key) @@ -388,6 +394,7 @@ def __init__( def __call__( self, + method: Optional[str] = None, url: Optional[Union[str, Plugin]] = None, path: Optional[Union[str, Plugin]] = None, cookies: Optional[List[Cookie]] = None, @@ -409,18 +416,21 @@ def __call__( template = deepcopy(self) + if method: + template.method = method + if url: template.url = url + if path: template.path = path if cookies: - for cookie in cookies: - template.cookies.set(cookie) + template.cookies.merge(CookieStore(cookies)) if headers: - for header in headers: - template.headers.set(header) + template.headers.merge(HeaderStore(headers)) + if data: if isinstance(data, PostBody): template.data.update(data.to_dict()) diff --git a/raider/structures.py b/raider/structures.py index e9d5167..c92fe2a 100644 --- a/raider/structures.py +++ b/raider/structures.py @@ -139,6 +139,11 @@ def set(self, header: Header) -> None: """ super().update({header.name.lower(): header.value}) + def merge(self, headerstore: "HeaderStore") -> None: + """Merge HeaderStore object with another one.""" + for item in headerstore: + self._store[item] = headerstore[item] + @classmethod def from_dict(cls, data: Optional[Dict[str, str]]) -> "HeaderStore": """Creates a HeaderStore object from a dictionary. @@ -202,6 +207,11 @@ def set(self, cookie: Cookie) -> None: """ super().update({cookie.name: cookie.value}) + def merge(self, cookiestore: "CookieStore") -> None: + """Merge CookieStore object with another one.""" + for item in cookiestore: + self._store[item] = cookiestore[item] + @classmethod def from_dict(cls, data: Optional[Dict[str, str]]) -> "CookieStore": """Creates a CookieStore object from a dictionary. diff --git a/raider/utils.py b/raider/utils.py index 991c8d9..4de0bc9 100644 --- a/raider/utils.py +++ b/raider/utils.py @@ -146,6 +146,7 @@ def import_raider_objects() -> Dict[str, Any]: "Empty " ), "flow": "Flow", + "parsers": "UrlParser", "request": "Request PostBody Template", "operations": ( "Http " "Grep " "Print " "Error " "NextStage " "Operation " "Save " From cb7f6e43872e8eb490a3dd0cf4e07e3d03e51cc3 Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Fri, 13 Aug 2021 20:04:45 +0200 Subject: [PATCH 3/8] Split plugins into smaller groups. --- raider/application.py | 2 +- raider/attacks.py | 2 +- raider/authentication.py | 2 +- raider/flow.py | 2 +- raider/operations.py | 2 +- raider/{plugins.py => plugins/basic.py} | 297 +----------------------- raider/plugins/common.py | 220 ++++++++++++++++++ raider/plugins/modifiers.py | 164 +++++++++++++ raider/{ => plugins}/parsers.py | 22 +- raider/raider.py | 2 +- raider/request.py | 3 +- raider/structures.py | 2 +- raider/user.py | 3 +- raider/utils.py | 16 +- 14 files changed, 408 insertions(+), 331 deletions(-) rename raider/{plugins.py => plugins/basic.py} (69%) create mode 100644 raider/plugins/common.py create mode 100644 raider/plugins/modifiers.py rename raider/{ => plugins}/parsers.py (81%) diff --git a/raider/application.py b/raider/application.py index 286f4e2..b06eb20 100644 --- a/raider/application.py +++ b/raider/application.py @@ -83,7 +83,7 @@ def __init__(self, project: str = None) -> None: output = self.config.load_project(project) self.users = UserStore(output["_users"]) active_user = output.get("_active_user") - if active_user: + if active_user and active_user in self.users: self.active_user = self.users[active_user] else: self.active_user = self.users.active diff --git a/raider/attacks.py b/raider/attacks.py index bf3feff..23d90d9 100644 --- a/raider/attacks.py +++ b/raider/attacks.py @@ -24,7 +24,7 @@ from raider.application import Application from raider.flow import Flow -from raider.plugins import Plugin +from raider.plugins.common import Plugin class Fuzz: diff --git a/raider/authentication.py b/raider/authentication.py index 2c33f5a..9ae7ab1 100644 --- a/raider/authentication.py +++ b/raider/authentication.py @@ -22,7 +22,7 @@ from raider.config import Config from raider.flow import Flow -from raider.plugins import Cookie, Header, Html, Json, Regex +from raider.plugins.basic import Cookie, Header, Html, Json, Regex from raider.user import User diff --git a/raider/flow.py b/raider/flow.py index 9e259d9..f67c0ef 100644 --- a/raider/flow.py +++ b/raider/flow.py @@ -23,7 +23,7 @@ from raider.config import Config from raider.operations import Operation -from raider.plugins import Plugin +from raider.plugins.common import Plugin from raider.request import Request from raider.user import User diff --git a/raider/operations.py b/raider/operations.py index 36dc65f..06c38df 100644 --- a/raider/operations.py +++ b/raider/operations.py @@ -24,7 +24,7 @@ import requests -from raider.plugins import Plugin +from raider.plugins.common import Plugin def execute_actions( diff --git a/raider/plugins.py b/raider/plugins/basic.py similarity index 69% rename from raider/plugins.py rename to raider/plugins/basic.py index b7bb3cd..df36476 100644 --- a/raider/plugins.py +++ b/raider/plugins/basic.py @@ -12,8 +12,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -"""Plugins used as inputs/outputs in Flows. +"""Basic plugins. """ import json @@ -21,184 +20,16 @@ import os import re from base64 import b64encode -from typing import Callable, Dict, List, Optional, Union +from typing import Callable, Dict, Optional import hy import requests from bs4 import BeautifulSoup +from raider.plugins.common import Plugin from raider.utils import hy_dict_to_python, match_tag, parse_json_filter -class Plugin: - """Parent class for all plugins. - - Each Plugin class inherits from here. "get_value" function should - be called when extracting the value from the plugin, which will then - be stored in the "value" attribute. - - Attributes: - name: - A string used as an identifier for the Plugin. - function: - A function which will be called to extract the "value" of the - Plugin when used as an input in a Flow. The function should set - self.value and also return it. - value: - A string containing the Plugin's output value to be used as - input in the HTTP request. - flags: - An integer containing the flags that define the Plugin's - behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is - supported. If NEEDS_USERDATA is set, the plugin will get its - value from the user's data, which will be sent to the function - defined here. If NEEDS_RESPONSE is set, the Plugin will extract - its value from the HTTP response instead. - - """ - - # Plugin flags - NEEDS_USERDATA = 0x01 - NEEDS_RESPONSE = 0x02 - DEPENDS_ON_OTHER_PLUGINS = 0x04 - - def __init__( - self, - name: str, - function: Optional[Callable[..., Optional[str]]] = None, - flags: int = 0, - value: Optional[str] = None, - ) -> None: - """Initializes a Plugin object. - - Creates a Plugin object, holding a "function" defining how to - extract the "value". - - Args: - name: - A string with the unique identifier of the Plugin. - function: - A Callable function that will be used to extract the - Plugin's value. - value: - A string with the extracted value from the Plugin. - flags: - An integer containing the flags that define the Plugin's - behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is - supported. If NEEDS_USERDATA is set, the plugin will get its - value from the user's data, which will be sent to the function - defined here. If NEEDS_RESPONSE is set, the Plugin will extract - its value from the HTTP response instead. - - """ - self.name = name - self.plugins: List["Plugin"] = [] - self.value: Optional[str] = value - self.flags = flags - - self.function: Callable[..., Optional[str]] - - if (flags & Plugin.NEEDS_USERDATA) and not function: - self.function = self.extract_from_userdata - elif not function: - self.function = self.return_value - else: - self.function = function - - def get_value( - self, - userdata: Dict[str, str], - ) -> Optional[str]: - """Gets the value from the Plugin. - - Depending on the Plugin's flags, extract and return its value. - - Args: - userdata: - A dictionary with the user specific data. - """ - if not self.needs_response: - if self.needs_userdata: - self.value = self.function(userdata) - elif self.depends_on_other_plugins: - for item in self.plugins: - item.get_value(userdata) - self.value = self.function() - else: - self.value = self.function() - return self.value - - def extract_value_from_response( - self, - response: Optional[requests.models.Response], - ) -> None: - """Extracts the value of the Plugin from the HTTP response. - - If NEEDS_RESPONSE flag is set, the Plugin will extract its value - upon receiving the HTTP response, and store it inside the "value" - attribute. - - Args: - response: - An requests.models.Response object with the HTTP response. - - """ - output = self.function(response) - if output: - self.value = output - logging.debug( - "Found ouput %s = %s", - self.name, - self.value, - ) - else: - logging.warning("Couldn't extract output: %s", str(self.name)) - - def extract_from_userdata( - self, data: Dict[str, str] = None - ) -> Optional[str]: - """Extracts the plugin value from userdata. - - Given a dictionary with the userdata, return its value with the - same name as the "name" attribute from this Plugin. - - Args: - data: - A dictionary with user specific data. - - Returns: - A string with the value of the variable found. None if no such - variable has been defined. - - """ - if data and self.name in data: - self.value = data[self.name] - return self.value - - def return_value(self) -> Optional[str]: - """Just return plugin's value. - - This is used when needing a function just to return the value. - - """ - return self.value - - @property - def needs_userdata(self) -> bool: - """Returns True if the NEEDS_USERDATA flag is set.""" - return bool(self.flags & self.NEEDS_USERDATA) - - @property - def needs_response(self) -> bool: - """Returns True if the NEEDS_RESPONSE flag is set.""" - return bool(self.flags & self.NEEDS_RESPONSE) - - @property - def depends_on_other_plugins(self) -> bool: - """Returns True if the DEPENDS_ON_OTHER_PLUGINS flag is set.""" - return bool(self.flags & self.DEPENDS_ON_OTHER_PLUGINS) - - class Regex(Plugin): """Plugin to extract something using regular expressions. @@ -502,7 +333,7 @@ def extract_json_field(self, text: str) -> Optional[str]: self.value = str(temp) logging.debug("Json filter %s: %s", self.name, str(self.value)) else: - self.value = None + return None return self.value @@ -848,123 +679,3 @@ def from_plugin(cls, parent_plugin: Plugin, name: str) -> "Header": header.plugins = [parent_plugin] header.function = lambda: header.plugins[0].value return header - - -class Alter(Plugin): - """Plugin used to alter other plugin's value. - - If the value extracted from other plugins cannot be used in it's raw - form and needs to be somehow processed, Alter plugin can be used to - do that. Initialize it with the original plugin and a function which - will process the string and return the modified value. - - Attributes: - alter_function: - A function which will be given the plugin's value. It should - return a string with the processed value. - - """ - - def __init__( - self, - parent_plugin: Plugin, - alter_function: Callable[[str], Optional[str]], - ) -> None: - """Initializes the Alter Plugin. - - Given the original plugin, and a function to alter the data, - initialize the object, and get the modified value. - - Args: - plugin: - The original Plugin where the value is to be found. - alter_function: - The Function with instructions on how to alter the value. - """ - super().__init__( - name=parent_plugin.name, - value=parent_plugin.value, - flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, - function=self.process_value, - ) - self.plugins = [parent_plugin] - self.alter_function = alter_function - - def process_value(self) -> Optional[str]: - """Process the original plugin's value. - - Gives the original plugin's value to ``alter_function``. Return - the processed value and store it in self.value. - - Returns: - A string with the processed value. - - """ - if self.plugins[0].value: - self.value = self.alter_function(self.plugins[0].value) - - return self.value - - @classmethod - def prepend(cls, parent_plugin: Plugin, string: str) -> "Alter": - """Prepend a string to plugin's value.""" - alter = cls( - parent_plugin=parent_plugin, - alter_function=lambda value: string + value, - ) - - return alter - - @classmethod - def append(cls, parent_plugin: Plugin, string: str) -> "Alter": - """Append a string after the plugin's value""" - alter = cls( - parent_plugin=parent_plugin, - alter_function=lambda value: value + string, - ) - - return alter - - -class Combine(Plugin): - """Plugin to combine the values of other plugins.""" - - def __init__(self, *args: Union[str, Plugin]): - """Initialize Combine object.""" - self.args = args - name = str(sum(hash(item) for item in args)) - super().__init__( - name=name, - flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, - function=self.concatenate_values, - ) - self.plugins = [] - for item in args: - if isinstance(item, Plugin): - self.plugins.append(item) - - def concatenate_values(self) -> str: - """Concatenate the provided values. - - This function will concatenate the arguments values. Accepts - both strings and plugins. - - """ - combined = "" - for item in self.args: - if isinstance(item, str): - combined += item - elif item.value: - combined += item.value - return combined - - -class Empty(Plugin): - """Empty plugin to use for fuzzing new data.""" - - def __init__(self, name: str): - """Initialize Empty plugin.""" - super().__init__( - name=name, - flags=0, - ) diff --git a/raider/plugins/common.py b/raider/plugins/common.py new file mode 100644 index 0000000..1bc4a09 --- /dev/null +++ b/raider/plugins/common.py @@ -0,0 +1,220 @@ +# Copyright (C) 2021 DigeeX +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Common Plugin classes used by other plugins. +""" + + +import logging +from typing import Callable, Dict, List, Optional + +import requests + + +class Plugin: + """Parent class for all plugins. + + Each Plugin class inherits from here. "get_value" function should + be called when extracting the value from the plugin, which will then + be stored in the "value" attribute. + + Attributes: + name: + A string used as an identifier for the Plugin. + function: + A function which will be called to extract the "value" of the + Plugin when used as an input in a Flow. The function should set + self.value and also return it. + value: + A string containing the Plugin's output value to be used as + input in the HTTP request. + flags: + An integer containing the flags that define the Plugin's + behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is + supported. If NEEDS_USERDATA is set, the plugin will get its + value from the user's data, which will be sent to the function + defined here. If NEEDS_RESPONSE is set, the Plugin will extract + its value from the HTTP response instead. + + """ + + # Plugin flags + NEEDS_USERDATA = 0x01 + NEEDS_RESPONSE = 0x02 + DEPENDS_ON_OTHER_PLUGINS = 0x04 + + def __init__( + self, + name: str, + function: Optional[Callable[..., Optional[str]]] = None, + flags: int = 0, + value: Optional[str] = None, + ) -> None: + """Initializes a Plugin object. + + Creates a Plugin object, holding a "function" defining how to + extract the "value". + + Args: + name: + A string with the unique identifier of the Plugin. + function: + A Callable function that will be used to extract the + Plugin's value. + value: + A string with the extracted value from the Plugin. + flags: + An integer containing the flags that define the Plugin's + behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is + supported. If NEEDS_USERDATA is set, the plugin will get its + value from the user's data, which will be sent to the function + defined here. If NEEDS_RESPONSE is set, the Plugin will extract + its value from the HTTP response instead. + + """ + self.name = name + self.plugins: List["Plugin"] = [] + self.value: Optional[str] = value + self.flags = flags + + self.function: Callable[..., Optional[str]] + + if (flags & Plugin.NEEDS_USERDATA) and not function: + self.function = self.extract_from_userdata + elif not function: + self.function = self.return_value + else: + self.function = function + + def get_value( + self, + userdata: Dict[str, str], + ) -> Optional[str]: + """Gets the value from the Plugin. + + Depending on the Plugin's flags, extract and return its value. + + Args: + userdata: + A dictionary with the user specific data. + """ + if not self.needs_response: + if self.needs_userdata: + self.value = self.function(userdata) + elif self.depends_on_other_plugins: + for item in self.plugins: + item.get_value(userdata) + self.value = self.function() + else: + self.value = self.function() + return self.value + + def extract_value_from_response( + self, + response: Optional[requests.models.Response], + ) -> None: + """Extracts the value of the Plugin from the HTTP response. + + If NEEDS_RESPONSE flag is set, the Plugin will extract its value + upon receiving the HTTP response, and store it inside the "value" + attribute. + + Args: + response: + An requests.models.Response object with the HTTP response. + + """ + output = self.function(response) + if output: + self.value = output + logging.debug( + "Found ouput %s = %s", + self.name, + self.value, + ) + else: + logging.warning("Couldn't extract output: %s", str(self.name)) + + def extract_from_userdata( + self, data: Dict[str, str] = None + ) -> Optional[str]: + """Extracts the plugin value from userdata. + + Given a dictionary with the userdata, return its value with the + same name as the "name" attribute from this Plugin. + + Args: + data: + A dictionary with user specific data. + + Returns: + A string with the value of the variable found. None if no such + variable has been defined. + + """ + if data and self.name in data: + self.value = data[self.name] + return self.value + + def return_value(self) -> Optional[str]: + """Just return plugin's value. + + This is used when needing a function just to return the value. + + """ + return self.value + + @property + def needs_userdata(self) -> bool: + """Returns True if the NEEDS_USERDATA flag is set.""" + return bool(self.flags & self.NEEDS_USERDATA) + + @property + def needs_response(self) -> bool: + """Returns True if the NEEDS_RESPONSE flag is set.""" + return bool(self.flags & self.NEEDS_RESPONSE) + + @property + def depends_on_other_plugins(self) -> bool: + """Returns True if the DEPENDS_ON_OTHER_PLUGINS flag is set.""" + return bool(self.flags & self.DEPENDS_ON_OTHER_PLUGINS) + + +class Parser(Plugin): + """Plugins that parse other plugins.""" + + def __init__( + self, + name: str, + function: Callable[[], Optional[str]], + value: str = None, + ) -> None: + """Initializes the Parser plugin.""" + super().__init__( + name=name, + value=value, + function=function, + flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, + ) + + +class Empty(Plugin): + """Empty plugin to use for fuzzing new data.""" + + def __init__(self, name: str): + """Initialize Empty plugin.""" + super().__init__( + name=name, + flags=0, + ) diff --git a/raider/plugins/modifiers.py b/raider/plugins/modifiers.py new file mode 100644 index 0000000..51833e4 --- /dev/null +++ b/raider/plugins/modifiers.py @@ -0,0 +1,164 @@ +# Copyright (C) 2021 DigeeX +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Plugins that modify other plugins. +""" + +from functools import partial +from typing import Callable, Optional, Union + +from raider.plugins.common import Plugin + + +class Alter(Plugin): + """Plugin used to alter other plugin's value. + + If the value extracted from other plugins cannot be used in it's raw + form and needs to be somehow processed, Alter plugin can be used to + do that. Initialize it with the original plugin and a function which + will process the string and return the modified value. + + Attributes: + alter_function: + A function which will be given the plugin's value. It should + return a string with the processed value. + + """ + + def __init__( + self, + parent_plugin: Plugin, + alter_function: Optional[Callable[[str], Optional[str]]] = None, + ) -> None: + """Initializes the Alter Plugin. + + Given the original plugin, and a function to alter the data, + initialize the object, and get the modified value. + + Args: + plugin: + The original Plugin where the value is to be found. + alter_function: + The Function with instructions on how to alter the value. + """ + super().__init__( + name=parent_plugin.name, + value=parent_plugin.value, + flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, + function=self.process_value, + ) + self.plugins = [parent_plugin] + self.alter_function = alter_function + + def process_value(self) -> Optional[str]: + """Process the original plugin's value. + + Gives the original plugin's value to ``alter_function``. Return + the processed value and store it in self.value. + + Returns: + A string with the processed value. + + """ + if self.plugins[0].value: + if self.alter_function: + self.value = self.alter_function(self.plugins[0].value) + else: + self.value = None + + return self.value + + @classmethod + def prepend(cls, parent_plugin: Plugin, string: str) -> "Alter": + """Prepend a string to plugin's value.""" + alter = cls( + parent_plugin=parent_plugin, + alter_function=lambda value: string + value, + ) + + return alter + + @classmethod + def append(cls, parent_plugin: Plugin, string: str) -> "Alter": + """Append a string after the plugin's value""" + alter = cls( + parent_plugin=parent_plugin, + alter_function=lambda value: value + string, + ) + + return alter + + @classmethod + def replace( + cls, + parent_plugin: Plugin, + old_value: str, + new_value: Union[str, Plugin], + ) -> "Alter": + """Replace a substring from plugin's value with something else.""" + + def replace_old_value( + value: str, old: str, new: Union[str, Plugin] + ) -> Optional[str]: + """Replaces an old substring with the new one.""" + if isinstance(new, Plugin): + if not new.value: + return None + return value.replace(old, new.value) + return value.replace(old, new) + + alter = cls( + parent_plugin=parent_plugin, + alter_function=partial( + replace_old_value, old=old_value, new=new_value + ), + ) + + if isinstance(new_value, Plugin): + alter.plugins.append(new_value) + + return alter + + +class Combine(Plugin): + """Plugin to combine the values of other plugins.""" + + def __init__(self, *args: Union[str, Plugin]): + """Initialize Combine object.""" + self.args = args + name = str(sum(hash(item) for item in args)) + super().__init__( + name=name, + flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, + function=self.concatenate_values, + ) + self.plugins = [] + for item in args: + if isinstance(item, Plugin): + self.plugins.append(item) + + def concatenate_values(self) -> str: + """Concatenate the provided values. + + This function will concatenate the arguments values. Accepts + both strings and plugins. + + """ + combined = "" + for item in self.args: + if isinstance(item, str): + combined += item + elif item.value: + combined += item.value + return combined diff --git a/raider/parsers.py b/raider/plugins/parsers.py similarity index 81% rename from raider/parsers.py rename to raider/plugins/parsers.py index 5c5acd1..6def104 100644 --- a/raider/parsers.py +++ b/raider/plugins/parsers.py @@ -16,28 +16,10 @@ """Plugins used to parse data. """ -from typing import Callable, Optional, Union +from typing import Optional, Union from urllib.parse import parse_qs, urlsplit -from raider.plugins import Plugin - - -class Parser(Plugin): - """Plugins that parse other plugins.""" - - def __init__( - self, - name: str, - function: Callable[[], Optional[str]], - value: str = None, - ) -> None: - """Initializes the Parser plugin.""" - super().__init__( - name=name, - value=value, - function=function, - flags=Plugin.DEPENDS_ON_OTHER_PLUGINS, - ) +from raider.plugins.common import Parser, Plugin class UrlParser(Parser): diff --git a/raider/raider.py b/raider/raider.py index eaefa83..748db12 100644 --- a/raider/raider.py +++ b/raider/raider.py @@ -24,7 +24,7 @@ from raider.attacks import Fuzz from raider.authentication import Authentication from raider.config import Config -from raider.plugins import Plugin +from raider.plugins.common import Plugin from raider.user import User diff --git a/raider/request.py b/raider/request.py index d3e8537..249ec49 100644 --- a/raider/request.py +++ b/raider/request.py @@ -28,7 +28,8 @@ from urllib3.exceptions import InsecureRequestWarning from raider.config import Config -from raider.plugins import Cookie, Header, Plugin +from raider.plugins.basic import Cookie, Header +from raider.plugins.common import Plugin from raider.structures import CookieStore, DataStore, HeaderStore from raider.user import User diff --git a/raider/structures.py b/raider/structures.py index c92fe2a..b5702bf 100644 --- a/raider/structures.py +++ b/raider/structures.py @@ -18,7 +18,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple -from raider.plugins import Cookie, Header +from raider.plugins.basic import Cookie, Header class DataStore: diff --git a/raider/user.py b/raider/user.py index 140ffef..f9cb697 100644 --- a/raider/user.py +++ b/raider/user.py @@ -21,7 +21,8 @@ import hy -from raider.plugins import Cookie, Header, Plugin +from raider.plugins.basic import Cookie, Header +from raider.plugins.common import Plugin from raider.structures import CookieStore, DataStore, HeaderStore from raider.utils import hy_dict_to_python diff --git a/raider/utils.py b/raider/utils.py index 4de0bc9..f33cc22 100644 --- a/raider/utils.py +++ b/raider/utils.py @@ -131,22 +131,20 @@ def import_raider_objects() -> Dict[str, Any]: """ hy_imports = { - "plugins": ( - "Variable " - "Prompt " + "plugins.common": ("Empty " "Plugin " "Parser "), + "plugins.basic": ( "Regex " "Html " "Json " + "Variable " + "Command " + "Prompt " "Cookie " "Header " - "Command " - "Plugin " - "Alter " - "Combine " - "Empty " ), + "plugins.modifiers": ("Alter " "Combine "), + "plugins.parsers": ("Parser " "UrlParser "), "flow": "Flow", - "parsers": "UrlParser", "request": "Request PostBody Template", "operations": ( "Http " "Grep " "Print " "Error " "NextStage " "Operation " "Save " From 23ed1fcd3c474bdd3ff49a2c6639f09e2f26acb1 Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Mon, 16 Aug 2021 13:03:50 +0200 Subject: [PATCH 4/8] Add note that documentation is incomplete. --- docs/index.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index a2cab4d..204f390 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,15 @@ Welcome to Raider's documentation! ================================== +.. note:: + Since this started getting some traffic, I have to mention, this + documentation is still work in progress. Many things are not + finished, stuff is missing, other stuff is not working as expected, + and so on... In the near future I'll start a user forum where you + can ask questions. Meanwhile, read the source code to understand + better how Raider works and open Github issues if you find some + mistakes. + **Raider** is a framework designed to test :term:`authentication` for web applications. While web proxies like `ZAProxy From 299c818ff3066c99adb022c6cd72852145694f76 Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Mon, 16 Aug 2021 19:23:06 +0200 Subject: [PATCH 5/8] Update README with a note --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 36e1a77..c3f0cb9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ behaviour. **Raider** aims to make testing easier, by providing the interface to interact with all important elements found in modern authentication systems. +**Note:** + +Raider is still a work in progress. Bugs and missing features are to +be expected. If you find something that doesn't work as expected, open +a Github issue and let us know. You can also [join the community +forum](https://community.digeex.de/) and start asking questions there. + + # Features **Raider** has the goal to support most of the modern authentication From a224d3d548c85a3ed6c0f8789440d7eae005b5f8 Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Fri, 20 Aug 2021 18:58:44 +0200 Subject: [PATCH 6/8] Link to community forum in the documentation --- docs/index.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 204f390..3031217 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,13 +2,14 @@ Welcome to Raider's documentation! ================================== .. note:: - Since this started getting some traffic, I have to mention, this - documentation is still work in progress. Many things are not - finished, stuff is missing, other stuff is not working as expected, - and so on... In the near future I'll start a user forum where you - can ask questions. Meanwhile, read the source code to understand - better how Raider works and open Github issues if you find some - mistakes. + This documentation and the entire Raider framework is still work in + progress. Many things are not finished, stuff is missing, other stuff + is not working as expected, and so on... Meanwhile, `read the source + code `_ to understand better how + Raider works, `open Github issues + `_ if you find some + mistakes, or come `talk to us in the community forum + `_. **Raider** is a framework designed to test :term:`authentication` for From d1f38a2ace3f984b45ea6002acc9bf68364b18f2 Mon Sep 17 00:00:00 2001 From: Daniel Neagaru Date: Mon, 23 Aug 2021 17:26:21 +0200 Subject: [PATCH 7/8] Update documentation with plugin subtypes --- docs/dev/plugins.rst | 113 +++++-- docs/index.rst | 2 +- docs/user/definitions.rst | 2 +- docs/user/faq.rst | 5 +- docs/user/tutorial.rst | 620 -------------------------------------- docs/user/tutorials.rst | 8 + 6 files changed, 97 insertions(+), 653 deletions(-) delete mode 100644 docs/user/tutorial.rst create mode 100644 docs/user/tutorials.rst diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 7f2ef9a..135b723 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -1,5 +1,5 @@ .. _plugins: -.. module:: raider.plugins +.. module:: raider.plugins.common Plugins ======= @@ -11,10 +11,34 @@ between :ref:`Flows `. Below there's a list of predefined Plugins. The users are also encouraged to write their own plugins. +Common +------ + +Plugin +++++++ + +.. autoclass:: Plugin + +Parser +++++++ + +.. autoclass:: Parser + +Empty ++++++ + +.. autoclass:: Empty + + +.. module:: raider.plugins.basic + +Basic +----- + .. _plugin_variable: Variable --------- +++++++++ Use this when the value of the plugin should be extracted from the user data. At the moment only ``username`` and ``password`` are @@ -33,7 +57,7 @@ Example: .. _plugin_prompt: Prompt ------- +++++++ Prompt plugin should be used when the information is not known in advance, for example when receiving the SMS code. @@ -49,27 +73,10 @@ Example: .. _plugin_command: -Command -------- - -Use Command plugin if you want to extract information using a shell -command. - -Example: - -.. code-block:: hylang - - (setv mfa_code (Command - :name "otp" - :command "pass otp personal/app1")) - -.. autoclass:: Command - :members: - .. _plugin_cookie: Cookie ------- +++++++ Use Cookie plugin to extract and set new cookies: @@ -86,7 +93,7 @@ Example: .. _plugin_header: Header ------- +++++++ Use Header plugin to extract and set new headers. It also allows easier setup for basic and bearer authentication using the provided @@ -112,10 +119,29 @@ Example: .. autoclass:: Header :members: + +Command ++++++++ + +Use Command plugin if you want to extract information using a shell +command. + +Example: + +.. code-block:: hylang + + (setv mfa_code (Command + :name "otp" + :command "pass otp personal/app1")) + +.. autoclass:: Command + :members: + + .. _plugin_regex: Regex ------ ++++++ Use Regex plugin if the data you want extracted can be easily identified with a regular expression. The string matched in between @@ -138,7 +164,7 @@ Example: .. _plugin_html: Html ----- +++++ Use the Html plugin when the data you want can be easily extracted by parsing HTML tags. Create a new plugin by giving it a name, the tag @@ -168,19 +194,45 @@ Example: .. _plugin_json: Json ----- +++++ .. autoclass:: Json :members: -.. _plugin_api: +.. module:: raider.plugins.modifiers -Plugin API ----------- +Modifiers +--------- -.. autoclass:: Plugin +Alter ++++++ + +.. autoclass:: Alter + :members: + +Combine ++++++++ + +.. autoclass:: Combine + :members: + + + +.. module:: raider.plugins.parsers + +Parsers +------- +UrlParser ++++++++++ + +.. autoclass:: UrlParser + :members: + + + +.. _plugin_api: Writing custom plugins ---------------------- @@ -240,3 +292,6 @@ And we can create a new variable that will use this class: Now whenever we use the ``mfa_code`` in our requests, its value will be extracted from the password store. + + + diff --git a/docs/index.rst b/docs/index.rst index 3031217..ae20176 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -97,7 +97,7 @@ User guide user/install user/architecture - user/tutorial + user/tutorials user/definitions user/faq diff --git a/docs/user/definitions.rst b/docs/user/definitions.rst index cb7d900..9667885 100644 --- a/docs/user/definitions.rst +++ b/docs/user/definitions.rst @@ -87,7 +87,7 @@ Definitions A piece of code that can be used to generate inputs for outgoing HTTP :term:`Requests `, and/or extract outputs from incoming term:`Responses `. All plugins inherit from - :class:`Plugin ` class. + :class:`Plugin ` class. When used inside a :term:`Request `, Plugins acts as input and replace themselves with the actual value. diff --git a/docs/user/faq.rst b/docs/user/faq.rst index f1a7d7a..cfd881b 100644 --- a/docs/user/faq.rst +++ b/docs/user/faq.rst @@ -24,8 +24,9 @@ files for each new project. Those files contain information describing the authentication process. **Raider** evaluates them, and gives you back a Python object to interact with the application. -Read the :ref:`Architecture ` and :ref:`Tutorial -` pages for more information and examples. +Read the :ref:`Architecture ` and `Tutorials +`_ for more information and +examples. .. _faq_eval: diff --git a/docs/user/tutorial.rst b/docs/user/tutorial.rst deleted file mode 100644 index 8331de1..0000000 --- a/docs/user/tutorial.rst +++ /dev/null @@ -1,620 +0,0 @@ -.. _tutorial: - -Tutorial -======== - -Preparation ------------ - -Before you can use **Raider**, you have to set up the -:term:`authentication` inside :term:`hyfiles`. To do that, you'll -probably need to use a web proxy (`BurpSuite -`_, `ZAProxy -`_, `mitmproxy `_, -etc...) to see the :term:`requests ` the application is -generating, and identify all the important inputs and outputs for each -request. - -After the traffic was captured, there will probably be lots of HTTP -requests that are irrelevant to the authentication. Start by removing -all static files (.png, .js, .pdf, etc...). When you're left with a -fewer requests to deal with, it's time to dive deeper and understand -how the authentication works. - -At this point we assume *you already know* the basics of Python and -Hylang so this documentation will not cover information that can be -found somewhere else. - -This tutorial will show the authentication in use by Reddit at the -time of writing this. It could be different in the future when you're -reading this, if they update the way authentication works or change -the HTML structure, so you will have to do this all by yourself -anyways. - -The easiest way to start this is by going backwards starting with one -authenticated request. This should be some kind of request that only -works when the user is already authenticated. I choose the -"unread_message_count" one for reddit, and the request looks like -this: - -.. code-block:: - - GET https://s.reddit.com/api/v1/sendbird/unread_message_count HTTP/1.1 - User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0 - Accept: application/json - Accept-Language: en-US,en;q=0.5 - Content-Type: application/json - Origin: https://www.reddit.com - DNT: 1 - Authorization: Bearer [REDACTED TOKEN] - Referer: https://www.reddit.com/ - Connection: keep-alive - Host: s.reddit.com - - -As you can see from this, the only information we sent to this URL -from our authentication is the Bearer token. - -We define a new :term:`Flow` that will check for the unread messages -in hy: - -.. code-block:: hylang - - (setv get_unread_messages - (Flow - :name "get_unread_messages" - :request (Request - :method "GET" - :headers [(Header.bearerauth access_token)] - :url "https://s.reddit.com/api/v1/sendbird/unread_message_count"))) - - -In Hy, ``setv`` is used to set up new variables. Here we created the -variable ``get_unread_messages`` that will hold the information about -this Flow. This will be hold in the :ref:`_functions special variable -` which stores the Flows which aren't affecting the -authentication. - -The only required parameters for :class:`Flow ` -objects are the name and the request. The name is a string that is used -for reference purposes, and the request contains the actual HTTP request -definition as a :class:`Request ` object. - -The Request object requires only the method and url. Other parameters -are optional. We translate the original request into **Raider** config -format, and to use the access token we need to define it in the request -header. Since this is a bearer header, we use :class:`Header.bearerauth -` with the ``access_token`` which we will create -later on. - - -Getting the access token ------------------------- - -The next step would be to find out where is this token generated and -how we can extract it. Searching for this token in previous responses, -we can see it was first seen in a request to the main reddit -page. It's located inside the `