Skip to content

Commit

Permalink
Exploit prevention for Shell Injection / Command Injection (#7615)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jandro996 authored Dec 19, 2024
1 parent a19f73a commit 1a33732
Show file tree
Hide file tree
Showing 22 changed files with 872 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public interface KnownAddresses {
/** Login success business event */
Address<String> LOGIN_SUCCESS = new Address<>("server.business_logic.users.login.success");

/** The Exec command being executed */
Address<String> EXEC_CMD = new Address<>("server.sys.exec.cmd");

/** The Shell command being executed */
Address<String> SHELL_CMD = new Address<>("server.sys.shell.cmd");

Address<Map<String, Object>> WAF_CONTEXT_PROCESSOR = new Address<>("waf.context.processor");

static Address<?> forName(String name) {
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public class GatewayBridge {
private volatile DataSubscriberInfo sessionIdSubInfo;
private final ConcurrentHashMap<Address<String>, DataSubscriberInfo> userIdSubInfo =
new ConcurrentHashMap<>();
private volatile DataSubscriberInfo execCmdSubInfo;
private volatile DataSubscriberInfo shellCmdSubInfo;

public GatewayBridge(
SubscriptionService subscriptionService,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -257,6 +261,56 @@ private Flow<Void> onNetworkConnection(RequestContext ctx_, String url) {
}
}

private Flow<Void> 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<Void> 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<Void> onFileLoaded(RequestContext ctx_, String path) {
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
if (ctx == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ class GatewayBridgeSpecification extends DDSpecification {
TriFunction<RequestContext, UserIdCollectionMode, String, Flow<Void>> userIdCB
TriFunction<RequestContext, UserIdCollectionMode, String, Flow<Void>> loginSuccessCB
TriFunction<RequestContext, UserIdCollectionMode, String, Flow<Void>> loginFailureCB

BiFunction<RequestContext, String[], Flow<Void>> execCmdCB
BiFunction<RequestContext, String, Flow<Void>> shellCmdCB

void setup() {
callInitAndCaptureCBs()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 1a33732

Please sign in to comment.