Attacker Value
Very Low
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
None
Privileges Required
None
Attack Vector
Network
2

CVE-2024-24942

Disclosure Date: February 06, 2024
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

In JetBrains TeamCity before 2023.11.3 path traversal allowed reading data within JAR archives

Add Assessment

2
Ratings
  • Attacker Value
    Very Low
  • Exploitability
    Very High
Technical Analysis

CVE-2024-24942 is described by the vendor:

Path traversal allowed reading data within JAR archives.

If we decompile and diff the REST API from TeamCity 2023.11.2 (C:\TeamCity\webapps\ROOT\WEB-INF\plugins\rest-api\server\rest-api-2023.09-147486.jar) against TeamCity 2023.11.3 (C:\TeamCity\webapps\ROOT\WEB-INF\plugins\rest-api\server\rest-api-2023.09-147512.jar), we can see the SwaggerUI class has been modified.

And reading the below diff, if appears this issue lies in an attacker being able to supply an arbitrary path when getting swagger resources from an unauthenticated endpoint.

--- "a/C:\\Users\\Administrator\\Desktop\\Decomp_2023.11.2_restapi\\jetbrains\\buildServer\\server\\rest\\swagger\\SwaggerUI.java"
+++ "b/C:\\Users\\Administrator\\Desktop\\Decomp_2023.11.3_restapi\\jetbrains\\buildServer\\server\\rest\\swagger\\SwaggerUI.java"
@@ -26,7 +26,7 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.UriInfo;
-import jetbrains.buildServer.server.rest.SwaggerUIUtil;
+import jetbrains.buildServer.server.rest.swagger.SwaggerUtil;

 @Path(value="/app/rest/swaggerui")
 @Api(hidden=true)
@@ -42,7 +42,7 @@ public class SwaggerUI {
     @GET
     @Produces(value={"text/html"})
     public String serveSwaggerUI() {
-        try (InputStream input = SwaggerUIUtil.getFileFromResources("index.html");){
+        try (InputStream input = SwaggerUtil.getFileFromResources("index.html");){
             String string = StreamUtil.readText((InputStream)input, (String)"UTF-8");
             return string;
         }
@@ -62,7 +62,7 @@ public class SwaggerUI {
         if (path.equals("index.html")) {
             return this.serveSwaggerUI();
         }
-        try (InputStream input = SwaggerUIUtil.getFileFromResources(path);){
+        try (InputStream input = SwaggerUtil.getFileFromResources(path);){
             if (path.endsWith(".js") || path.endsWith(".css")) {
                 String string = StreamUtil.readText((InputStream)input, (String)"UTF-8");
                 return string;

We can see in the patched code, that SwaggerUIUtil.getFileFromResources calls a helper method isValidResourcePath. This helper method will detect the presence of double dot notation in a path, and cause an exception to be thrown, preventing the use of double dot notation during SwaggerUtil.class.getClassLoader().getResourceAsStream.

--- "a/C:\\Users\\Administrator\\Desktop\\Decomp_2023.11.2_restapi\\jetbrains\\buildServer\\server\\rest\\SwaggerUIUtil.java"
+++ "b/C:\\Users\\Administrator\\Desktop\\Decomp_2023.11.3_restapi\\jetbrains\\buildServer\\server\\rest\\swagger\\SwaggerUtil.java"
@@ -1,24 +1,245 @@
 /*
  * Decompiled with CFR 0.152.
+ *
+ * Could not load the following classes:
+ *  com.intellij.openapi.diagnostic.Logger
+ *  io.swagger.models.Model
+ *  io.swagger.models.Operation
+ *  io.swagger.models.Path
+ *  io.swagger.models.RefModel
+ *  io.swagger.models.Response
+ *  io.swagger.models.Swagger
+ *  io.swagger.models.parameters.BodyParameter
+ *  io.swagger.models.properties.ArrayProperty
+ *  io.swagger.models.properties.MapProperty
+ *  io.swagger.models.properties.Property
+ *  io.swagger.models.properties.RefProperty
+ *  org.jetbrains.annotations.NotNull
  */
-package jetbrains.buildServer.server.rest;
+package jetbrains.buildServer.server.rest.swagger;

+import com.intellij.openapi.diagnostic.Logger;
+import io.swagger.models.Model;
+import io.swagger.models.Operation;
+import io.swagger.models.Path;
+import io.swagger.models.RefModel;
+import io.swagger.models.Response;
+import io.swagger.models.Swagger;
+import io.swagger.models.parameters.BodyParameter;
+import io.swagger.models.properties.ArrayProperty;
+import io.swagger.models.properties.MapProperty;
+import io.swagger.models.properties.Property;
+import io.swagger.models.properties.RefProperty;
 import java.io.InputStream;
-import java.net.URL;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.jetbrains.annotations.NotNull;

-public class SwaggerUIUtil {
+public class SwaggerUtil {
+    private static final Logger LOG = Logger.getInstance((String)SwaggerUtil.class.getName());
     public static final String INDEX = "index.html";
-    public static final String RESOURCE_PATH = "swagger/";
+    private static final String RESOURCE_PATH = "swagger/";

-    public static InputStream getFileFromResources(String path) {
-        String fullPath = RESOURCE_PATH + path;
-        ClassLoader classLoader = SwaggerUIUtil.class.getClassLoader();
-        URL resource = classLoader.getResource(fullPath);
-        if (resource == null) {
+    static void doAnalyzeSwaggerDefinitionReferences(Swagger swagger) {
+        HashSet<String> usedReferences = new HashSet<String>();
+        for (Path path : swagger.getPaths().values()) {
+            for (Operation operation : path.getOperations()) {
+                String ref;
+                Model schema;
+                Object parameter2;
+                List parameters = operation.getParameters();
+                for (Object parameter2 : parameters) {
+                    BodyParameter bp;
+                    if (!(parameter2 instanceof BodyParameter) || !((schema = (bp = (BodyParameter)parameter2).getSchema()) instanceof RefModel)) continue;
+                    RefModel rm = (RefModel)schema;
+                    ref = rm.getSimpleRef();
+                    usedReferences.add(ref);
+                }
+                Map responses = operation.getResponses();
+                parameter2 = responses.values().iterator();
+                while (parameter2.hasNext()) {
+                    Response response = (Response)parameter2.next();
+                    schema = response.getSchema();
+                    if (!(schema instanceof RefProperty)) continue;
+                    RefProperty rp = (RefProperty)schema;
+                    ref = rp.getSimpleRef();
+                    usedReferences.add(ref);
+                }
+            }
+        }
+        Map definitions = swagger.getDefinitions();
+        ArrayDeque<String> queue = new ArrayDeque<String>();
+        queue.addAll(usedReferences);
+        while (!queue.isEmpty()) {
+            String name = (String)queue.pop();
+            Model model = (Model)definitions.get(name);
+            if (model == null) {
+                LOG.warn("Swagger definition '" + name + "' referenced but not found.");
+                continue;
+            }
+            Map properties = model.getProperties();
+            if (properties == null) continue;
+            for (Property property : properties.values()) {
+                String ref = SwaggerUtil.getPropertySimpleRef(property);
+                if (ref == null || !usedReferences.add(ref)) continue;
+                queue.add(ref);
+            }
+        }
+        int used = usedReferences.size();
+        int total = definitions.size();
+        LOG.info("Swagger definitions stats: Total=" + total + " Used=" + used);
+        if (used != total) {
+            LinkedHashSet unused = new LinkedHashSet(definitions.keySet());
+            unused.removeAll(usedReferences);
+            if (unused.size() > 30) {
+                LOG.warn("Too much unused definitions. Enable debug logs to see them");
+                LOG.debug("Unused definitions: " + unused);
+            } else {
+                LOG.info("Unused definitions: " + unused);
+            }
+        }
+    }
+
+    static <K extends Comparable<? super K>, V> Map<K, V> getOrderedMap(Map<K, V> input) {
+        if (input == null) {
+            return null;
+        }
+        LinkedHashMap<Comparable, V> sorted = new LinkedHashMap<Comparable, V>();
+        ArrayList<K> keys = new ArrayList<K>();
+        keys.addAll(input.keySet());
+        Collections.sort(keys);
+        for (Comparable key : keys) {
+            sorted.put(key, input.get(key));
+        }
+        return sorted;
+    }
+
+    private static String getPropertySimpleRef(Property property) {
+        if (property instanceof RefProperty) {
+            RefProperty rp = (RefProperty)property;
+            return rp.getSimpleRef();
+        }
+        if (property instanceof ArrayProperty) {
+            ArrayProperty ap = (ArrayProperty)property;
+            Property items = ap.getItems();
+            return SwaggerUtil.getPropertySimpleRef(items);
+        }
+        if (property instanceof MapProperty) {
+            MapProperty mp = (MapProperty)property;
+            Property items = mp.getAdditionalProperties();
+            return SwaggerUtil.getPropertySimpleRef(items);
+        }
+        return null;
+    }
+
+    @NotNull
+    public static InputStream getFileFromResources(@NotNull String path) {
+        String fullPath;
+        if (path == null) {
+            SwaggerUtil.$$$reportNull$$$0(0);
+        }
+        if (!SwaggerUtil.isValidResourcePath(fullPath = RESOURCE_PATH + path)) {
             throw new IllegalArgumentException(String.format("File %s was not found", fullPath));
         }
-        InputStream stream = classLoader.getResourceAsStream(fullPath);
-        return stream;
+        InputStream inputStream = Objects.requireNonNull(SwaggerUtil.class.getClassLoader().getResourceAsStream(fullPath));
+        if (inputStream == null) {
+            SwaggerUtil.$$$reportNull$$$0(1);
+        }
+        return inputStream;
+    }
+
+    private static boolean isValidResourcePath(@NotNull String path) {
+        if (path == null) {
+            SwaggerUtil.$$$reportNull$$$0(2);
+        }
+        return !path.contains("..") && SwaggerUtil.class.getClassLoader().getResource(path) != null;
+    }
+

We can reach this via an unauthenticated HTTP(S) GET request to the endpoint /app/rest/swaggerui, and provide an arbitrary path parameter, delineated via a semicolon character, e.g. ;/../schema.graphqls. The below curl request will hit the target endpoint.

curl -ik --path-as-is http://172.29.236.183:8111/app/rest/swaggerui;/../schema.graphqls

In a debugger, we can see the request being processed by a vulnerable TeamCity 2023.11.2 server.

stack1.png

However we were unable to successfully read a file by using double dot notation.

>curl -ik --path-as-is http://172.29.236.183:8111/app/rest/swaggerui;/../schema.graphqls
HTTP/1.1 500
TeamCity-Node-Id: MAIN_SERVER
Cache-Control: no-store
Content-Type: text/plain
Transfer-Encoding: chunked
Date: Mon, 12 Feb 2024 17:13:15 GMT
Connection: close

Error has occurred during request processing, status code: 500 (Internal Server Error).
Details: java.lang.IllegalArgumentException: File swagger/../schema.graphqls was not found
Error occurred while processing this request.

As such, I have tagged this as Difficult to weaponize and given an attacker value rating of very low. I marked Exploitability as very high, as you can reach the vulnerable code via a single unauthenticated HTTP(S) request.

CVSS V3 Severity and Metrics
Base Score:
5.3 Medium
Impact Score:
1.4
Exploitability Score:
3.9
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Attack Vector (AV):
Network
Attack Complexity (AC):
Low
Privileges Required (PR):
None
User Interaction (UI):
None
Scope (S):
Unchanged
Confidentiality (C):
Low
Integrity (I):
None
Availability (A):
None

General Information

Vendors

  • jetbrains

Products

  • teamcity

Additional Info

Technical Analysis