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
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
PUBLISH_ASSETS steps were interesting in particular as they were passed the
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
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
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
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.
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
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