Bricksforge CVE-2026-85097 Active RCE — Sentinel Hunts

Threat Overview

Patchstack published its primary research on October 8, 2026, and The Hacker News reported the activity on October 12. CVE-2026-85097 (CVSS 10.0) affects the Bricksforge extension for WordPress/Bricks Builder. An unauthenticated form-upload workflow validates an image at upload time but later trusts client-supplied `temporaryFileUploads` metadata, allowing the destination URL to carry a PHP extension. Patchstack observed active exploitation attempts from October 7, 21:47 UTC. The reported chain involves nonce retrieval, an image/PHP polyglot, a crafted form submission, and a request to the resulting PHP file. This is a demonstrated arbitrary-file-upload-to-code-execution condition; telemetry describing exploit attempts does not prove that every targeted site was compromised.

References

Impacted Systems

  • **Vendor/product:** Bricksforge WordPress plugin (extension for Bricks Builder), Pro Forms upload/form-submit functionality.
  • **Affected:** Bricksforge **3.1.8.9 and earlier**; **fixed in 3.1.8.10** (later vendor versions also contain the fix). Other WordPress plugins are not implied affected by this CVE.
  • **Platform/role:** Self-managed or hosted WordPress/PHP website running the plugin; Internet-accessible form endpoints over HTTP/HTTPS.
  • **Prerequisite:** Vulnerable Bricksforge plugin with reachable upload/form submission handling; no authenticated account required. The exact hosting OS is not a vulnerability condition.

Why this matters

An Internet-facing unauthenticated RCE path can yield webshell access and sensitive data exposure across many independently managed WordPress sites. Form endpoints are frequently accessible to the public and attacks can be automated.

Exploitation Status

**Confirmed by Patchstack:** exploit attempts observed from 63 source IPs beginning 2026-10-07; the form-submit REST endpoint and `bricksforge_form_submit` AJAX action were targeted. **Researcher assessment:** two request clusters indicate automated/distributed activity; attribution to one actor is not established. **Not confirmed:** compromise of a particular MSSP customer or a specific post-exploitation payload in the customer environment.

What this hunt looks for

AzureDiagnostics WAF request metadata, CommonSecurityLog URL-aware perimeter events, Defender DeviceFileEvents/DeviceProcessEvents/DeviceNetworkEvents for onboarded web servers, and Syslog only where web or file audit messages are actually forwarded.

Required logs

AzureDiagnostics, CommonSecurityLog, DeviceFileEvents, DeviceNetworkEvents, DeviceProcessEvents, Syslog

Hunt 1: First pass — Bricksforge form-submit requests and exploit metadata

AzureDiagnostics
| where TimeGenerated >= ago(14d)
| extend Uri=tolower(strcat(tostring(column_ifexists("requestUri_s",""))," ",tostring(column_ifexists("originalRequestUriWithArgs_s",""))))
| extend Detail=tolower(strcat(tostring(column_ifexists("details_message_s",""))," ",tostring(column_ifexists("requestBody_s",""))))
| where Uri contains "/wp-json/bricksforge/v1/form_submit" or (Uri contains "/wp-admin/admin-ajax.php" and (Uri contains "bricksforge_form_submit" or Detail contains "bricksforge_form_submit"))
| project TimeGenerated, Resource=Resource, ClientIP=tostring(column_ifexists("clientIP_s","")), Uri, Detail, Action=tostring(column_ifexists("action_s","")), Status=tostring(column_ifexists("httpStatus_d",""))
| order by TimeGenerated desc

Hunt 2: WAF — nonce requests followed by form submissions from same source

let Hits=AzureDiagnostics
| where TimeGenerated >= ago(14d)
| extend Src=tostring(column_ifexists("clientIP_s","")), Uri=tolower(strcat(tostring(column_ifexists("requestUri_s",""))," ",tostring(column_ifexists("originalRequestUriWithArgs_s",""))))
| where Uri contains "bricksforge_regenerate_nonce" or Uri contains "bricksforge/v1/form_submit" or Uri contains "bricksforge_form_submit"
| extend Step=iff(Uri contains "regenerate_nonce","Nonce","Form")
| summarize Nonce=countif(Step=="Nonce"), Forms=countif(Step=="Form"), URIs=make_set(Uri,8) by Src, Resource, bin(TimeGenerated,30m)
| where Nonce > 0 and Forms > 0
| order by Forms desc

Hunt 3: Firewall/proxy — Bricksforge form endpoints and PHP upload requests

CommonSecurityLog
| where TimeGenerated >= ago(14d)
| extend Uri=tolower(tostring(column_ifexists("RequestURL","")))
| where Uri contains "bricksforge/v1/form_submit" or Uri contains "bricksforge_form_submit" or (Uri contains "/wp-content/uploads/bricksforge/" and (Uri contains ".php" or Uri contains ".phtml"))
| project TimeGenerated, SourceIP, DestinationIP, DestinationHostName, DestinationPort, Uri, DeviceAction, DeviceVendor, DeviceProduct
| order by TimeGenerated desc

Hunt 4: Endpoint — executable PHP created under WordPress uploads

DeviceFileEvents
| where TimeGenerated >= ago(30d)
| where FolderPath contains "/wp-content/uploads/" or FolderPath contains @"\wp-content\uploads\"
| where FileName matches regex @"(?i)\.(php[578]?|phtml|pht|phtm|phps)$"
| project TimeGenerated, DeviceName, FileName, FolderPath, ActionType, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA256
| order by TimeGenerated desc

Hunt 5: Endpoint — Bricksforge temp images and suspicious PHP pairs

DeviceFileEvents
| where TimeGenerated >= ago(30d)
| where FolderPath contains "bricksforge" and FolderPath contains "tmp"
| where FileName matches regex @"(?i)\.(gif|png|php[578]?|phtml|pht|phtm)$"
| summarize Files=make_set(FileName,30), Paths=make_set(FolderPath,15), Types=make_set(ActionType,10), Count=count() by DeviceName, bin(TimeGenerated,1h)
| where Count >= 2
| order by Count desc

Hunt 6: Endpoint — PHP/web-server processes spawning operating-system commands

DeviceProcessEvents
| where TimeGenerated >= ago(30d)
| where InitiatingProcessFileName in~ ("php","php-fpm","php-fpm8.2","php-fpm8.3","apache2","httpd","nginx","w3wp.exe","php-cgi.exe")
| where FileName in~ ("sh","bash","dash","curl","wget","python","python3","perl","nc","ncat","cmd.exe","powershell.exe","pwsh.exe")
| project TimeGenerated, DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine, FolderPath
| order by TimeGenerated desc

Hunt 7: Network — PHP/web-server processes contacting rare external destinations

let Net=DeviceNetworkEvents
| where TimeGenerated >= ago(30d)
| where InitiatingProcessFileName in~ ("php","php-fpm","php-fpm8.2","php-fpm8.3","apache2","httpd","nginx","php-cgi.exe")
| where isnotempty(RemoteIP) and not(ipv4_is_private(RemoteIP));
let Baseline=Net | where TimeGenerated between (ago(30d)..ago(3d)) | summarize by DeviceName, RemoteIP;
Net | where TimeGenerated >= ago(3d)
| join kind=leftanti Baseline on DeviceName, RemoteIP
| project TimeGenerated, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteIP, RemoteUrl, RemotePort, ActionType
| order by TimeGenerated desc

Hunt 8: Linux Syslog — WordPress upload/webshell and shell execution fallback

Syslog
| where TimeGenerated >= ago(30d)
| where SyslogMessage contains "bricksforge" or (SyslogMessage contains "/wp-content/uploads/" and (SyslogMessage contains ".php" or SyslogMessage contains ".phtml"))
| project TimeGenerated, Computer, ProcessName, Facility, SeverityLevel, SyslogMessage
| order by TimeGenerated desc

Hunt 9: Reported source IPs — contextual network pivot

let Reported=dynamic(["177.75.57.20","23.97.62.146","84.247.60.125","38.154.185.97","150.109.16.166","153.75.90.146"]);
CommonSecurityLog
| where TimeGenerated >= ago(14d)
| where SourceIP in (Reported) or DestinationIP in (Reported)
| project TimeGenerated, SourceIP, DestinationIP, DestinationPort, DestinationHostName, RequestURL, DeviceAction, DeviceProduct
| order by TimeGenerated desc

Detection Notes

**Highest signal:** requests carrying `temporaryFileUploads` plus an image-to-PHP URL mismatch (requires request-body logging), new executable PHP files in uploads, and PHP/web-server child shell processes. **Expected noise:** legitimate Bricksforge form submissions, image uploads, WAF blocks, and shared proxy IPs. **Blind spots:** Azure WAF often excludes HTTP request bodies; default Syslog does not capture access logs or file creation; MDE may not be installed on Linux web hosts; missing plugin inventory prevents exposure confirmation. A blocked probe is not evidence of execution.

Visibility is limited to ingested Microsoft Sentinel tables and retention; a clean result does not establish absence of compromise. Queries have not been executed against customer workspaces.

Leave a Reply