Skip to content

Commit 6e8331e

Browse files
committed
SHI exploit prevention on one sink for java.lang.Runtime.exec(java.lang.String)
1 parent 5dc6a14 commit 6e8331e

File tree

15 files changed

+328
-10
lines changed

15 files changed

+328
-10
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
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_SHI;
1516
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI;
1617
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF;
1718
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
@@ -116,6 +117,7 @@ private void subscribeConfigurationPoller() {
116117
if (tracerConfig.isAppSecRaspEnabled()) {
117118
capabilities |= CAPABILITY_ASM_RASP_SQLI;
118119
capabilities |= CAPABILITY_ASM_RASP_SSRF;
120+
capabilities |= CAPABILITY_ASM_RASP_SHI;
119121
}
120122
this.configurationPoller.addCapabilities(capabilities);
121123
}
@@ -359,6 +361,7 @@ public void close() {
359361
| CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE
360362
| CAPABILITY_ASM_RASP_SQLI
361363
| CAPABILITY_ASM_RASP_SSRF
364+
| CAPABILITY_ASM_RASP_SHI
362365
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
363366
| CAPABILITY_ENDPOINT_FINGERPRINT
364367
// TODO enable when usr.id and usr.session_id addresses are added

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ public interface KnownAddresses {
125125

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

128+
/** The Shell command being executed */
129+
Address<String> SHELL_CMD = new Address<>("server.sys.shell.cmd");
130+
128131
static Address<?> forName(String name) {
129132
switch (name) {
130133
case "server.request.body":

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public class GatewayBridge {
8585
private volatile DataSubscriberInfo requestEndSubInfo;
8686
private volatile DataSubscriberInfo dbSqlQuerySubInfo;
8787
private volatile DataSubscriberInfo ioNetUrlSubInfo;
88+
private volatile DataSubscriberInfo shellCmdSubInfo;
8889

8990
public GatewayBridge(
9091
SubscriptionService subscriptionService,
@@ -124,6 +125,7 @@ public void init() {
124125
subscriptionService.registerCallback(EVENTS.databaseConnection(), this::onDatabaseConnection);
125126
subscriptionService.registerCallback(EVENTS.databaseSqlQuery(), this::onDatabaseSqlQuery);
126127
subscriptionService.registerCallback(EVENTS.networkConnection(), this::onNetworkConnection);
128+
subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd);
127129

128130
if (additionalIGEvents.contains(EVENTS.requestPathParams())) {
129131
subscriptionService.registerCallback(EVENTS.requestPathParams(), this::onRequestPathParams);
@@ -159,6 +161,31 @@ private Flow<Void> onNetworkConnection(RequestContext ctx_, String url) {
159161
}
160162
}
161163

164+
private Flow<Void> onShellCmd(RequestContext ctx_, String command) {
165+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
166+
if (ctx == null) {
167+
return NoopFlow.INSTANCE;
168+
}
169+
while (true) {
170+
DataSubscriberInfo subInfo = shellCmdSubInfo;
171+
if (subInfo == null) {
172+
subInfo = producerService.getDataSubscribers(KnownAddresses.SHELL_CMD);
173+
shellCmdSubInfo = subInfo;
174+
}
175+
if (subInfo == null || subInfo.isEmpty()) {
176+
return NoopFlow.INSTANCE;
177+
}
178+
DataBundle bundle =
179+
new MapDataBundle.Builder(CAPACITY_0_2).add(KnownAddresses.SHELL_CMD, command).build();
180+
try {
181+
GatewayContext gwCtx = new GatewayContext(true, RuleType.SHI);
182+
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
183+
} catch (ExpiredSubscriberInfoException e) {
184+
shellCmdSubInfo = null;
185+
}
186+
}
187+
}
188+
162189
private Flow<Void> onDatabaseSqlQuery(RequestContext ctx_, String sql) {
163190
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
164191
if (ctx == null) {

dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ private static Collection<Address<?>> getUsedAddresses(PowerwafContext ctx) {
403403
addressList.add(KnownAddresses.DB_TYPE);
404404
addressList.add(KnownAddresses.DB_SQL_QUERY);
405405
addressList.add(KnownAddresses.IO_NET_URL);
406+
addressList.add(KnownAddresses.SHELL_CMD);
406407

407408
return addressList;
408409
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.datadog.appsec.config
33
import com.datadog.appsec.AppSecSystem
44
import com.datadog.appsec.api.security.ApiSecurityRequestSampler
55
import com.datadog.appsec.util.AbortStartupException
6+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI
67
import datadog.remoteconfig.ConfigurationChangesTypedListener
78
import datadog.remoteconfig.ConfigurationDeserializer
89
import datadog.remoteconfig.ConfigurationEndListener
@@ -269,6 +270,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
269270
| CAPABILITY_ASM_TRUSTED_IPS
270271
| CAPABILITY_ASM_RASP_SQLI
271272
| CAPABILITY_ASM_RASP_SSRF
273+
| CAPABILITY_ASM_RASP_SHI
272274
| CAPABILITY_ENDPOINT_FINGERPRINT
273275
// | CAPABILITY_ASM_SESSION_FINGERPRINT
274276
| CAPABILITY_ASM_NETWORK_FINGERPRINT
@@ -420,6 +422,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
420422
| CAPABILITY_ASM_TRUSTED_IPS
421423
| CAPABILITY_ASM_RASP_SQLI
422424
| CAPABILITY_ASM_RASP_SSRF
425+
| CAPABILITY_ASM_RASP_SHI
423426
| CAPABILITY_ENDPOINT_FINGERPRINT
424427
// | CAPABILITY_ASM_SESSION_FINGERPRINT
425428
| CAPABILITY_ASM_NETWORK_FINGERPRINT
@@ -492,6 +495,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
492495
| CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE
493496
| CAPABILITY_ASM_RASP_SQLI
494497
| CAPABILITY_ASM_RASP_SSRF
498+
| CAPABILITY_ASM_RASP_SHI
495499
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
496500
| CAPABILITY_ENDPOINT_FINGERPRINT
497501
// | CAPABILITY_ASM_SESSION_FINGERPRINT

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class GatewayBridgeSpecification extends DDSpecification {
8282
BiConsumer<RequestContext, String> databaseConnectionCB
8383
BiFunction<RequestContext, String, Flow<Void>> databaseSqlQueryCB
8484
BiFunction<RequestContext, String, Flow<Void>> networkConnectionCB
85+
BiFunction<RequestContext, String, Flow<Void>> shellCmdCB
8586

8687
void setup() {
8788
callInitAndCaptureCBs()
@@ -416,6 +417,7 @@ class GatewayBridgeSpecification extends DDSpecification {
416417
1 * ig.registerCallback(EVENTS.databaseConnection(), _) >> { databaseConnectionCB = it[1]; null }
417418
1 * ig.registerCallback(EVENTS.databaseSqlQuery(), _) >> { databaseSqlQueryCB = it[1]; null }
418419
1 * ig.registerCallback(EVENTS.networkConnection(), _) >> { networkConnectionCB = it[1]; null }
420+
1 * ig.registerCallback(EVENTS.shellCmd(), _) >> { shellCmdCB = it[1]; null }
419421
0 * ig.registerCallback(_, _)
420422

421423
bridge.init()
@@ -798,6 +800,26 @@ class GatewayBridgeSpecification extends DDSpecification {
798800
gatewayContext.isRasp == true
799801
}
800802

803+
void 'process shell cmd'() {
804+
setup:
805+
final cmd = '&lt;!--#exec%20cmd=&quot;/bin/cat%20/etc/passwd&quot;--&gt;'
806+
eventDispatcher.getDataSubscribers({ KnownAddresses.SHELL_CMD in it }) >> nonEmptyDsInfo
807+
DataBundle bundle
808+
GatewayContext gatewayContext
809+
810+
when:
811+
Flow<?> flow = shellCmdCB.apply(ctx, cmd)
812+
813+
then:
814+
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
815+
{ a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE }
816+
bundle.get(KnownAddresses.SHELL_CMD) == cmd
817+
flow.result == null
818+
flow.action == Flow.Action.Noop.INSTANCE
819+
gatewayContext.isTransient == true
820+
gatewayContext.isRasp == true
821+
}
822+
801823
void 'calls trace segment post processor'() {
802824
setup:
803825
AgentSpan span = Stub()

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package datadog.trace.instrumentation.java.lang;
22

33
import datadog.trace.agent.tooling.csi.CallSite;
4+
import datadog.trace.api.appsec.RaspCallSites;
45
import datadog.trace.api.iast.IastCallSites;
56
import datadog.trace.api.iast.InstrumentationBridge;
67
import datadog.trace.api.iast.Sink;
@@ -10,20 +11,16 @@
1011
import javax.annotation.Nullable;
1112

1213
@Sink(VulnerabilityTypes.COMMAND_INJECTION)
13-
@CallSite(spi = IastCallSites.class)
14+
@CallSite(
15+
spi = {IastCallSites.class, RaspCallSites.class},
16+
helpers = ShellCmdRaspHelper.class)
1417
public class RuntimeCallSite {
1518

1619
@CallSite.Before("java.lang.Process java.lang.Runtime.exec(java.lang.String)")
1720
public static void beforeStart(@CallSite.Argument @Nullable final String command) {
1821
if (command != null) { // runtime fails if null
19-
final CommandInjectionModule module = InstrumentationBridge.COMMAND_INJECTION;
20-
if (module != null) {
21-
try {
22-
module.onRuntimeExec(command);
23-
} catch (final Throwable e) {
24-
module.onUnexpectedException("beforeExec threw", e);
25-
}
26-
}
22+
iastCallback(command);
23+
raspCallback(command);
2724
}
2825
}
2926

@@ -109,4 +106,19 @@ public static void beforeExec(
109106
}
110107
}
111108
}
109+
110+
private static void iastCallback(String command) {
111+
final CommandInjectionModule module = InstrumentationBridge.COMMAND_INJECTION;
112+
if (module != null) {
113+
try {
114+
module.onRuntimeExec(command);
115+
} catch (final Throwable e) {
116+
module.onUnexpectedException("beforeExec threw", e);
117+
}
118+
}
119+
}
120+
121+
private static void raspCallback(String command) {
122+
ShellCmdRaspHelper.INSTANCE.beforeShellCmd(command);
123+
}
112124
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package datadog.trace.instrumentation.java.lang;
2+
3+
import static datadog.trace.api.gateway.Events.EVENTS;
4+
5+
import datadog.appsec.api.blocking.BlockingException;
6+
import datadog.trace.api.Config;
7+
import datadog.trace.api.gateway.BlockResponseFunction;
8+
import datadog.trace.api.gateway.Flow;
9+
import datadog.trace.api.gateway.RequestContext;
10+
import datadog.trace.api.gateway.RequestContextSlot;
11+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
12+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
13+
import java.util.function.BiFunction;
14+
import javax.annotation.Nonnull;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
public class ShellCmdRaspHelper {
19+
20+
public static ShellCmdRaspHelper INSTANCE = new ShellCmdRaspHelper();
21+
22+
private static final Logger LOGGER = LoggerFactory.getLogger(ShellCmdRaspHelper.class);
23+
24+
private ShellCmdRaspHelper() {
25+
// prevent instantiation
26+
}
27+
28+
public void beforeShellCmd(@Nonnull final String cmd) {
29+
if (!Config.get().isAppSecRaspEnabled()) {
30+
return;
31+
}
32+
try {
33+
final BiFunction<RequestContext, String, Flow<Void>> shellCmdCallback =
34+
AgentTracer.get()
35+
.getCallbackProvider(RequestContextSlot.APPSEC)
36+
.getCallback(EVENTS.shellCmd());
37+
38+
if (shellCmdCallback == null) {
39+
return;
40+
}
41+
42+
final AgentSpan span = AgentTracer.get().activeSpan();
43+
if (span == null) {
44+
return;
45+
}
46+
47+
final RequestContext ctx = span.getRequestContext();
48+
if (ctx == null) {
49+
return;
50+
}
51+
52+
Flow<Void> flow = shellCmdCallback.apply(ctx, cmd);
53+
Flow.Action action = flow.getAction();
54+
if (action instanceof Flow.Action.RequestBlockingAction) {
55+
BlockResponseFunction brf = ctx.getBlockResponseFunction();
56+
if (brf != null) {
57+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
58+
brf.tryCommitBlockingResponse(
59+
ctx.getTraceSegment(),
60+
rba.getStatusCode(),
61+
rba.getBlockingContentType(),
62+
rba.getExtraHeaders());
63+
}
64+
throw new BlockingException("Blocked request (for SHI attempt)");
65+
}
66+
} catch (final BlockingException e) {
67+
// re-throw blocking exceptions
68+
throw e;
69+
} catch (final Throwable e) {
70+
// suppress anything else
71+
LOGGER.debug("Exception during SHI rasp callback", e);
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.config.IastConfig
6+
import datadog.trace.api.gateway.CallbackProvider
7+
import datadog.trace.api.gateway.Flow
8+
import datadog.trace.api.gateway.RequestContext
9+
import datadog.trace.api.gateway.RequestContextSlot
10+
import datadog.trace.api.internal.TraceSegment
11+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
12+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
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 ShellCmdRaspHelperForkedTest 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(IastConfig.IAST_ENABLED, 'true')
50+
injectSysConfig(AppSecConfig.APPSEC_ENABLED, 'true')
51+
injectSysConfig(AppSecConfig.APPSEC_RASP_ENABLED, 'true')
52+
}
53+
54+
void 'test Helper'() {
55+
56+
setup:
57+
final callbackProvider = Mock(CallbackProvider)
58+
final listener = Mock(BiFunction)
59+
final flow = Mock(Flow)
60+
tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider
61+
62+
when:
63+
ShellCmdRaspHelper.INSTANCE.beforeShellCmd(*args)
64+
65+
then:
66+
1 * callbackProvider.getCallback(EVENTS.shellCmd()) >> listener
67+
1 * listener.apply(reqCtx, expected) >> flow
68+
69+
where:
70+
args | expected
71+
['&lt;!--#exec%20cmd=&quot;/bin/cat%20/etc/passwd&quot;--&gt;'] | '&lt;!--#exec%20cmd=&quot;/bin/cat%20/etc/passwd&quot;--&gt;'
72+
}
73+
}

dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,27 @@ public String ssrfQuery(@RequestParam("domain") String domain) {
5656
}
5757
return "EXECUTED";
5858
}
59+
60+
@GetMapping("/shi/cmd")
61+
public String shiCmd(@RequestParam("cmd") String cmd) {
62+
withProcess(() -> Runtime.getRuntime().exec(cmd));
63+
return "EXECUTED";
64+
}
65+
66+
private void withProcess(final Operation<Process> op) {
67+
Process process = null;
68+
try {
69+
process = op.run();
70+
} catch (final Throwable e) {
71+
// ignore it
72+
} finally {
73+
if (process != null && process.isAlive()) {
74+
process.destroyForcibly();
75+
}
76+
}
77+
}
78+
79+
private interface Operation<E> {
80+
E run() throws Throwable;
81+
}
5982
}

0 commit comments

Comments
 (0)