143 - Cloudflare Pages, Hacking a Bank, and Attacking Price Oracles
Five vulnerabilities in Cloudflare Pages across 3 blog posts. Three vulns are command injection, one is a container escape, and one is a lack of access control.
**Part 1 - Two command injections in CLONE_REPO
and PUBLISH_ASSETS
build steps
They focused their early reserach on azure pipelines, and the build_tool
python script that would get ran by the workflow. The CLONE_REPO
and PUBLISH_ASSETS
steps were interesting in particular as they were passed the GITHUB_PROD_PRIVATE_KEY
and CF_PROD_API_TOKEN
through the environment. The CLONE_REPO
step would use a user-controllable root_dir
parameter to build up a path that gets passed into a mv
command. This is done purely with string concatenation and is vulnerable to command injection. This same issue existed in the PUBLISH_ASSETS
step with the asset path.
Exploiting these issues allowed them to escalate privileges from the unprivileged buildbot
user to AzDevops
, which was effectively root (inside the container), as AzDevops
had passwordless sudo access. Furthermore, with the ability to dump the environment, they got the github and cloudflare API private keys. It turns out the cloudflare API token was not scoped to just that project, but had global access. This allowed them to get access to the repositories of all 18k users of Cloudflare pages!
Part 2 - Container escape/jailbreak
In part 2, they found another command injection directly in the workflow through the account_env
variable. They try to pass this variable to the build script with no special care against command injection, again giving them root access inside the container. They wanted to take it further to escape the container, and discovered docker was being used. They also found that the docker socket was accessible from inside the container via /var/run/docker.socket
.
From there, they were able to use docker to create a privileged container that used the host namespace and mounted the host filesystem inside the container, which they could use to list all cloudflare users and access build history for all users.
Part 3 - Revisit after infrastructure overhaul After many months, assetnote received a notice from cloudflare that their new pages environment could be opt’d into for testing. The environment was very different, not even using Azure pipelines anymore and moving to GKE / Kubernetes. Furthermore, the build scripts are no longer readable, and a lot of previous privesc vectors were killed by GKE and gVisor’s stronger sandboxing.
They discovered an internal kubelet API endpoint running on :10255 after some port scanning and API enumeration. This endpoint was accessible to unprivileged, and contained the git access token. Unlike before though, this token only works for that organization’s repositories, so impact of this issue is much lower.
Two fundamental issues allowing for XSS in Ruby on Rails (RoR) applications. As RoR is just a framework, these all depend on an application using the framework in a way that exposes these vulnerabilities.
There were two reported base issues that were present across multiple methods. The first set of issues is with the options
argument to methods from FormTagHelper
. In the options
argument if an attacker is able to control a key in the option dictionary/hashmap passed to the data
field, aria
field, or passed in directly it would be possible to provide a malicious input that escapes any sanitization.
The second set of issues has the same issue with the options
but for the generic tag
and content_tag
methods from TagHelper
, but it also is vulnerable for the first argument, the tag name.
The original report is limited on details about why this happened but taking a look at the patch. It appears that it simply was only escaping the values but not the keys. So straight forward exploitation, but a bit surprising it wasn’t caught sooner.
This starts off in a pretty straight-forward way with an arbitrary file upload vulnerability, but also includes a bit of discussion about exploiting it in a more hardened environment which had some interesting insight.
On the vulnerability-side, the handlers for multipart PUTs and POSTs had optional authentication. The default configuration was to set CONTENT_APIS_ALLOW_ANONYMOUS=WRITE
which would allow an anonymous user to use these endpoints. In addition there was no sanitization of the provided filename, so while the final write location is prepended with the temporary directory, one could escape out and write a malicious JSP.
Unfortunately for the authors, but good on the bank there were some additional layers of defence at play, two in particular:
First was discovering where to upload a file to that would matter and be writable.The authors here used a fun little trick of writting through /proc/self/cwd
to get the working directory of the execution agent. So ending up inside of the tomcat hierarchy, avoiding the need to guess where it was located. Their intent was to write to ROOT/html
but this was hardened by the bank and could not be written to.
To get around this, they took another route that I cannot recall ever seeing before. Its not a universla technique as it is going to depend on how your particular server deals with caching and etags but they retrieved the etag for a javascript file. And then wrote to the local path for that cached version of the JavaScript file to get a stored XSS. For tomcat this ended up being in the form of FIRST-CHAR/SECOND-CHAR/FULL-ETAG/fileAsset/FILE-NAME
but if you want to try this technique out you’ll need to determine what it is for your target’s server.
An access control issue in a fallback price oracle contract. Under normal circumstances, Aave V3 will try to use chainlink oracle for getting price information. However, if that fails and returns a null value, they have a fallback contract that gets called. This fallback contract’s setAssetPrice()
method has no access control, and can be called by an attacker to manipulate the price. Exploiting this issue might be tricky, as you’d have to force the fallback contract to be invoked. The authors propose two scenarios this could happen. One is due to Aave’s use of a deprecated function (latestAnswer()
), which could fail to reach an answer and return 0. The other scenario is if an asset is loaded as collateral before the price feed of that asset is configured.
This seemed to be a test contract that accidentally made it into production due to the fact the contract was in a mocks/
directory.
Seems like a case of a generic endpoint being implemented up update any field provided without consideration of other restrictions on said field. In this case we have a PATCH /api/v2.0/accounts/<account_id>
endpoint which ultimately takes in a dictionary containing field/value pairs to be updated for the account id. By editing the request to include the email
field, it can be updated to any new value without going through the normal verification process. While I cannot be sure without seeing Reddit’s code this seems to me like the endpoint probably just takes the provided fields checks if it exists and updates it creating this situation where you could provide unexpected fields to be updated.