# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
"""Basic plugins.
"""
import json
import logging
import os
import re
from base64 import b64encode
from functools import partial
from typing import Callable, Dict, Optional, Union
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
[docs]class Variable(Plugin):
""":class:`Plugin <raider.plugins.common.Plugin>` to extract data
from the :class:`User <raider.user.User>`
Use this when the ``value`` of the plugin should be extracted from the
user data. ``username`` and ``password`` are mandatory and can be
accessed with ``(Variable "username")`` and ``(Variable
"password")`` respectively. Other data can be accessed similarly.
Attributes:
name:
A string used as an identifier for the :class:`Variable`
:class:`Plugin <raider.plugins.common.Plugin>`
"""
[docs] def __init__(self, name: str) -> None:
"""Initializes the :class:`Variable` :class:`Plugin
<raider.plugins.common.Plugin>`.
Creates a :class:`Variable` object that will return the data
from the :class:`User <raider.user.User>` object.
Args:
name:
A String with the name of the variable.
"""
super().__init__(
name=name,
function=lambda data: data[self.name],
flags=Plugin.NEEDS_USERDATA,
)
[docs]class Prompt(Plugin):
""":class:`Plugin <raider.plugins.common.Plugin>` to prompt the
user for some data.
Use this :class:`Plugin <raider.plugins.common.Plugin>` when the ``value``
cannot be known in advance, for example when asking for
:term:`multi-factor authentication (MFA)` code that is going to be
sent over SMS or E-mail.
Attributes:
name:
A String used both as an identifier for this :class:`Prompt`
:class:`Plugin <raider.plugins.common.Plugin>` and as a prompt
message on the terminal.
"""
[docs] def __init__(self, name: str) -> None:
"""Initializes the :class:`Prompt` :class:`Plugin
<raider.plugins.common.Plugin>`.
Creates a :class:`Prompt` :class:`Plugin
<raider.plugins.common.Plugin>` which will ask the user's
input to get the :class:`Plugin's
<raider.plugins.common.Plugin>` ``value``.
Args:
name:
A String containing the prompt asking the user for input.
"""
super().__init__(name=name, function=self.get_user_prompt)
[docs] def get_user_prompt(self) -> str:
"""Gets the ``value`` from user input.
Creates a prompt asking the user for input and stores the ``value``
in the Plugin.
Returns:
A string with the input received from the user.
"""
self.value = None
while not self.value:
print("Please provide the input value")
self.value = input(self.name + " = ")
return self.value
[docs]class Cookie(Plugin):
""":class:`Plugin <raider.plugins.common.Plugin>` dealing with the
:class:`Cookies <raider.plugins.basic.Cookie>` in HTTP
:term:`Requests <Request>` and :term:`Responses <Response>`.
Use the :class:`Cookie` :class:`Plugin` when working with the data
found in :class:`Cookie` headers.
Attributes:
name:
A String with the :class:`Cookie's <Cookie>` name. Also used
as an identifier for the :class:`Plugin
<raider.plugins.common.Plugin>`
function:
A Callable which will be called to extract the ``value`` of
the :class:`Cookie` when used as an input in a :ref:`Flow
<flows>`. The function should set ``self.value`` and also
return it.
name_function:
A Callable which will be called to extract the ``name`` of the
:class:`Cookie` when it's not known in advance and the flag
``NAME_NOT_KNOWN_IN_ADVANCE`` is set.
plugins:
A List of :class:`Plugins <Plugin>` whose value needs to be
extracted first before current :class:`Cookie's <Cookie>`
value can be extracted. Used when the flag
``DEPENDS_ON_OTHER_PLUGINS`` is set.
value:
A string containing the :class:`Cookie's <Cookie>` output
``value`` to be used as input in the HTTP :term:`Requests
<Request>`.
flags:
An integer containing the flags that define the
:class:`Plugin's <raider.plugins.common.Plugin>` behaviour.
"""
[docs] def __init__(
self,
name: str,
value: Optional[str] = None,
function: Optional[Callable[..., Optional[str]]] = None,
flags: int = Plugin.NEEDS_RESPONSE,
) -> None:
"""Initializes the :class:`Cookie` :class:`Plugin
<raider.plugins.common.Plugin>`.
Creates a :class:`Cookie` :class:`Plugin
<raider.plugins.common.Plugin>`, either with predefined
``value``, or by using a ``function`` defining how the
``value`` should be generated on runtime.
Args:
name:
A String with the name of the :class:`Cookie`.
value:
An Optional String with the ``value`` of the
:class:`Cookie` in case it's already known.
function:
A Callable which is used to get the ``value`` of the
Cookie on runtime.
flags:
An integer containing the flags that define the
:class:`Plugin's <raider.plugins.common.Plugin>`
behaviour. By default only ``NEEDS_RESPONSE`` flag is set.
"""
if not function:
if flags & Plugin.NEEDS_RESPONSE:
super().__init__(
name=name,
function=self.extract_from_response,
value=value,
flags=flags,
)
else:
super().__init__(
name=name,
value=value,
flags=flags,
)
else:
super().__init__(
name=name, function=function, value=value, flags=flags
)
[docs] def __str__(self) -> str:
"""Returns a string representation of the cookie."""
return str({self.name: self.value})
[docs] @classmethod
def regex(cls, regex: str) -> "Cookie":
"""Extracts the :class:`Cookie` using regular expressions.
When the name of the :class:`Cookie` is unknown in advance,
but can be matched against a regular expression, you can use
``Cookie.regex`` to extract it. The ``name`` of the
:class:`Cookie` should be supplied as a regular expression
inside a group, i.e. between ``(`` and ``)``.
For example the following code will match the :class:`Cookie`
whose name is a 10 character string containing letters and
digits:
.. code-block:: hylang
(setv csrf_token
(Cookie.regex "([a-zA-Z0-9]{10})"))
Args:
regex:
A String with the regular expression to match the name of
the :class:`Cookie`.
Returns:
A :class:`Cookie` object configured to match the regular
expression.
"""
def extract_cookie_value_regex(
response: requests.models.Response,
regex: str,
) -> Optional["str"]:
"""Finds the :class:`Cookie` ``value`` matching the given
``regex``.
Args:
response:
A :class:`requests.models.Response` object with the
HTTP :term:`Response`.
regex:
A String containing the regular expression to match for.
Returns:
An Optional String with the ``value`` extracted from the
:class:`Cookie` matching the ``name`` with the supplied
``regex``.
"""
for name, value in response.cookies.items():
if re.search(regex, name):
return value
return None
def extract_cookie_name_regex(
response: requests.models.Response,
regex: str,
) -> Optional["str"]:
"""Find the :class:`Cookie` ``name`` matching the given
``regex``.
Args:
response:
A :class:`requests.models.Response` object with the
HTTP :term:`Response`.
regex:
A String containing the regular expression to match for.
Returns:
An Optional String with the ``name`` extracted from the
:class:`Cookie` matching the ``name`` with the supplied
``regex``.
"""
for name in response.cookies.keys():
if re.search(regex, name):
return name
return None
cookie = cls(
name=regex,
function=partial(extract_cookie_value_regex, regex=regex),
flags=Plugin.NEEDS_RESPONSE | Plugin.NAME_NOT_KNOWN_IN_ADVANCE,
)
cookie.name_function = partial(extract_cookie_name_regex, regex=regex)
return cookie
[docs] @classmethod
def from_plugin(cls, parent_plugin: Plugin, name: str) -> "Cookie":
"""Creates a :class:`Cookie` from another :class:`Plugin
<raider.plugins.common.Plugin>`.
Given another :class:`Plugin <raider.plugins.common.Plugin>`,
and a ``name``, create a :class:`Cookie
<raider.plugins.basic.Cookie>`.
For example, if we need to extract the ``access_token`` from JSON:
.. code-block:: hylang
(setv access_token
(Json
:name "access_token"
:extract "token"))
And use it later as a :class:`Cookie`:
.. code-block:: hylang
(setv my_function
(Flow
:name "my_function"
:request (Request
:method "GET"
:url "https://www.example.com/my_function"
;; Sends the cookie `mycookie` with the value of
;; `access_token` extracted from JSON.
:cookies [(Cookie.from_plugin access_token "mycookie" )])))
Args:
name:
The :class:`Cookie` ``name`` to use.
plugin:
The :class:`Plugin <raider.plugins.common.Plugin>` which
will contain the ``value`` we need.
Returns:
A :class:`Cookie` object with the ``name`` and the
:class:`Plugin's <raider.plugins.common.Plugin>` ``value``.
"""
cookie = cls(
name=name,
value=parent_plugin.value,
flags=Plugin.DEPENDS_ON_OTHER_PLUGINS,
)
return cookie
[docs]class Regex(Plugin):
"""Plugin class to extract regular expressions.
This plugin will match the regex provided, and extract the ``value``
inside the first matched group. A group is the string that matched
inside the brackets.
For example if the regular expression is:
"accessToken":"([^"]+)"
and the text to match it against contains:
"accessToken":"0123456789abcdef"
then only the string "0123456789abcdef" will be extracted and saved
in the ``value`` attribute.
Attributes:
name:
A string used as an identifier for the :class:`Regex`.
regex:
A string containing the regular expression to be matched.
"""
[docs] def __init__(
self,
name: str,
regex: str,
function: Callable[[str], Optional[str]] = None,
flags: int = Plugin.NEEDS_RESPONSE,
) -> None:
"""Initializes the Regex Plugin.
Creates a Regex Plugin with the given regular expression, and
extracts the matched group given in the "extract" argument, or
the first matching group if not specified.
Args:
name:
A string with the name of the Plugin.
regex:
A string containing the regular expression to be matched.
"""
if not function:
super().__init__(
name=name,
function=self.extract_regex_from_response,
flags=flags,
)
else:
super().__init__(
name=name,
function=function,
flags=flags,
)
self.regex = regex
[docs] @classmethod
def from_plugin(cls, parent_plugin: Plugin, regex: str) -> "Regex":
"""Extracts Regex from another plugin's ``value``."""
regex_plugin = cls(
name=regex,
regex=regex,
flags=Plugin.DEPENDS_ON_OTHER_PLUGINS,
)
regex_plugin.plugins = [parent_plugin]
regex_plugin.function = regex_plugin.extract_regex_from_plugin
return regex_plugin
[docs] def __str__(self) -> str:
"""Returns a string representation of the Plugin."""
return "Regex:" + self.regex
[docs]class Html(Plugin):
"""
This Plugin will find the HTML "tag" containing the specified
"attributes" and store the "extract" attribute of the matched tag
in its ``value`` attribute.
Attributes:
tag:
A string defining the HTML tag to look for.
attributes:
A dictionary with attributes matching the desired HTML tag. The
keys in the dictionary are strings matching the tag's attributes,
and the ``value``s are treated as regular expressions, to help
match tags that don't have a static ``value``.
extract:
A string defining the HTML tag's attribute that needs to be
extracted and stored inside ``value``.
"""
[docs] def __init__(
self,
name: str,
tag: str,
attributes: Dict[hy.models.Keyword, str],
extract: str,
) -> None:
"""Initializes the Html Plugin.
Creates a Html Plugin with the given "tag" and
"attributes". Stores the "extract" attribute in the plugin's
``value``.
Args:
name:
A string with the name of the Plugin.
tag:
A string with the HTML tag to look for.
attributes:
A hy dictionary with the attributes to look inside HTML
tags. The ``value``s of dictionary elements are treated as
regular expressions.
extract:
A string with the HTML tag attribute that needs to be
extracted and stored in the Plugin's object.
"""
super().__init__(
name=name,
function=self.extract_html_tag,
flags=Plugin.NEEDS_RESPONSE,
)
self.tag = tag
self.attributes = hy_dict_to_python(attributes)
self.extract = extract
[docs] def __str__(self) -> str:
"""Returns a string representation of the Plugin."""
return (
"Html:"
+ self.tag
+ ":"
+ str(self.attributes)
+ ":"
+ str(self.extract)
)
[docs]class Json(Plugin):
"""
The "extract" attribute is used to specify which field to store in
the ``value``. Using the dot ``.`` character you can go deeper inside
the JSON object. To look inside an array, use square brackets
`[]`.
Keys with special characters should be written inside double quotes
``"``. Keep in mind that when written inside ``hyfiles``,
it'll already be between double quotes, so you'll have to escape
them with the backslash character ``\\``.
Examples:
``env.production[0].field``
``production.keys[1].x5c[0][1][0]."with space"[3]``
Attributes:
extract:
A string defining the location of the field that needs to be
extracted. For now this is still quite primitive, and cannot
access data from JSON arrays.
"""
[docs] def __init__(
self,
name: str,
extract: str,
function: Callable[[str], Optional[str]] = None,
flags: int = Plugin.NEEDS_RESPONSE,
) -> None:
"""Initializes the Json Plugin.
Creates the Json Plugin and extracts the specified field.
Args:
name:
A string with the name of the Plugin.
extract:
A string with the location of the JSON field to extract.
"""
if not function:
super().__init__(
name=name,
function=self.extract_json_from_response,
flags=flags,
)
else:
super().__init__(
name=name,
function=function,
flags=flags,
)
self.extract = extract
[docs] @classmethod
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 = [parent_plugin]
json_plugin.function = json_plugin.extract_json_from_plugin
return json_plugin
[docs] def __str__(self) -> str:
"""Returns a string representation of the Plugin."""
return "Json:" + str(self.extract)
[docs]class Command(Plugin):
"""
Use this to run a shell command and extract the output.
"""
[docs] def __init__(self, name: str, command: str) -> None:
"""Initializes the Command Plugin.
The specified command will be executed with os.popen() and the
output with the stripped last newline, will be saved inside the
``value``.
Args:
name:
A unique identifier for the plugin.
command:
The command to be executed.
"""
self.command = command
super().__init__(
name=name,
function=self.run_command,
)
[docs] def run_command(self) -> Optional[str]:
"""Runs a command and returns its ``value``.
Given a dictionary with the predefined variables, return the
``value`` of the with the same name as the "name" attribute from
this Plugin.
Args:
data:
A dictionary with the predefined variables.
Returns:
A string with the ``value`` of the variable found. None if no such
variable has been defined.
"""
self.value = os.popen(self.command).read().strip()
return self.value
[docs]class File(Plugin):
"""
Use this plugin to manipulate files.
"""
[docs] def __init__(
self,
path: str,
function: Callable[..., Optional[Union[str, bytes]]] = None,
flags: int = 0,
) -> None:
"""Initializes the File Plugin.
Creates a File Plugin which will set its ``value`` to the contents
of the file.
Args:
path:
A string containing the file path.
"""
self.path = path
if not function:
super().__init__(name=path, function=self.read_file, flags=flags)
else:
super().__init__(name=path, function=function, flags=flags)
[docs] def read_file(self) -> bytes:
"""Sets the plugin's ``value`` to the file content."""
with open(self.path, "rb") as finput:
self.value = finput.read()
return self.value
[docs] @classmethod
def replace(
cls, path: str, old_value: str, new_value: Union[str, int, Plugin]
) -> "File":
"""Read a file and replace strings with new ``value``s."""
def replace_string(
original: bytes, old: str, new: Union[str, int, Plugin]
) -> Optional[bytes]:
if isinstance(new, Plugin):
if not new.value:
return None
return original.replace(
old.encode("utf-8"), new.value.encode("utf-8")
)
return original.replace(
old.encode("utf-8"), str(new).encode("utf-8")
)
with open(path, "rb") as finput:
file_contents = finput.read()
file_replace_plugin = cls(
path=path,
function=partial(
replace_string,
original=file_contents,
old=old_value,
new=new_value,
),
flags=Plugin.DEPENDS_ON_OTHER_PLUGINS,
)
if isinstance(new_value, Plugin):
file_replace_plugin.plugins.append(new_value)
return file_replace_plugin