burp-ui/burpui/misc/backend/multi.py
2023-03-19 15:13:01 +01:00

661 lines
23 KiB
Python

# -*- coding: utf8 -*-
import errno
import json
import re
import socket
import struct
from werkzeug.datastructures import ImmutableMultiDict as _ImmutableMultiDict
from ..._compat import pickle, to_bytes, to_unicode
from ...datastructures import ImmutableMultiDict
from ...decorators import implement
from ...exceptions import BUIserverException
from ..parser.interface import BUIparser
from .interface import BUIbackend
INTERFACE_METHODS = BUIbackend.__abstractmethods__
PARSER_INTERFACE_METHODS = BUIparser.__abstractmethods__
AGENT_VERSION_CAST = "0.4.9999"
class ProxyCall(object):
"""Class to dispatch call of unknown methods in order to dynamically
call agents one without maintaining the explicit list of methods.
"""
def __init__(self, proxy, method, network=False):
"""
:param proxy: Class to proxify
:type proxy: :class:`burpui.misc.backend.multi.Burp`
:param method: Name of the method to proxify
:type method: str
:param network: Is it a custom call over network
:type network: bool
"""
self.proxy = proxy
self.method = method
self.network = network
def __call__(self, *args, **kwargs):
"""This is where the proxy call (and the magic) occurs"""
# retrieve the original function prototype
proto = getattr(BUIbackend, self.method)
args_name = list(proto.__code__.co_varnames)
# skip self
args_name.pop(0)
# we transform unnamed arguments to named ones
# example:
# def my_function(toto, tata=None, titi=None):
#
# x = my_function('blah', titi='blih')
#
# => {'toto': 'blah', 'titi': 'blih'}
encoded_args = {}
for idx, opt in enumerate(args):
encoded_args[args_name[idx]] = opt
encoded_args.update(kwargs)
# Special case for network calls
if self.network:
data = {"func": self.method, "args": encoded_args}
return json.loads(self.proxy.do_command(data))
# normal case for "standard" interface
if "agent" not in encoded_args:
raise AttributeError(str(encoded_args))
agentName = encoded_args["agent"]
# we don't need this argument anymore
del encoded_args["agent"]
try:
agent = self.proxy.servers[agentName]
except KeyError:
# This exception should be forwarded to the final user
if not agentName:
msg = "You must provide an agent name"
else:
msg = "Agent '{}' not found".format(agentName)
raise BUIserverException(msg)
return getattr(agent, self.method)(**encoded_args)
class ProxyParserCall(object):
"""Class that actually calls the Parser method"""
def __init__(self, agent, method):
"""
:param agent: Agent to use
:type agent: :class:`burpui.misc.backend.multi.NClient`
:param method: Name of the method to proxify
:type method: str
"""
self.agent = agent
self.method = method
def __call__(self, *args, **kwargs):
"""This is where the proxy call (and the magic) occurs"""
# retrieve the original function prototype
proto = getattr(BUIparser, self.method)
args_name = list(proto.__code__.co_varnames)
# skip self
args_name.pop(0)
# we transform unnamed arguments to named ones
# example:
# def my_function(toto, tata=None, titi=None):
#
# x = my_function('blah', titi='blih')
#
# => {'toto': 'blah', 'titi': 'blih'}
encoded_args = {}
for idx, opt in enumerate(args):
encoded_args[args_name[idx]] = opt
encoded_args.update(kwargs)
data = {"func": "proxy_parser", "method": self.method, "args": encoded_args}
return json.loads(self.agent.do_command(data))
class ProxyParser(BUIparser):
"""Class to generate a "virtual" parser object"""
# These functions MUST be implemented because we inherit an abstract class.
# The hack here is to get the list of the functions and let the interpreter
# think we don't have to implement them.
# Thanks to this list, we know what function are implemented by our backend.
foreign = PARSER_INTERFACE_METHODS
BUIparser.__abstractmethods__ = frozenset()
def __init__(self, agent):
"""
:param agent: Agent to use
:type agent: :class:`burpui.misc.backend.multi.NClient`
"""
self.agent = agent
def __getattribute__(self, name):
# always return this value because we need it and if we don't do that
# we'll end up with an infinite loop
if name == "foreign":
return object.__getattribute__(self, name)
# now we can retrieve the 'foreign' list and know if the object called
# needs to be "proxyfied"
if name in self.foreign:
return ProxyParserCall(self.agent, name)
return object.__getattribute__(self, name)
class Burp(BUIbackend):
"""The :class:`burpui.misc.backend.multi.Burp` class provides a consistent
backend to interact with ``agents``.
It is actually the *real* multi backend implementing the
:class:`burpui.misc.backend.interface.BUIbackend` class.
For each agent found in the configuration, it will load a
:class:`burpui.misc.backend.multi.NClient` class.
:param server: ``Burp-UI`` server instance in order to access logger
and/or some global settings
:type server: :class:`burpui.engines.server.BUIServer`
:param conf: Configuration file to use
:type conf: str
"""
# These functions MUST be implemented because we inherit an abstract class.
# The hack here is to get the list of the functions and let the interpreter
# think we don't have to implement them.
# Thanks to this list, we know what function are implemented by our backend.
foreign = INTERFACE_METHODS
BUIbackend.__abstractmethods__ = frozenset()
def __init__(self, server=None, conf=None):
"""
:param server: Application context
:type server: :class:`burpui.engines.server.BUIServer`
:param conf: Configuration
:type conf: :class:`burpui.config.BUIConfig`
"""
self.app = server
self.acl_handler = server.acl_handler
self.servers = {}
self.app.config["SERVERS"] = []
if conf:
for sect in conf.options.keys():
r = re.match("^Agent:(.+)$", sect)
if r:
host = conf.safe_get("host", section=sect)
port = conf.safe_get("port", "integer", section=sect) or 10000
password = conf.safe_get("password", section=sect)
ssl = conf.safe_get("ssl", "boolean", section=sect) or False
timeout = conf.safe_get("timeout", "integer", section=sect) or 5
self.servers[r.group(1)] = NClient(
self.app, host, port, password, ssl, timeout
)
self.app.config["SERVERS"].append(r.group(1))
if not self.servers:
self.logger.error("No agent configured!")
else:
self.logger.debug(self.servers)
def __getattribute__(self, name):
# always return this value because we need it and if we don't do that
# we'll end up with an infinite loop
if name == "foreign":
return object.__getattribute__(self, name)
# now we can retrieve the 'foreign' list and know if the object called
# needs to be "proxyfied"
if name in self.foreign:
proxy = True
func = None
try:
func = object.__getattribute__(self, name)
proxy = not getattr(func, "__ismethodimplemented__", False)
except:
pass
self.logger.debug("func: {} - {}".format(name, proxy))
if proxy:
return ProxyCall(self, name)
elif func:
return func
return object.__getattribute__(self, name)
@implement
def is_one_backup_running(self, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.is_one_backup_running`"""
res = []
if agent:
try:
res = self.servers[agent].is_one_backup_running(agent)
except BUIserverException:
pass
else:
res = {}
for name, serv in self.servers.items():
try:
res[name] = serv.is_one_backup_running()
except BUIserverException:
res[name] = []
return res
def _get_version(self, method=None):
"""get versions"""
if not method:
raise BUIserverException("Wrong method call")
res = {}
for name, serv in self.servers.items():
func = getattr(serv, method)
try:
res[name] = func()
except BUIserverException:
res[name] = "Unknown"
return res
def _get_statistics(self):
"""get statistics"""
res = {}
for name, serv in self.servers.items():
res[name] = serv.statistics()
return res
@implement
def get_parser(self, agent=None):
# Need to return a proxy object to interact with a remote parser
if not agent:
raise BUIserverException("No agent provided")
return ProxyParser(self.servers.get(agent))
@implement
def get_client_version(self, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_client_version`"""
if not agent:
return self._get_version("get_client_version")
return self.servers[agent].get_client_version()
@implement
def get_server_version(self, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_server_version`"""
if not agent:
return self._get_version("get_server_version")
return self.servers[agent].get_server_version()
@implement
def statistics(self, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.statistics"""
if not agent:
return self._get_statistics()
return self.servers[agent].statistics()
class Gsocket:
def __init__(self, host, port, ssl=False, timeout=5, notimeout=False):
self.host = host
self.port = port
self.ssl = ssl
self.timeout = timeout
self.notimeout = notimeout
def conn(self):
if self.ssl:
import ssl
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if not self.notimeout:
s.settimeout(self.timeout)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
ret = ssl.wrap_socket(
s, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_SSLv23
)
ret.connect((self.host, self.port))
else:
if not self.notimeout:
ret = socket.create_connection(
(self.host, self.port), timeout=self.timeout
)
else:
ret = socket.create_connection((self.host, self.port))
ret.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.sock = ret
self.connected = True
def __enter__(self):
self.conn()
return self.sock, self
def __exit__(self, type, value, traceback):
if self.connected:
self.sock.close()
self.connected = False
def recvall(self, length=1024):
"""Read the answer of the agent"""
buf = b""
bsize = 1024
received = 0
if length < bsize:
bsize = length
while received < length:
newbuf = self.sock.recv(bsize)
if not newbuf:
return None
buf += newbuf
received += len(newbuf)
return buf
class NClient(BUIbackend):
"""The :class:`burpui.misc.backend.multi.NClient` class provides a
consistent backend to interact with ``agents``.
It acts as a proxy so it works with any agent running a backend implementing
the :class:`burpui.misc.backend.interface.BUIbackend` class.
:param app: The application context
:type app: :class:`burpui.engines.server.BUIServer`
:param host: Address of the remote agent
:type host: str
:param port: Port of the remote agent
:type port: int
:param password: Secret between the agent and the burp-ui server
:type password: str
:param ssl: Use SSL to communicate with the agent
:type ssl: bool
"""
# These functions MUST be implemented because we inherit an abstract class.
# The hack here is to get the list of the functions and let the interpreter
# think we don't have to implement them.
# Thanks to this list, we know what function are implemented by our backend.
foreign = INTERFACE_METHODS
BUIbackend.__abstractmethods__ = frozenset()
def __init__(
self, app=None, host=None, port=None, password=None, ssl=None, timeout=5
):
self.host = host
self.port = port
self.password = password
self.ssl = ssl
self.app = app
self.timeout = timeout or 5
self._agent_version = None
def __getattribute__(self, name):
# always return this value because we need it and if we don't do that
# we'll end up with an infinite loop
if name == "foreign":
return object.__getattribute__(self, name)
# now we can retrieve the 'foreign' list and know if the object called
# needs a dynamic implementation
if name in self.foreign:
proxy = True
func = None
try:
func = object.__getattribute__(self, name)
proxy = not getattr(func, "__ismethodimplemented__", False)
except:
pass
self.logger.debug("func: {} - {}".format(name, proxy))
if proxy:
return ProxyCall(self, name, network=True)
elif func:
return func
return object.__getattribute__(self, name)
def _get_agent_version(self):
if self.ping() and not self._agent_version:
data = {"func": "agent_version"}
try:
vers = self.do_command(data)
self._agent_version = json.loads(to_unicode(vers))
except BUIserverException:
# just ignore the error if this custom function is not
# implemented
pass
return self._agent_version
def ping(self):
"""Check if we are connected to the agent"""
res = False
try:
with Gsocket(self.host, self.port, self.ssl, self.timeout) as (sock, gsock):
sock.sendall(struct.pack("!Q", 2))
sock.sendall(b"RE")
res = True
except socket.error:
pass
return res
def setup(self, sock, gsock, data):
data = to_bytes(data)
length = struct.pack("!Q", len(data))
sock.sendall(length)
sock.sendall(data)
self.logger.debug(f"Sending: {data!r}")
tmp = to_unicode(sock.recv(2))
self.logger.debug("recv: '{}'".format(tmp))
if "ER" == tmp:
lengthbuf = sock.recv(8)
(length,) = struct.unpack("!Q", lengthbuf)
err = to_unicode(gsock.recvall(length))
raise BUIserverException(err)
if "OK" != tmp:
self.logger.debug("Ooops, unsuccessful!")
return False
self.logger.debug("Data sent successfully")
return True
def do_command(self, data=None, restarted=False):
"""Send a command to the remote agent"""
res = "[]"
err = None
notimeout = False
timeout = self.timeout
if not data:
raise BUIserverException("Missing data")
data["password"] = self.password
# manage long running operations
if data["func"] in ["restore_files", "get_file", "del_file"]:
notimeout = True
if data["func"] == "get_tree" and data["args"].get("root") == "*":
# arbitrary raise timeout
timeout = max(timeout, 300)
if data["func"] == "proxy_parser" and data["method"] == "remove_client":
notimeout = True
try:
# don't need a context manager here
if data["func"] == "get_file":
gsock = Gsocket(self.host, self.port, self.ssl, notimeout=True)
gsock.conn()
raw = json.dumps(data)
if not self.setup(gsock.sock, gsock, raw):
return res
return gsock.sock
with Gsocket(self.host, self.port, self.ssl, timeout, notimeout) as (
sock,
gsock,
):
try:
raw = json.dumps(data)
if not self.setup(gsock.sock, gsock, raw):
return res
lengthbuf = sock.recv(8)
(length,) = struct.unpack("!Q", lengthbuf)
res = to_unicode(gsock.recvall(length))
except IOError as exc:
if not restarted and exc.errno == errno.EPIPE:
self.logger.warning("Broken pipe, restarting the request")
return self.do_command(data, True)
elif exc.errno == errno.ECONNRESET:
self.logger.error(
"!!! {} !!!\nPlease check your SSL configuration on both sides!".format(
str(exc)
)
)
else:
self.logger.error("!!! {} !!!".format(str(exc)), exc_info=True)
raise exc
except socket.timeout as exc:
if self.app.gunicorn and not restarted:
self.logger.warning("Socket timed-out, restarting the request")
return self.do_command(data, True)
self.logger.error("!!! {} !!!".format(str(exc)), exc_info=exc)
raise exc
# catch all
except Exception as exc:
self.logger.error("!!! {} !!!".format(str(exc)), exc_info=exc)
if data["func"] == "restore_files":
err = str(exc)
elif isinstance(exc, BUIserverException):
raise
else:
raise BUIserverException(str(exc))
except Exception as exc:
if isinstance(exc, BUIserverException):
raise
self.logger.error("!!! {} !!!".format(str(exc)), exc_info=exc)
raise BUIserverException(str(exc))
if data["func"] == "restore_files":
if err:
res = None
return res, err
return res
"""
Utilities functions
"""
@implement
def store_conf_cli(
self,
data,
client=None,
conf=None,
template=False,
statictemplate=False,
content="",
agent=None,
):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`"""
# serialize data as it is a nested dict
import hashlib
import hmac
from base64 import b64encode
if not isinstance(data, (_ImmutableMultiDict, ImmutableMultiDict)):
msg = "Wrong data type"
self.logger.warning(msg)
raise BUIserverException(msg)
vers = self._get_agent_version()
if vers and vers >= AGENT_VERSION_CAST:
# convert the data to our custom ImmutableMultiDict
data = ImmutableMultiDict(data.to_dict(False))
key = "{}{}".format(self.password, "store_conf_cli")
key = to_bytes(key)
pickles = to_unicode(
b64encode(
pickle.dumps(
{
"data": data,
"conf": conf,
"client": client,
"template": template,
"statictemplate": statictemplate,
"content": content,
},
2,
)
)
)
bytes_pickles = to_bytes(pickles)
digest = to_unicode(hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest())
data = {
"func": "store_conf_cli",
"args": pickles,
"pickled": True,
"digest": digest,
}
return json.loads(self.do_command(data))
@implement
def store_conf_srv(self, data, conf=None, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv`"""
# serialize data as it is a nested dict
import hashlib
import hmac
from base64 import b64encode
if not isinstance(data, (_ImmutableMultiDict, ImmutableMultiDict)):
msg = "Wrong data type"
self.logger.warning(msg)
raise BUIserverException(msg)
vers = self._get_agent_version()
if vers and vers >= AGENT_VERSION_CAST:
# convert the data to our custom ImmutableMultiDict
data = ImmutableMultiDict(data.to_dict(False))
key = "{}{}".format(self.password, "store_conf_srv")
key = to_bytes(key)
pickles = to_unicode(b64encode(pickle.dumps({"data": data, "conf": conf}, 2)))
bytes_pickles = to_bytes(pickles)
digest = to_unicode(hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest())
data = {
"func": "store_conf_srv",
"args": pickles,
"pickled": True,
"digest": digest,
}
return json.loads(self.do_command(data))
@implement
def restore_files(
self,
name=None,
backup=None,
files=None,
strip=None,
archive="zip",
password=None,
agent=None,
):
"""See :func:`burpui.misc.backend.interface.BUIbackend.restore_files`"""
data = {
"func": "restore_files",
"args": {
"name": name,
"backup": backup,
"files": files,
"strip": strip,
"archive": archive,
"password": password,
},
}
return self.do_command(data)
@implement
def get_file(self, path, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_file`"""
data = {"func": "get_file", "path": path}
return self.do_command(data)
@implement
def del_file(self, path, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.del_file`"""
data = {"func": "del_file", "path": path}
return json.loads(self.do_command(data))