Skip to content

Exploit prevention for Shell Injection / Command Injection #7615

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 25 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bf9a508
SHI exploit prevention on one sink for java.lang.Runtime.exec(java.la…
jandro996 Sep 13, 2024
bf04013
fix spotless
jandro996 Nov 28, 2024
7043b2c
first steps to add cmdArray support (not blocking)
jandro996 Nov 29, 2024
f3bb30f
Fix known addresses
jandro996 Dec 10, 2024
8323532
Fix test
jandro996 Dec 10, 2024
663712b
Add support for arrayCmd methods and more smoke tests
jandro996 Dec 10, 2024
68f08b2
Add support for arrayCmd methods and more smoke tests
jandro996 Dec 10, 2024
3b62d1c
Add support for arrayCmd methods and more smoke tests
jandro996 Dec 10, 2024
d7226e1
Add support for ProcessBuilder
jandro996 Dec 10, 2024
b488d8a
Move to ProcessImplInstrumentation approach
jandro996 Dec 11, 2024
6d97145
Change to cmdi keeping ProcessImpl approach
jandro996 Dec 11, 2024
2c7672e
fix
jandro996 Dec 13, 2024
f3c4fe1
Add SHI
jandro996 Dec 13, 2024
996ab77
Add metrics to cmdi and shi with rule_variant tag
jandro996 Dec 13, 2024
cf855f7
Add another test
jandro996 Dec 13, 2024
92f9021
fix cmdi capability
jandro996 Dec 14, 2024
7846c74
change cmdi payloads
jandro996 Dec 16, 2024
dd3d414
Merge branch 'master' into alejandro.gonzalez/rasp-command-injection
jandro996 Dec 16, 2024
431cba1
format test
jandro996 Dec 16, 2024
650378e
fix comment
jandro996 Dec 16, 2024
c37357d
Merge branch 'master' into alejandro.gonzalez/rasp-command-injection
jandro996 Dec 16, 2024
5615a0a
change Runtime instrumentation to Appsec
jandro996 Dec 16, 2024
f384a09
Merge branch 'master' into alejandro.gonzalez/rasp-command-injection
jandro996 Dec 17, 2024
a2bc8f8
Merge branch 'master' into alejandro.gonzalez/rasp-command-injection
jandro996 Dec 18, 2024
15ba143
Merge branch 'master' into alejandro.gonzalez/rasp-command-injection
jandro996 Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, since this just follows the current convention, but we should probably move RASP/APPSEC/IAST code in instrumentations to *.iast packages, to make sure codeowners apply to our team instead of APM IDM.


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
Loading