From f054a99d275f54f0f0a377b6066f10f3e5c2b3dd Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 2 Aug 2024 16:13:03 +0200 Subject: [PATCH 1/9] Add wrapper for builtins : open() function --- aikido_firewall/__init__.py | 1 + aikido_firewall/sinks/builtins.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 aikido_firewall/sinks/builtins.py diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index 73b1b3d11..8c36b081e 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.builtins logger.info("Aikido python firewall started") diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py new file mode 100644 index 000000000..5a7636834 --- /dev/null +++ b/aikido_firewall/sinks/builtins.py @@ -0,0 +1,28 @@ +""" +Sink module for `builtins`, python's built-in function +""" + +import copy +import importhook +from aikido_firewall.helpers.logging import logger + + +@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.info("File name openend : %s, in mode : `%s`", args[0], args[1]) + return former_open(*args, **kwargs) + + # pylint: disable=no-member + setattr(builtins, "open", aikido_new_open) + logger.debug("Wrapped `builtins` module") + return modified_builtins From 13a0063877933550eb5e7d9dc2252737d3620fff Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 2 Aug 2024 16:58:38 +0200 Subject: [PATCH 2/9] Add a open_file demo path traversal to flask-mysql --- sample-apps/flask-mysql/app.py | 10 ++++++++++ .../flask-mysql/templates/open_file.html | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 sample-apps/flask-mysql/templates/open_file.html diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index b10c8541f..9b5e00139 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -42,3 +42,13 @@ 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 new file mode 100644 index 000000000..cd2eee8dd --- /dev/null +++ b/sample-apps/flask-mysql/templates/open_file.html @@ -0,0 +1,17 @@ + + + + + + + Open file + + +

Open file

+
+ + + +
+ + From 0c07e4e91be43c4e4742fe299732798bc212f893 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 2 Aug 2024 16:58:58 +0200 Subject: [PATCH 3/9] Create a path traversal error and actually check for pt and raise it --- aikido_firewall/errors/__init__.py | 8 ++++++++ aikido_firewall/sinks/builtins.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/aikido_firewall/errors/__init__.py b/aikido_firewall/errors/__init__.py index a6f76bd9f..9f8da0f34 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 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 diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py index 5a7636834..d08fa47c3 100644 --- a/aikido_firewall/sinks/builtins.py +++ b/aikido_firewall/sinks/builtins.py @@ -5,6 +5,11 @@ 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") @@ -19,6 +24,11 @@ def on_builtins_import(builtins): former_open = copy.deepcopy(builtins.open) def aikido_new_open(*args, **kwargs): + result = check_context_for_path_traversal( + filename=args[0], operation="builtins.open", context=get_current_context() + ) + if len(result) != 0: + raise AikidoPathTraversal() logger.info("File name openend : %s, in mode : `%s`", args[0], args[1]) return former_open(*args, **kwargs) From 937bc2a76652324b1c6f5549c3f4fedc232c3939 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 2 Aug 2024 16:59:09 +0200 Subject: [PATCH 4/9] Add list of functions to wrap in os and os.path --- aikido_firewall/sinks/os.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 aikido_firewall/sinks/os.py diff --git a/aikido_firewall/sinks/os.py b/aikido_firewall/sinks/os.py new file mode 100644 index 000000000..81a0feb0c --- /dev/null +++ b/aikido_firewall/sinks/os.py @@ -0,0 +1,29 @@ +# 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) From 0a8977ff0d3af7688bab60bf86d5fedbaaac2b2f Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 2 Aug 2024 17:04:58 +0200 Subject: [PATCH 5/9] Fix logging, make it debug and only filename --- aikido_firewall/sinks/builtins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py index d08fa47c3..d04d4829e 100644 --- a/aikido_firewall/sinks/builtins.py +++ b/aikido_firewall/sinks/builtins.py @@ -24,12 +24,12 @@ def on_builtins_import(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() - logger.info("File name openend : %s, in mode : `%s`", args[0], args[1]) return former_open(*args, **kwargs) # pylint: disable=no-member From 965477c7011783d0d24d3c9ca49f1d4ffb492134 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 09:41:01 +0200 Subject: [PATCH 6/9] Wrap `os` module and `os.path` --- aikido_firewall/sinks/os.py | 60 ++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/aikido_firewall/sinks/os.py b/aikido_firewall/sinks/os.py index 81a0feb0c..584c065f0 100644 --- a/aikido_firewall/sinks/os.py +++ b/aikido_firewall/sinks/os.py @@ -1,3 +1,16 @@ +""" +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", @@ -26,4 +39,49 @@ "getatime", "getctime", ] -# os.path.join(path, *paths) +# 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 From 9e5a3b474a1e64846038a44fc2eb7fa286ac52a4 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 09:41:37 +0200 Subject: [PATCH 7/9] Make sure to import os module --- aikido_firewall/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index 8c36b081e..76858b335 100644 --- a/aikido_firewall/__init__.py +++ b/aikido_firewall/__init__.py @@ -41,5 +41,6 @@ def protect(module="any", server=True): import aikido_firewall.sinks.pymongo import aikido_firewall.sinks.psycopg2 import aikido_firewall.sinks.builtins + import aikido_firewall.sinks.os logger.info("Aikido python firewall started") From cdd606f301f43fecd6cd27f9d0791085e48e5ff9 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 5 Aug 2024 10:15:16 +0200 Subject: [PATCH 8/9] Check if should block and if context is set --- aikido_firewall/sinks/builtins.py | 13 +++++++++++-- aikido_firewall/sinks/os.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py index d04d4829e..c03e18ed3 100644 --- a/aikido_firewall/sinks/builtins.py +++ b/aikido_firewall/sinks/builtins.py @@ -9,6 +9,7 @@ check_context_for_path_traversal, ) from aikido_firewall.context import get_current_context +from aikido_firewall.background_process import get_comms from aikido_firewall.errors import AikidoPathTraversal @@ -25,11 +26,19 @@ def on_builtins_import(builtins): def aikido_new_open(*args, **kwargs): logger.debug("`builtins` wrapper, filepath : `%s`;", args[0]) + context = get_current_context() + if not context: + return former_open(*args, **kwargs) result = check_context_for_path_traversal( - filename=args[0], operation="builtins.open", context=get_current_context() + filename=args[0], operation="builtins.open", context=context ) if len(result) != 0: - raise AikidoPathTraversal() + get_comms().send_data_to_bg_process("ATTACK", (result, context)) + should_block = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block: + raise AikidoPathTraversal() return former_open(*args, **kwargs) # pylint: disable=no-member diff --git a/aikido_firewall/sinks/os.py b/aikido_firewall/sinks/os.py index 584c065f0..3976c720b 100644 --- a/aikido_firewall/sinks/os.py +++ b/aikido_firewall/sinks/os.py @@ -9,6 +9,7 @@ check_context_for_path_traversal, ) from aikido_firewall.context import get_current_context +from aikido_firewall.background_process import get_comms from aikido_firewall.errors import AikidoPathTraversal # File functions : @@ -50,11 +51,19 @@ def generate_aikido_function(op, former_func): def aikido_new_func(*args, op=op, former_func=former_func, **kwargs): logger.debug("`os` wrapper, filepath : `%s`; OP : `%s`", args[0], op) + context = get_current_context() + if not context: + return former_func(*args, **kwargs) result = check_context_for_path_traversal( - filename=args[0], operation=f"os.{op}", context=get_current_context() + filename=args[0], operation=f"os.{op}", context=context ) if len(result) != 0: - raise AikidoPathTraversal() + get_comms().send_data_to_bg_process("ATTACK", (result, context)) + should_block = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block: + raise AikidoPathTraversal() return former_func(*args, **kwargs) return aikido_new_func From 0c26a78b7f86d2d14241572555d895f608d23e21 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Tue, 6 Aug 2024 13:46:59 +0200 Subject: [PATCH 9/9] Use the new way of checking blocking --- aikido_firewall/sinks/builtins.py | 4 ++-- aikido_firewall/sinks/os.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py index c03e18ed3..3a451bcc3 100644 --- a/aikido_firewall/sinks/builtins.py +++ b/aikido_firewall/sinks/builtins.py @@ -34,10 +34,10 @@ def aikido_new_open(*args, **kwargs): ) if len(result) != 0: get_comms().send_data_to_bg_process("ATTACK", (result, 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 AikidoPathTraversal() return former_open(*args, **kwargs) diff --git a/aikido_firewall/sinks/os.py b/aikido_firewall/sinks/os.py index 3976c720b..a45aa10d2 100644 --- a/aikido_firewall/sinks/os.py +++ b/aikido_firewall/sinks/os.py @@ -59,10 +59,10 @@ def aikido_new_func(*args, op=op, former_func=former_func, **kwargs): ) if len(result) != 0: get_comms().send_data_to_bg_process("ATTACK", (result, 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 AikidoPathTraversal() return former_func(*args, **kwargs)