Very Low
CVE-2024-24942
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2024-24942
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
In JetBrains TeamCity before 2023.11.3 path traversal allowed reading data within JAR archives
Add Assessment
Ratings
-
Attacker ValueVery Low
-
ExploitabilityVery 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.
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.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- jetbrains
Products
- teamcity
References
Miscellaneous
Additional Info
Technical Analysis
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: