Skip to content

Commit 8a43c76

Browse files
committed
SHI exploit prevention on one sink for java.lang.Runtime.exec(java.lang.String)
1 parent 7c67b63 commit 8a43c76

File tree

14 files changed

+327
-11
lines changed

14 files changed

+327
-11
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
@@ -13,6 +13,7 @@
1313
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING;
1414
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT;
1515
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI;
16+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI;
1617
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI;
1718
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF;
1819
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
@@ -118,6 +119,7 @@ private void subscribeConfigurationPoller() {
118119
capabilities |= CAPABILITY_ASM_RASP_SQLI;
119120
capabilities |= CAPABILITY_ASM_RASP_SSRF;
120121
capabilities |= CAPABILITY_ASM_RASP_LFI;
122+
capabilities |= CAPABILITY_ASM_RASP_SHI;
121123
}
122124
this.configurationPoller.addCapabilities(capabilities);
123125
}
@@ -362,6 +364,7 @@ public void close() {
362364
| CAPABILITY_ASM_RASP_SQLI
363365
| CAPABILITY_ASM_RASP_SSRF
364366
| CAPABILITY_ASM_RASP_LFI
367+
| CAPABILITY_ASM_RASP_SHI
365368
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
366369
| CAPABILITY_ENDPOINT_FINGERPRINT
367370
| CAPABILITY_ASM_SESSION_FINGERPRINT

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
@@ -133,6 +133,9 @@ public interface KnownAddresses {
133133

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

136+
/** The Shell command being executed */
137+
Address<String> SHELL_CMD = new Address<>("server.sys.shell.cmd");
138+
136139
static Address<?> forName(String name) {
137140
switch (name) {
138141
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
@@ -94,6 +94,7 @@ public class GatewayBridge {
9494
private volatile DataSubscriberInfo sessionIdSubInfo;
9595
private final ConcurrentHashMap<Address<String>, DataSubscriberInfo> userIdSubInfo =
9696
new ConcurrentHashMap<>();
97+
private volatile DataSubscriberInfo shellCmdSubInfo;
9798

9899
public GatewayBridge(
99100
SubscriptionService subscriptionService,
@@ -140,6 +141,7 @@ public void init() {
140141
EVENTS.loginSuccess(), this.onUserEvent(KnownAddresses.LOGIN_SUCCESS));
141142
subscriptionService.registerCallback(
142143
EVENTS.loginFailure(), this.onUserEvent(KnownAddresses.LOGIN_FAILURE));
144+
subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd);
143145

144146
if (additionalIGEvents.contains(EVENTS.requestPathParams())) {
145147
subscriptionService.registerCallback(EVENTS.requestPathParams(), this::onRequestPathParams);
@@ -258,6 +260,31 @@ private Flow<Void> onNetworkConnection(RequestContext ctx_, String url) {
258260
}
259261
}
260262

263+
private Flow<Void> onShellCmd(RequestContext ctx_, String command) {
264+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
265+
if (ctx == null) {
266+
return NoopFlow.INSTANCE;
267+
}
268+
while (true) {
269+
DataSubscriberInfo subInfo = shellCmdSubInfo;
270+
if (subInfo == null) {
271+
subInfo = producerService.getDataSubscribers(KnownAddresses.SHELL_CMD);
272+
shellCmdSubInfo = subInfo;
273+
}
274+
if (subInfo == null || subInfo.isEmpty()) {
275+
return NoopFlow.INSTANCE;
276+
}
277+
DataBundle bundle =
278+
new MapDataBundle.Builder(CAPACITY_0_2).add(KnownAddresses.SHELL_CMD, command).build();
279+
try {
280+
GatewayContext gwCtx = new GatewayContext(true, RuleType.SHI);
281+
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
282+
} catch (ExpiredSubscriberInfoException e) {
283+
shellCmdSubInfo = null;
284+
}
285+
}
286+
}
287+
261288
private Flow<Void> onFileLoaded(RequestContext ctx_, String path) {
262289
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
263290
if (ctx == null) {

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
@@ -27,6 +27,7 @@ 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_SHI
3031
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI
3132
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF
3233
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING
@@ -271,6 +272,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
271272
| CAPABILITY_ASM_TRUSTED_IPS
272273
| CAPABILITY_ASM_RASP_SQLI
273274
| CAPABILITY_ASM_RASP_SSRF
275+
| CAPABILITY_ASM_RASP_SHI
274276
| CAPABILITY_ASM_RASP_LFI
275277
| CAPABILITY_ENDPOINT_FINGERPRINT
276278
| CAPABILITY_ASM_SESSION_FINGERPRINT
@@ -423,6 +425,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
423425
| CAPABILITY_ASM_TRUSTED_IPS
424426
| CAPABILITY_ASM_RASP_SQLI
425427
| CAPABILITY_ASM_RASP_SSRF
428+
| CAPABILITY_ASM_RASP_SHI
426429
| CAPABILITY_ASM_RASP_LFI
427430
| CAPABILITY_ENDPOINT_FINGERPRINT
428431
| CAPABILITY_ASM_SESSION_FINGERPRINT
@@ -496,6 +499,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
496499
| CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE
497500
| CAPABILITY_ASM_RASP_SQLI
498501
| CAPABILITY_ASM_RASP_SSRF
502+
| CAPABILITY_ASM_RASP_SHI
499503
| CAPABILITY_ASM_RASP_LFI
500504
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
501505
| CAPABILITY_ENDPOINT_FINGERPRINT

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ 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>> shellCmdCB
9696

9797
void setup() {
9898
callInitAndCaptureCBs()
@@ -432,6 +432,7 @@ class GatewayBridgeSpecification extends DDSpecification {
432432
1 * ig.registerCallback(EVENTS.userId(), _) >> { userIdCB = it[1]; null }
433433
1 * ig.registerCallback(EVENTS.loginSuccess(), _) >> { loginSuccessCB = it[1]; null }
434434
1 * ig.registerCallback(EVENTS.loginFailure(), _) >> { loginFailureCB = it[1]; null }
435+
1 * ig.registerCallback(EVENTS.shellCmd(), _) >> { shellCmdCB = it[1]; null }
435436
0 * ig.registerCallback(_, _)
436437

437438
bridge.init()
@@ -834,6 +835,26 @@ class GatewayBridgeSpecification extends DDSpecification {
834835
gatewayContext.isRasp == true
835836
}
836837

838+
void 'process shell cmd'() {
839+
setup:
840+
final cmd = '&lt;!--#exec%20cmd=&quot;/bin/cat%20/etc/passwd&quot;--&gt;'
841+
eventDispatcher.getDataSubscribers({ KnownAddresses.SHELL_CMD in it }) >> nonEmptyDsInfo
842+
DataBundle bundle
843+
GatewayContext gatewayContext
844+
845+
when:
846+
Flow<?> flow = shellCmdCB.apply(ctx, cmd)
847+
848+
then:
849+
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
850+
{ a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE }
851+
bundle.get(KnownAddresses.SHELL_CMD) == cmd
852+
flow.result == null
853+
flow.action == Flow.Action.Noop.INSTANCE
854+
gatewayContext.isTransient == true
855+
gatewayContext.isRasp == true
856+
}
857+
837858
void 'calls trace segment post processor'() {
838859
setup:
839860
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
@@ -158,4 +158,27 @@ public ResponseEntity<String> session(final HttpServletRequest request) {
158158
final HttpSession session = request.getSession(true);
159159
return new ResponseEntity<>(session.getId(), HttpStatus.OK);
160160
}
161+
162+
@GetMapping("/shi/cmd")
163+
public String shiCmd(@RequestParam("cmd") String cmd) {
164+
withProcess(() -> Runtime.getRuntime().exec(cmd));
165+
return "EXECUTED";
166+
}
167+
168+
private void withProcess(final Operation<Process> op) {
169+
Process process = null;
170+
try {
171+
process = op.run();
172+
} catch (final Throwable e) {
173+
// ignore it
174+
} finally {
175+
if (process != null && process.isAlive()) {
176+
process.destroyForcibly();
177+
}
178+
}
179+
}
180+
181+
private interface Operation<E> {
182+
E run() throws Throwable;
183+
}
161184
}

0 commit comments

Comments
 (0)