From 1a337326e954d0d771d7ec6c7a0d8d0093295c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez=20Garc=C3=ADa?= Date: Thu, 19 Dec 2024 07:52:29 +0100 Subject: [PATCH] Exploit prevention for Shell Injection / Command Injection (#7615) Added support for Command Injection (CMDI) exploit prevention: Reused existing instrumentation of java.lang.ProcessImpl. Added support for Shell Injection (SHI) exploit prevention: Instrumented java.lang.Runtime#exec(String, String[], File) for detection. Leveraged SHI heuristics as a workaround for cases where the command is a single String, given that WAF heuristics for CMDI only support String[]. Enhanced RASP metrics mechanism: Introduced a new rule_variant tag to metrics. For CMDI: exec. For SHI: shell. Both variants are categorized under the ruletype as command_injection. --- .../bytebuddy/matcher/ignored_class_name.trie | 2 + .../config/AppSecConfigServiceImpl.java | 6 + .../appsec/event/data/KnownAddresses.java | 10 ++ .../datadog/appsec/gateway/GatewayBridge.java | 54 +++++++ ...ppSecConfigServiceImplSpecification.groovy | 8 + .../data/KnownAddressesSpecification.groovy | 8 +- .../gateway/GatewayBridgeSpecification.groovy | 45 +++++- .../java/lang/ProcessImplStartAdvice.java | 1 + .../java/lang/RuntimeExecStringAdvice.java | 20 +++ .../java/lang/RuntimeInstrumentation.java | 37 +++++ ...nstrumentationExecCmdRaspForkedTest.groovy | 68 ++++++++ .../RuntimeInstrumentationForkedTest.groovy | 113 +++++++++++++ .../springboot/controller/WebController.java | 63 ++++++++ .../appsec/SpringBootSmokeTest.groovy | 150 ++++++++++++++++++ .../datadog/trace/api/gateway/Events.java | 20 +++ .../api/gateway/InstrumentationGateway.java | 17 ++ .../datadog/trace/api/telemetry/RuleType.java | 57 +++++-- .../api/telemetry/WafMetricCollector.java | 33 +++- .../ProcessImplInstrumentationHelpers.java | 128 +++++++++++++++ .../telemetry/WafMetricCollectorTest.groovy | 40 ++++- .../gateway/InstrumentationGatewayTest.java | 8 + .../datadog/remoteconfig/Capabilities.java | 1 + 22 files changed, 872 insertions(+), 17 deletions(-) create mode 100644 dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeExecStringAdvice.java create mode 100644 dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeInstrumentation.java create mode 100644 dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationExecCmdRaspForkedTest.groovy create mode 100644 dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/RuntimeInstrumentationForkedTest.groovy diff --git a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie index 85d15f102d4..cdef8ab74a2 100644 --- a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie +++ b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie @@ -47,6 +47,8 @@ 0 java.lang.Error # allow ProcessImpl instrumentation 0 java.lang.ProcessImpl +# allow Runtime instrumentation for RASP +0 java.lang.Runtime 0 java.net.http.* 0 java.net.HttpURLConnection 0 java.net.Socket diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index c2cb5fc7edc..80c7956ea25 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -12,7 +12,9 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT; import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING; import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT; +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI; import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI; +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI; import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI; import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF; import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING; @@ -118,6 +120,8 @@ private void subscribeConfigurationPoller() { capabilities |= CAPABILITY_ASM_RASP_SQLI; capabilities |= CAPABILITY_ASM_RASP_SSRF; capabilities |= CAPABILITY_ASM_RASP_LFI; + capabilities |= CAPABILITY_ASM_RASP_CMDI; + capabilities |= CAPABILITY_ASM_RASP_SHI; } this.configurationPoller.addCapabilities(capabilities); } @@ -362,6 +366,8 @@ public void close() { | CAPABILITY_ASM_RASP_SQLI | CAPABILITY_ASM_RASP_SSRF | CAPABILITY_ASM_RASP_LFI + | CAPABILITY_ASM_RASP_CMDI + | CAPABILITY_ASM_RASP_SHI | CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java index 65649e149ec..7b2f3bdd4e1 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java @@ -131,6 +131,12 @@ public interface KnownAddresses { /** Login success business event */ Address LOGIN_SUCCESS = new Address<>("server.business_logic.users.login.success"); + /** The Exec command being executed */ + Address EXEC_CMD = new Address<>("server.sys.exec.cmd"); + + /** The Shell command being executed */ + Address SHELL_CMD = new Address<>("server.sys.shell.cmd"); + Address> WAF_CONTEXT_PROCESSOR = new Address<>("waf.context.processor"); static Address forName(String name) { @@ -205,6 +211,10 @@ static Address forName(String name) { return LOGIN_SUCCESS; case "server.business_logic.users.login.failure": return LOGIN_FAILURE; + case "server.sys.exec.cmd": + return EXEC_CMD; + case "server.sys.shell.cmd": + return SHELL_CMD; default: return null; } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index 6b98dad668c..c7b0321ff35 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -93,6 +93,8 @@ public class GatewayBridge { private volatile DataSubscriberInfo sessionIdSubInfo; private final ConcurrentHashMap, DataSubscriberInfo> userIdSubInfo = new ConcurrentHashMap<>(); + private volatile DataSubscriberInfo execCmdSubInfo; + private volatile DataSubscriberInfo shellCmdSubInfo; public GatewayBridge( SubscriptionService subscriptionService, @@ -139,6 +141,8 @@ public void init() { EVENTS.loginSuccess(), this.onUserEvent(KnownAddresses.LOGIN_SUCCESS)); subscriptionService.registerCallback( EVENTS.loginFailure(), this.onUserEvent(KnownAddresses.LOGIN_FAILURE)); + subscriptionService.registerCallback(EVENTS.execCmd(), this::onExecCmd); + subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd); if (additionalIGEvents.contains(EVENTS.requestPathParams())) { subscriptionService.registerCallback(EVENTS.requestPathParams(), this::onRequestPathParams); @@ -257,6 +261,56 @@ private Flow onNetworkConnection(RequestContext ctx_, String url) { } } + private Flow onExecCmd(RequestContext ctx_, String[] command) { + AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC); + if (ctx == null) { + return NoopFlow.INSTANCE; + } + while (true) { + DataSubscriberInfo subInfo = execCmdSubInfo; + if (subInfo == null) { + subInfo = producerService.getDataSubscribers(KnownAddresses.EXEC_CMD); + execCmdSubInfo = subInfo; + } + if (subInfo == null || subInfo.isEmpty()) { + return NoopFlow.INSTANCE; + } + DataBundle bundle = + new MapDataBundle.Builder(CAPACITY_0_2).add(KnownAddresses.EXEC_CMD, command).build(); + try { + GatewayContext gwCtx = new GatewayContext(true, RuleType.COMMAND_INJECTION); + return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx); + } catch (ExpiredSubscriberInfoException e) { + execCmdSubInfo = null; + } + } + } + + private Flow onShellCmd(RequestContext ctx_, String command) { + AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC); + if (ctx == null) { + return NoopFlow.INSTANCE; + } + while (true) { + DataSubscriberInfo subInfo = shellCmdSubInfo; + if (subInfo == null) { + subInfo = producerService.getDataSubscribers(KnownAddresses.SHELL_CMD); + shellCmdSubInfo = subInfo; + } + if (subInfo == null || subInfo.isEmpty()) { + return NoopFlow.INSTANCE; + } + DataBundle bundle = + new MapDataBundle.Builder(CAPACITY_0_2).add(KnownAddresses.SHELL_CMD, command).build(); + try { + GatewayContext gwCtx = new GatewayContext(true, RuleType.SHELL_INJECTION); + return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx); + } catch (ExpiredSubscriberInfoException e) { + shellCmdSubInfo = null; + } + } + } + private Flow onFileLoaded(RequestContext ctx_, String path) { AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC); if (ctx == null) { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index c07c430f5de..207f07cf521 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -27,6 +27,8 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRIN import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING @@ -271,6 +273,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_TRUSTED_IPS | CAPABILITY_ASM_RASP_SQLI | CAPABILITY_ASM_RASP_SSRF + | CAPABILITY_ASM_RASP_CMDI + | CAPABILITY_ASM_RASP_SHI | CAPABILITY_ASM_RASP_LFI | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT @@ -423,6 +427,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_TRUSTED_IPS | CAPABILITY_ASM_RASP_SQLI | CAPABILITY_ASM_RASP_SSRF + | CAPABILITY_ASM_RASP_CMDI + | CAPABILITY_ASM_RASP_SHI | CAPABILITY_ASM_RASP_LFI | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT @@ -496,6 +502,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE | CAPABILITY_ASM_RASP_SQLI | CAPABILITY_ASM_RASP_SSRF + | CAPABILITY_ASM_RASP_CMDI + | CAPABILITY_ASM_RASP_SHI | CAPABILITY_ASM_RASP_LFI | CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE | CAPABILITY_ENDPOINT_FINGERPRINT diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecification.groovy index 1d285fc7913..5ee1776db7a 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecification.groovy @@ -39,13 +39,17 @@ class KnownAddressesSpecification extends Specification { 'usr.session_id', 'server.business_logic.users.login.failure', 'server.business_logic.users.login.success', - 'waf.context.processor', + 'server.io.net.url', + 'server.io.fs.file', + 'server.sys.exec.cmd', + 'server.sys.shell.cmd', + 'waf.context.processor' ] } void 'number of known addresses is expected number'() { expect: - Address.instanceCount() == 35 + Address.instanceCount() == 37 KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1 } } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy index 2cc6cbc5170..cdad0461d7a 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy @@ -92,7 +92,8 @@ class GatewayBridgeSpecification extends DDSpecification { TriFunction> userIdCB TriFunction> loginSuccessCB TriFunction> loginFailureCB - + BiFunction> execCmdCB + BiFunction> shellCmdCB void setup() { callInitAndCaptureCBs() @@ -432,6 +433,8 @@ class GatewayBridgeSpecification extends DDSpecification { 1 * ig.registerCallback(EVENTS.userId(), _) >> { userIdCB = it[1]; null } 1 * ig.registerCallback(EVENTS.loginSuccess(), _) >> { loginSuccessCB = it[1]; null } 1 * ig.registerCallback(EVENTS.loginFailure(), _) >> { loginFailureCB = it[1]; null } + 1 * ig.registerCallback(EVENTS.execCmd(), _) >> { execCmdCB = it[1]; null } + 1 * ig.registerCallback(EVENTS.shellCmd(), _) >> { shellCmdCB = it[1]; null } 0 * ig.registerCallback(_, _) bridge.init() @@ -834,6 +837,46 @@ class GatewayBridgeSpecification extends DDSpecification { gatewayContext.isRasp == true } + void 'process exec cmd'() { + setup: + final cmd = ['/bin/../usr/bin/reboot', '-f'] as String[] + eventDispatcher.getDataSubscribers({ KnownAddresses.EXEC_CMD in it }) >> nonEmptyDsInfo + DataBundle bundle + GatewayContext gatewayContext + + when: + Flow flow = execCmdCB.apply(ctx, cmd) + + then: + 1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> + { a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE } + bundle.get(KnownAddresses.EXEC_CMD) == cmd + flow.result == null + flow.action == Flow.Action.Noop.INSTANCE + gatewayContext.isTransient == true + gatewayContext.isRasp == true + } + + void 'process shell cmd'() { + setup: + final cmd = '$(cat /etc/passwd 1>&2 ; echo .)' + eventDispatcher.getDataSubscribers({ KnownAddresses.SHELL_CMD in it }) >> nonEmptyDsInfo + DataBundle bundle + GatewayContext gatewayContext + + when: + Flow flow = shellCmdCB.apply(ctx, cmd) + + then: + 1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> + { a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE } + bundle.get(KnownAddresses.SHELL_CMD) == cmd + flow.result == null + flow.action == Flow.Action.Noop.INSTANCE + gatewayContext.isTransient == true + gatewayContext.isRasp == true + } + void 'calls trace segment post processor'() { setup: AgentSpan span = Stub() diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java index 7c1983c2cd1..028f4f22eea 100644 --- a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java +++ b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java @@ -23,6 +23,7 @@ public static AgentSpan startSpan(@Advice.Argument(0) final String[] command) th span.setResourceName(ProcessImplInstrumentationHelpers.determineResource(command)); span.setTag("component", "subprocess"); ProcessImplInstrumentationHelpers.setTags(span, command); + ProcessImplInstrumentationHelpers.cmdiRaspCheck(command); return span; } diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeExecStringAdvice.java b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeExecStringAdvice.java new file mode 100644 index 00000000000..c8b24590609 --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeExecStringAdvice.java @@ -0,0 +1,20 @@ +package datadog.trace.instrumentation.java.lang; + +import datadog.trace.bootstrap.instrumentation.api.java.lang.ProcessImplInstrumentationHelpers; +import java.io.IOException; +import net.bytebuddy.asm.Advice; + +class RuntimeExecStringAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void beforeExec(@Advice.Argument(0) final String command) throws IOException { + if (command == null) { + return; + } + ProcessImplInstrumentationHelpers.shiRaspCheck(command); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void afterExec() { + ProcessImplInstrumentationHelpers.resetCheckShi(); + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeInstrumentation.java b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeInstrumentation.java new file mode 100644 index 00000000000..00824a1c003 --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/RuntimeInstrumentation.java @@ -0,0 +1,37 @@ +package datadog.trace.instrumentation.java.lang; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Platform; +import java.io.File; + +@AutoService(InstrumenterModule.class) +public class RuntimeInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.ForBootstrap { + + public RuntimeInstrumentation() { + super("java-lang-appsec"); + } + + @Override + protected boolean defaultEnabled() { + return super.defaultEnabled() + && !Platform.isNativeImageBuilder(); // not applicable in native-image + } + + @Override + public String instrumentedType() { + return "java.lang.Runtime"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("exec").and(takesArguments(String.class, String[].class, File.class)), + packageName + ".RuntimeExecStringAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationExecCmdRaspForkedTest.groovy b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationExecCmdRaspForkedTest.groovy new file mode 100644 index 00000000000..1746329f2ee --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationExecCmdRaspForkedTest.groovy @@ -0,0 +1,68 @@ +package datadog.trace.instrumentation.java.lang + +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.config.AppSecConfig +import datadog.trace.api.gateway.CallbackProvider +import datadog.trace.api.gateway.Flow +import datadog.trace.api.gateway.RequestContext +import datadog.trace.api.gateway.RequestContextSlot +import datadog.trace.api.internal.TraceSegment +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.AgentTracer +import datadog.trace.bootstrap.instrumentation.api.java.lang.ProcessImplInstrumentationHelpers +import spock.lang.Shared + +import java.util.function.BiFunction + +import static datadog.trace.api.gateway.Events.EVENTS + +class ProcessImplInstrumentationExecCmdRaspForkedTest extends AgentTestRunner { + + @Shared + protected static final ORIGINAL_TRACER = AgentTracer.get() + + protected traceSegment + protected reqCtx + protected span + protected tracer + + void setup() { + traceSegment = Stub(TraceSegment) + reqCtx = Stub(RequestContext) { + getTraceSegment() >> traceSegment + } + span = Stub(AgentSpan) { + getRequestContext() >> reqCtx + } + tracer = Stub(AgentTracer.TracerAPI) { + activeSpan() >> span + } + AgentTracer.forceRegister(tracer) + } + + void cleanup() { + AgentTracer.forceRegister(ORIGINAL_TRACER) + } + + @Override + protected void configurePreAgent() { + injectSysConfig(AppSecConfig.APPSEC_ENABLED, 'true') + injectSysConfig(AppSecConfig.APPSEC_RASP_ENABLED, 'true') + } + + void 'test cmdiRaspCheck'() { + + setup: + final callbackProvider = Mock(CallbackProvider) + final listener = Mock(BiFunction) + final flow = Mock(Flow) + tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider + + when: + ProcessImplInstrumentationHelpers.cmdiRaspCheck(['/bin/../usr/bin/reboot', '-f'] as String[]) + + then: + 1 * callbackProvider.getCallback(EVENTS.execCmd()) >> listener + 1 * listener.apply(reqCtx, ['/bin/../usr/bin/reboot', '-f']) >> flow + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/RuntimeInstrumentationForkedTest.groovy b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/RuntimeInstrumentationForkedTest.groovy new file mode 100644 index 00000000000..acfc16a5b9b --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/RuntimeInstrumentationForkedTest.groovy @@ -0,0 +1,113 @@ +package datadog.trace.instrumentation.java.lang + +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.config.AppSecConfig +import datadog.trace.api.gateway.CallbackProvider +import static datadog.trace.api.gateway.Events.EVENTS +import datadog.trace.api.gateway.Flow +import datadog.trace.api.gateway.RequestContext +import datadog.trace.api.gateway.RequestContextSlot +import datadog.trace.api.internal.TraceSegment +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.AgentTracer +import spock.lang.Shared + +import java.util.function.BiFunction + +class RuntimeInstrumentationForkedTest extends AgentTestRunner{ + + @Shared + protected static final ORIGINAL_TRACER = AgentTracer.get() + + protected traceSegment + protected reqCtx + protected span + protected tracer + + void setup() { + traceSegment = Stub(TraceSegment) + reqCtx = Stub(RequestContext) { + getTraceSegment() >> traceSegment + } + span = Stub(AgentSpan) { + getRequestContext() >> reqCtx + } + tracer = Stub(AgentTracer.TracerAPI) { + activeSpan() >> span + } + AgentTracer.forceRegister(tracer) + } + + void cleanup() { + AgentTracer.forceRegister(ORIGINAL_TRACER) + } + + @Override + protected void configurePreAgent() { + injectSysConfig(AppSecConfig.APPSEC_ENABLED, 'true') + injectSysConfig(AppSecConfig.APPSEC_RASP_ENABLED, 'true') + } + + void 'test shiRaspCheck'() { + + setup: + final callbackProvider = Mock(CallbackProvider) + final listener = Mock(BiFunction) + final flow = Mock(Flow) + tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider + + when: + try { + Runtime.getRuntime().exec(*args) + }catch (Exception e){ + // ignore + } + + then: + cmdiExpected * callbackProvider.getCallback(EVENTS.execCmd()) >> listener + shiExpected * callbackProvider.getCallback(EVENTS.shellCmd()) >> listener + 1 * listener.apply(reqCtx, args[0]) >> flow + + where: + args | cmdiExpected | shiExpected + ['$(cat /etc/passwd 1>&2 ; echo .)'] | 0 | 1 + ['$(cat /etc/passwd 1>&2 ; echo .)', ['test'] as String[]] | 0 | 1 + ['$(cat /etc/passwd 1>&2 ; echo .)', ['test'] as String[], new File('')] | 0 | 1 + [['/bin/../usr/bin/reboot', '-f'] as String[]] | 1 | 0 + [['/bin/../usr/bin/reboot', '-f'] as String[], ['test'] as String[]] | 1 | 0 + [['/bin/../usr/bin/reboot', '-f'] as String[], ['test'] as String[], new File('')] | 1 | 0 + } + + void 'test shiCheck reset'() { + + setup: + final callbackProvider = Mock(CallbackProvider) + final listener = Mock(BiFunction) + final flow = Mock(Flow) + tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider + + when: + try { + Runtime.getRuntime().exec('$(cat /etc/passwd 1>&2 ; echo .)') + }catch (Exception e){ + // ignore + } + + then: + 0 * callbackProvider.getCallback(EVENTS.execCmd()) >> listener + 1 * callbackProvider.getCallback(EVENTS.shellCmd()) >> listener + 1 * listener.apply(reqCtx, _) >> flow + + when: + try { + Runtime.getRuntime().exec(['/bin/../usr/bin/reboot', '-f'] as String[]) + }catch (Exception e){ + // ignore + } + + then: + 1 * callbackProvider.getCallback(EVENTS.execCmd()) >> listener + 0 * callbackProvider.getCallback(EVENTS.shellCmd()) >> listener + 1 * listener.apply(reqCtx, _) >> flow + } +} diff --git a/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java b/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java index 6f69f9dd29f..8529a048fbd 100644 --- a/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java +++ b/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java @@ -158,4 +158,67 @@ public ResponseEntity session(final HttpServletRequest request) { final HttpSession session = request.getSession(true); return new ResponseEntity<>(session.getId(), HttpStatus.OK); } + + @PostMapping("/cmdi/arrayCmd") + public String shiArrayCmd(@RequestParam("cmd") String[] arrayCmd) { + withProcess(() -> Runtime.getRuntime().exec(arrayCmd)); + return "EXECUTED"; + } + + @PostMapping("/cmdi/arrayCmdWithParams") + public String shiArrayCmdWithParams( + @RequestParam("cmd") String[] arrayCmd, @RequestParam("params") String[] params) { + withProcess(() -> Runtime.getRuntime().exec(arrayCmd, params)); + return "EXECUTED"; + } + + @PostMapping("/cmdi/arrayCmdWithParamsAndFile") + public String shiArrayCmdWithParamsAndFile( + @RequestParam("cmd") String[] arrayCmd, @RequestParam("params") String[] params) { + withProcess(() -> Runtime.getRuntime().exec(arrayCmd, params, new File(""))); + return "EXECUTED"; + } + + @PostMapping("/cmdi/processBuilder") + public String shiProcessBuilder(@RequestParam("cmd") String[] cmd) { + withProcess(() -> new ProcessBuilder(cmd).start()); + return "EXECUTED"; + } + + @PostMapping("/shi/cmd") + public String shiCmd(@RequestParam("cmd") String cmd) { + withProcess(() -> Runtime.getRuntime().exec(cmd)); + return "EXECUTED"; + } + + @PostMapping("/shi/cmdWithParams") + public String shiCmdWithParams( + @RequestParam("cmd") String cmd, @RequestParam("params") String[] params) { + withProcess(() -> Runtime.getRuntime().exec(cmd, params)); + return "EXECUTED"; + } + + @PostMapping("/shi/cmdParamsAndFile") + public String shiCmdParamsAndFile( + @RequestParam("cmd") String cmd, @RequestParam("params") String[] params) { + withProcess(() -> Runtime.getRuntime().exec(cmd, params, new File(""))); + return "EXECUTED"; + } + + private void withProcess(final Operation op) { + Process process = null; + try { + process = op.run(); + } catch (final Throwable e) { + // ignore it + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + } + } + } + + private interface Operation { + E run() throws Throwable; + } } diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy index adb0d15a85c..3830e31113e 100644 --- a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy @@ -2,6 +2,7 @@ package datadog.smoketest.appsec import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.agent.test.utils.ThreadUtils +import okhttp3.FormBody import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody @@ -150,6 +151,54 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest { ], transformers: [], on_match : ['block'] + ], + [ + id : 'rasp-932-110', // to replace default rule + name : 'Command injection exploit', + enable : 'true', + tags : [ + type: 'command_injection', + category: 'vulnerability_trigger', + cwe: '77', + capec: '1000/152/248/88', + confidence: '0', + module: 'rasp' + ], + conditions : [ + [ + parameters: [ + resource: [[address: 'server.sys.exec.cmd']], + params : [[address: 'server.request.body']], + ], + operator : "cmdi_detector", + ], + ], + transformers: [], + on_match : ['block'] + ], + [ + id : 'rasp-932-100', // to replace default rule + name : 'Shell command injection exploit', + enable : 'true', + tags : [ + type: 'command_injection', + category: 'vulnerability_trigger', + cwe: '77', + capec: '1000/152/248/88', + confidence: '0', + module: 'rasp' + ], + conditions : [ + [ + parameters: [ + resource: [[address: 'server.sys.shell.cmd']], + params : [[address: 'server.request.body']], + ], + operator : "shi_detector", + ], + ], + transformers: [], + on_match : ['block'] ] ]) } @@ -503,4 +552,105 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest { } assert trigger != null, 'test trigger not found' } + + void 'rasp blocks on CMDI'() { + when: + String url = "http://localhost:${httpPort}/cmdi/"+endpoint + def formBuilder = new FormBody.Builder() + for (s in cmd) { + formBuilder.add("cmd", s) + } + if (params != null) { + for (s in params) { + formBuilder.add("params", s) + } + } + final body = formBuilder.build() + def request = new Request.Builder() + .url(url) + .post(body) + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + + then: + response.code() == 403 + responseBodyStr.contains('You\'ve been blocked') + + when: + waitForTraceCount(1) + + then: + def rootSpans = this.rootSpans.toList() + rootSpans.size() == 1 + def rootSpan = rootSpans[0] + assert rootSpan.meta.get('appsec.blocked') == 'true', 'appsec.blocked is not set' + assert rootSpan.meta.get('_dd.appsec.json') != null, '_dd.appsec.json is not set' + def trigger = null + for (t in rootSpan.triggers) { + if (t['rule']['id'] == 'rasp-932-110') { + trigger = t + break + } + } + assert trigger != null, 'test trigger not found' + + where: + endpoint | cmd | params + 'arrayCmd' | ['/bin/../usr/bin/reboot', '-f'] | null + 'arrayCmdWithParams' | ['/bin/../usr/bin/reboot', '-f'] | ['param'] + 'arrayCmdWithParamsAndFile' | ['/bin/../usr/bin/reboot', '-f'] | ['param'] + 'processBuilder' | ['/bin/../usr/bin/reboot', '-f'] | null + } + + void 'rasp blocks on SHI'() { + when: + String url = "http://localhost:${httpPort}/shi/"+endpoint + def formBuilder = new FormBody.Builder() + for (s in cmd) { + formBuilder.add("cmd", s) + } + if (params != null) { + for (s in params) { + formBuilder.add("params", s) + } + } + final body = formBuilder.build() + def request = new Request.Builder() + .url(url) + .post(body) + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + + then: + response.code() == 403 + responseBodyStr.contains('You\'ve been blocked') + + when: + waitForTraceCount(1) + + then: + def rootSpans = this.rootSpans.toList() + rootSpans.size() == 1 + def rootSpan = rootSpans[0] + assert rootSpan.meta.get('appsec.blocked') == 'true', 'appsec.blocked is not set' + assert rootSpan.meta.get('_dd.appsec.json') != null, '_dd.appsec.json is not set' + def trigger = null + for (t in rootSpan.triggers) { + if (t['rule']['id'] == 'rasp-932-100') { + trigger = t + break + } + } + assert trigger != null, 'test trigger not found' + + where: + endpoint | cmd | params + 'cmd' | ['$(cat /etc/passwd 1>&2 ; echo .)'] | null + 'cmdWithParams' | ['$(cat /etc/passwd 1>&2 ; echo .)'] | ['param'] + 'cmdParamsAndFile' | ['$(cat /etc/passwd 1>&2 ; echo .)'] | ['param'] + + } + } diff --git a/internal-api/src/main/java/datadog/trace/api/gateway/Events.java b/internal-api/src/main/java/datadog/trace/api/gateway/Events.java index df111af28ee..fa123118e48 100644 --- a/internal-api/src/main/java/datadog/trace/api/gateway/Events.java +++ b/internal-api/src/main/java/datadog/trace/api/gateway/Events.java @@ -308,6 +308,26 @@ public EventType("exec.cmd", EXEC_CMD_ID); + + @SuppressWarnings("unchecked") + public EventType>> execCmd() { + return (EventType>>) EXEC_CMD; + } + + static final int SHELL_CMD_ID = 26; + + @SuppressWarnings("rawtypes") + private static final EventType SHELL_CMD = new ET<>("shell.cmd", SHELL_CMD_ID); + + @SuppressWarnings("unchecked") + public EventType>> shellCmd() { + return (EventType>>) SHELL_CMD; + } + static final int MAX_EVENTS = nextId.get(); private static final class ET extends EventType { diff --git a/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java b/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java index 45d734f0d11..aa6d5811c95 100644 --- a/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java +++ b/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java @@ -2,6 +2,7 @@ import static datadog.trace.api.gateway.Events.DATABASE_CONNECTION_ID; import static datadog.trace.api.gateway.Events.DATABASE_SQL_QUERY_ID; +import static datadog.trace.api.gateway.Events.EXEC_CMD_ID; import static datadog.trace.api.gateway.Events.FILE_LOADED_ID; import static datadog.trace.api.gateway.Events.GRAPHQL_SERVER_REQUEST_MESSAGE_ID; import static datadog.trace.api.gateway.Events.GRPC_SERVER_METHOD_ID; @@ -25,6 +26,7 @@ import static datadog.trace.api.gateway.Events.RESPONSE_HEADER_DONE_ID; import static datadog.trace.api.gateway.Events.RESPONSE_HEADER_ID; import static datadog.trace.api.gateway.Events.RESPONSE_STARTED_ID; +import static datadog.trace.api.gateway.Events.SHELL_CMD_ID; import static datadog.trace.api.gateway.Events.USER_ID; import datadog.trace.api.UserIdCollectionMode; @@ -418,6 +420,7 @@ public Flow apply(RequestContext ctx, String arg) { case DATABASE_SQL_QUERY_ID: case NETWORK_CONNECTION_ID: case FILE_LOADED_ID: + case SHELL_CMD_ID: return (C) new BiFunction>() { @Override @@ -431,6 +434,20 @@ public Flow apply(RequestContext ctx, String arg) { } } }; + case EXEC_CMD_ID: + return (C) + new BiFunction>() { + @Override + public Flow apply(RequestContext ctx, String[] arg) { + try { + return ((BiFunction>) callback) + .apply(ctx, arg); + } catch (Throwable t) { + log.warn("Callback for {} threw.", eventType, t); + return Flow.ResultFlow.empty(); + } + } + }; default: log.warn("Unwrapped callback for {}", eventType); return callback; diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/RuleType.java b/internal-api/src/main/java/datadog/trace/api/telemetry/RuleType.java index 64705e7b0f0..e4e8276c0eb 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/RuleType.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/RuleType.java @@ -1,23 +1,62 @@ package datadog.trace.api.telemetry; +import javax.annotation.Nullable; + public enum RuleType { - LFI("lfi"), - SQL_INJECTION("sql_injection"), - SSRF("ssrf"); + LFI(Type.LFI), + SQL_INJECTION(Type.SQL_INJECTION), + SSRF(Type.SSRF), + SHELL_INJECTION(Type.COMMAND_INJECTION, Variant.SHELL), + COMMAND_INJECTION(Type.COMMAND_INJECTION, Variant.EXEC); - public final String name; + public final Type type; + @Nullable public final Variant variant; private static final int numValues = RuleType.values().length; - RuleType(String name) { - this.name = name; + RuleType(Type type) { + this(type, null); + } + + RuleType(Type type, @Nullable Variant variant) { + this.type = type; + this.variant = variant; } public static int getNumValues() { return numValues; } - @Override - public String toString() { - return name; + public enum Type { + LFI("lfi"), + SQL_INJECTION("sql_injection"), + SSRF("ssrf"), + COMMAND_INJECTION("command_injection"); + + public final String name; + + Type(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + public enum Variant { + SHELL("shell"), + EXEC("exec"); + + public final String name; + + Variant(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } } } diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java index 3f093106d28..23acf58653f 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java @@ -269,19 +269,46 @@ public WafRequestsRawMetric( public static class RaspRuleEval extends WafMetric { public RaspRuleEval(final long counter, final RuleType ruleType, final String wafVersion) { - super("rasp.rule.eval", counter, "rule_type:" + ruleType, "waf_version:" + wafVersion); + super( + "rasp.rule.eval", + counter, + ruleType.variant != null + ? new String[] { + "rule_type:" + ruleType.type, + "rule_variant:" + ruleType.variant, + "waf_version:" + wafVersion + } + : new String[] {"rule_type:" + ruleType.type, "waf_version:" + wafVersion}); } } public static class RaspRuleMatch extends WafMetric { public RaspRuleMatch(final long counter, final RuleType ruleType, final String wafVersion) { - super("rasp.rule.match", counter, "rule_type:" + ruleType, "waf_version:" + wafVersion); + super( + "rasp.rule.match", + counter, + ruleType.variant != null + ? new String[] { + "rule_type:" + ruleType.type, + "rule_variant:" + ruleType.variant, + "waf_version:" + wafVersion + } + : new String[] {"rule_type:" + ruleType.type, "waf_version:" + wafVersion}); } } public static class RaspTimeout extends WafMetric { public RaspTimeout(final long counter, final RuleType ruleType, final String wafVersion) { - super("rasp.timeout", counter, "rule_type:" + ruleType, "waf_version:" + wafVersion); + super( + "rasp.timeout", + counter, + ruleType.variant != null + ? new String[] { + "rule_type:" + ruleType.type, + "rule_variant:" + ruleType.variant, + "waf_version:" + wafVersion + } + : new String[] {"rule_type:" + ruleType.type, "waf_version:" + wafVersion}); } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/java/lang/ProcessImplInstrumentationHelpers.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/java/lang/ProcessImplInstrumentationHelpers.java index 1d19fccc298..cdac9cdc89a 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/java/lang/ProcessImplInstrumentationHelpers.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/java/lang/ProcessImplInstrumentationHelpers.java @@ -1,7 +1,14 @@ package datadog.trace.bootstrap.instrumentation.api.java.lang; +import static datadog.trace.api.gateway.Events.EVENTS; import static java.lang.invoke.MethodType.methodType; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.Config; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.bootstrap.ActiveSubsystems; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -14,10 +21,18 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.BiFunction; import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** This class is included here because it needs to injected into the bootstrap clasloader. */ public class ProcessImplInstrumentationHelpers { + + private static final Logger LOGGER = + LoggerFactory.getLogger(ProcessImplInstrumentationHelpers.class); + private static final int LIMIT = 4096; public static final boolean ONLINE; private static final MethodHandle PROCESS_ON_EXIT; @@ -29,6 +44,10 @@ public class ProcessImplInstrumentationHelpers { + "a(?:ccess|uth)_token|mysql_pwd|credentials|(?:stripe)?token)$"); private static final Set REDACTED_BINARIES = Collections.singleton("md5"); + // This check is used to avoid command injection exploit prevention if shell injection exploit + // prevention checked + private static final ThreadLocal checkShi = ThreadLocal.withInitial(() -> false); + static { MethodHandle processOnExit = null; Executor executor = null; @@ -185,6 +204,115 @@ public static CharSequence determineResource(String[] command) { return first.substring(pos + 1); } + /* + Check if there is a cmd injection attempt to block it + */ + public static void cmdiRaspCheck(@Nonnull final String[] cmdArray) { + if (!Config.get().isAppSecRaspEnabled()) { + return; + } + // if shell injection was checked, skip cmd injection check + if (checkShi.get()) { + return; + } + try { + final BiFunction> execCmdCallback = + AgentTracer.get() + .getCallbackProvider(RequestContextSlot.APPSEC) + .getCallback(EVENTS.execCmd()); + + if (execCmdCallback == null) { + return; + } + + final AgentSpan span = AgentTracer.get().activeSpan(); + if (span == null) { + return; + } + + final RequestContext ctx = span.getRequestContext(); + if (ctx == null) { + return; + } + + Flow flow = execCmdCallback.apply(ctx, cmdArray); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + BlockResponseFunction brf = ctx.getBlockResponseFunction(); + if (brf != null) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + brf.tryCommitBlockingResponse( + ctx.getTraceSegment(), + rba.getStatusCode(), + rba.getBlockingContentType(), + rba.getExtraHeaders()); + } + throw new BlockingException("Blocked request (for CMDI attempt)"); + } + } catch (final BlockingException e) { + // re-throw blocking exceptions + throw e; + } catch (final Throwable e) { + // suppress anything else + LOGGER.debug("Exception during CMDI rasp callback", e); + } + } + + public static void resetCheckShi() { + checkShi.set(false); + } + + /* + Check if there is a chell injection attempt to block it + */ + public static void shiRaspCheck(@Nonnull final String cmd) { + if (!Config.get().isAppSecRaspEnabled()) { + return; + } + checkShi.set(true); + try { + final BiFunction> shellCmdCallback = + AgentTracer.get() + .getCallbackProvider(RequestContextSlot.APPSEC) + .getCallback(EVENTS.shellCmd()); + + if (shellCmdCallback == null) { + return; + } + + final AgentSpan span = AgentTracer.get().activeSpan(); + if (span == null) { + return; + } + + final RequestContext ctx = span.getRequestContext(); + if (ctx == null) { + return; + } + + Flow flow = shellCmdCallback.apply(ctx, cmd); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + BlockResponseFunction brf = ctx.getBlockResponseFunction(); + if (brf != null) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + brf.tryCommitBlockingResponse( + ctx.getTraceSegment(), + rba.getStatusCode(), + rba.getBlockingContentType(), + rba.getExtraHeaders()); + } + throw new BlockingException("Blocked request (for SHI attempt)"); + } + } catch (final BlockingException e) { + // re-throw blocking exceptions + throw e; + } catch (final Throwable e) { + // suppress anything else + LOGGER.debug("Exception during SHI rasp callback", e); + } + } + private static AgentScope.Continuation captureContinuation() { final AgentScope parentScope = AgentTracer.activeScope(); return parentScope == null ? null : parentScope.capture(); diff --git a/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy index 7baafe02e8c..bb3c1d2e4a2 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy @@ -29,8 +29,6 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspTimeout(RuleType.SQL_INJECTION) - - WafMetricCollector.get().prepareMetrics() then: @@ -202,4 +200,42 @@ class WafMetricCollectorTest extends DDSpecification { metric.value == 1 metric.tags == [] } + + def "test Rasp #ruleType metrics"() { + when: + WafMetricCollector.get().wafInit('waf_ver1', 'rules.1') + WafMetricCollector.get().raspRuleEval(ruleType) + WafMetricCollector.get().raspRuleEval(ruleType) + WafMetricCollector.get().raspRuleMatch(ruleType) + WafMetricCollector.get().raspRuleEval(ruleType) + WafMetricCollector.get().raspTimeout(ruleType) + WafMetricCollector.get().prepareMetrics() + + then: + def metrics = WafMetricCollector.get().drain() + + def raspRuleEval = (WafMetricCollector.RaspRuleEval)metrics[1] + raspRuleEval.type == 'count' + raspRuleEval.value == 3 + raspRuleEval.namespace == 'appsec' + raspRuleEval.metricName == 'rasp.rule.eval' + raspRuleEval.tags.toSet() == ['rule_type:command_injection', 'rule_variant:'+ruleType.variant, 'waf_version:waf_ver1'].toSet() + + def raspRuleMatch = (WafMetricCollector.RaspRuleMatch)metrics[2] + raspRuleMatch.type == 'count' + raspRuleMatch.value == 1 + raspRuleMatch.namespace == 'appsec' + raspRuleMatch.metricName == 'rasp.rule.match' + raspRuleMatch.tags.toSet() == ['rule_type:command_injection', 'rule_variant:'+ruleType.variant, 'waf_version:waf_ver1'].toSet() + + def raspTimeout = (WafMetricCollector.RaspTimeout)metrics[3] + raspTimeout.type == 'count' + raspTimeout.value == 1 + raspTimeout.namespace == 'appsec' + raspTimeout.metricName == 'rasp.timeout' + raspTimeout.tags.toSet() == ['rule_type:command_injection', 'rule_variant:'+ruleType.variant, 'waf_version:waf_ver1'].toSet() + + where: + ruleType << [RuleType.COMMAND_INJECTION, RuleType.SHELL_INJECTION] + } } diff --git a/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java b/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java index ea441fd372a..65c9bfa874a 100644 --- a/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java +++ b/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java @@ -221,6 +221,10 @@ public void testNormalCalls() { cbp.getCallback(events.loginSuccess()).apply(null, null, null); ss.registerCallback(events.loginFailure(), callback); cbp.getCallback(events.loginFailure()).apply(null, null, null); + ss.registerCallback(events.execCmd(), callback); + cbp.getCallback(events.execCmd()).apply(null, null); + ss.registerCallback(events.shellCmd(), callback); + cbp.getCallback(events.shellCmd()).apply(null, null); assertThat(callback.count).isEqualTo(Events.MAX_EVENTS); } @@ -289,6 +293,10 @@ public void testThrowableBlocking() { cbp.getCallback(events.loginSuccess()).apply(null, null, null); ss.registerCallback(events.loginFailure(), throwback); cbp.getCallback(events.loginFailure()).apply(null, null, null); + ss.registerCallback(events.execCmd(), throwback); + cbp.getCallback(events.execCmd()).apply(null, null); + ss.registerCallback(events.shellCmd(), throwback); + cbp.getCallback(events.shellCmd()).apply(null, null); assertThat(throwback.count).isEqualTo(Events.MAX_EVENTS); } diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index 079ec1a5f81..5aea9ca50bd 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -37,4 +37,5 @@ public interface Capabilities { long CAPABILITY_ASM_SESSION_FINGERPRINT = 1L << 33; long CAPABILITY_ASM_NETWORK_FINGERPRINT = 1L << 34; long CAPABILITY_ASM_HEADER_FINGERPRINT = 1L << 35; + long CAPABILITY_ASM_RASP_CMDI = 1L << 37; }