Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .github/workflows/validate-privilege-escalation-paths.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ on:
pull_request:
paths:
- 'src/privilege-escalation-paths/**'
- 'src/privilege-escalation-paths-cspm-reviewed/**'
- 'src/privilege-escalation-paths-research/**'
- 'scripts/privesc_paths_schema.py'
- 'scripts/validate_privesc_paths.py'
- '.github/workflows/validate-privilege-escalation-paths.yml'
push:
branches: [master]
paths:
- 'src/privilege-escalation-paths/**'
- 'src/privilege-escalation-paths-cspm-reviewed/**'
- 'src/privilege-escalation-paths-research/**'
- 'scripts/privesc_paths_schema.py'
- 'scripts/validate_privesc_paths.py'
workflow_dispatch:
Expand All @@ -25,3 +29,5 @@ jobs:
python-version: '3.12'
- run: python -m pip install PyYAML
- run: python scripts/validate_privesc_paths.py --book-root .
- run: python scripts/validate_privesc_paths.py --book-root . --catalog-path src/privilege-escalation-paths-cspm-reviewed
- run: python scripts/validate_privesc_paths.py --book-root . --catalog-path src/privilege-escalation-paths-research
11 changes: 11 additions & 0 deletions scripts/test_validate_privesc_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,14 @@ def test_source_errors_checks_techniques_and_exclusions(tmp_path):
assert any('scopescloud-platform' in error for error in errors)
assert any('discover-secrets-in-etcd' in error for error in errors)
assert any('not-a-heading' in error for error in errors)


def test_source_errors_preserves_angle_brackets_in_inline_code(tmp_path):
page = tmp_path / 'src' / 'page.md'
page.parent.mkdir()
page.write_text('## Pivot via `<send-request>`\n', encoding='utf-8')
catalog = tmp_path / 'src' / 'privilege-escalation-paths' / 'azure'
catalog.mkdir(parents=True)
(catalog / '_excluded.yaml').write_text(
'- source: src/page.md#pivot-via-send-request\n', encoding='utf-8')
assert source_errors(tmp_path, catalog.parent) == []
8 changes: 7 additions & 1 deletion scripts/validate_privesc_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import yaml

PATH = 'src/privilege-escalation-paths'
CATALOG_PATHS = (PATH, 'src/privilege-escalation-paths-cspm-reviewed',
'src/privilege-escalation-paths-research')
PROVIDERS = ('aws', 'gcp', 'azure', 'k8s')
FILE_NAME = re.compile(r'^_?[a-z0-9][a-z0-9_.-]*\.yaml$')
ROOT_EXTRAS = re.compile(r'^(README\.md|[A-Za-z0-9_.-]+\.schema\.json)$')
Expand All @@ -31,6 +33,8 @@
def heading_slug(title):
"""Use the same heading slug rules as the Blue-CloudPEASS source audit."""
title = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', title)
# Angle brackets inside inline code are literal operation names, not HTML tags.
title = re.sub(r'`([^`]*)`', lambda match: match.group(1).replace('<', '').replace('>', ''), title)
title = re.sub(r'<[^>]+>', '', title).strip().lower()
return ''.join(ch if ch.isalnum() or ch in '_-' else '-' if ch.isspace() else '' for ch in title)

Expand Down Expand Up @@ -102,9 +106,11 @@ def main():
parser.add_argument('--book-root', type=Path, default=Path('.'))
parser.add_argument('--peaso-root', type=Path,
help='optional Blue-CloudPEASS checkout to compare the loader and exported schema')
parser.add_argument('--catalog-path', choices=CATALOG_PATHS, default=PATH,
help='catalog directory to validate (default: current reviewed paths)')
args = parser.parse_args()
book = args.book_root.resolve()
root = book / PATH
root = book / args.catalog_path
spec = importlib.util.spec_from_file_location('privesc_paths_schema', book / 'scripts/privesc_paths_schema.py')
schema = importlib.util.module_from_spec(spec)
spec.loader.exec_module(schema)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The structural fact that makes it an escalation target: **there is no Analysis S

**Minimum permissions:** `Microsoft.AnalysisServices/servers/write` on the target server (Contributor/Owner — no AS-specific role exists).

**Potential Impact:** the attacker appends their own identity to `properties.asAdministrators.members`, becoming a **server (and therefore database) administrator**. They then connect to `asazure://<region>/<server>` over XMLA/DAX and **read and export every tabular model** on the server — the full BI dataset, bypassing any per-model security roles and row-level security. Escalates from control-plane RBAC to complete BI data access.
**Potential Impact:** the attacker adds a user or service principal they control in the server's Microsoft Entra tenant to `properties.asAdministrators.members`, becoming a **server (and therefore database) administrator**. A managed identity cannot sign in to Azure Analysis Services, so control of only a managed identity does not establish this data-plane access without another usable principal. Once the server is running and reachable, the new administrator can connect over XMLA/DAX and **read and export hosted tabular models**, bypassing per-model security roles and row-level security. Escalates from control-plane RBAC to BI data access.

**Stealth:** **High** for the exfil — the XMLA reads hit only the off-by-default `Engine`/`Service` logs. The `servers/write` that grants access is control-plane and logged, but the read itself is not.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ For more information about API Management access:

The permission strings below are Azure RBAC operations for the `Microsoft.ApiManagement` resource provider. Their documented effects are used to distinguish read, secret-retrieval, write, and network-configuration capabilities.<sup>[[1]](#references)</sup>

## `Microsoft.ApiManagement/service/users/token/action`
## `Microsoft.ApiManagement/service/users/generateSsoUrl/action`

**Minimum permissions:** `Microsoft.ApiManagement/service/users/token/action`
**Minimum permissions:** `Microsoft.ApiManagement/service/users/generateSsoUrl/action` on the selected user.

**Potential Impact:** Generates an SSO URL with a token that logs in as a chosen developer-portal user, inheriting that user's API subscriptions and portal visibility (not an Azure control-plane token; unavailable on Consumption tier).

Expand All @@ -27,6 +27,14 @@ az rest --method POST \

The portal session inherits that user's API subscriptions and visibility; it is not an Azure control-plane token. The operation is unavailable on the Consumption tier. See [User - Generate SSO URL](https://learn.microsoft.com/en-us/rest/api/apimanagement/user/generate-sso-url?view=rest-apimanagement-2024-05-01).

## `Microsoft.ApiManagement/service/users/token/action`

**Minimum permissions:** `Microsoft.ApiManagement/service/users/token/action` on the selected user.

**Potential Impact:** Returns a user shared-access token that may sign in as that developer-portal user, with access to the user's API subscriptions and portal visibility.

Microsoft documents using this token with `/signin-sso` on a self-hosted developer portal and in a delegated sign-in flow to a managed developer portal. This operation is separate from `generateSsoUrl/action`; it does not authorize the `generateSsoUrl` ARM route. See [Get Shared Access Token](https://learn.microsoft.com/en-us/rest/api/apimanagement/user/get-shared-access-token?view=rest-apimanagement-2024-05-01), [developer portal SSO FAQ](https://learn.microsoft.com/en-us/azure/api-management/developer-portal-faq#how-do-i-enable-single-sign-on-sso-authentication-to-self-hosted-developer-portal), and [delegated sign-in](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-setup-delegation#delegating-user-registration-and-sign-in).

<details>
<summary>Logs generated</summary>

Expand Down Expand Up @@ -689,6 +697,8 @@ az rest --method PUT \
--body '{"properties":{"format":"rawxml","value":"<policies><inbound><base /><authentication-managed-identity resource=\"https://management.azure.com/\" output-token-variable-name=\"mi\" ignore-error=\"false\" /><return-response><set-status code=\"200\" /><set-body>@((string)context.Variables[\"mi\"])</set-body></return-response></inbound><backend><base /></backend><outbound><base /></outbound></policies>"}}'
```

Azure RBAC may grant policy editing on one API rather than the whole APIM service. A child-scoped grant reaches the service identity only through a policy on that API or one of its operations; a product-scoped grant similarly applies to that product. The policy still has to run on a request, and its token must be delivered to the editor or a destination they control. See [APIM role-based access control](https://learn.microsoft.com/en-us/azure/api-management/api-management-role-based-access-control) and [managed identity security considerations](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity#security-considerations-for-managed-identities).

> [!NOTE]
> Scope the policy to a single obscure operation and revert it after collecting the token to minimize the config-diff window.

Expand Down Expand Up @@ -790,9 +800,9 @@ az rest --method PUT \
</details>

## `Microsoft.ApiManagement/service/tenant/deploy/action` — bulk config overwrite via built-in Git
**Minimum permissions:** `Microsoft.ApiManagement/service/tenant/deploy/action` (and/or `service/tenant/save/action`). Built-in role **API Management Service Contributor**. Requires the built-in Git configuration repository to be enabled on the target service + Git credentials (obtainable via the documented `tenant/listSecrets` gitAccess).
**Historical permissions:** `Microsoft.ApiManagement/service/tenant/deploy/action` and `service/tenant/save/action` were used with the built-in Git configuration repository.

**Potential Impact:** APIM's built-in Git configuration repository lets `tenant/save` snapshot live config into Git and `tenant/deploy` **push a whole Git branch's configuration live in one action** — policies across all scopes, named values, backends, products. An attacker with Git write access plus `deploy` performs mass policy injection / backdoor persistence across the entire service atomically, and a single logged event masks sweeping changes.<sup>[[22]](https://learn.microsoft.com/en-us/azure/api-management/api-management-configuration-repository-git)</sup>
**Current status:** Microsoft retired built-in Git configuration save and deploy on 15 March 2025. These permission names must not be treated as an active path for changing policies or reaching an APIM managed identity. Before retirement, a principal with Git write access and `tenant/deploy` could publish a branch's APIM configuration, including policies. Use current policy-write permissions to assess that identity path. See [Microsoft's retirement notice](https://learn.microsoft.com/en-us/azure/api-management/breaking-changes/git-configuration-retirement-march-2025).

<details>
<summary>Logs generated</summary>
Expand Down Expand Up @@ -825,9 +835,9 @@ az rest --method PUT \
</details>

## `Microsoft.ApiManagement/service/backup/action` — full-service exfiltration to attacker storage
**Minimum permissions:** `Microsoft.ApiManagement/service/backup/action` (built-in role **API Management Service Contributor**). Sibling `service/restore/action` = overwrite/rollback tamper.
**Minimum permissions:** `Microsoft.ApiManagement/service/backup/action` (built-in role **API Management Service Contributor**). The separate `service/restore/action` restores an APIM-created backup over the service.

**Potential Impact:** Backs up the **entire APIM service** — APIs, policies, products, groups, users, subscriptions (with keys), certificates and named values — into an **attacker-specified Azure Storage container** (the call body takes the target `storageAccount`/`containerName` + SAS or MI). One call exfiltrates the whole secret-bearing configuration to attacker-controlled storage, sidestepping per-resource `listSecrets` enumeration entirely. `restore/action` conversely re-imports a tampered backup, overwriting live config (bulk persistence / destructive tamper).<sup>[[17]](https://learn.microsoft.com/en-us/rest/api/apimanagement/api-management-service/backup?view=rest-apimanagement-2024-05-01)</sup>
**Potential Impact:** Backs up the **entire APIM service** — APIs, policies, products, groups, users, subscriptions (with keys), certificates and named values — into an **attacker-specified Azure Storage container** (the call body takes the target `storageAccount`/`containerName` and either a storage access key or APIM managed identity). One call exfiltrates the whole secret-bearing configuration to attacker-controlled storage, sidestepping per-resource `listSecrets` enumeration entirely. `restore/action` can overwrite live configuration with a valid APIM-created backup whose policies were already changed. An arbitrary edited backup blob is not established as restorable.<sup>[[17]](https://learn.microsoft.com/en-us/rest/api/apimanagement/api-management-service/backup?view=rest-apimanagement-2024-05-01)</sup>

<details>
<summary>Logs generated</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ For store deletion and purge, see [App Configuration post exploitation](../az-po

**Potential Impact:** a snapshot is an **immutable, point-in-time capture of every matching key-value** in one object. With catch-all filters, `snapshots/write` can capture the entire store (all app secrets, endpoints, connection material, and feature flags); otherwise it captures only the selected key/label subset. `snapshots/read` returns the captured data. Because [snapshots are immutable and can only be created or archived](https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-snapshots), a created snapshot is also a **loot-persistence primitive**: deleting or rotating live values does not modify the captured copy.

A privilege-escalation path exists only when a named, readable snapshot actually contains a still-usable credential for an identified, more privileged principal. Snapshot read permission or a secret-looking key name alone does not establish that path. A Key Vault reference exposes its URI, not the referenced secret.

**Stealth:** medium — the snapshot object lingers and is visible to anyone listing snapshots, but its creation/read over the data plane is not in the Activity Log.

> [!WARNING]
Expand Down
Loading
Loading