Show Notes

95 - Discourse SNS RCE, a Stored XSS in GitLab, and a Reddit Race Condition

An unauthenticated file read in GoCD’s Business Continuity Addon (installed and enabled by default) due to change in configuration that that exposed the add-on to unauthenticated users.

Prior to a 2018 patch the /add-on/*/admin/** and /add-on/*/api/** endpoints required authentication though any other endpoints required the add-ons to perform their own authentication. After this patch all /add-on/** would all unauthorized access. This ultimately exposed some endpoints that might have otherwise been privileged off, there are three of note:

  1. /plugin as handled by PrimaryStatusProviderController.java had a trivial directory traversal vulnerability. While the folderName parameter was checked against a whitelist, the pluginName which was appended, was not validated at all allowing for arbitrary files to be read.
  2. /cipher.aes would expose the private key used to encrypt secrets such as tokens.
  3. /cruise_config exposed the primary application configuration, this includes secrets for agent registration, webhooks, and token generation

Together these could be used by an unauthenticated attacker to gain access to further authenticated endpoints within the application.

When purchasing coins for Reddit on Android there is a call to a /verify_purchase endpoint which is vulnerable to a race condition. The idea being that this endpoint, being provided some of the transaction information would validate it and give the coins to the purchaser, however there is a problem when handling multiple concurrent requests to endpoint. Allowing for multiple threads to end the “add coins” area of the code with a single on a single transaction. This is fundamentally a “Time-of-Check Time-of-Use” (TOCTOU) style issue. In Reddit’s codebase there is likely some code similar to the following:

transaction = lookup_transaction(transaction_id)
if not transaction.finalized:
    give_user_coins(transaction.purchaser, transaction.coin_amount)
    finalize_transaction(transaction)

The issue being that without any synchronization between threads you could have multiple requests hit that if statement before any has reached the finalize_transaction call; and so multiple threads will give the user coins.

Mermaid is a markdown-like syntax for generating flowcharts and is supported by GitLab’s markdown parser. The Mermaid parser itself can be provided various configuration options at initialization time, and then some of those can be overloaded by inline directives. There are some directives that cannot be overloaded, such as secure and securityLevel as that would defeat their purpose. One configuration option that could be overloaded by an inline directive is htmlLabels which leads to the abilit to place arbitrary HTML in the labels by including the following line:

%%{init: {"flowchart": {"htmlLabels": "false"}} }%%

Content Security Policy (CSP) Bypass

GitLab used the following CSP:

script-src 'self' 'unsafe-inline' 'unsafe-eval' https://assets.gitlab-static.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/ https://apis.google.com 'nonce-<nonce>'

The problem bypass in this case was to take advantage of the ability to serve files (pipeline artifacts) on the same domain, and these files were served with an appropiate Content-Type header (gets around X-Content-Type: nosniff). While they were served with a Content-Disposition: attachment meaning they couldn’t be used in some cases, here it could be included as a script source.

The last trick involved was that you cannot use a .innerHTML write to inject a <script> tag, so the author opted to inject an <iframe srcdoc='<script ...></script></iframe>'

Discourse exposes a webhook that takes a user-provided “subscribe URL” and passes it into open() unsanitized. Due to Discourse being written in Ruby, it’s possible to get command execution via the subscribe URL by way of the pipeline operator. However, the payload containing this URL is supposed to be signed by AWS, and a valid and verified certificate URL must be provided for the request to succeed.

Various checks are performed on the certificate URL, including verifying it’s HTTPS, and ensuring the URL pattern matches with the sns.*.amazonaws.com host. The path must also be a .pem extension. It’s possible that an attacker can reflect a fake certificate in an AWS page to bypass these checks and provide a seemingly valid signature. Further, because Ruby’s X509 certificate parser does loose parsing, it won’t reject a certificate with unrecognized data before and after the cert. So by getting a fake cert reflected in a page’s contents, these checks can be bypassed and a malicious payload can be signed.

Initially, they tried abusing the error page’s reflection of an invalid action to fake a cert. This wasn’t sufficient though, because an error page would serve a 400 error code, and the certificate request needed to receive a 200 OK. The final attack was to use the GetEndpointAttributes method, which allows you to pass a CustomUserData parameter that gets reflected in the page. This allows an attacker to bypass all the necessary checks and exploit the open() call to launch a reverse shell.

Mermaid is a markdown-like syntax for generating flowcharts and is supported by GitLab’s markdown parser. The Mermaid parser itself can be provided various configuration options at initialization time, and then some of those can be overloaded by inline directives. There are some directives that cannot be overloaded, such as secure and securityLevel as that would defeat their purpose. One configuration option that could be overloaded by an inline directive is htmlLabels which leads to the abilit to place arbitrary HTML in the labels by including the following line:

%%{init: {"flowchart": {"htmlLabels": "false"}} }%%

Content Security Policy (CSP) Bypass

GitLab used the following CSP:

script-src 'self' 'unsafe-inline' 'unsafe-eval' https://assets.gitlab-static.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/ https://apis.google.com 'nonce-<nonce>'

The problem bypass in this case was to take advantage of the ability to serve files (pipeline artifacts) on the same domain, and these files were served with an appropiate Content-Type header (gets around X-Content-Type: nosniff). While they were served with a Content-Disposition: attachment meaning they couldn’t be used in some cases, here it could be included as a script source.

The last trick involved was that you cannot use a .innerHTML write to inject a <script> tag, so the author opted to inject an <iframe srcdoc='<script ...></script></iframe>'