From 7dc182547c5d70091da1fc2b0d9ab14eb8d21a03 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 09:56:03 +0200 Subject: [PATCH 01/10] Add AikidoShellInjection error --- aikido_firewall/errors/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aikido_firewall/errors/__init__.py b/aikido_firewall/errors/__init__.py index a6f76bd9f..45f491eb7 100644 --- a/aikido_firewall/errors/__init__.py +++ b/aikido_firewall/errors/__init__.py @@ -13,3 +13,11 @@ class AikidoSQLInjection(AikidoException): class AikidoNoSQLInjection(AikidoException): """Exception because of NoSQL Injection""" + + +class AikidoShellInjection(AikidoException): + """Exception becausen of Shell Injection""" + + def __init__(self, message="Possible Shell Injection"): + super().__init__(message) + self.message = message From 148e99bcc58a698faaf428fcb33b56202b6d5880 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 10:02:34 +0200 Subject: [PATCH 02/10] Add os.system wrapper --- aikido_firewall/__init__.py | 1 + aikido_firewall/sinks/os_system.py | 51 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 aikido_firewall/sinks/os_system.py diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index 73b1b3d11..397947d20 100644 --- a/aikido_firewall/__init__.py +++ b/aikido_firewall/__init__.py @@ -40,5 +40,6 @@ def protect(module="any", server=True): import aikido_firewall.sinks.mysqlclient import aikido_firewall.sinks.pymongo import aikido_firewall.sinks.psycopg2 + import aikido_firewall.sinks.os_system logger.info("Aikido python firewall started") diff --git a/aikido_firewall/sinks/os_system.py b/aikido_firewall/sinks/os_system.py new file mode 100644 index 000000000..5aa372dc1 --- /dev/null +++ b/aikido_firewall/sinks/os_system.py @@ -0,0 +1,51 @@ +""" +Sink module for `os`, wrapping os.system +""" + +import copy +import json +import importhook +from aikido_firewall.context import get_current_context +from aikido_firewall.vulnerabilities.shell_injection.check_context_for_shell_injection import ( + check_context_for_shell_injection, +) +from aikido_firewall.helpers.logging import logger +from aikido_firewall.background_process import get_comms +from aikido_firewall.errors import AikidoShellInjection + + +@importhook.on_import("os") +def on_os_import(os): + """ + Hook 'n wrap on `os.system()` function + Returns : Modified os object + """ + modified_os = importhook.copy_module(os) + + former_system_func = copy.deepcopy(os.system) + + def aikido_new_system(*args, former_system_func=former_system_func, **kwargs): + logger.debug("Wrapper - `os`") + + context = get_current_context() + if not context: + former_system_func(*args, **kwargs) + contains_injection = check_context_for_shell_injection( + command=args[0], operation="os.system", context=context + ) + + logger.debug("Shell injection results : %s", json.dumps(contains_injection)) + if contains_injection: + get_comms().send_data_to_bg_process("ATTACK", (contains_injection, context)) + should_block = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block: + raise AikidoShellInjection() + + return former_system_func(*args, **kwargs) + + # pylint: disable=no-member + setattr(os, "system", aikido_new_system) + logger.debug("Wrapped `os` module") + return modified_os From 81cbe6d81bf9465a3ea68242c13f9a4a1c44ab72 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 10:11:18 +0200 Subject: [PATCH 03/10] os_system to return if no context and bugfix modified obj not set --- aikido_firewall/sinks/os_system.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aikido_firewall/sinks/os_system.py b/aikido_firewall/sinks/os_system.py index 5aa372dc1..8d8e311b6 100644 --- a/aikido_firewall/sinks/os_system.py +++ b/aikido_firewall/sinks/os_system.py @@ -25,11 +25,11 @@ def on_os_import(os): former_system_func = copy.deepcopy(os.system) def aikido_new_system(*args, former_system_func=former_system_func, **kwargs): - logger.debug("Wrapper - `os`") + logger.debug("Wrapper - `os` on system() function") context = get_current_context() if not context: - former_system_func(*args, **kwargs) + return former_system_func(*args, **kwargs) contains_injection = check_context_for_shell_injection( command=args[0], operation="os.system", context=context ) @@ -45,7 +45,8 @@ def aikido_new_system(*args, former_system_func=former_system_func, **kwargs): return former_system_func(*args, **kwargs) - # pylint: disable=no-member setattr(os, "system", aikido_new_system) + setattr(modified_os, "system", aikido_new_system) + logger.debug("Wrapped `os` module") return modified_os From 216d6b587bf4ed66edc1c5103a776af95653c61e Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 10:12:17 +0200 Subject: [PATCH 04/10] Revert "Merge branch 'AIK-3264' into AIK-3267" This reverts commit cea865a1505aa14102d06b82468c75fb1149ab1f, reversing changes made to 148e99bcc58a698faaf428fcb33b56202b6d5880. --- aikido_firewall/__init__.py | 2 - aikido_firewall/errors/__init__.py | 8 -- aikido_firewall/sinks/builtins.py | 38 -------- aikido_firewall/sinks/os.py | 87 ------------------- sample-apps/flask-mysql/app.py | 10 --- .../flask-mysql/templates/open_file.html | 17 ---- 6 files changed, 162 deletions(-) delete mode 100644 aikido_firewall/sinks/builtins.py delete mode 100644 aikido_firewall/sinks/os.py delete mode 100644 sample-apps/flask-mysql/templates/open_file.html diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index 965b26b6a..397947d20 100644 --- a/aikido_firewall/__init__.py +++ b/aikido_firewall/__init__.py @@ -40,8 +40,6 @@ def protect(module="any", server=True): import aikido_firewall.sinks.mysqlclient import aikido_firewall.sinks.pymongo import aikido_firewall.sinks.psycopg2 - import aikido_firewall.sinks.builtins - import aikido_firewall.sinks.os import aikido_firewall.sinks.os_system logger.info("Aikido python firewall started") diff --git a/aikido_firewall/errors/__init__.py b/aikido_firewall/errors/__init__.py index e191bd4d6..45f491eb7 100644 --- a/aikido_firewall/errors/__init__.py +++ b/aikido_firewall/errors/__init__.py @@ -15,14 +15,6 @@ class AikidoNoSQLInjection(AikidoException): """Exception because of NoSQL Injection""" -class AikidoPathTraversal(AikidoException): - """Exception because of a path traversal""" - - def __init__(self, message="This is a path traversal attack, halted by Aikido."): - super().__init__(self, message) - self.message = message - - class AikidoShellInjection(AikidoException): """Exception becausen of Shell Injection""" diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py deleted file mode 100644 index d04d4829e..000000000 --- a/aikido_firewall/sinks/builtins.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Sink module for `builtins`, python's built-in function -""" - -import copy -import importhook -from aikido_firewall.helpers.logging import logger -from aikido_firewall.vulnerabilities.path_traversal.check_context_for_path_traversal import ( - check_context_for_path_traversal, -) -from aikido_firewall.context import get_current_context -from aikido_firewall.errors import AikidoPathTraversal - - -@importhook.on_import("builtins") -def on_builtins_import(builtins): - """ - Hook 'n wrap on `builtins`, python's built-in functions - Our goal is to wrap the open() function, which you use when opening files - Returns : Modified builtins object - """ - modified_builtins = importhook.copy_module(builtins) - - former_open = copy.deepcopy(builtins.open) - - def aikido_new_open(*args, **kwargs): - logger.debug("`builtins` wrapper, filepath : `%s`;", args[0]) - result = check_context_for_path_traversal( - filename=args[0], operation="builtins.open", context=get_current_context() - ) - if len(result) != 0: - raise AikidoPathTraversal() - return former_open(*args, **kwargs) - - # pylint: disable=no-member - setattr(builtins, "open", aikido_new_open) - logger.debug("Wrapped `builtins` module") - return modified_builtins diff --git a/aikido_firewall/sinks/os.py b/aikido_firewall/sinks/os.py deleted file mode 100644 index 584c065f0..000000000 --- a/aikido_firewall/sinks/os.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Sink module for python's `os` -""" - -import copy -import importhook -from aikido_firewall.helpers.logging import logger -from aikido_firewall.vulnerabilities.path_traversal.check_context_for_path_traversal import ( - check_context_for_path_traversal, -) -from aikido_firewall.context import get_current_context -from aikido_firewall.errors import AikidoPathTraversal - -# File functions : -OS_FILE_FUNCTIONS = [ - "access", - "chmod", - "chown", - "lstat", - "mkdir", - "listdir", - "readlink", - "unlink", - "rename", - "rmdir", - "remove", - "symlink", - "stat", - "utime", - "link", - "makedirs", - "walk", -] -OS_PATH_FUNCTIONS = [ - "exists", - "realpath", - "getsize", - "getmtime", - "getatime", - "getctime", -] -# os.path.join(path, *paths) is not wrapped - - -def generate_aikido_function(op, former_func): - """ - Returns a generated aikido function given an operation - and the previous function - """ - - def aikido_new_func(*args, op=op, former_func=former_func, **kwargs): - logger.debug("`os` wrapper, filepath : `%s`; OP : `%s`", args[0], op) - result = check_context_for_path_traversal( - filename=args[0], operation=f"os.{op}", context=get_current_context() - ) - if len(result) != 0: - raise AikidoPathTraversal() - return former_func(*args, **kwargs) - - return aikido_new_func - - -@importhook.on_import("os") -def on_os_import(os): - """ - Hook 'n wrap on `os`, python's built-in functions - Our goal is to wrap the open() function, which you use when opening files - Returns : Modified os object - """ - modified_os = importhook.copy_module(os) - for op in OS_FILE_FUNCTIONS: - # Wrap os. functions - former_func = copy.deepcopy(getattr(os, op)) - aikido_new_func = generate_aikido_function(op, former_func) - setattr(os, op, aikido_new_func) - setattr(modified_os, op, aikido_new_func) - - for op in OS_PATH_FUNCTIONS: - # Wrap os.path functions - former_func = copy.deepcopy(getattr(os.path, op)) - aikido_new_func = generate_aikido_function(f"path.{op}", former_func) - setattr(os.path, op, aikido_new_func) - # pylint: disable=no-member - setattr(modified_os.path, op, aikido_new_func) - - logger.debug("Wrapped `os` module") - return modified_os diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index 9b5e00139..b10c8541f 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -42,13 +42,3 @@ def create_dog(): cursor.execute(f'INSERT INTO dogs (dog_name, isAdmin) VALUES ("%s", 0)' % (dog_name)) connection.commit() return f'Dog {dog_name} created successfully' - -@app.route("/open_file", methods=['GET']) -def show_open_file_form(): - return render_template('open_file.html') - -@app.route("/open_file", methods=['POST']) -def open_file(): - filepath = request.form['filepath'] - file = open(filepath, 'r', encoding='utf-8') - return file.read() diff --git a/sample-apps/flask-mysql/templates/open_file.html b/sample-apps/flask-mysql/templates/open_file.html deleted file mode 100644 index cd2eee8dd..000000000 --- a/sample-apps/flask-mysql/templates/open_file.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - Open file - - -

Open file

-
- - - -
- - From 001fb52a719be13867b06ba22f7205b540721239 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 10:32:04 +0200 Subject: [PATCH 05/10] Create wrapper for subprocesses shell funcs --- aikido_firewall/sinks/subprocess.py | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 aikido_firewall/sinks/subprocess.py diff --git a/aikido_firewall/sinks/subprocess.py b/aikido_firewall/sinks/subprocess.py new file mode 100644 index 000000000..8c0fcfdb6 --- /dev/null +++ b/aikido_firewall/sinks/subprocess.py @@ -0,0 +1,69 @@ +""" +Sink module for `subprocess` +""" + +import copy +import json +import importhook +from aikido_firewall.context import get_current_context +from aikido_firewall.vulnerabilities.shell_injection.check_context_for_shell_injection import ( + check_context_for_shell_injection, +) +from aikido_firewall.helpers.logging import logger +from aikido_firewall.background_process import get_comms +from aikido_firewall.errors import AikidoShellInjection + +SUBPROCESS_OPERATIONS = ["call", "run", "check_call", "Popen", "check_output"] + + +def generate_aikido_function(op, former_func): + """ + Generates an aikido shell function given + an operation and a former function + """ + + def aikido_new_func(*args, op=op, former_func=former_func, **kwargs): + logger.debug("Wrapper - `subprocess` on %s() function", op) + if isinstance(args[0], list): + command = " ".join(args[0]) + else: + command = args[0] + + context = get_current_context() + if not context: + return former_func(*args, **kwargs) + contains_injection = check_context_for_shell_injection( + command=command, operation=f"subprocess.{op}", context=context + ) + + logger.debug("Shell injection results : %s", json.dumps(contains_injection)) + if contains_injection: + get_comms().send_data_to_bg_process("ATTACK", (contains_injection, context)) + should_block = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block: + raise AikidoShellInjection() + + return former_func(*args, **kwargs) + + return aikido_new_func + + +@importhook.on_import("subprocess") +def on_subprocess_import(subprocess): + """ + Hook 'n wrap on `subproccess`, wrapping multiple functions + Returns : Modified subprocess object + """ + modified_subprocess = importhook.copy_module(subprocess) + for op in SUBPROCESS_OPERATIONS: + former_func = copy.deepcopy(getattr(subprocess, op)) + setattr( + modified_subprocess, + op, + generate_aikido_function(op=op, former_func=former_func), + ) + + logger.debug("Wrapped `subprocess` module") + return modified_subprocess From 47c24d998b60617612b92b5b71456af09c75b8f7 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 10:32:54 +0200 Subject: [PATCH 06/10] Update module imports --- aikido_firewall/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index 397947d20..fa5df32ae 100644 --- a/aikido_firewall/__init__.py +++ b/aikido_firewall/__init__.py @@ -35,11 +35,14 @@ def protect(module="any", server=True): if not module in ["django", "django-gunicorn"]: import aikido_firewall.sources.flask - # Import sinks + # Import DB Sinks import aikido_firewall.sinks.pymysql import aikido_firewall.sinks.mysqlclient import aikido_firewall.sinks.pymongo import aikido_firewall.sinks.psycopg2 + + # Import shell sinks import aikido_firewall.sinks.os_system + import aikido_firewall.sinks.subprocess logger.info("Aikido python firewall started") From 1f33c7fa45c16d84508549cfb246c729f8194cda Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 11:43:48 +0200 Subject: [PATCH 07/10] Add /shell route to sample app --- sample-apps/flask-mysql/README.md | 1 + sample-apps/flask-mysql/app.py | 10 ++++++++++ sample-apps/flask-mysql/templates/shell.html | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 sample-apps/flask-mysql/templates/shell.html diff --git a/sample-apps/flask-mysql/README.md b/sample-apps/flask-mysql/README.md index f864c7614..24f3e2d55 100644 --- a/sample-apps/flask-mysql/README.md +++ b/sample-apps/flask-mysql/README.md @@ -10,3 +10,4 @@ docker-compose up --build - You'll be able to access the Flask Server at : [localhost:8080](http://localhost:8080) - To Create a reference test dog use `http://localhost:8080/create/doggo` - To test a sql injection enter the following dog name : `Malicious dog", 1); -- ` +- To test commands : `http://localhost:8080/shell`, uses subprocess.run diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index b10c8541f..d9ef6f7e8 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -1,6 +1,7 @@ import aikido_firewall # Aikido package import aikido_firewall.protect() +import subprocess from flask import Flask, render_template, request from flaskext.mysql import MySQL @@ -42,3 +43,12 @@ def create_dog(): cursor.execute(f'INSERT INTO dogs (dog_name, isAdmin) VALUES ("%s", 0)' % (dog_name)) connection.commit() return f'Dog {dog_name} created successfully' + +@app.route("/shell", methods=['GET']) +def show_shell_form(): + return render_template('shell.html') +@app.route("/shell", methods=['POST']) +def execute_command(): + command = request.form['command'] + result = subprocess.run(command.split(), capture_output=True, text=True) + return str(result.stdout) diff --git a/sample-apps/flask-mysql/templates/shell.html b/sample-apps/flask-mysql/templates/shell.html new file mode 100644 index 000000000..8c2e0b3c7 --- /dev/null +++ b/sample-apps/flask-mysql/templates/shell.html @@ -0,0 +1,17 @@ + + + + + + + Execute shell + + +

Execute a shell command

+
+ + + +
+ + From af70c16bb33857ef6f6f540c6d5484046af824bd Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Tue, 6 Aug 2024 13:44:57 +0200 Subject: [PATCH 08/10] Use the correct should_block --- aikido_firewall/sinks/os_system.py | 4 ++-- aikido_firewall/sinks/psycopg2.py | 6 ++++-- aikido_firewall/sinks/subprocess.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/aikido_firewall/sinks/os_system.py b/aikido_firewall/sinks/os_system.py index 8d8e311b6..6cf6f3aa5 100644 --- a/aikido_firewall/sinks/os_system.py +++ b/aikido_firewall/sinks/os_system.py @@ -37,10 +37,10 @@ def aikido_new_system(*args, former_system_func=former_system_func, **kwargs): logger.debug("Shell injection results : %s", json.dumps(contains_injection)) if contains_injection: get_comms().send_data_to_bg_process("ATTACK", (contains_injection, context)) - should_block = get_comms().send_data_to_bg_process( + should_block_res = get_comms().send_data_to_bg_process( action="READ_PROPERTY", obj="block", receive=True ) - if should_block: + if should_block_res["success"] and should_block_res["data"]: raise AikidoShellInjection() return former_system_func(*args, **kwargs) diff --git a/aikido_firewall/sinks/psycopg2.py b/aikido_firewall/sinks/psycopg2.py index c1a2eec10..b53608f92 100644 --- a/aikido_firewall/sinks/psycopg2.py +++ b/aikido_firewall/sinks/psycopg2.py @@ -46,8 +46,10 @@ def execute_sql_detection_code(sql): logger.info("sql_injection results : %s", json.dumps(contains_injection)) if contains_injection: get_comms().send_data_to_bg_process("ATTACK", (contains_injection, context)) - should_block = get_comms().poll_config("block") - if should_block: + should_block_res = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block_res["success"] and should_block_res["data"]: raise AikidoSQLInjection("SQL Injection [aikido_firewall]") diff --git a/aikido_firewall/sinks/subprocess.py b/aikido_firewall/sinks/subprocess.py index 8c0fcfdb6..b40f86673 100644 --- a/aikido_firewall/sinks/subprocess.py +++ b/aikido_firewall/sinks/subprocess.py @@ -39,10 +39,10 @@ def aikido_new_func(*args, op=op, former_func=former_func, **kwargs): logger.debug("Shell injection results : %s", json.dumps(contains_injection)) if contains_injection: get_comms().send_data_to_bg_process("ATTACK", (contains_injection, context)) - should_block = get_comms().send_data_to_bg_process( + should_block_res = get_comms().send_data_to_bg_process( action="READ_PROPERTY", obj="block", receive=True ) - if should_block: + if should_block_res["success"] and should_block_res["data"]: raise AikidoShellInjection() return former_func(*args, **kwargs) From 2175f72b42e56cdd2ba883be0e461af38a885caa Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Tue, 6 Aug 2024 14:12:12 +0200 Subject: [PATCH 09/10] Linting --- aikido_firewall/sources/flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aikido_firewall/sources/flask.py b/aikido_firewall/sources/flask.py index d13596365..31863281e 100644 --- a/aikido_firewall/sources/flask.py +++ b/aikido_firewall/sources/flask.py @@ -28,7 +28,7 @@ def dispatch(self, request, call_next): context.set_as_current_context() response = call_next(request) - comms = get_comms() # get IPC facilitator + comms = get_comms() # get IPC facilitator is_curr_route_useful = is_useful_route( response._status_code, context.route, context.method From 27bf832b1bbfc16cdf3f8061af0ffbd35c378876 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Tue, 6 Aug 2024 14:12:52 +0200 Subject: [PATCH 10/10] Linting --- aikido_firewall/errors/__init__.py | 1 - .../vulnerabilities/ssrf/inspect_getaddrinfo_result.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/aikido_firewall/errors/__init__.py b/aikido_firewall/errors/__init__.py index a4b298302..f73aa4285 100644 --- a/aikido_firewall/errors/__init__.py +++ b/aikido_firewall/errors/__init__.py @@ -23,7 +23,6 @@ def __init__(self, message="You are rate limited by Aikido firewall."): self.message = message - class AikidoShellInjection(AikidoException): """Exception becausen of Shell Injection""" diff --git a/aikido_firewall/vulnerabilities/ssrf/inspect_getaddrinfo_result.py b/aikido_firewall/vulnerabilities/ssrf/inspect_getaddrinfo_result.py index e6da6055b..5a40f0dfc 100644 --- a/aikido_firewall/vulnerabilities/ssrf/inspect_getaddrinfo_result.py +++ b/aikido_firewall/vulnerabilities/ssrf/inspect_getaddrinfo_result.py @@ -12,6 +12,7 @@ from .is_private_ip import is_private_ip from .find_hostname_in_context import find_hostname_in_context + # gets called when the result of the DNS resolution has come in def inspect_getaddrinfo_result(dns_results, hostname, port): """Inspect the results of a getaddrinfo() call"""