Skip to content

Wrap subprocess and os.system #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 6, 2024
6 changes: 5 additions & 1 deletion aikido_firewall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ 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
Expand All @@ -45,4 +45,8 @@ def protect(module="any", server=True):
import aikido_firewall.sinks.http_client
import aikido_firewall.sinks.socket

# Import shell sinks
import aikido_firewall.sinks.os_system
import aikido_firewall.sinks.subprocess

logger.info("Aikido python firewall started")
8 changes: 8 additions & 0 deletions aikido_firewall/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def __init__(self, message="You are rate limited by Aikido firewall."):
self.message = message


class AikidoShellInjection(AikidoException):
"""Exception becausen of Shell Injection"""

def __init__(self, message="Possible Shell Injection"):
super().__init__(message)
self.message = message


class AikidoPathTraversal(AikidoException):
"""Exception because of a path traversal"""

Expand Down
52 changes: 52 additions & 0 deletions aikido_firewall/sinks/os_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""
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` on system() function")

context = get_current_context()
if not context:
return 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_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 AikidoShellInjection()

return former_system_func(*args, **kwargs)

setattr(os, "system", aikido_new_system)
setattr(modified_os, "system", aikido_new_system)

logger.debug("Wrapped `os` module")
return modified_os
6 changes: 4 additions & 2 deletions aikido_firewall/sinks/psycopg2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]")


Expand Down
69 changes: 69 additions & 0 deletions aikido_firewall/sinks/subprocess.py
Original file line number Diff line number Diff line change
@@ -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_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 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
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
1 change: 1 addition & 0 deletions sample-apps/flask-mysql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions sample-apps/flask-mysql/app.py
Original file line number Diff line number Diff line change
@@ -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
import requests
Expand Down Expand Up @@ -44,6 +45,14 @@ def create_dog():
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)

@app.route("/open_file", methods=['GET'])
def show_open_file_form():
Expand Down
17 changes: 17 additions & 0 deletions sample-apps/flask-mysql/templates/shell.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Execute shell</title>
</head>
<body>
<h1>Execute a shell command</h1>
<form method="post">
<label for="command">Command:</label>
<input type="text" id="command" name="command" required>
<button type="submit">Execute!</button>
</form>
</body>
</html>
Loading