From 1e3c3bf45cb577e09f60e84a05c0205a3f415150 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Thu, 9 Jan 2025 14:44:28 +0100 Subject: [PATCH] add support for shell injection on subprocess --- ddtrace/appsec/_common_module_patches.py | 2 +- ddtrace/contrib/internal/subprocess/patch.py | 8 ++++++-- tests/appsec/contrib_appsec/django_app/urls.py | 13 +++++++++++-- tests/appsec/contrib_appsec/fastapi_app/app.py | 13 +++++++++++-- tests/appsec/contrib_appsec/flask_app/app.py | 13 +++++++++++-- tests/appsec/contrib_appsec/utils.py | 6 +++--- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index 1eb8440a3f..0b455dbba6 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -272,7 +272,7 @@ def popen_FD233052260D8B4D(arg_list: Union[List[str], str]) -> None: if in_asm_context(): res = call_waf_callback( - {EXPLOIT_PREVENTION.ADDRESS.CMDI: arg_list}, + {EXPLOIT_PREVENTION.ADDRESS.CMDI: arg_list if isinstance(arg_list, list) else [arg_list]}, crop_trace="popen_FD233052260D8B4D", rule_type=EXPLOIT_PREVENTION.TYPE.CMDI, ) diff --git a/ddtrace/contrib/internal/subprocess/patch.py b/ddtrace/contrib/internal/subprocess/patch.py index b7c6ea95c3..76530c195d 100644 --- a/ddtrace/contrib/internal/subprocess/patch.py +++ b/ddtrace/contrib/internal/subprocess/patch.py @@ -395,8 +395,12 @@ def _traced_subprocess_init(module, pin, wrapped, instance, args, kwargs): try: cmd_args = args[0] if len(args) else kwargs["args"] if isinstance(cmd_args, (list, tuple, str)): - for callback in _LST_CALLBACKS.values(): - callback(cmd_args) + if kwargs.get("shell", False): + for callback in _STR_CALLBACKS.values(): + callback(cmd_args) + else: + for callback in _LST_CALLBACKS.values(): + callback(cmd_args) cmd_args_list = shlex.split(cmd_args) if isinstance(cmd_args, str) else cmd_args is_shell = kwargs.get("shell", False) shellcmd = SubprocessCmdLine(cmd_args_list, shell=is_shell) # nosec diff --git a/tests/appsec/contrib_appsec/django_app/urls.py b/tests/appsec/contrib_appsec/django_app/urls.py index 5cbc23b209..aaff69169b 100644 --- a/tests/appsec/contrib_appsec/django_app/urls.py +++ b/tests/appsec/contrib_appsec/django_app/urls.py @@ -136,7 +136,10 @@ def rasp(request, endpoint: str): if param.startswith("cmd"): cmd = query_params[param] try: - res.append(f'cmd stdout: {os.system(f"ls {cmd}")}') + if param.startswith("cmdsys"): + res.append(f'cmd stdout: {os.system(f"ls {cmd}")}') + else: + res.append(f'cmd stdout: {subprocess.run(f"ls {cmd}", shell=True)}') except Exception as e: res.append(f"Error: {e}") tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) @@ -144,12 +147,18 @@ def rasp(request, endpoint: str): elif endpoint == "command_injection": res = ["command_injection endpoint"] for param in query_params: - if param.startswith("cmd"): + if param.startswith("cmda"): cmd = query_params[param] try: res.append(f'cmd stdout: {subprocess.run([cmd, "-c", "3", "localhost"])}') except Exception as e: res.append(f"Error: {e}") + elif param.startswith("cmds"): + cmd = query_params[param] + try: + res.append(f"cmd stdout: {subprocess.run(cmd)}") + except Exception as e: + res.append(f"Error: {e}") tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) return HttpResponse("<\\br>\n".join(res)) tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) diff --git a/tests/appsec/contrib_appsec/fastapi_app/app.py b/tests/appsec/contrib_appsec/fastapi_app/app.py index 5747a7320f..c5b765c4bb 100644 --- a/tests/appsec/contrib_appsec/fastapi_app/app.py +++ b/tests/appsec/contrib_appsec/fastapi_app/app.py @@ -185,7 +185,10 @@ async def rasp(endpoint: str, request: Request): if param.startswith("cmd"): cmd = query_params[param] try: - res.append(f'cmd stdout: {os.system(f"ls {cmd}")}') + if param.startswith("cmdsys"): + res.append(f'cmd stdout: {os.system(f"ls {cmd}")}') + else: + res.append(f'cmd stdout: {subprocess.run(f"ls {cmd}", shell=True)}') except Exception as e: res.append(f"Error: {e}") tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) @@ -193,12 +196,18 @@ async def rasp(endpoint: str, request: Request): elif endpoint == "command_injection": res = ["command_injection endpoint"] for param in query_params: - if param.startswith("cmd"): + if param.startswith("cmda"): cmd = query_params[param] try: res.append(f'cmd stdout: {subprocess.run([cmd, "-c", "3", "localhost"])}') except Exception as e: res.append(f"Error: {e}") + elif param.startswith("cmds"): + cmd = query_params[param] + try: + res.append(f"cmd stdout: {subprocess.run(cmd)}") + except Exception as e: + res.append(f"Error: {e}") tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) return HTMLResponse("<\\br>\n".join(res)) tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) diff --git a/tests/appsec/contrib_appsec/flask_app/app.py b/tests/appsec/contrib_appsec/flask_app/app.py index d938c0c8e4..939a7cad67 100644 --- a/tests/appsec/contrib_appsec/flask_app/app.py +++ b/tests/appsec/contrib_appsec/flask_app/app.py @@ -133,7 +133,10 @@ def rasp(endpoint: str): if param.startswith("cmd"): cmd = query_params[param] try: - res.append(f'cmd stdout: {os.system(f"ls {cmd}")}') + if param.startswith("cmdsys"): + res.append(f'cmd stdout: {os.system(f"ls {cmd}")}') + else: + res.append(f'cmd stdout: {subprocess.run(f"ls {cmd}", shell=True)}') except Exception as e: res.append(f"Error: {e}") tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) @@ -141,12 +144,18 @@ def rasp(endpoint: str): elif endpoint == "command_injection": res = ["command_injection endpoint"] for param in query_params: - if param.startswith("cmd"): + if param.startswith("cmda"): cmd = query_params[param] try: res.append(f'cmd stdout: {subprocess.run([cmd, "-c", "3", "localhost"])}') except Exception as e: res.append(f"Error: {e}") + elif param.startswith("cmds"): + cmd = query_params[param] + try: + res.append(f"cmd stdout: {subprocess.run(cmd)}") + except Exception as e: + res.append(f"Error: {e}") tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) return "<\\br>\n".join(res) tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint) diff --git a/tests/appsec/contrib_appsec/utils.py b/tests/appsec/contrib_appsec/utils.py index c0c2296378..d3691e2bea 100644 --- a/tests/appsec/contrib_appsec/utils.py +++ b/tests/appsec/contrib_appsec/utils.py @@ -1309,7 +1309,7 @@ def test_stream_response( + [ ( "shell_injection", - "cmd_1=$(cat /etc/passwd 1>%262 ; echo .)&cmd_2=$(uname -a 1>%262 ; echo .)", + "cmdsys_1=$(cat /etc/passwd 1>%262 ; echo .)&cmdrun_2=$(uname -a 1>%262 ; echo .)", "rasp-932-100", ("system", "rasp"), ) @@ -1317,7 +1317,7 @@ def test_stream_response( + [ ( "command_injection", - "cmd_1=/sbin/ping&cmd_2=/usr/bin/ls", + "cmda_1=/sbin/ping&cmds_2=/usr/bin/ls%20-la", "rasp-932-110", ("Popen", "rasp"), ) @@ -1529,7 +1529,7 @@ def test_fingerprinting(self, interface, root_span, get_tag, asm_enabled, user_a def test_iast(self, interface, root_span, get_tag): from ddtrace.ext import http - url = "/rasp/command_injection/?cmd=." + url = "/rasp/command_injection/?cmds=." self.update_tracer(interface) response = interface.client.get(url) assert self.status(response) == 200