Detection rules › Splunk

PTC Windchill Gateway Command Execution

Status
production
Severity
low
Group by
activity, query_param, query_value, src
Author
Nasreddine Bencherchali, Splunk
Source
github.com/splunk/security_content

This analytic detects Windchill MethodServer log4j events showing suspicious c= command execution or p= file read parameters sent to Windchill gateway paths associated with CVE-2026-4681 exploitation. PTC identifies run?c=, run?p=, .jsp?c=, and .jsp?p= request patterns as indicators to monitor during Windchill and FlexPLM exploitation response. Successful activity may allow an unauthenticated attacker to execute operating system commands or read files through a weaponized gateway or JSP component.

Known false positives

  • Requests to `GW/run` or randomly named `dpr_<8 hex>.jsp` files with `c=` or `p=` parameters should not be expected during normal Windchill operation. Validate whether red-team testing, vulnerability scanning, or incident response replay generated the activity before closing as benign.

MITRE ATT&CK coverage

Rule body

name: PTC Windchill Gateway Command Execution
id: d725b390-fe86-47df-b9a1-57497915403e
version: 1
creation_date: '2026-06-14'
modification_date: '2026-06-14'
author: Nasreddine Bencherchali, Splunk
status: production
type: Anomaly
description: |
    This analytic detects Windchill MethodServer log4j events showing suspicious `c=` command execution or `p=` file read parameters sent to Windchill gateway paths associated with CVE-2026-4681 exploitation.
    PTC identifies `run?c=`, `run?p=`, `.jsp?c=`, and `.jsp?p=` request patterns as indicators to monitor during Windchill and FlexPLM exploitation response.
    Successful activity may allow an unauthenticated attacker to execute operating system commands or read files through a weaponized gateway or JSP component.
data_source:
    - Windchill Log4j
search: |-
    `windchill_log4j`
    ("WindchillGW/GW/run" OR "WindchillAuthGW/GW/run" OR "/GW/run?" OR "run?c=" OR "run?p=" OR ".jsp?c=" OR ".jsp?p=" OR "dpr_")
    | rex field=_raw "^(?:[^:\r\n]+:)?(?<log_ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3})\s+(?<log_level>\w+)\s+\[(?<thread>[^\]]+)\]\s+(?<logger>\S+)\s+-\s+(?<payload>.*)$"
    | search logger IN ("wt.servlet.ServletRequestMonitor.request", "wt.method.MethodContextMonitor.contexts.servletRequest")
    | rex field=payload "^(?<event_ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(?<event_tz>[+\-]\d{4}),\s+(?<rest>.*)$"
    | eval parts=split(rest,", ")
    | eval event_type=case(logger="wt.servlet.ServletRequestMonitor.request","servlet_request",logger="wt.method.MethodContextMonitor.contexts.servletRequest","method_context_servlet_request",true(),"other")
    | eval src_ip=case(event_type="servlet_request",mvindex(parts,2),event_type="method_context_servlet_request",mvindex(parts,5))
    | eval uri_path=case(event_type="servlet_request",mvindex(parts,3),event_type="method_context_servlet_request",mvindex(parts,8))
    | eval query_string=if(event_type="servlet_request",mvindex(parts,4),null())
    | eval http_method=if(event_type="servlet_request",mvindex(parts,5),null())
    | eval status=if(event_type="servlet_request",tonumber(mvindex(parts,6)),null())
    | rex field=uri_path "^(?<uri_only>[^\?]+)(?:\?(?<uri_query>.*))?$"
    | eval query_string=if(query_string="-",null(),query_string)
    | eval query_string=coalesce(query_string,uri_query)
    | rex field=query_string "(?i)(?:^|&)(?<query_param>[cp])=(?<query_value>[^&]*)"
    | eval query_param=lower(query_param), query_value=urldecode(replace(query_value,"\+","%20"))
    | where isnotnull(uri_only) AND isnotnull(query_param)
    | where (match(uri_only,"(?i)(^|/)GW/run$") OR match(uri_only,"(?i)/servlet/(WindchillGW|WindchillAuthGW)/GW/run$") OR match(uri_only,"(?i)(^|/)dpr_[0-9a-f]{8}\.jsp$")) AND (query_param="c" OR query_param="p")
    | where NOT (query_param="c" AND match(query_value,"(?i)^echo(\s|20)+GW_READY_OK$"))
    | eval activity=case(query_param="c","command_execution_parameter",query_param="p","file_read_parameter",true(),"unknown")
    | eval src=src_ip
    
    | stats count min(_time) as firstTime
                  max(_time) as lastTime
                  values(log_level) as log_level
                  values(logger) as logger
                  values(http_method) as http_method
                  values(status) as status
                  values(uri_only) as uri_path
                  values(query_string) as query_string
      by src activity query_param query_value
    
    | `security_content_ctime(firstTime)`
    | `security_content_ctime(lastTime)`
    | `ptc_windchill_gateway_command_execution_filter`
how_to_implement: |
    To implement this analytic, ingest PTC Windchill MethodServer log4j logs into Splunk with sourcetype `log4j`.
    Update the `windchill_log4j` macro to include the appropriate index constraints for your environment.
    The analytic expects log entries from the `wt.servlet.ServletRequestMonitor.request` and `wt.method.MethodContextMonitor.contexts.servletRequest` loggers and parses embedded servlet URI, query parameter, source IP, HTTP method, and status fields.
known_false_positives: |
    Requests to `GW/run` or randomly named `dpr_<8 hex>.jsp` files with `c=` or `p=` parameters should not be expected during normal Windchill operation.
    Validate whether red-team testing, vulnerability scanning, or incident response replay generated the activity before closing as benign.
references:
    - https://www.ptc.com/en/about/trust-center/advisory-center/active-advisories/windchill-flexplm-critical-vulnerability
    - https://nvd.nist.gov/vuln/detail/CVE-2026-4681
    - https://www.cisa.gov/news-events/ics-advisories/icsa-26-085-03
intermediate_findings:
    entities:
        - field: src
          type: system
          score: 20
          message: Potential PTC Windchill gateway command or file read activity detected from $src$.
threat_objects:
    - field: src
      type: ip_address
analytic_story:
    - PTC Windchill Exploitation
asset_type: Web Application
cve:
    - CVE-2026-4681
mitre_attack_id:
    - T1190
    - T1059
    - T1005
product:
    - Splunk Enterprise
    - Splunk Enterprise Security
    - Splunk Cloud
category: application
security_domain: network

Stages and Predicates

Stage 1: search

`windchill_log4j`
("WindchillGW/GW/run" OR "WindchillAuthGW/GW/run" OR "/GW/run?" OR "run?c=" OR "run?p=" OR ".jsp?c=" OR ".jsp?p=" OR "dpr_")

Stage 2: rex

| rex field=_raw "^(?:[^:\r\n]+:)?(?<log_ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3})\s+(?<log_level>\w+)\s+\[(?<thread>[^\]]+)\]\s+(?<logger>\S+)\s+-\s+(?<payload>.*)$"

Stage 3: search

| search logger IN ("wt.servlet.ServletRequestMonitor.request", "wt.method.MethodContextMonitor.contexts.servletRequest")

Stage 4: rex

| rex field=payload "^(?<event_ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(?<event_tz>[+\-]\d{4}),\s+(?<rest>.*)$"

Stage 5: eval

| eval parts=split(rest,", ")

Stage 6: eval

| eval event_type=case(logger="wt.servlet.ServletRequestMonitor.request","servlet_request",logger="wt.method.MethodContextMonitor.contexts.servletRequest","method_context_servlet_request",true(),"other")
event_type =
iflogger = "wt.servlet.ServletRequestMonitor.request""servlet_request"
eliflogger = "wt.method.MethodContextMonitor.contexts.servletRequest""method_context_servlet_request"
else"other"

Stage 7: eval

| eval src_ip=case(event_type="servlet_request",mvindex(parts,2),event_type="method_context_servlet_request",mvindex(parts,5))
src_ip =
ifevent_type = "servlet_request"mvindex(parts, 2)
elsemvindex(parts, 5)

Stage 8: eval

| eval uri_path=case(event_type="servlet_request",mvindex(parts,3),event_type="method_context_servlet_request",mvindex(parts,8))
uri_path =
ifevent_type = "servlet_request"mvindex(parts, 3)
elsemvindex(parts, 8)

Stage 9: eval

| eval query_string=if(event_type="servlet_request",mvindex(parts,4),null())

Stage 10: eval

| eval http_method=if(event_type="servlet_request",mvindex(parts,5),null())

Stage 11: eval

| eval status=if(event_type="servlet_request",tonumber(mvindex(parts,6)),null())

Stage 12: rex

| rex field=uri_path "^(?<uri_only>[^\?]+)(?:\?(?<uri_query>.*))?$"

Stage 13: eval

| eval query_string=if(query_string="-",null(),query_string)

Stage 14: eval

| eval query_string=coalesce(query_string,uri_query)

Stage 15: rex

| rex field=query_string "(?i)(?:^|&)(?<query_param>[cp])=(?<query_value>[^&]*)"

Stage 16: eval

| eval query_param=lower(query_param), query_value=urldecode(replace(query_value,"\+","%20"))

Stage 17: where

| where isnotnull(uri_only) AND isnotnull(query_param)

Stage 18: where

| where (match(uri_only,"(?i)(^|/)GW/run$") OR match(uri_only,"(?i)/servlet/(WindchillGW|WindchillAuthGW)/GW/run$") OR match(uri_only,"(?i)(^|/)dpr_[0-9a-f]{8}\.jsp$")) AND (query_param="c" OR query_param="p")

Stage 19: where

| where NOT (query_param="c" AND match(query_value,"(?i)^echo(\s|20)+GW_READY_OK$"))

Stage 20: eval

| eval activity=case(query_param="c","command_execution_parameter",query_param="p","file_read_parameter",true(),"unknown")
activity =
ifquery_param = "c""command_execution_parameter"
elifquery_param = "p""file_read_parameter"
else"unknown"

Stage 21: eval

| eval src=src_ip

Stage 22: stats

| stats count min(_time) as firstTime
              max(_time) as lastTime
              values(log_level) as log_level
              values(logger) as logger
              values(http_method) as http_method
              values(status) as status
              values(uri_only) as uri_path
              values(query_string) as query_string
  by src activity query_param query_value

Stage 23: search

| `security_content_ctime(firstTime)`

Stage 24: search

| `security_content_ctime(lastTime)`

Stage 25: search

| `ptc_windchill_gateway_command_execution_filter`

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
query_parameq"c"excludes:query_param
query_valueregex_match"(?i)^echo(\s|20)+GW_READY_OK$"excludes:query_value

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
loggerin
  • "wt.method.MethodContextMonitor.contexts.servletRequest"
  • "wt.servlet.ServletRequestMonitor.request"
field:"logger" kind:in
query_parameq
  • "c"
  • "p"
field:"query_param" kind:eq
query_paramis_not_null
  • (no value, null check)
field:"query_param" kind:is_not_null
sourcetypeeq
  • log4j
field:"sourcetype" kind:eq value:"log4j"
uri_onlyis_not_null
  • (no value, null check)
field:"uri_only" kind:is_not_null
uri_onlyregex_match
  • "(?i)(^|/)GW/run$"
  • "(?i)(^|/)dpr_[0-9a-f]{8}.jsp$"
  • "(?i)/servlet/(WindchillGW|WindchillAuthGW)/GW/run$"
field:"uri_only" kind:regex_match

Search terms

These SPL tokens match against raw event text.

StageTerm
1"WindchillGW/GW/run"
1"WindchillAuthGW/GW/run"
1"/GW/run?"
1"run?c="
1"run?p="
1".jsp?c="
1".jsp?p="
1"dpr_"