Atlassian CVE-2026-21589 Arbitrary File Access Hunt

Threat Overview

Atlassian disclosed CVE-2026-21589, a critical path-traversal/arbitrary-file-access vulnerability affecting self-managed editions of eight products. An unauthenticated remote attacker who knows or can infer an exact file path can read a file accessible to the application service account. The flaw does not provide directory listing, so exploitation requires a target filename or path; configuration files, application secrets, private keys, tokens, and other predictable files remain meaningful targets.

Atlassian rates the issue CVSS 9.3 and states that all earlier versions of the listed products are affected. The company patched its cloud services and reported no evidence of exploitation in its cloud investigation as of the advisory date. That statement does not establish that self-managed instances have not been probed or exploited. Atlassian also provided temporary WAF/rewrite guidance that blocks .. adjacent to /, \, or ::, including URL-encoded forms. This hunt therefore concentrates on raw and decoded traversal patterns, successful responses, source fan-out, and subsequent authenticated access from the same source rather than speculative endpoint execution.

References

Impacted Systems

  • Bitbucket Data Center: all earlier releases; fixed in 9.4.26, 10.2.8, and 10.5.1 maintenance lines. Bitbucket Cloud is patched/not affected by customer action.
  • Confluence Data Center: all earlier releases; fixed in 9.2.26 and 10.2.19.
  • Jira Software Data Center: all earlier releases; fixed in 9.12.40, 10.3.26, and 11.3.12.
  • Jira Service Management Data Center: all earlier releases; fixed in 5.12.40, 10.3.26, and 11.3.12.
  • Bamboo Data Center: all earlier releases; the advisory’s fixed-version list states 10.2.24 and 12.1.12. Atlassian page fields have displayed an inconsistent 10.2.4/10.2.24 value; use the current vendor download/advisory and install the newest supported maintenance release.
  • Crowd Data Center: all earlier releases; the advisory’s fixed-version list states 6.3.7, 7.0.3, 7.1.7, and 7.2.4. Atlassian page fields/tickets have displayed a 7.1.6/7.1.7 discrepancy; use the newest supported release rather than treating the lower value as authoritative.
  • Crucible and Fisheye: all earlier releases; fixed in 4.9.15.
  • Platform/deployment: self-managed/on-premises Data Center applications on supported Windows or Linux hosts, commonly Java/Tomcat based and exposed through a reverse proxy or load balancer.
  • Prerequisites: no authentication; the attacker must know or infer an exact readable path. No directory-listing capability is documented.
  • Exposure: highest for Internet-facing product URLs. Internal-only instances remain reachable after phishing, VPN compromise, or another foothold.
  • Cloud: Atlassian-hosted cloud products were patched by Atlassian; no customer patch action is required for those services.

Why this matters

The affected product set is broad and includes collaboration, source-code, CI/CD, service-management, and identity-adjacent systems that often store reusable credentials or integration secrets. A read-only exploit may not create processes or files, so perimeter and application request telemetry are the primary opportunity to identify attempts. Internet-facing Data Center deployments warrant rapid version and exposure validation even though exploitation has not been confirmed.

Exploitation Status

No confirmed active exploitation in the cited sources. Atlassian reported no evidence of exploitation in the cloud-service investigation and did not publish attacker infrastructure, hashes, or a product-specific exploit URI. Public scanners may quickly adopt generic encoded traversal patterns. A request matching the mitigation syntax is an attempt indicator, not proof of file disclosure; response status and byte counts are important context.

What this hunt looks for

Affected product versions, literal and encoded traversal syntax, one- and two-pass decoding, apparently successful responses, one-source fan-out across paths or hosts, follow-on authenticated access, WAF blocks, and Syslog access-log fallback activity.

Required logs

Authoritative Atlassian product inventory; WAF, reverse-proxy, load-balancer, or application access logs in CommonSecurityLog or AzureDiagnostics that preserve raw request targets and response status; and Syslog for web/application access-log fallback coverage.

Hunt 1 — First-pass Atlassian product and version inventory

let lookback = 14d;
DeviceTvmSoftwareInventory
| where Timestamp >= ago(lookback)
| where SoftwareVendor has "Atlassian" or SoftwareName has_any ("Bitbucket", "Confluence", "Jira", "Bamboo", "Crowd", "Crucible", "Fisheye")
| summarize LastSeen=max(Timestamp), Devices=make_set(DeviceName, 50), DeviceCount=dcount(DeviceId) by SoftwareVendor, SoftwareName, SoftwareVersion
| order by SoftwareName asc, SoftwareVersion asc

Hunt 2 — Raw encoded and literal traversal patterns in perimeter logs

let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", "")), Dst=coalesce(column_ifexists("DestinationHostName", ""), column_ifexists("host_s", ""), column_ifexists("requestUri_s", ""))
| where Raw has_any ("../", @"..\", "..::", "%2e%2e%2f", "%2e%2e%5c", "%2e%2e%3a%3a", "%252e%252e%252f", "%252e%252e%255c")
| project TimeGenerated, Src, Dst, Raw=substring(Raw, 0, 1800)
| order by TimeGenerated desc

Hunt 3 — One- and two-pass decoded traversal detection

let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", ""))
| extend Decoded1=url_decode_component(Raw)
| extend Decoded2=url_decode_component(Decoded1)
| where Decoded1 matches regex @"(?i)\.\.(\/|\\|::)" or Decoded2 matches regex @"(?i)\.\.(\/|\\|::)"
| project TimeGenerated, Src, Raw=substring(Raw, 0, 900), Decoded=substring(Decoded2, 0, 1400)
| order by TimeGenerated desc

Hunt 4 — Traversal attempts with apparently successful HTTP responses

let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", "")), Status=tostring(coalesce(column_ifexists("HttpStatusCode", ""), column_ifexists("scStatus_s", ""), column_ifexists("httpStatus_d", ""))), Bytes=tolong(coalesce(column_ifexists("SentBytes", long(0)), column_ifexists("responseBytes_d", long(0)), column_ifexists("bytesSent_d", long(0))))
| extend Decoded2=url_decode_component(url_decode_component(Raw))
| where Decoded2 matches regex @"(?i)\.\.(\/|\\|::)"
| where Status startswith "2" or Status startswith "3"
| project TimeGenerated, Src, Status, Bytes, Request=substring(Decoded2, 0, 1600)
| order by TimeGenerated desc

Hunt 5 — One source probing multiple paths or Atlassian hosts

let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", "")), Host=coalesce(column_ifexists("DestinationHostName", ""), column_ifexists("host_s", ""), column_ifexists("Resource", ""))
| extend Decoded2=url_decode_component(url_decode_component(Raw))
| where Decoded2 matches regex @"(?i)\.\.(\/|\\|::)"
| summarize Requests=count(), Hosts=dcount(Host), HostSet=make_set(Host, 20), Samples=make_set(substring(Decoded2, 0, 500), 5), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by Src, bin(TimeGenerated, 10m)
| where Requests >= 3 or Hosts >= 2
| order by Requests desc

Hunt 6 — Traversal attempt followed by authenticated or administrative access

let lookback = 30d;
let Http = union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", "")), User=coalesce(column_ifexists("SourceUserName", ""), column_ifexists("user_s", ""), column_ifexists("User", ""))
| extend Decoded2=url_decode_component(url_decode_component(Raw));
let Attempts = Http
| where Decoded2 matches regex @"(?i)\.\.(\/|\\|::)"
| summarize AttemptTime=min(TimeGenerated) by Src;
Http
| where isnotempty(User) or Decoded2 has_any ("/admin", "/plugins/servlet", "/rest/api", "/secure/admin", "/projects", "/repos")
| join kind=inner Attempts on Src
| where TimeGenerated between (AttemptTime .. AttemptTime + 2h)
| project TimeGenerated, Src, User, AttemptTime, Request=substring(Decoded2, 0, 1600)
| order by TimeGenerated desc

Hunt 7 — WAF blocks matching Atlassian temporary mitigation syntax

let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", "")), Action=coalesce(column_ifexists("DeviceAction", ""), column_ifexists("action_s", ""), column_ifexists("Action", ""))
| extend Decoded2=url_decode_component(url_decode_component(Raw))
| where Decoded2 matches regex @"(?i)\.\.(\/|\\|::)"
| where Action has_any ("block", "deny", "drop", "prevent") or Raw has_any ("Blocked", "Matched", "WAF")
| summarize Blocks=count(), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Samples=make_set(substring(Decoded2, 0, 600), 5) by Src, Action
| order by Blocks desc

Hunt 8 — Syslog/application-access-log fallback

let lookback = 30d;
Syslog
| where TimeGenerated >= ago(lookback)
| where ProcessName has_any ("nginx", "httpd", "apache", "tomcat", "haproxy", "jira", "confluence", "bitbucket", "bamboo", "crowd") or Facility in ("local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7")
| extend Decoded2=url_decode_component(url_decode_component(SyslogMessage))
| where SyslogMessage has_any ("../", @"..\", "%2e%2e", "%252e%252e") or Decoded2 matches regex @"(?i)\.\.(\/|\\|::)"
| project TimeGenerated, Computer, HostName, ProcessName, SeverityLevel, SyslogMessage, Decoded=substring(Decoded2, 0, 1600)
| order by TimeGenerated desc

Detection Notes

  • Hunts 2–4 are the highest-value request-layer detections. Preserve the raw request target before the proxy or application normalizes it; otherwise %2e, %2f, %5c, and double encoding may disappear from logs.
  • Generic traversal strings attract Internet scanning and false positives. Confidence rises when the destination is a confirmed Atlassian instance, multiple exact paths are attempted, or a 2xx response returns a nontrivial byte count.
  • A 200 response does not prove file disclosure; some products return branded error pages with status 200. Compare body size, content type, and normal response patterns in the native connector.
  • This flaw is file read, not documented remote code execution. Endpoint process/file-creation hunts were intentionally not added because they would imply unsupported post-exploitation behavior.
  • pack_all() provides schema-tolerant discovery but can be expensive and may encounter connector truncation. Replace it with validated URI, query, response-code, response-byte, action, source-IP, and hostname fields after confirming each workspace schema.
  • If TLS terminates on the application and access logs are not ingested, the exploit can be invisible to Sentinel. Product inventory without request telemetry supports exposure validation but not retrospective exploitation determination.