Detection rules › Splunk
Cisco IOS XE VTY Access Class Tampering
This analytic detects rapid modification of Cisco IOS-XE VTY access-class settings. The Salt Typhoon notes describe configure HTTP activity followed by line vty changes and removal/re-application of an access-class within 60 seconds.
Known false positives
- VTY ACL changes occur during legitimate management access updates. Filter approved maintenance windows and known administrators.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Lateral Movement |
Rule body
name: Cisco IOS XE VTY Access Class Tampering
id: a01ca274-16cb-476d-a814-d2ffe29d8905
version: 1
creation_date: '2026-05-19'
modification_date: '2026-05-20'
author: Nasreddine Bencherchali
status: production
type: Anomaly
description: |
This analytic detects rapid modification of Cisco IOS-XE VTY access-class settings. The Salt Typhoon notes describe configure HTTP activity followed by line vty changes and removal/re-application of an access-class within 60 seconds.
data_source:
- Cisco IOS Logs
search: |-
`cisco_ios`
facility IN ("HA_EM", "PARSER")
mnemonic IN ("LOG", "CFGLOG_LOGGEDCMD")
message_text IN (
"*access-class*",
"*configure http*",
"*line vty*"
)
| rex field=message_text "^(?:[^:]+:\s+)?(?:catchall:\s+)?(?<eem_command>.+?)\s*$"
| rex field=message_text "<enteredCommand><cli>(?<parser_command>.*?)</cli>"
| rex field=message_text "<user>(?<parser_user>[^<]+)</user>"
| rex field=message_text "<srcIP>(?<parser_src_ip>[^<]+)</srcIP>"
| eval command=lower(trim(coalesce(parser_command, eem_command, "")))
| eval event_type=case(
like(command, "configure http%") OR like(command, "ip http secure-server%"), "http_config",
like(command, "line vty%"), "line_vty",
like(command, "no access-class%"), "remove_access_class",
like(command, "access-class%"), "add_access_class",
true(), null())
| where isnotnull(event_type)
| eval user=coalesce(parser_user, user, "unknown")
| eval src_ip=coalesce(parser_src_ip, src_ip, "unknown")
| eval dest=coalesce(host, dvc, dest, "unknown")
| bin _time span=1m
| stats count min(_time) as firstTime
max(_time) as lastTime
values(event_type) as event_types
values(user) as user
values(src_ip) as src_ip
values(command) as commands
by _time dest
| where mvfind(event_types, "line_vty") >= 0
AND
mvfind(event_types, "remove_access_class") >= 0
AND
mvfind(event_types, "add_access_class") >= 0
| `security_content_ctime(firstTime)`
| `security_content_ctime(lastTime)`
| `cisco_ios_xe_vty_access_class_tampering_filter`
how_to_implement: |
Use the Cisco Catalyst Add-on for Splunk (https://splunkbase.splunk.com/app/7538) to Ingest Cisco IOS-XE syslog with sourcetype "cisco:ios" and enable archive/config command logging.
known_false_positives: |
VTY ACL changes occur during legitimate management access updates. Filter approved maintenance windows and known administrators.
references:
- https://www.cisa.gov/news-events/cybersecurity-advisories/aa25-239a
- https://blog.talosintelligence.com/salt-typhoon-analysis/
intermediate_findings:
entities:
- field: dest
type: system
score: 20
message: Cisco IOS-XE VTY access-class settings were removed and reapplied on $dest$ by $user$.
threat_objects:
- field: commands
type: command
analytic_story:
- Salt Typhoon
asset_type: Network
mitre_attack_id:
- T1562
- T1021
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
category: application
security_domain: network
Stages and Predicates
Stage 1: search
`cisco_ios`
facility IN ("HA_EM", "PARSER")
mnemonic IN ("LOG", "CFGLOG_LOGGEDCMD")
message_text IN (
"*access-class*",
"*configure http*",
"*line vty*"
)
Stage 2: rex
| rex field=message_text "^(?:[^:]+:\s+)?(?:catchall:\s+)?(?<eem_command>.+?)\s*$"
Stage 3: rex
| rex field=message_text "<enteredCommand><cli>(?<parser_command>.*?)</cli>"
Stage 4: rex
| rex field=message_text "<user>(?<parser_user>[^<]+)</user>"
Stage 5: rex
| rex field=message_text "<srcIP>(?<parser_src_ip>[^<]+)</srcIP>"
Stage 6: eval
| eval command=lower(trim(coalesce(parser_command, eem_command, "")))
Stage 7: eval
| eval event_type=case(
like(command, "configure http%") OR like(command, "ip http secure-server%"), "http_config",
like(command, "line vty%"), "line_vty",
like(command, "no access-class%"), "remove_access_class",
like(command, "access-class%"), "add_access_class",
true(), null())
event_type =if
like(command, "configure http%") OR like(command, "ip http secure-server%")"http_config"elif
like(command, "line vty%")"line_vty"elif
like(command, "no access-class%")"remove_access_class"elif
like(command, "access-class%")"add_access_class"else
null()Stage 8: where
| where isnotnull(event_type)
Stage 9: eval
| eval user=coalesce(parser_user, user, "unknown")
Stage 10: eval
| eval src_ip=coalesce(parser_src_ip, src_ip, "unknown")
Stage 11: eval
| eval dest=coalesce(host, dvc, dest, "unknown")
Stage 12: bucket
| bin _time span=1m
Stage 13: stats
| stats count min(_time) as firstTime
max(_time) as lastTime
values(event_type) as event_types
values(user) as user
values(src_ip) as src_ip
values(command) as commands
by _time dest
Stage 14: where
| where mvfind(event_types, "line_vty") >= 0
AND
mvfind(event_types, "remove_access_class") >= 0
AND
mvfind(event_types, "add_access_class") >= 0
Stage 15: search
| `security_content_ctime(firstTime)`
Stage 16: search
| `security_content_ctime(lastTime)`
Stage 17: search
| `cisco_ios_xe_vty_access_class_tampering_filter`
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
event_type | is_not_null | field:"event_type" kind:is_not_null | |
facility | in |
| field:"facility" kind:in |
message_text | in |
| field:"message_text" kind:in |
mnemonic | in |
| field:"mnemonic" kind:in |
sourcetype | eq |
| field:"sourcetype" kind:eq value:"cisco:ios" |