Skip to content

Commit 1a33732

Browse files
authored
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.
1 parent a19f73a commit 1a33732

File tree

22 files changed

+872
-17
lines changed

22 files changed

+872
-17
lines changed

dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
0 java.lang.Error
4848
# allow ProcessImpl instrumentation
4949
0 java.lang.ProcessImpl
50+
# allow Runtime instrumentation for RASP
51+
0 java.lang.Runtime
5052
0 java.net.http.*
5153
0 java.net.HttpURLConnection
5254
0 java.net.Socket

dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java

+6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT;
1313
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING;
1414
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT;
15+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI;
1516
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI;
17+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI;
1618
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI;
1719
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF;
1820
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
@@ -118,6 +120,8 @@ private void subscribeConfigurationPoller() {
118120
capabilities |= CAPABILITY_ASM_RASP_SQLI;
119121
capabilities |= CAPABILITY_ASM_RASP_SSRF;
120122
capabilities |= CAPABILITY_ASM_RASP_LFI;
123+
capabilities |= CAPABILITY_ASM_RASP_CMDI;
124+
capabilities |= CAPABILITY_ASM_RASP_SHI;
121125
}
122126
this.configurationPoller.addCapabilities(capabilities);
123127
}
@@ -362,6 +366,8 @@ public void close() {
362366
| CAPABILITY_ASM_RASP_SQLI
363367
| CAPABILITY_ASM_RASP_SSRF
364368
| CAPABILITY_ASM_RASP_LFI
369+
| CAPABILITY_ASM_RASP_CMDI
370+
| CAPABILITY_ASM_RASP_SHI
365371
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
366372
| CAPABILITY_ENDPOINT_FINGERPRINT
367373
| CAPABILITY_ASM_SESSION_FINGERPRINT

dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java

+10
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ public interface KnownAddresses {
131131
/** Login success business event */
132132
Address<String> LOGIN_SUCCESS = new Address<>("server.business_logic.users.login.success");
133133

134+
/** The Exec command being executed */
135+
Address<String> EXEC_CMD = new Address<>("server.sys.exec.cmd");
136+
137+
/** The Shell command being executed */
138+
Address<String> SHELL_CMD = new Address<>("server.sys.shell.cmd");
139+
134140
Address<Map<String, Object>> WAF_CONTEXT_PROCESSOR = new Address<>("waf.context.processor");
135141

136142
static Address<?> forName(String name) {
@@ -205,6 +211,10 @@ static Address<?> forName(String name) {
205211
return LOGIN_SUCCESS;
206212
case "server.business_logic.users.login.failure":
207213
return LOGIN_FAILURE;
214+
case "server.sys.exec.cmd":
215+
return EXEC_CMD;
216+
case "server.sys.shell.cmd":
217+
return SHELL_CMD;
208218
default:
209219
return null;
210220
}

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

+54
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ public class GatewayBridge {
9393
private volatile DataSubscriberInfo sessionIdSubInfo;
9494
private final ConcurrentHashMap<Address<String>, DataSubscriberInfo> userIdSubInfo =
9595
new ConcurrentHashMap<>();
96+
private volatile DataSubscriberInfo execCmdSubInfo;
97+
private volatile DataSubscriberInfo shellCmdSubInfo;
9698

9799
public GatewayBridge(
98100
SubscriptionService subscriptionService,
@@ -139,6 +141,8 @@ public void init() {
139141
EVENTS.loginSuccess(), this.onUserEvent(KnownAddresses.LOGIN_SUCCESS));
140142
subscriptionService.registerCallback(
141143
EVENTS.loginFailure(), this.onUserEvent(KnownAddresses.LOGIN_FAILURE));
144+
subscriptionService.registerCallback(EVENTS.execCmd(), this::onExecCmd);
145+
subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd);
142146

143147
if (additionalIGEvents.contains(EVENTS.requestPathParams())) {
144148
subscriptionService.registerCallback(EVENTS.requestPathParams(), this::onRequestPathParams);
@@ -257,6 +261,56 @@ private Flow<Void> onNetworkConnection(RequestContext ctx_, String url) {
257261
}
258262
}
259263

264+
private Flow<Void> onExecCmd(RequestContext ctx_, String[] command) {
265+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
266+
if (ctx == null) {
267+
return NoopFlow.INSTANCE;
268+
}
269+
while (true) {
270+
DataSubscriberInfo subInfo = execCmdSubInfo;
271+
if (subInfo == null) {
272+
subInfo = producerService.getDataSubscribers(KnownAddresses.EXEC_CMD);
273+
execCmdSubInfo = subInfo;
274+
}
275+
if (subInfo == null || subInfo.isEmpty()) {
276+
return NoopFlow.INSTANCE;
277+
}
278+
DataBundle bundle =
279+
new MapDataBundle.Builder(CAPACITY_0_2).add(KnownAddresses.EXEC_CMD, command).build();
280+
try {
281+
GatewayContext gwCtx = new GatewayContext(true, RuleType.COMMAND_INJECTION);
282+
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
283+
} catch (ExpiredSubscriberInfoException e) {
284+
execCmdSubInfo = null;
285+
}
286+
}
287+
}
288+
289+
private Flow<Void> onShellCmd(RequestContext ctx_, String command) {
290+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
291+
if (ctx == null) {
292+
return NoopFlow.INSTANCE;
293+
}
294+
while (true) {
295+
DataSubscriberInfo subInfo = shellCmdSubInfo;
296+
if (subInfo == null) {
297+
subInfo = producerService.getDataSubscribers(KnownAddresses.SHELL_CMD);
298+
shellCmdSubInfo = subInfo;
299+
}
300+
if (subInfo == null || subInfo.isEmpty()) {
301+
return NoopFlow.INSTANCE;
302+
}
303+
DataBundle bundle =
304+
new MapDataBundle.Builder(CAPACITY_0_2).add(KnownAddresses.SHELL_CMD, command).build();
305+
try {
306+
GatewayContext gwCtx = new GatewayContext(true, RuleType.SHELL_INJECTION);
307+
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
308+
} catch (ExpiredSubscriberInfoException e) {
309+
shellCmdSubInfo = null;
310+
}
311+
}
312+
}
313+
260314
private Flow<Void> onFileLoaded(RequestContext ctx_, String path) {
261315
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
262316
if (ctx == null) {

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRIN
2727
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING
2828
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT
2929
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI
30+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI
31+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI
3032
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI
3133
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF
3234
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING
@@ -271,6 +273,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
271273
| CAPABILITY_ASM_TRUSTED_IPS
272274
| CAPABILITY_ASM_RASP_SQLI
273275
| CAPABILITY_ASM_RASP_SSRF
276+
| CAPABILITY_ASM_RASP_CMDI
277+
| CAPABILITY_ASM_RASP_SHI
274278
| CAPABILITY_ASM_RASP_LFI
275279
| CAPABILITY_ENDPOINT_FINGERPRINT
276280
| CAPABILITY_ASM_SESSION_FINGERPRINT
@@ -423,6 +427,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
423427
| CAPABILITY_ASM_TRUSTED_IPS
424428
| CAPABILITY_ASM_RASP_SQLI
425429
| CAPABILITY_ASM_RASP_SSRF
430+
| CAPABILITY_ASM_RASP_CMDI
431+
| CAPABILITY_ASM_RASP_SHI
426432
| CAPABILITY_ASM_RASP_LFI
427433
| CAPABILITY_ENDPOINT_FINGERPRINT
428434
| CAPABILITY_ASM_SESSION_FINGERPRINT
@@ -496,6 +502,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
496502
| CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE
497503
| CAPABILITY_ASM_RASP_SQLI
498504
| CAPABILITY_ASM_RASP_SSRF
505+
| CAPABILITY_ASM_RASP_CMDI
506+
| CAPABILITY_ASM_RASP_SHI
499507
| CAPABILITY_ASM_RASP_LFI
500508
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
501509
| CAPABILITY_ENDPOINT_FINGERPRINT

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecification.groovy

+6-2
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,17 @@ class KnownAddressesSpecification extends Specification {
3939
'usr.session_id',
4040
'server.business_logic.users.login.failure',
4141
'server.business_logic.users.login.success',
42-
'waf.context.processor',
42+
'server.io.net.url',
43+
'server.io.fs.file',
44+
'server.sys.exec.cmd',
45+
'server.sys.shell.cmd',
46+
'waf.context.processor'
4347
]
4448
}
4549

4650
void 'number of known addresses is expected number'() {
4751
expect:
48-
Address.instanceCount() == 35
52+
Address.instanceCount() == 37
4953
KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1
5054
}
5155
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy

+44-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ class GatewayBridgeSpecification extends DDSpecification {
9292
TriFunction<RequestContext, UserIdCollectionMode, String, Flow<Void>> userIdCB
9393
TriFunction<RequestContext, UserIdCollectionMode, String, Flow<Void>> loginSuccessCB
9494
TriFunction<RequestContext, UserIdCollectionMode, String, Flow<Void>> loginFailureCB
95-
95+
BiFunction<RequestContext, String[], Flow<Void>> execCmdCB
96+
BiFunction<RequestContext, String, Flow<Void>> shellCmdCB
9697

9798
void setup() {
9899
callInitAndCaptureCBs()
@@ -432,6 +433,8 @@ class GatewayBridgeSpecification extends DDSpecification {
432433
1 * ig.registerCallback(EVENTS.userId(), _) >> { userIdCB = it[1]; null }
433434
1 * ig.registerCallback(EVENTS.loginSuccess(), _) >> { loginSuccessCB = it[1]; null }
434435
1 * ig.registerCallback(EVENTS.loginFailure(), _) >> { loginFailureCB = it[1]; null }
436+
1 * ig.registerCallback(EVENTS.execCmd(), _) >> { execCmdCB = it[1]; null }
437+
1 * ig.registerCallback(EVENTS.shellCmd(), _) >> { shellCmdCB = it[1]; null }
435438
0 * ig.registerCallback(_, _)
436439

437440
bridge.init()
@@ -834,6 +837,46 @@ class GatewayBridgeSpecification extends DDSpecification {
834837
gatewayContext.isRasp == true
835838
}
836839

840+
void 'process exec cmd'() {
841+
setup:
842+
final cmd = ['/bin/../usr/bin/reboot', '-f'] as String[]
843+
eventDispatcher.getDataSubscribers({ KnownAddresses.EXEC_CMD in it }) >> nonEmptyDsInfo
844+
DataBundle bundle
845+
GatewayContext gatewayContext
846+
847+
when:
848+
Flow<?> flow = execCmdCB.apply(ctx, cmd)
849+
850+
then:
851+
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
852+
{ a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE }
853+
bundle.get(KnownAddresses.EXEC_CMD) == cmd
854+
flow.result == null
855+
flow.action == Flow.Action.Noop.INSTANCE
856+
gatewayContext.isTransient == true
857+
gatewayContext.isRasp == true
858+
}
859+
860+
void 'process shell cmd'() {
861+
setup:
862+
final cmd = '$(cat /etc/passwd 1>&2 ; echo .)'
863+
eventDispatcher.getDataSubscribers({ KnownAddresses.SHELL_CMD in it }) >> nonEmptyDsInfo
864+
DataBundle bundle
865+
GatewayContext gatewayContext
866+
867+
when:
868+
Flow<?> flow = shellCmdCB.apply(ctx, cmd)
869+
870+
then:
871+
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
872+
{ a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE }
873+
bundle.get(KnownAddresses.SHELL_CMD) == cmd
874+
flow.result == null
875+
flow.action == Flow.Action.Noop.INSTANCE
876+
gatewayContext.isTransient == true
877+
gatewayContext.isRasp == true
878+
}
879+
837880
void 'calls trace segment post processor'() {
838881
setup:
839882
AgentSpan span = Stub()

dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static AgentSpan startSpan(@Advice.Argument(0) final String[] command) th
2323
span.setResourceName(ProcessImplInstrumentationHelpers.determineResource(command));
2424
span.setTag("component", "subprocess");
2525
ProcessImplInstrumentationHelpers.setTags(span, command);
26+
ProcessImplInstrumentationHelpers.cmdiRaspCheck(command);
2627
return span;
2728
}
2829

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package datadog.trace.instrumentation.java.lang;
2+
3+
import datadog.trace.bootstrap.instrumentation.api.java.lang.ProcessImplInstrumentationHelpers;
4+
import java.io.IOException;
5+
import net.bytebuddy.asm.Advice;
6+
7+
class RuntimeExecStringAdvice {
8+
@Advice.OnMethodEnter(suppress = Throwable.class)
9+
public static void beforeExec(@Advice.Argument(0) final String command) throws IOException {
10+
if (command == null) {
11+
return;
12+
}
13+
ProcessImplInstrumentationHelpers.shiRaspCheck(command);
14+
}
15+
16+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
17+
public static void afterExec() {
18+
ProcessImplInstrumentationHelpers.resetCheckShi();
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package datadog.trace.instrumentation.java.lang;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
5+
6+
import com.google.auto.service.AutoService;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import datadog.trace.agent.tooling.InstrumenterModule;
9+
import datadog.trace.api.Platform;
10+
import java.io.File;
11+
12+
@AutoService(InstrumenterModule.class)
13+
public class RuntimeInstrumentation extends InstrumenterModule.AppSec
14+
implements Instrumenter.ForSingleType, Instrumenter.ForBootstrap {
15+
16+
public RuntimeInstrumentation() {
17+
super("java-lang-appsec");
18+
}
19+
20+
@Override
21+
protected boolean defaultEnabled() {
22+
return super.defaultEnabled()
23+
&& !Platform.isNativeImageBuilder(); // not applicable in native-image
24+
}
25+
26+
@Override
27+
public String instrumentedType() {
28+
return "java.lang.Runtime";
29+
}
30+
31+
@Override
32+
public void methodAdvice(MethodTransformer transformer) {
33+
transformer.applyAdvice(
34+
named("exec").and(takesArguments(String.class, String[].class, File.class)),
35+
packageName + ".RuntimeExecStringAdvice");
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package datadog.trace.instrumentation.java.lang
2+
3+
import datadog.trace.agent.test.AgentTestRunner
4+
import datadog.trace.api.config.AppSecConfig
5+
import datadog.trace.api.gateway.CallbackProvider
6+
import datadog.trace.api.gateway.Flow
7+
import datadog.trace.api.gateway.RequestContext
8+
import datadog.trace.api.gateway.RequestContextSlot
9+
import datadog.trace.api.internal.TraceSegment
10+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
11+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
12+
import datadog.trace.bootstrap.instrumentation.api.java.lang.ProcessImplInstrumentationHelpers
13+
import spock.lang.Shared
14+
15+
import java.util.function.BiFunction
16+
17+
import static datadog.trace.api.gateway.Events.EVENTS
18+
19+
class ProcessImplInstrumentationExecCmdRaspForkedTest extends AgentTestRunner {
20+
21+
@Shared
22+
protected static final ORIGINAL_TRACER = AgentTracer.get()
23+
24+
protected traceSegment
25+
protected reqCtx
26+
protected span
27+
protected tracer
28+
29+
void setup() {
30+
traceSegment = Stub(TraceSegment)
31+
reqCtx = Stub(RequestContext) {
32+
getTraceSegment() >> traceSegment
33+
}
34+
span = Stub(AgentSpan) {
35+
getRequestContext() >> reqCtx
36+
}
37+
tracer = Stub(AgentTracer.TracerAPI) {
38+
activeSpan() >> span
39+
}
40+
AgentTracer.forceRegister(tracer)
41+
}
42+
43+
void cleanup() {
44+
AgentTracer.forceRegister(ORIGINAL_TRACER)
45+
}
46+
47+
@Override
48+
protected void configurePreAgent() {
49+
injectSysConfig(AppSecConfig.APPSEC_ENABLED, 'true')
50+
injectSysConfig(AppSecConfig.APPSEC_RASP_ENABLED, 'true')
51+
}
52+
53+
void 'test cmdiRaspCheck'() {
54+
55+
setup:
56+
final callbackProvider = Mock(CallbackProvider)
57+
final listener = Mock(BiFunction)
58+
final flow = Mock(Flow)
59+
tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider
60+
61+
when:
62+
ProcessImplInstrumentationHelpers.cmdiRaspCheck(['/bin/../usr/bin/reboot', '-f'] as String[])
63+
64+
then:
65+
1 * callbackProvider.getCallback(EVENTS.execCmd()) >> listener
66+
1 * listener.apply(reqCtx, ['/bin/../usr/bin/reboot', '-f']) >> flow
67+
}
68+
}

0 commit comments

Comments
 (0)