CVE-2024-23683 (published 2024-01-19T21:15Z)

Artemis Java Test Sandbox versions less than 1.7.6 are vulnerable to a sandbox escape when an attacker crafts a special subclass of InvocationTargetException. An attacker can abuse this issue to execute arbitrary Java when a victim executes the supposedly sandboxed code.\n\n\n\n\n\n

CVSS v3 base score: 8.2, HIGH (CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/ls1intum/Ares
commit_details: repo_url: https://github.com/ls1intum/Ares
author: Christian Femers
author_date: 2022-01-17 13:40:35+01:00
msg: Merge pull request from GHSA-883x-6fch-6wjx

* Blacklist ReflectionUtils.getUnderlyingCause as it is in the invocation

This means that trusted classes are no longer have a fully whitelisted
stack when the cause is fetched and exception methods are invoked.

* Allow security manager to work with method names when checking the stack

* Add security test for a malicious InvocationTargetException

A variant of the exploit found by Daniel, related to GHSA-883x-6fch-6wjx

Co-authored-by: Daniel Kirschten <melodicahaspa@gmail.com>
Co-authored-by: Christian Femers <c.femers@tum.de>

Co-authored-by: Daniel Kirschten <melodicahaspa@gmail.com>
repository: repo_url: https://github.com/ls1intum/Ares
repo_name: ls1intum/Ares
description: The Artemis Java Test Sandbox. A JUnit 5 Extension for Easy and Secure Artemis Java Testing
repo_language: Java
owner: ls1intum
file_changes: filename: ArtemisSecurityManager.java
change_type: ModificationType.MODIFY
diff: @@ -511,19 +511,20 @@ public final class ArtemisSecurityManager extends SecurityManager {
 		return !AccessController.class.getName().equals(stackFrame.getClassName());
 	}
 
-	private boolean isCallNotWhitelisted(String call) {
+	private boolean isCallNotWhitelisted(String className, String methodName) {
+		String call = className + "." + methodName; //$NON-NLS-1$
 		return SecurityConstants.STACK_BLACKLIST.stream().anyMatch(call::startsWith)
 				|| (SecurityConstants.STACK_WHITELIST.stream().noneMatch(call::startsWith)
-						&& (configuration == null || !(configuration.whitelistedClassNames().contains(call)
-								|| configuration.trustedPackages().stream().anyMatch(pm -> pm.matches(call)))));
+						&& (configuration == null || !(configuration.whitelistedClassNames().contains(className)
+								|| configuration.trustedPackages().stream().anyMatch(pm -> pm.matches(className)))));
 	}
 
 	private boolean isStackFrameNotWhitelisted(StackFrame sf) {
-		return isCallNotWhitelisted(sf.getClassName());
+		return isCallNotWhitelisted(sf.getClassName(), sf.getMethodName());
 	}
 
 	private boolean isStackFrameNotWhitelisted(StackTraceElement ste) {
-		return isCallNotWhitelisted(ste.getClassName());
+		return isCallNotWhitelisted(ste.getClassName(), ste.getMethodName());
 	}
 
 	public static Optional<StackTraceElement> firstNonWhitelisted(StackTraceElement... elements) {

programming_language: Java
filename: SecurityConstants.java
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,8 @@ public final class SecurityConstants {
 	static final Set<String> STACK_WHITELIST = Set.of("java.", "org.junit.", "jdk.", "org.eclipse.", "com.intellij", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
 			"org.assertj", "org.opentest4j.", "com.sun.", "sun.", "org.apache.", "de.tum.in.test.", "net.jqwik", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
 			"ch.qos.logback", "org.jacoco", "javax.", "org.json", SECURITY_PACKAGE_NAME); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
-	static final Set<String> STACK_BLACKLIST = Set.of(BlacklistedInvoker.class.getName());
+	static final Set<String> STACK_BLACKLIST = Set.of(BlacklistedInvoker.class.getName(),
+			"org.junit.platform.commons.util.ReflectionUtils.getUnderlyingCause"); //$NON-NLS-1$
 
 	static final Set<String> PACKAGE_USE_BLACKLIST = Set.of(SECURITY_PACKAGE_NAME, "de.tum.in.test.api.internal", //$NON-NLS-1$
 			"jdk.internal", "sun."); //$NON-NLS-1$ //$NON-NLS-2$

programming_language: Java
filename: SecurityTest.java
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@ class SecurityTest {
 	private final String testExecuteGit = "testExecuteGit";
 	private final String testMaliciousExceptionA = "testMaliciousExceptionA";
 	private final String testMaliciousExceptionB = "testMaliciousExceptionB";
+	private final String testMaliciousInvocationTargetException = "testMaliciousInvocationTargetException";
 	private final String testNewClassLoader = "testNewClassLoader";
 	private final String testNewSecurityManager = "testNewSecurityManager";
 	private final String tryManageProcess = "tryManageProcess";
@@ -80,6 +81,12 @@ class SecurityTest {
 		tests.assertThatEvents().haveExactly(1, testFailedWith(testMaliciousExceptionB, SecurityException.class));
 	}
 
+	@TestTest
+	void test_testMaliciousInvocationTargetException() {
+		tests.assertThatEvents().haveExactly(1,
+				testFailedWith(testMaliciousInvocationTargetException, SecurityException.class));
+	}
+
 	@TestTest
 	void test_testNewClassLoader() {
 		tests.assertThatEvents().haveExactly(1, testFailedWith(testNewClassLoader, SecurityException.class));

programming_language: Java
filename: SecurityUser.java
change_type: ModificationType.MODIFY
diff: @@ -77,6 +77,11 @@ public class SecurityUser {
 		assertFalse(SecurityPenguin.maliciousExceptionB());
 	}
 
+	@Test
+	void testMaliciousInvocationTargetException() throws Exception {
+		SecurityPenguin.maliciousInvocationTargetException();
+	}
+
 	@Test
 	void testNewClassLoader() throws IOException {
 		SecurityPenguin.newClassLoader();

programming_language: Java
filename: SecurityPenguin.java
change_type: ModificationType.MODIFY
diff: @@ -19,6 +19,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.xyz.Circumvention;
 import org.apache.xyz.FakeTrustedClass;
 import org.apache.xyz.MaliciousExceptionB;
+import org.apache.xyz.MaliciousInvocationTargetException;
 
 import de.tum.in.test.api.io.IOTester;
 
@@ -49,6 +50,10 @@ public final class SecurityPenguin {
 		return ab.get();
 	}
 
+	public static void maliciousInvocationTargetException() throws Exception {
+		throw new MaliciousInvocationTargetException();
+	}
+
 	@SuppressWarnings("resource")
 	public static void newClassLoader() throws IOException {
 		new URLClassLoader(new URL[0]).close();

programming_language: Java
filename: MaliciousInvocationTargetException.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,22 @@
+package org.apache.xyz;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class MaliciousInvocationTargetException extends InvocationTargetException {
+
+	private static final long serialVersionUID = 1L;
+
+	@Override
+	public Throwable getTargetException() {
+		try {
+			Files.readString(Path.of("pom.xml"));
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+		return new Error("succeeded");
+	}
+}

programming_language: Java