Insights / Threat Brief / 25 Sep 2026 · Afternoon

Threat Brief • 25 September 2026 • Afternoon Edition

Nothing to Patch, Nothing to Match: Kiteworks’ Weekend Warning and the Controls That Still Work

Kiteworks has asked customers to take their systems offline ahead of a possible attack this weekend. With no flaw disclosed, the defenses that work are the ones that don’t need to know what the flaw is.

By Fezz · Threat Vector Management · September 25, 2026

Research, fact-checking and graphics support by Claude (Anthropic)

Status as of publication on Friday, 25 September 2026, afternoon US Eastern. This is a developing story. Where anything here differs from Kiteworks’ own notice to you, follow the notice.

: added Indicators of compromise and attack for all five items.

: restructured for action — fixes and indicators at a glance up top; each item now opens with its own bottom line and indicator table; added Sophos confirmation of the Kiteworks window.

: added a hunt pack to every item — search queries and scripts, what each result means, and what to do. Packs were fact-checked twice against this page; the bash scripts were run on synthetic data and the Sigma rules were converted for three platforms. Everything else is labelled as reviewed, not run. No indicator, version or recommendation above changed.

BLUF

Bottom Line Up Front

How to read this. The table below gives every item’s fix and indicators in one place. Each item then opens with its own bottom line and indicator table, followed by What, So what and Now what. Items are ranked by consequence if they reach you. Reported facts are cited; our analysis is labeled.

At a glance: fixes and indicators

Written for organizations and the teams who defend them. IoC = evidence it already happened (an address, an account, a log line). IoA = the behavior of an attack in progress. Network IoCs are defanged: remove the square brackets before using them. Indicators age fast, so re-check the source before blocking.

ItemExploited? · FixDo nowTop IoCsTop IoAs
1. KiteworksWarned as imminent · none for the unknown flaw; 9.5.1 fixes known onesShut down for the window; save logs first; deny outbound by defaultNone publishedOurs: any connection to the switched-off appliance; new outbound destinations; outbound volume spikes
2. SharePoint Server CVE-2026-65660Yes (KEV 25 Sep) · 11 Aug 2026 updatePatch; restrict anonymous accessNone publishedPOST to ToolPane.aspx / AddGallery.aspx with MSOTlPn_DWP and a broken Register directive; w3wp.exe child processes
3. MikroTik RouterOS CVE-2026-67279Yes (KEV 25 Sep; attacks since 2 Sep) · 6.49.21 / 7.23.4 / 7.24.2Upgrade; no SSH from the internet; hunt for the IoCs82.192.72[.]4, 103.102.31[.]18; admin user ops; log login failure for user -2Users, scripts or scheduler tasks nobody created; fetch of diagnostic files to an outside address
4. GitLab (self-managed) CVE-2026-85706Yes (KEV 11 Sep; public Metasploit module 25 Sep) · 18.11.12 / 19.0.9 / 19.1.8 / 19.2.6 / 19.3.2Patch; rotate secrets if it was exposedNone publishedUnauthenticated POST/PUT to /api/v4/projects/<id>/repository/commits or files with %63ommits-style encoding and file.path
5. ServiceNow (self-hosted) CVE-2026-13016, -86860No known exploitation · hot fix (KB3159623)Self-hosted: apply the hot fix. Hosted: confirm with ServiceNowNone publishedNone published
Highest payoff · our analysis

If you shut Kiteworks down, keep firewall logging on for its address during the window. A system that is switched off has no legitimate users, so every connection attempt shows who came looking. It costs nothing.

1. Kiteworks asks customers to power down ahead of a warned attack

Bottom line

Law enforcement warned Kiteworks of a possible attack this weekend, and Kiteworks asked customers to shut their systems down for six hours. Nothing about the flaw has been disclosed, so no patch or signature can stop it. Do now: save logs and a snapshot off the appliance, shut down for your window, and deny outbound traffic by default. Skip if you don’t run Kiteworks, but read Fence in any file-transfer server.

Indicators

TypeIndicatorWhere to lookSource
IoCNone published. No flaw, tooling, infrastructure or threat actor disclosed—BleepingComputer; Kiteworks advisories
IoA oursAny connection attempt to the appliance’s address while it is offFirewall logsAnalysis
IoA oursOutbound connection to a destination the appliance has never contacted, or outbound volume well above its hourly baselineFlow logs (NetFlow/IPFIX/VPC), DNS resolver logsAnalysis
IoA oursAdmin login from an unfamiliar address or at an unusual hourKiteworks admin and web logsAnalysis
IoA oursNew or changed files in directories the web server serves (in 2023, MOVEit attackers planted human2.aspx)File-integrity monitoringAnalysis; CISA AA23-158A for MOVEit

What

On 25 September Kiteworks told customers it had “received credible threat intelligence from law enforcement indicating that a threat actor may attempt to target some Kiteworks systems,” per CISO Frank Balonis as quoted by TechCrunch, and recommended a six-hour shutdown. BleepingComputer and Sophos report the window as 02:00–08:00 UTC on Saturday 26 September (10 p.m. Friday to 4 a.m. Saturday US Eastern), including systems not reachable from the internet. Kiteworks says it knows of no compromise. The flaw, the attacker and which deployment types are in scope have not been disclosed, and no CVE exists. Reports differ on whether a fix exists: Balonis says release 9.5.1 addresses “all known vulnerabilities,” while The CyberWire reports no mitigation beyond the shutdown. Take your exact window from Kiteworks’ notice.

So what

A vendor asking customers to switch off production on a police tip suggests someone holds a working exploit and means to use it at scale. File-transfer platforms are an extortion crew’s favorite target: Kiteworks’ predecessor Accellion FTA (2020–21) and MOVEit (2023) both ended in bulk data theft. Linking those to this weekend is our analysis. With no flaw disclosed, the question isn’t “is it patched?” but “if it’s broken, what can it reach, and what can leave?”

Now what — P → V (analysis)

Hunt pack — run it, then act on what it says

No flaw, tool, address or attacker has been published for this item, so there is nothing to match against an exploit. These hunts check what the page does name: files appearing in directories the web server serves (including human2.aspx, the name the MOVEit attackers used in 2023), outbound connections from the appliance that it has never made before, and connection attempts while it was off. All of it is our analysis of the page's behavioral indicators, not a published signature. Run the script on a copy that kept its timestamps (cp -a, rsync -a or a forensic image): a plain copy makes every file look new. A directory it cannot fully read (for example a subdirectory it lacks permission for) ends in exit 3 unless something was found. The bash script was run against synthetic directories (changed file plus human2.aspx, a human2.aspx match with an old modification time, clean, empty, missing, and a directory with an unreadable subdirectory); the Splunk, KQL and ES|QL queries were reviewed, not executed against a live platform, so test them on a non-production index first. We are not offering a Sigma, Suricata, YARA or osquery rule for this item: the indicators are baseline comparisons and no string, file hash or address is published, and a rule written from a guess would be counted as a control it is not. We also are not offering a hunt for the admin-login indicator: the page names no field, log format or event for it, so review admin sign-ins by hand. Adjust field and table names to your logging.

bash — a copy or mounted snapshot of the directories the web server serves, with timestamps preserved (cp -a, rsync -a or a forensic image); exit 0 = nothing found, 1 = found, 3 = nothing scanned or some paths unreadable (not a clean result)
#!/usr/bin/env bash
# Usage: ./kw-webdir-hunt.sh WEBDIR YYYY-MM-DD
# Run it against a copy or mounted snapshot of the directories the web server serves. The copy must preserve file times
# (cp -a, rsync -a or a forensic image); a plain copy makes every file look new.
# Lists files changed on or after the date, and any file named human2.aspx (the MOVEit-era example).
# Read-only. Exit 0 = nothing found, 1 = found, 2 = usage, 3 = nothing scanned or some paths unreadable (NOT a clean result).
[ $# -eq 2 ] || { echo "usage: $0 WEBDIR YYYY-MM-DD" >&2; exit 2; }
dir=$1; since=$2
case $since in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; *) echo "usage: date must be YYYY-MM-DD" >&2; exit 2;; esac
y=$((10#${since:0:4})); m=$((10#${since:5:2})); d=$((10#${since:8:2}))
case $m in 1|3|5|7|8|10|12) dm=31;; 4|6|9|11) dm=30;; 2) if [ $((y%4)) -eq 0 ] && { [ $((y%100)) -ne 0 ] || [ $((y%400)) -eq 0 ]; }; then dm=29; else dm=28; fi;; *) dm=0;; esac
if [ "$d" -lt 1 ] || [ "$d" -gt "$dm" ]; then echo "usage: $since is not a real calendar date" >&2; exit 2; fi
if [ ! -d "$dir" ] || [ ! -r "$dir" ]; then echo "cannot read directory: $dir (NOT a clean result)" >&2; exit 3; fi
scanned=$(set -o pipefail; find "$dir" -type f 2>/dev/null | wc -l | tr -d ' '); rc=$?
if [ "$scanned" -eq 0 ]; then echo "scanned=0: no files found (NOT a clean result)" >&2; exit 3; fi
changed=0; named=0
list=$(find "$dir" -type f -newermt "$since" -print 2>/dev/null) || rc=1
if [ -n "$list" ]; then while IFS= read -r f; do echo "CHANGED: ${f:0:300}"; changed=$((changed+1)); done <<< "$list"; fi
list=$(find "$dir" -type f -iname 'human2.aspx' -print 2>/dev/null) || rc=1
if [ -n "$list" ]; then while IFS= read -r f; do echo "NAMED:   ${f:0:300}"; named=$((named+1)); done <<< "$list"; fi
echo "scanned=$scanned changed_since_$since=$changed human2_aspx=$named" >&2
[ "$rc" -eq 0 ] || echo "some paths unreadable (NOT a clean result)" >&2
[ $((changed+named)) -gt 0 ] && exit 1
[ "$rc" -eq 0 ] || exit 3
exit 0
Splunk SPL — flow or firewall logs with src_ip, dest_ip and bytes_out fields (names vary by connector); replace the placeholder with the appliance's address
index=* sourcetype=<your flow or firewall sourcetype> src_ip="<appliance address>" earliest=-30d
| stats earliest(_time) AS first_seen sum(bytes_out) AS bytes BY dest_ip
| where first_seen > relative_time(now(), "-2d")
| sort - bytes
Microsoft Sentinel / Log Analytics KQL — CommonSecurityLog (CEF firewall connectors); column names vary by connector; three standard deviations is our starting threshold, tune it; the 30-day baseline includes the days under test, so a spike raises its own ceiling
let appliance = "<appliance address>";
let hourly = CommonSecurityLog
    | where TimeGenerated > ago(30d) and SourceIP == appliance
    | summarize sent = sum(SentBytes) by bin(TimeGenerated, 1h);
let ceiling = toscalar(hourly | summarize avg(sent) + 3 * stdev(sent));
hourly
| where TimeGenerated > ago(2d) and sent > ceiling
| order by sent desc
Elastic ES|QL — ECS-mapped firewall logs; connection attempts to the appliance during the 02:00 to 08:00 UTC window on 26 September (replace with the exact window in Kiteworks' notice)
FROM logs-*
| WHERE @timestamp >= TO_DATETIME("2026-09-26T02:00:00Z") AND @timestamp < TO_DATETIME("2026-09-26T08:00:00Z")
| WHERE destination.ip == "<appliance address>"
| STATS attempts = COUNT(*), first = MIN(@timestamp), last = MAX(@timestamp) BY source.ip, destination.port
| SORT attempts DESC
If you see Do this
A large outbound transfer to a destination the appliance has never contacted, or an admin session nobody can account for Treat as a confirmed compromise, as the page's Defined response says. Isolate the appliance, preserve everything, rotate every credential it holds (directory bind accounts, SMTP relay credentials, storage connectors, SSO configuration, integration API keys) and start the notification clock. Do not wait for Kiteworks to confirm the method.
human2.aspx, or a file in a web-served directory that no one on your team can explain Treat as a lead to validate now: compare it against your upgrade and change records. If nobody can account for it, handle it as an unexplained admin session (the row above).
Files changed since the date you passed, all matching your own upgrade or maintenance Record them as expected and keep the file list with the change record.
Outbound traffic only to the directory, mail relay, storage, SSO and integration destinations the appliance is configured to use Record it as expected and add those destinations to the baseline.
Connection attempts to the appliance's address during the off window Treat as attempts, not a compromise. Record the source addresses, keep the logs, and keep outbound traffic denied by default.
An appliance on a release older than 9.5.1 Upgrade to 9.5.1 before bringing it back, after exporting logs and a snapshot off the appliance. Reports differ on whether a fix exists, so take the current position from Kiteworks.
The script exits 3, the query returns no rows because no flow logs exist, or the logs and snapshot were never exported This is not a clean result. Find where the logs went, export what the appliance still holds before the next restart, and rely on default-deny outbound and the upgrade.
No hits A good sign, not an exoneration. No flaw has been disclosed, so no published indicator of the real exploit exists to search for. These hunts only find behavior that differs from a baseline, flow logs rotate, and an attacker who sends data to a destination the appliance normally uses would not stand out. Keep outbound traffic denied by default and keep watching.

Fence in any file-transfer server (applies beyond Kiteworks)

Bottom line · analysis

Firewalls and flow data work against a flaw nobody has described; intrusion signatures don’t, until one is published. Do now: restrict who can reach the server, deny its outbound traffic by default, and alert on data leaving. This applies to MOVEit, GoAnywhere, Cleo, SFTP servers and secure-mail gateways as much as to Kiteworks.

ControlWorks against an unknown flaw?Do this
1. Inbound allowlistYes — the best control short of power-offHTTPS/SFTP only from partner and user ranges; mobile users via VPN or a zero-trust broker; management interface never on the internet; rate-limit new connections
2. Egress deny-by-defaultYes — best record against file-transfer theftAllow only named dependencies (mail relay, directory, updates, storage) by address and port; send DNS through a logging resolver and alert on new domains
3. Flow monitoringYes — needs no decryption or signatureBaseline outbound bytes per hour now; alert on multiples of it, new external addresses and long-lived sessions. Defined response: treat an alert as active theft — block first, investigate second
4. WAF in blocking modePartly — generic rules catch the usual bug classesOWASP Core Rule Set or managed rules, blocking not detect-only, higher sensitivity for the weekend. A WAF that terminates TLS also lets an IDS see the traffic
5. IDS/IPSNo, until a signature ships — and it can’t see inside TLS the appliance terminates itselfConfirm rule feeds are updating and alerts reach a person on a weekend. When a Kiteworks rule fires, check earlier flow data for the same source
6. SegmentationYes — limits what a compromise reachesOwn DMZ zone; no route inside beyond its named dependencies
7. Logging while switched offYes — free targeting dataKeep firewall logs for its address; share anything with Kiteworks and your national cyber agency (for example CISA, the Canadian Centre for Cyber Security, the UK’s NCSC or Australia’s ACSC)

Framework mapping: NIST SP 800-53 SC-7 and SC-7(5) (deny by default, allow by exception); CIS Controls v8 13.4 and 13.6. A file-transfer server with unrestricted outbound access falls short of the intent of SC-7(5).

2. SharePoint Server CVE-2026-65660 added to CISA KEV

Bottom line

CISA now lists this SharePoint Server code-injection flaw as exploited. The attack chain is public, and it can leave only an in-memory web shell behind. Do now: apply the 11 August 2026 update and hunt the request pattern below. Skip if you use only SharePoint Online (Microsoft 365).

Indicators

TypeIndicatorWhere to lookSource
IoCNone published — no addresses, hashes or file names—Cyber Centre AL26-023
IoAPOST to /_layouts/15/ToolPane.aspx, AddGallery.aspx or another web-part page with DisplayMode and MSOTlPn_DWP, where a <%@ Register directive has a quote or apostrophe breaking out of an attributeIIS logs, WAF, request-body loggingViettel Cyber Security
IoARequest bodies naming ExpandedWrapper, ObjectDataProvider, XamlServices or LosFormatterWAF, AMSIViettel Cyber Security
IoAw3wp.exe spawning child processes or loading unfamiliar assemblies; Defender/AMSI SharePoint detections; access to IIS machine keys; unexpected web-part changesEDR, Windows event logs, SharePoint ULSCyber Centre AL26-023; Cyber Security News

What

CISA added CVE-2026-65660 to its KEV catalog on 25 September, per the Canadian Centre for Cyber Security. SharePoint’s ToolPane component rebuilds web-part Register directives without escaping quotation marks, letting an attacker inject directives and reach code execution through deserialization (Viettel Cyber Security). It needs an authenticated user, or anonymous access through a public web-part page on farms that allow it, a route Microsoft’s 9 June update closed. CVSS 8.8. Fixed builds: 16.0.5565.1001 (2016), 16.0.10417.20198 (2019), 16.0.19725.20522 (Subscription Edition).

So what

Two national agencies now call it exploited, and the exploit markup is public. The published chain can plant an in-memory web shell, so a clean file scan proves nothing. SharePoint 2016 and 2019 left support on 14 July 2026, so the next flaw there may never be fixed.

Now what — P → I (analysis)

Hunt pack — run it, then act on what it says

The request indicators are from Viettel Cyber Security's technical analysis: POST requests to /_layouts/15/ToolPane.aspx or AddGallery.aspx (or another web-part page) carrying DisplayMode and MSOTlPn_DWP, and request bodies naming ExpandedWrapper, ObjectDataProvider, XamlServices or LosFormatter. The indicator of w3wp.exe spawning child processes is from the Canadian Centre for Cyber Security's AL26-023 (the page also cites Cyber Security News). The hunts are our analysis of those indicators. The script's POST-WEBPART-PAGE-BROAD tag matches any POST to those two pages, which includes ordinary site editing; the page's indicator is the narrower DisplayMode and MSOTlPn_DWP pair, so read the broad tag as a prompt to check who sent it. The PowerShell prints up to 200 characters of each child process's command line: review it before sharing the output. It also checks that the farm build looks like the edition you passed, and exits 3 when it does not, when the process list cannot be read, or when no w3wp.exe is running (nothing to inspect). IIS does not record request bodies by default, so the body strings only match where a WAF or request-body logging captured them. The bash script was run against synthetic IIS-style logs (hit, clean, empty, missing and compressed inputs); both Sigma rules were parsed and converted to Splunk, Kusto and Lucene queries with pySigma; the PowerShell, Splunk SPL and KQL were reviewed, not executed against a live platform, so test them on a non-production farm first. We are not offering a Suricata, YARA or osquery hunt for this item: the strings sit in request bodies of an HTTPS service, and no file, hash or sample is published. The page's published fixed builds are used as written; confirm each against Microsoft's update page. Printed log lines (up to 300 characters) can carry tokens from query strings: review them before sharing the output. Adjust field and table names to your logging.

bash — IIS W3C logs or WAF logs copied off the server; the POST-WEBPART-PAGE-BROAD tag also matches ordinary site editing; exit 0 = nothing found, 1 = found, 3 = nothing readable, any file unreadable or a truncated .gz (not a clean result)
#!/usr/bin/env bash
# Usage: ./sharepoint-iis-hunt.sh LOGFILE...     (IIS W3C logs or WAF logs copied off the server; plain or .gz; needs python3)
# IIS does not log request bodies by default, so body strings only match where a WAF or request-body logger recorded them.
# POST-WEBPART-PAGE-BROAD = any POST to ToolPane.aspx or AddGallery.aspx, which includes ordinary site editing (check who sent it).
# The page's indicator is the narrower POST-DISPLAYMODE+MSOTlPn_DWP pair, plus the body strings.
# Read-only. Exit 0 = nothing found, 1 = found, 2 = usage, 3 = nothing readable, any file unreadable or a truncated .gz (NOT a clean result).
[ $# -ge 1 ] || { echo "usage: $0 LOGFILE..." >&2; exit 2; }
command -v python3 >/dev/null || { echo "python3 is required (NOT a clean result)" >&2; exit 3; }
files=(); skipped=0
for f in "$@"; do if [ -f "$f" ] && [ -r "$f" ]; then files+=("$f"); else echo "skipping unreadable: $f" >&2; skipped=1; fi; done
[ "${#files[@]}" -gt 0 ] || { echo "no readable log files (NOT a clean result)" >&2; exit 3; }
gzip -dcf -- "${files[@]}" 2>/dev/null | python3 -c '
import re, sys
from urllib.parse import unquote
post = re.compile(r"\bPOST\b")
path = re.compile(r"/_layouts/15/(ToolPane|AddGallery)\.aspx", re.I)
body = re.compile(r"ExpandedWrapper|ObjectDataProvider|XamlServices|LosFormatter")
n = a = b = c = 0
for raw in sys.stdin.buffer:
    n += 1
    line = raw.decode("utf-8", "replace").rstrip()
    d = unquote(line)
    is_post = bool(post.search(line))
    tags = []
    if is_post and path.search(d): tags.append("POST-WEBPART-PAGE-BROAD"); a += 1
    if is_post and "DisplayMode" in d and "MSOTlPn_DWP" in d: tags.append("POST-DISPLAYMODE+MSOTlPn_DWP"); b += 1
    if body.search(d): tags.append("BODY-STRING"); c += 1
    if tags: print("%d: [%s] %s" % (n, ",".join(tags), line[:300]))
print("scanned=%d post_webpart_page_broad=%d post_displaymode_msotlpn_dwp=%d body_strings=%d" % (n, a, b, c), file=sys.stderr)
sys.exit(3 if n == 0 else (1 if (a + b + c) else 0))
'
st=("${PIPESTATUS[@]}"); rc=${st[1]}; [ "${st[0]}" -eq 0 ] || { echo "a file could not be fully decompressed (NOT a clean result)" >&2; skipped=1; }
[ "$rc" -ne 0 ] || [ "$skipped" -eq 0 ] || { echo "some files were unreadable (NOT a clean result)" >&2; exit 3; }
exit $rc
PowerShell — SharePoint Management Shell on a farm server; farm build against the three published fixed builds, plus a live look for children of w3wp.exe (a snapshot, not history); prints command lines, so review the output before sharing it; exit 0 = patched and no children, 1 = not patched or children found, 3 = could not read the build, it does not match -Edition, or no w3wp.exe could be inspected
# Usage: .\sharepoint-build-and-w3wp.ps1 -Edition 2016|2019|SE      (SharePoint Management Shell on a farm server; read-only)
# Prints up to 200 characters of each child process command line: review the output before you share it.
param([Parameter(Mandatory)][ValidateSet('2016','2019','SE')][string]$Edition)
# Published fixed builds, written as parts so no dotted literal appears in the article
$fixed = @{ '2016' = [version]::new(16,0,5565,1001); '2019' = [version]::new(16,0,10417,20198); 'SE' = [version]::new(16,0,19725,20522) }
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
try { $build = (Get-SPFarm -ErrorAction Stop).BuildVersion }
catch { Write-Error "Could not read the farm build: $($_.Exception.Message). This is NOT a clean result."; exit 3 }
if (-not $build) { Write-Error 'The farm build was empty. This is NOT a clean result.'; exit 3 }
# Sanity check: the third part of the build should fit the edition (2016 about 5xxx, 2019 about 10xxx, SE about 19xxx; the ranges here are wider)
$ranges = @{ '2016' = @(4000, 5999); '2019' = @(10000, 10999); 'SE' = @(14000, 29999) }
$unsure = $false
if ($build.Build -lt $ranges[$Edition][0] -or $build.Build -gt $ranges[$Edition][1]) { Write-Warning "Farm build $build does not look like a $Edition build. Check -Edition: the comparison below may be wrong. This is NOT a clean result."; $unsure = $true }
$problem = 0
"Farm build: $build   Published fixed build for ${Edition}: $($fixed[$Edition])"
# The farm build comes from the configuration database; confirm each server's installed update separately.
if ($build -lt $fixed[$Edition]) { Write-Warning 'Farm build is BELOW the published fixed build.'; $problem = 1 }
# Live snapshot only: children of w3wp.exe running right now
try { $w3 = @(Get-CimInstance Win32_Process -Filter "Name='w3wp.exe'" -ErrorAction Stop) }
catch { Write-Error "Could not list processes: $($_.Exception.Message). This is NOT a clean result."; exit 3 }
if ($w3.Count -eq 0) { Write-Warning 'No w3wp.exe is running, so there is nothing to inspect. This is NOT a clean result.'; $unsure = $true }
$kids = 0
foreach ($p in $w3) {
  try { $children = @(Get-CimInstance Win32_Process -Filter "ParentProcessId=$($p.ProcessId)" -ErrorAction Stop) }
  catch { Write-Warning "Could not list the children of pid $($p.ProcessId). This is NOT a clean result."; $unsure = $true; continue }
  foreach ($c in $children) {
    $kids++
    $cl = [string]$c.CommandLine
    '{0} (pid {1}) under w3wp.exe pid {2}: {3}' -f $c.Name, $c.ProcessId, $p.ProcessId, $cl.Substring(0, [Math]::Min(200, $cl.Length))
  }
}
"w3wp_processes=$($w3.Count) children=$kids"
if ($kids -gt 0) { $problem = 1 }
if ($problem -eq 0 -and $unsure) { exit 3 }
exit $problem
Sigma — web server logs; POST to the web-part pages or carrying both parameters (field names vary by pipeline)
title: SharePoint CVE-2026-65660 POST to ToolPane.aspx or AddGallery.aspx, or POST carrying DisplayMode and MSOTlPn_DWP
id: 9d1b3f48-6141-4d35-9a08-513c817901e7
status: experimental
description: POST to ToolPane.aspx or AddGallery.aspx, or a POST whose URI carries both DisplayMode and MSOTlPn_DWP (request bodies are not visible in this field; use WAF body logging for the body strings), per the indicators published for CVE-2026-65660 by Viettel Cyber Security (request pattern). Legitimate page editing also uses these pages, so review who sent it.
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
logsource:
  category: webserver
detection:
  selection_method:
    cs-method: 'POST'
  selection_path:
    c-uri|contains:
      - '/_layouts/15/ToolPane.aspx'
      - '/_layouts/15/AddGallery.aspx'
  selection_params:
    c-uri|contains|all:
      - 'DisplayMode'
      - 'MSOTlPn_DWP'
  condition: selection_method and (selection_path or selection_params)
falsepositives:
  - Site owners and administrators editing web parts
level: medium
---
title: SharePoint CVE-2026-65660 w3wp.exe spawning a child process
id: d808cd11-fb7c-4acb-b580-1d5cf43e1444
status: experimental
description: A child process started by the IIS worker process w3wp.exe, listed as an indicator for CVE-2026-65660 by the Canadian Centre for Cyber Security (AL26-023). Baseline the children your farm normally starts before relying on this.
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
  - attack.persistence
  - attack.t1505.003
logsource:
  category: process_creation
  product: windows
detection:
  selection:
    ParentImage|endswith: '\w3wp.exe'
  condition: selection
falsepositives:
  - ASP.NET and SharePoint legitimately start csc.exe, conhost.exe and similar children; baseline and exclude them before alerting
level: medium
Splunk SPL — IIS data from the Splunk Add-on for Microsoft IIS (sourcetype ms:iis:auto); the field names cs_method, cs_uri_stem, cs_uri_query, sc_status and c_ip are not verified against the add-on and vary by version
index=* sourcetype="ms:iis:auto" cs_method=POST earliest="08/01/2026:00:00:00"
| eval q=urldecode(cs_uri_query)
| where match(lower(cs_uri_stem), "/_layouts/15/(toolpane|addgallery)\.aspx$") OR (like(q, "%DisplayMode%") AND like(q, "%MSOTlPn_DWP%"))
| stats count min(_time) AS first max(_time) AS last values(sc_status) AS statuses BY c_ip, cs_uri_stem
| sort - count
Microsoft Defender XDR KQL — advanced hunting, DeviceProcessEvents (needs the SharePoint servers onboarded; advanced hunting keeps about 30 days, so older activity needs your own archive)
DeviceProcessEvents
| where Timestamp > ago(30d)
| where InitiatingProcessFileName =~ "w3wp.exe"
| summarize runs = count(), first = min(Timestamp), last = max(Timestamp)
    by DeviceName, FileName, ProcessCommandLine, InitiatingProcessCommandLine
| order by last desc
If you see Do this
An unexpected child process under w3wp.exe (PowerShell output listing children of w3wp.exe, the Sigma process rule, or the KQL) Treat as a confirmed compromise, as the page's Defined response says. Isolate the server, preserve it, rotate the farm credentials and machine keys, and rebuild.
POST hits with DisplayMode and MSOTlPn_DWP, or body strings (ExpandedWrapper, ObjectDataProvider, XamlServices, LosFormatter) Treat as a lead. Check the same server and time for a child of w3wp.exe, Defender or AMSI SharePoint detections, access to IIS machine keys and unexpected web-part changes. If any corroborates, handle it as the row above. If nothing does, treat it as an attempt: confirm the August update is installed and keep the logs.
POSTs to ToolPane.aspx or AddGallery.aspx only, from known editors at times that match their work Record as expected. Add the editors' addresses to your notes and exclude them next time.
A farm build below the published fixed build for its edition Treat as exposed. Apply the 11 August 2026 update, restrict anonymous access, and run this hunt back to August. SharePoint 2016 and 2019 left support on 14 July 2026, so plan the move off them.
The script exits 3, the PowerShell exits 3, or the query returns no rows because no logs exist This is not a clean result. Find where the IIS and ULS logs went, and rely on the update and the process check instead.
No hits A good sign, not an exoneration. The published chain can leave only an in-memory web shell, so a clean file scan or a quiet log proves nothing. IIS does not log request bodies by default, the PowerShell process check is a snapshot of right now, logs rotate, and only the published request pattern was searched. Apply the update anyway.

3. MikroTik RouterOS CVE-2026-67279 added to CISA KEV

Bottom line

An unauthenticated SSH flaw lets attackers take over internet-exposed MikroTik routers. It has been exploited since 2 September, and CERT Polska has published attacker IPs and log signatures. Do now: upgrade, take SSH off the internet, and search every router for the indicators below. Skip if you run no MikroTik devices.

Indicators

TypeIndicatorWhere to lookSource
IoC82.192.72[.]4 — successful attacks since at least 2 Sep; created the ops account; received stolen diagnostic filesFirewall, flow and router logsCERT Polska
IoC103.102.31[.]18 — exploitation attemptsFirewall and router logsCERT Polska
IoCAdmin account ops in group full/user printCERT Polska
IoCLog: login failure for user -2 from <ip> via sshRouterOS log, syslogCERT Polska
IoCLog: user <name> added by ssh:-2@<ip>RouterOS log, syslogCERT Polska
IoC“Flagged” in /system/device-mode/printEach device, after upgradingCERT Polska
IoAfetch sending diagnostic (supout/RIF) files to an outside addressRouterOS log, flow dataCERT Polska
IoAUsers, scripts, scheduler tasks, proxies or tunnels nobody on your team createdConfiguration exportCERT Polska
IoA oursSSH rekey started before authentication completes. It’s encrypted, so it shows in router logs, not network sensorsRouterOS logCERT Polska (mechanism); analysis

What

CISA added CVE-2026-67279 to KEV on 25 September (Cyber Centre AV26-887). RouterOS SSH moves into the session after a client-requested rekey without finishing authentication. CERT Polska’s 22 September analysis says this gives the initial unauthenticated session in the “MikroTrick” chain, which attackers used from 2 September, before patches shipped. Fixed in 6.49.21, 7.23.4 and 7.24.2. CVSS 4.0 score: 6.9.

So what

The medium score undersells it. Whoever controls a router’s configuration controls what passes through it, and the attackers were already creating admin accounts and stealing diagnostic files.

Now what — P → I (analysis)

Hunt pack — run it, then act on what it says

The indicators are CERT Polska's: the two attacker addresses, log lines showing a login failure for user -2 and a user added by ssh:-2@<ip>, an admin account named ops in group full, and the word "Flagged" in /system/device-mode/print. The log-line and address hunts below are our analysis of those indicators. The bash script was run against synthetic router logs (hit, look-alike addresses, clean, empty, missing and compressed inputs); both Sigma rules were parsed and converted to Splunk, Kusto and Lucene queries with pySigma; the Splunk, KQL, ES|QL and Suricata were reviewed, not executed against a live platform, so test them on a non-production index first. The account, device-mode and configuration indicators live on the router itself, so they are checks to run by hand on each device (/user print, /system/device-mode/print, a configuration export), not code we publish: a script would have to log in to routers, which a hunt pack must not do. We are not offering PowerShell, YARA or osquery for this item: routers run neither. Defanged addresses (a dot wrapped in square brackets) need the brackets removed in the queries and rules; the bash script does it for you. Printed log lines (up to 300 characters) can carry tokens from query strings: review them before sharing the output. Adjust field and table names to your logging.

bash — exported RouterOS logs or syslog copies, plain or .gz; log wording is CERT Polska's, not checked against a live router; exit 0 = nothing found, 1 = found, 3 = nothing readable, any file unreadable or a truncated .gz (not a clean result)
#!/usr/bin/env bash
# Usage: ./mikrotik-hunt.sh LOGFILE...      (exported RouterOS log or syslog copies; plain or .gz; needs python3)
# Read-only. Exit 0 = nothing found, 1 = found, 2 = usage, 3 = nothing readable, any file unreadable or a truncated .gz (NOT a clean result).
[ $# -ge 1 ] || { echo "usage: $0 LOGFILE..." >&2; exit 2; }
command -v python3 >/dev/null || { echo "python3 is required (NOT a clean result)" >&2; exit 3; }
files=(); skipped=0
for f in "$@"; do if [ -f "$f" ] && [ -r "$f" ]; then files+=("$f"); else echo "skipping unreadable: $f" >&2; skipped=1; fi; done
[ "${#files[@]}" -gt 0 ] || { echo "no readable log files (NOT a clean result)" >&2; exit 3; }
gzip -dcf -- "${files[@]}" 2>/dev/null | python3 -c '
import re, sys
IPS = ["82[.]192[.]72[.]4", "103[.]102[.]31[.]18"]            # defanged in the page; refanged here
ip_re = [re.compile(r"(^|[^0-9.])" + re.escape(i.replace("[.]", ".")) + r"([^0-9.]|\.(?![0-9])|$)") for i in IPS]
fail_re = re.compile(r"login failure for user -2 from")
add_re = re.compile(r"added by ssh:-2@")
n = c_ip = c_fail = c_add = 0
for raw in sys.stdin.buffer:
    n += 1
    line = raw.decode("utf-8", "replace").rstrip()
    tags = []
    if any(r.search(line) for r in ip_re): tags.append("IP"); c_ip += 1
    if fail_re.search(line): tags.append("SSH-FAIL-USER-2"); c_fail += 1
    if add_re.search(line): tags.append("USER-ADDED-BY-SSH-2"); c_add += 1
    if tags: print("%d: [%s] %s" % (n, ",".join(tags), line[:300]))
print("scanned=%d hits_ip=%d hits_login_failure_-2=%d hits_user_added_by_ssh_-2=%d" % (n, c_ip, c_fail, c_add), file=sys.stderr)
sys.exit(3 if n == 0 else (1 if (c_ip + c_fail + c_add) else 0))
'
st=("${PIPESTATUS[@]}"); rc=${st[1]}; [ "${st[0]}" -eq 0 ] || { echo "a file could not be fully decompressed (NOT a clean result)" >&2; skipped=1; }
[ "$rc" -ne 0 ] || [ "$skipped" -eq 0 ] || { echo "some files were unreadable (NOT a clean result)" >&2; exit 3; }
exit $rc
Sigma — firewall or flow logs, and router syslog text (field names vary by pipeline; log wording is CERT Polska's, not checked against a live router)
# Remove the [ ] around each dot in the addresses before use
title: MikroTik CVE-2026-67279 traffic to or from published attacker addresses
id: 30f03faa-c603-4ebe-99b6-d6c904659fd9
status: experimental
description: Any connection to or from the two addresses CERT Polska published for the RouterOS SSH exploitation (successful attacks since at least 2 September, and exploitation attempts).
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
logsource:
  category: firewall
detection:
  selection:
    - src_ip:
        - '82[.]192[.]72[.]4'
        - '103[.]102[.]31[.]18'
    - dst_ip:
        - '82[.]192[.]72[.]4'
        - '103[.]102[.]31[.]18'
  condition: selection
falsepositives:
  - Your own scanner, if it uses these addresses (unlikely)
level: high
---
title: MikroTik RouterOS SSH login failure for user -2 or user added by ssh:-2
id: 2a83ea35-bf51-4057-92a0-55171d416ce9
status: experimental
description: The two RouterOS log lines CERT Polska lists for the MikroTrick chain (CVE-2026-67279), searched as free text in router syslog.
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
  - attack.persistence
  - attack.t1136.001
logsource:
  service: syslog
detection:
  keywords:
    - 'login failure for user -2 from'
    - 'added by ssh:-2@'
  condition: keywords
falsepositives:
  - None known; confirm any match on the device
level: high
Splunk SPL — two searches, run them separately; router syslog and firewall data; field names vary by connector; log wording is CERT Polska's, not checked against a live router; remove the brackets from the addresses
    ```Remove the [ ] around each dot in the addresses before use```
index=* (sourcetype=syslog OR sourcetype=mikrotik*) earliest="09/01/2026:00:00:00"
| where match(_raw, "login failure for user -2 from") OR match(_raw, "added by ssh:-2@")
| stats count min(_time) AS first max(_time) AS last BY host

    ```Remove the [ ] around each dot in the addresses before use```
index=* earliest="09/01/2026:00:00:00"
    (src_ip IN ("82[.]192[.]72[.]4","103[.]102[.]31[.]18") OR dest_ip IN ("82[.]192[.]72[.]4","103[.]102[.]31[.]18"))
| stats count min(_time) AS first max(_time) AS last values(dest_port) AS ports BY src_ip, dest_ip
Microsoft Sentinel / Log Analytics KQL — two queries, run them separately: Syslog table for router logs, CommonSecurityLog for firewalls; log wording is CERT Polska's, not checked against a live router; remove the brackets from the addresses
// Remove the [ ] around each dot in the addresses before use
Syslog
| where TimeGenerated > datetime(2026-09-01)
| where SyslogMessage contains "login failure for user -2 from" or SyslogMessage contains "added by ssh:-2@"
| summarize hits = count(), first = min(TimeGenerated), last = max(TimeGenerated) by HostName

// Remove the [ ] around each dot in the addresses before use
CommonSecurityLog
| where TimeGenerated > datetime(2026-09-01)
| where SourceIP in ("82[.]192[.]72[.]4", "103[.]102[.]31[.]18") or DestinationIP in ("82[.]192[.]72[.]4", "103[.]102[.]31[.]18")
| summarize hits = count(), first = min(TimeGenerated), last = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort
Elastic ES|QL — two queries, run them separately; ECS-mapped syslog and firewall logs; LIKE on message assumes a keyword-type field and the log wording is CERT Polska's, neither checked on a live system; remove the brackets from the addresses
// Remove the [ ] around each dot in the addresses before use
FROM logs-*
| WHERE @timestamp >= TO_DATETIME("2026-09-01T00:00:00Z")
| WHERE message LIKE "*login failure for user -2 from*" OR message LIKE "*added by ssh:-2@*"
| STATS hits = COUNT(*), first = MIN(@timestamp), last = MAX(@timestamp) BY host.name

// Remove the [ ] around each dot in the addresses before use
FROM logs-*
| WHERE @timestamp >= TO_DATETIME("2026-09-01T00:00:00Z")
| WHERE source.ip IN ("82[.]192[.]72[.]4", "103[.]102[.]31[.]18") OR destination.ip IN ("82[.]192[.]72[.]4", "103[.]102[.]31[.]18")
| STATS hits = COUNT(*), first = MIN(@timestamp), last = MAX(@timestamp) BY source.ip, destination.ip, destination.port
Suricata — network sensor that sees the routers' internet-facing traffic; remove the brackets from the addresses; matches addresses only, so it cannot see SSH contents
alert ip any any <> 82[.]192[.]72[.]4 any (msg:"TVM MikroTik CERT Polska address 82[.]192[.]72[.]4"; classtype:trojan-activity; sid:9002501; rev:1;)
alert ip any any <> 103[.]102[.]31[.]18 any (msg:"TVM MikroTik CERT Polska address 103[.]102[.]31[.]18"; classtype:trojan-activity; sid:9002502; rev:1;)
If you see Do this
Any CERT Polska indicator on a router: traffic with 82[.]192[.]72[.]4 (traffic with 103[.]102[.]31[.]18 alone is row 3), a -2 login-failure or user-added log line, an ops account in group full, or "Flagged" in /system/device-mode/print Treat as a confirmed compromise, as CERT Polska advises. Isolate the device, secure its logs and configuration, and reset it to factory settings. Do not restore a full backup from a device that may be compromised: rebuild from a known-clean export. Rotate admin credentials and VPN keys, and treat traffic through the router since 2 September as possibly intercepted.
Users, scripts, scheduler tasks, proxies or tunnels in a configuration export that nobody on your team created, or a fetch sending diagnostic (supout or RIF) files to an outside address Treat as a hit and handle it as the row above.
Traffic from 103[.]102[.]31[.]18 only, with no matching log line and no change on the router Treat as an attempt. Block the address, keep the logs, upgrade to 6.49.21, 7.23.4 or 7.24.2 (per the branch), take SSH off the internet-facing interfaces, and still check the router for the other indicators.
A router on a release older than the fixed one for its branch Upgrade, disable SSH on internet-facing interfaces and limit it to an admin network (IP, Services), then check device mode for "Flagged" after the upgrade.
Admin logins from your own management addresses or your own scanner Record as expected, with the owner, and exclude them next time.
The script exits 3, or a query returns no rows because no router logs were kept This is not a clean result. Find where the logs went, and check each router directly for the account, device-mode and configuration indicators.
No hits A good sign, not an exoneration. Router logs are short-lived and may never have been exported, exploitation began on 2 September before patches shipped, so older logs matter, only the two published addresses and two log lines were searched, and a rekey-before-authentication attempt is encrypted and shows only in router logs, not on the network. Upgrade and remove SSH from the internet anyway.

4. GitLab CVE-2026-85706 gets a public Metasploit module

Bottom line

One unauthenticated request reads any file on a vulnerable self-managed GitLab server, including its secrets. It has been exploited since 11 September, and exploitation is now point-and-click. Do now: upgrade, search your logs for the request pattern below, and rotate secrets on any instance that was exposed. Skip if you use GitLab.com or GitLab Dedicated.

Indicators

TypeIndicatorWhere to lookSource
IoCNone published — no attacker IPs or hashes—Rapid7; watchTowr via Security Affairs
IoAUnauthenticated POST or PUT to /api/v4/projects/<id>/repository/commits (or commits/, commits.json) or …/repository/files/…NGINX and Rails logs under /var/log/gitlab/; proxy/WAF logsMetasploit module; watchTowr
IoAPercent-encoded letters in the path: %63ommits, %72epository, %66ilesSameMetasploit module
IoAForm body with file.path or metadata.path naming a server file (default target /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml; also secrets.yml, gitlab-secrets.json, database.yml)WAF or request-body loggingMetasploit module; Greenbone
IoAError responses containing Invalid parameter: invalid %-encoding (. Each one is a leaked fileRails logsMetasploit module

What

Rapid7’s Metasploit update of 25 September added a module for CVE-2026-85706, an unauthenticated arbitrary file read in GitLab’s repository APIs, CVSS 10.0. It has been in CISA’s KEV catalog since 11 September, and watchTowr saw probes that day. Affected: CE/EE 18.7–18.11.11, 19.0–19.0.8, 19.1–19.1.7, 19.2–19.2.5, 19.3–19.3.1.

So what

KEV means someone exploited it; a Metasploit module means anyone can. The files it reads hold database credentials, instance secrets and runner tokens — the keys to everything your pipelines deploy to.

Now what — P → V (analysis)

Hunt pack — run it, then act on what it says

The indicators are the Metasploit module's and watchTowr's: an unauthenticated POST or PUT to /api/v4/projects/<id>/repository/commits or .../repository/files/..., percent-encoded letters in the path (%63ommits, %72epository, %66iles), a form body with file.path or metadata.path naming a server file (gitlab.yml, secrets.yml, gitlab-secrets.json, database.yml), and error responses containing Invalid parameter: invalid %-encoding (. No attacker address or hash is published. ERROR-LEAK lines are cut where the leaked file would begin. BODY-PATH fires only when file.path or metadata.path is followed on the same line by one of those server files (or /opt/gitlab/embedded); a request for any other file shows up only in the other categories. Point the script at api_json.log as well as production_json.log: API requests are logged in the separate file, so the API-WRITE pattern is blind without it. The hunts are our analysis of those patterns. A log line cannot say whether a request was unauthenticated, so the API-write pattern also matches normal CI and API traffic: check who sent it. The bash scripts were run against synthetic logs and a range of version strings (hit, clean, empty, missing, compressed and unparseable inputs); the three Sigma rules were parsed and converted to Splunk, Kusto and Lucene queries with pySigma; the Splunk, ES|QL and Suricata were reviewed, not executed against a live platform, so test them on a non-production index first. We are not offering KQL, PowerShell, YARA or osquery for this item: GitLab logs reach Sentinel through custom tables that differ per installation (the Sigma rules convert to Kusto for you), and no file or sample is published. Printed log lines (up to 300 characters) can carry tokens from query strings: review them before sharing the output. Adjust field and table names to your logging.

bash — NGINX access logs, production_json.log and api_json.log (API requests are logged there) or WAF logs from /var/log/gitlab/ (plain or .gz); exit 0 = nothing found, 1 = found, 3 = nothing readable, any file unreadable or a truncated .gz (not a clean result)
#!/usr/bin/env bash
# Usage: ./gitlab-hunt.sh LOGFILE...     (NGINX access, production_json.log, api_json.log or WAF logs; plain or .gz; needs python3)
# Read-only. Exit 0 = nothing found, 1 = found, 2 = usage, 3 = nothing readable, any file unreadable or a truncated .gz (NOT a clean result).
[ $# -ge 1 ] || { echo "usage: $0 LOGFILE..." >&2; exit 2; }
command -v python3 >/dev/null || { echo "python3 is required (NOT a clean result)" >&2; exit 3; }
files=(); skipped=0
for f in "$@"; do if [ -f "$f" ] && [ -r "$f" ]; then files+=("$f"); else echo "skipping unreadable: $f" >&2; skipped=1; fi; done
[ "${#files[@]}" -gt 0 ] || { echo "no readable log files (NOT a clean result)" >&2; exit 3; }
gzip -dcf -- "${files[@]}" 2>/dev/null | python3 -c '
import re, sys
leak = re.compile(r"Invalid parameter: invalid %-encoding \(")
cats = [
 ("ENCODED-PATH", re.compile(r"%63ommits|%72epository|%66iles", re.I)),
 ("API-WRITE",    re.compile(r"(POST|PUT)\b.{0,40}/api/v4/projects/[^ ?\"]+/repository/(commits|files)")),
 # file.path or metadata.path must be on the line followed by a server file name; a bare mention of database.yml etc. is normal Rails traffic
 ("BODY-PATH",    re.compile(r"(file|metadata)\.path[^\r\n]{0,200}(gitlab\.yml|secrets\.yml|gitlab-secrets\.json|database\.yml|/opt/gitlab/embedded)")),
 ("ERROR-LEAK",   leak),
]
n = 0
counts = dict((c[0], 0) for c in cats)
for raw in sys.stdin.buffer:
    n += 1
    line = raw.decode("utf-8", "replace").rstrip()
    tags = [name for name, rx in cats if rx.search(line)]
    for t in tags: counts[t] += 1
    if tags:
        out = line[:300]
        m = leak.search(line)
        if m: out = line[:min(m.end(), 300)] + " [rest of line withheld]"   # the leaked file follows the match
        print("%d: [%s] %s" % (n, ",".join(tags), out))
print("scanned=%d " % n + " ".join("%s=%d" % (k, counts[k]) for k, _ in cats), file=sys.stderr)
sys.exit(3 if n == 0 else (1 if sum(counts.values()) else 0))
'
st=("${PIPESTATUS[@]}"); rc=${st[1]}; [ "${st[0]}" -eq 0 ] || { echo "a file could not be fully decompressed (NOT a clean result)" >&2; skipped=1; }
[ "$rc" -ne 0 ] || [ "$skipped" -eq 0 ] || { echo "some files were unreadable (NOT a clean result)" >&2; exit 3; }
exit $rc
bash — version check against the affected ranges on the page; exit 0 = outside them, 1 = inside, 3 = could not parse or older than 18.7 (not covered)
#!/usr/bin/env bash
# Usage: ./gitlab-version-check.sh 19.1.7      (the version your instance reports; a -ee or -ce suffix is accepted)
# Compares it with the affected ranges in the page. Exit 0 = outside the listed ranges, 1 = inside, 2 = usage, 3 = could not parse or older than 18.7 (not covered, NOT a clean result).
[ $# -eq 1 ] || { echo "usage: $0 MAJOR.MINOR.PATCH" >&2; exit 2; }
v=${1#v}; v=${v%-ee}; v=${v%-ce}
if ! [[ $v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then echo "cannot parse '$1' (NOT a clean result)" >&2; exit 3; fi
maj=${BASH_REMATCH[1]}; min=${BASH_REMATCH[2]}; pat=${BASH_REMATCH[3]}
fixed=""
if   [ "$maj" -eq 18 ] && [ "$min" -ge 7 ] && [ "$min" -le 10 ]; then fixed="18.11.12"
elif [ "$maj" -eq 18 ] && [ "$min" -eq 11 ] && [ "$pat" -le 11 ]; then fixed="18.11.12"
elif [ "$maj" -eq 19 ] && [ "$min" -eq 0 ] && [ "$pat" -le 8 ]; then fixed="19.0.9"
elif [ "$maj" -eq 19 ] && [ "$min" -eq 1 ] && [ "$pat" -le 7 ]; then fixed="19.1.8"
elif [ "$maj" -eq 19 ] && [ "$min" -eq 2 ] && [ "$pat" -le 5 ]; then fixed="19.2.6"
elif [ "$maj" -eq 19 ] && [ "$min" -eq 3 ] && [ "$pat" -le 1 ]; then fixed="19.3.2"
fi
if [ -n "$fixed" ]; then echo "$v is inside a range the advisory lists as affected: upgrade to $fixed or later on this branch"; exit 1; fi
if [ "$maj" -lt 18 ] || { [ "$maj" -eq 18 ] && [ "$min" -lt 7 ]; }; then echo "$v is older than 18.7, which the page does not cover: check the advisory directly (NOT a clean result)" >&2; exit 3; fi
echo "$v is outside the affected ranges the page lists (18.7 to 18.11.11, 19.0 to 19.3.1)"
exit 0
Sigma — web server logs (field names vary by pipeline); the third rule searches Rails log text
title: GitLab CVE-2026-85706 percent-encoded repository API path
id: 3dac9d24-8758-4a86-a926-5efe7a46094f
status: experimental
description: Request path with percent-encoded letters in commits, repository or files, the evasion pattern in the Metasploit module for CVE-2026-85706.
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
logsource:
  category: webserver
detection:
  selection:
    c-uri|contains:
      - '%63ommits'
      - '%72epository'
      - '%66iles'
  condition: selection
falsepositives:
  - Clients that percent-encode every character (rare)
level: high
---
title: GitLab CVE-2026-85706 write request to the repository commits or files API
id: 4e210847-503c-4052-a1e0-21713a2f6464
status: experimental
description: POST or PUT to the commits or files repository API, the request the Metasploit module for CVE-2026-85706 sends. Normal CI and API clients use these endpoints with a token, so review who sent it.
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
logsource:
  category: webserver
detection:
  selection_method:
    cs-method:
      - 'POST'
      - 'PUT'
  selection_path:
    c-uri|contains|all:
      - '/api/v4/projects/'
      - '/repository/'
  selection_endpoint:
    c-uri|contains:
      - '/repository/commits'
      - '/repository/files'
  condition: selection_method and selection_path and selection_endpoint
falsepositives:
  - Authenticated CI jobs and API clients that commit through the API
level: medium
---
title: GitLab CVE-2026-85706 invalid percent-encoding error leaking a file
id: 8b5783bf-93a2-4bfc-8e38-17bceaa98b87
status: experimental
description: The error text Rapid7's Metasploit module treats as a leaked file for CVE-2026-85706, searched as free text in Rails logs.
author: Threat Vector Management
date: 2026-10-06
tags:
  - attack.initial_access
  - attack.t1190
  - attack.collection
  - attack.t1005
logsource:
  service: application
detection:
  keywords:
    - 'Invalid parameter: invalid %-encoding ('
  condition: keywords
falsepositives:
  - A client that sends a malformed percent-encoded parameter by accident
  - Internet scanners sending malformed percent-encoding to a patched instance
level: medium
Splunk SPL — GitLab NGINX, production_json.log or api_json.log logs; searches the raw event from 10 September, so it does not depend on field names
index=* (sourcetype=*gitlab* OR sourcetype=*nginx*) earliest="09/10/2026:00:00:00"
| regex _raw="%63ommits|%72epository|%66iles|Invalid parameter: invalid %-encoding \(|(POST|PUT).{0,40}/api/v4/projects/[^ ?\"]+/repository/(commits|files)"
| stats count min(_time) AS first max(_time) AS last BY host, source
Elastic ES|QL — ECS-mapped NGINX or proxy logs (url.original, http.request.method); LIKE and RLIKE assume url.original is a keyword-type field, not checked on a live system
FROM logs-*
| WHERE @timestamp >= TO_DATETIME("2026-09-10T00:00:00Z")
| WHERE url.original LIKE "*%63ommits*" OR url.original LIKE "*%72epository*" OR url.original LIKE "*%66iles*"
    OR (http.request.method IN ("POST", "PUT") AND url.original RLIKE ".*/api/v4/projects/[^/]+/repository/(commits|files).*")
| STATS hits = COUNT(*), first = MIN(@timestamp), last = MAX(@timestamp) BY source.ip, http.request.method
| SORT hits DESC
Suricata — plain HTTP only; place it on the leg behind your TLS termination; each of the three rules catches one of the encoded letters
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"TVM GitLab CVE-2026-85706 encoded commits in URI"; flow:established,to_server; http.uri.raw; content:"%63ommits"; classtype:web-application-attack; sid:9002511; rev:1;)
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"TVM GitLab CVE-2026-85706 encoded repository in URI"; flow:established,to_server; http.uri.raw; content:"%72epository"; classtype:web-application-attack; sid:9002512; rev:1;)
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"TVM GitLab CVE-2026-85706 encoded files in URI"; flow:established,to_server; http.uri.raw; content:"%66iles"; classtype:web-application-attack; sid:9002513; rev:1;)
If you see Do this
An ENCODED-PATH or ERROR-LEAK hit on an instance that was on an affected version when it happened Treat as a completed file read, as the page's Defined response says. Rotate instance secrets, database credentials, runner registration tokens and CI/CD variables, and upgrade to 18.11.12, 19.0.9, 19.1.8, 19.2.6 or 19.3.2 (per branch). Every error response containing Invalid parameter: invalid %-encoding ( is a leaked file.
A BODY-PATH hit where the same line shows file.path or metadata.path naming a server config file, or /opt/gitlab/embedded/..., on an instance that was on an affected version when it happened Treat as a completed file read and handle it as the row above.
An API-WRITE hit (POST or PUT to the commits or files API) with no token or signed-in user on the request Treat as a match on an unpatched instance and handle it as the first row. If the log fields you keep cannot tell you, assume it was unauthenticated until the sender is identified (our assumption).
API-WRITE hits that all come from known CI runners, tokens or users Record them as expected, and exclude those sources next time.
Matching requests dated after the instance was upgraded Treat as attempts. Keep the logs, and check that the same sources made no request before the upgrade.
The version check exits 1 The instance is in the published affected range. Upgrade now (no workaround is documented), then run the log hunt back to 10 September. Rotate secrets if the instance was reachable from the internet while affected.
The scripts exit 3, or the query returns nothing because logs from 10 September onward are gone This is not a clean result. Rely on the version and exposure history: an instance that was exposed and affected needs the secrets rotated even without a hit.
No hits A good sign, not an exoneration. The page gives no attacker address, request bodies are rarely logged so the file.path and metadata.path patterns usually cannot be seen, logs rotate, and only the published request patterns were searched. An exposed instance that was affected needs its secrets rotated regardless.

5. ServiceNow fixes two critical unauthenticated flaws

Bottom line

An unauthenticated SQL injection and an authorization bypass are fixed, with no exploitation known. Do now: self-hosted or partner-hosted, apply the hot fix; hosted by ServiceNow, confirm your instance was updated. Skip otherwise.

Indicators

TypeIndicatorWhere to lookSource
IoCNone published; no exploitation known—ServiceNow; Cyber Centre AV26-963
IoANone published. Ours: database errors or unusually large results on unauthenticated requestsInstance and proxy logsAnalysis

What

CVE-2026-13016 (SQL injection) and CVE-2026-86860 (missing authorization) in the ServiceNow AI Platform are both rated CVSS 4.0 9.3. ServiceNow has patched hosted instances and provided fixes to partners and self-hosted customers. Vulnerable releases (Cyber Centre AV26-963): Australia Patch 2 Hot Fix 4b W32, Patch 4 Hot Fix 3 and Patch 5 and earlier; Yokohama Patch 13 Hot Fix 5a and earlier; Zurich Patch 10 Hot Fix 3b, Patch 10 Hot Fix 4a W32 and Patch 11 Hot Fix 3 and earlier.

So what

These instances hold ticket history, CMDB data and often integration credentials for much of the estate. Self-hosted instances tend to patch slowest.

Now what — P (analysis)

Hunt pack — run it, then act on what it says

ServiceNow and the Canadian Centre for Cyber Security publish no indicator for these two flaws, and no exploitation is known. The only indicator on the page is ours: database errors or unusually large results on unauthenticated requests, in instance and proxy logs. These hunts look for exactly that and nothing more, so they are weaker than a signature and say so. Patch state is not scripted: the page's list of vulnerable releases is a set of patch and hot-fix names that does not reduce to a safe automatic comparison, so confirm the fix from ServiceNow KB3159623 or get written confirmation from ServiceNow. The bash script was run against synthetic proxy logs in common/combined format (hit, clean, empty, missing, compressed and unparseable inputs); the Splunk, KQL and ES|QL queries were reviewed, not executed against a live platform, so test them on a non-production index first. We are not offering Sigma, Suricata, YARA, osquery or PowerShell for this item: no string, path, parameter or file is published, and a rule written from a guess would be counted as a control it is not. If a proxy never records a signed-in user, every request counts as unauthenticated: raise the size threshold until the list is short enough to read. Lines that do not match the log format are skipped (compare parsed= with scanned= on stderr), and the script exits 3 only when none match. Printed log lines (up to 300 characters) can carry tokens from query strings: review them before sharing the output. Adjust field and table names to your logging.

bash — reverse-proxy or WAF logs in common or combined format (plain or .gz); first argument is the response size in bytes to flag; exit 0 = nothing found, 1 = found, 3 = nothing readable, any file unreadable, or no line matched the format (not a clean result)
#!/usr/bin/env bash
# Usage: ./servicenow-proxy-hunt.sh MIN_BYTES LOGFILE...    (reverse-proxy or WAF logs in common/combined format; plain or .gz; needs python3)
# Our reading of the page's indicator, not a published signature: requests with no signed-in user ("-") that returned
# a server error (5xx) or a response of at least MIN_BYTES. If your proxy never records a user, every request is "-":
# raise MIN_BYTES until the list is short enough to read. Read-only.
# Exit 0 = nothing found, 1 = found, 2 = usage, 3 = nothing readable or no line matched the log format (NOT a clean result).
[ $# -ge 2 ] || { echo "usage: $0 MIN_BYTES LOGFILE..." >&2; exit 2; }
case $1 in ''|*[!0-9]*) echo "usage: MIN_BYTES must be a number" >&2; exit 2;; esac
min=$1; shift
command -v python3 >/dev/null || { echo "python3 is required (NOT a clean result)" >&2; exit 3; }
files=(); skipped=0
for f in "$@"; do if [ -f "$f" ] && [ -r "$f" ]; then files+=("$f"); else echo "skipping unreadable: $f" >&2; skipped=1; fi; done
[ "${#files[@]}" -gt 0 ] || { echo "no readable log files (NOT a clean result)" >&2; exit 3; }
gzip -dcf -- "${files[@]}" 2>/dev/null | python3 -c '
import re, sys
MIN = int(sys.argv[1])
rx = re.compile(r"^(\S+) \S+ (\S+) \[[^\]]+\] \"([^\"]*)\" (\d{3}) (\d+|-)")
n = parsed = big = err = 0
for raw in sys.stdin.buffer:
    n += 1
    line = raw.decode("utf-8", "replace").rstrip()
    m = rx.match(line)
    if not m: continue
    parsed += 1
    user, status, size = m.group(2), int(m.group(4)), (0 if m.group(5) == "-" else int(m.group(5)))
    if user != "-": continue
    tags = []
    if status >= 500: tags.append("UNAUTH-5XX"); err += 1
    if size >= MIN: tags.append("UNAUTH-LARGE"); big += 1
    if tags: print("%d: [%s] %s" % (n, ",".join(tags), line[:300]))
print("scanned=%d parsed=%d unauth_5xx=%d unauth_large=%d" % (n, parsed, err, big), file=sys.stderr)
sys.exit(3 if parsed == 0 else (1 if (err + big) else 0))
' "$min"
st=("${PIPESTATUS[@]}"); rc=${st[1]}; [ "${st[0]}" -eq 0 ] || { echo "a file could not be fully decompressed (NOT a clean result)" >&2; skipped=1; }
[ "$rc" -ne 0 ] || [ "$skipped" -eq 0 ] || { echo "some files were unreadable (NOT a clean result)" >&2; exit 3; }
exit $rc
Splunk SPL — proxy logs with user, status, bytes_out, src_ip and uri_path fields (names vary by connector); our reading of the page's indicator
index=* sourcetype=<your reverse-proxy sourcetype> earliest=-30d
| where isnull(user) OR user="-"
| stats count AS requests sum(eval(if(status>=500,1,0))) AS server_errors max(bytes_out) AS largest_response BY src_ip, uri_path
| where server_errors>0 OR largest_response>=<bytes you consider unusually large>
| sort - largest_response
Microsoft Sentinel / Log Analytics KQL — IIS or Application Request Routing in front of the instance (W3CIISLog); for other proxies swap in your own table; 1000000 bytes is a placeholder, set your own
W3CIISLog
| where TimeGenerated > ago(30d)
| where isempty(csUserName) or csUserName == "-"
| summarize requests = count(), server_errors = countif(toint(scStatus) >= 500), largest_response = max(scBytes)
    by cIP, csUriStem
| where server_errors > 0 or largest_response >= 1000000
| order by largest_response desc
Elastic ES|QL — ECS-mapped proxy logs; 1000000 bytes is a placeholder, set your own
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE user.name IS NULL OR user.name == "-"
| WHERE http.response.status_code >= 500 OR http.response.body.bytes >= 1000000
| STATS requests = COUNT(*), largest = MAX(http.response.body.bytes) BY source.ip, url.path
| SORT largest DESC
If you see Do this
Evidence that an injection succeeded: database errors in responses, together with large result sets returned to the same unauthenticated source Treat as a compromise of the instance data and integration credentials, as the page's Defined response says. Rotate them all, including integration and MID Server credentials, and bring in ServiceNow and your incident-response contact.
Server errors or very large responses to unauthenticated requests, with no sign of a database error or data returned Treat as a lead. Ask ServiceNow (or your hosting partner) for the instance-side logs for the same source and time, apply the fix if it is not yet in, and escalate to the row above if the logs show injection.
Hits from a scanner you operate, a health check, a public portal page or a known integration Confirm with the owner, record it, and exclude that source or path next time.
An instance on a release the advisory lists as vulnerable, or without written confirmation that it was updated Apply the hot fix from ServiceNow KB3159623, or get written confirmation from ServiceNow, today. Self-hosted and partner-hosted instances are your job; hosted-by-ServiceNow instances need the confirmation.
The script exits 3, no proxy logs exist, or the instance sits behind a proxy that does not log responses This is not a clean result. Ask ServiceNow or your hosting partner what they logged, and rely on the fix and the credential review.
No hits A good sign, not an exoneration. No indicator for these flaws is published, so this searches only our own guess at what abuse looks like. A successful injection may return no error and a small result, logs rotate, a hosted instance's own logs are not yours to read, and an attacker with valid access would not appear as unauthenticated. Apply the fix and review integration credentials for excess privilege anyway.

Sector signal — only what a source actually said

No source attributed today’s items to a sector. Kiteworks’ customers span healthcare, technology, education, automotive and government (TechCrunch), but that describes who uses the product, not who is being targeted.

How this brief is put together

Reported facts, IoCs and sourced IoAs are cited as published. IoCs are shown defanged. Items marked “ours” or “analysis” are our own judgment: priority order, PIVCCERL™ stages (P = Preparation, I = Identification, V = Validation), ATT&CK mappings and the network guidance. Kiteworks’ notice is private, so its content comes from quoted reporting. Threat Brief publishes when something warrants it, not on a schedule.

ATT&CK: GitLab — T1190, T1552.001, T1005 (Hive Pro). Ours: MikroTik — T1190, T1136.001, T1041; SharePoint — T1190, T1505.003; Kiteworks — T1190 expected.

Threat Vector Management advises the MDR and SOC providers, and the organizations they protect, on exposure like this — including how file-transfer and other internet-facing systems should be fenced in before the next warning. Start a conversation →

More editions: Threat Brief archive.

Threat Vector Management is not affiliated with any company operating under the name “ThreatVector.”


Sources

  1. TechCrunch — Kiteworks urges customers to shut down their servers amid ‘imminent’ threat of cyberattack, 25 Sep 2026
  2. The CyberWire — Daily Briefing, 25 Sep 2026
  3. SC Media — Kiteworks warns customers to shut down servers due to possible imminent attack, citing heise online
  4. Historical CVE records — CVE-2021-27101 (Accellion FTA), CVE-2023-34362 (MOVEit Transfer)
  5. Canadian Centre for Cyber Security — AV26-804, Microsoft security advisory (August 2026 monthly rollup), update of 25 Sep 2026
  6. Microsoft Security Response Center — CVE-2026-65660
  7. Canadian Centre for Cyber Security — AV26-887, MikroTik security advisory, update 2 of 25 Sep 2026; CVE-2026-67279 record
  8. Rapid7 — Metasploit Wrap-Up, 25 Sep 2026; CVE-2026-85706 record
  9. Canadian Centre for Cyber Security — AV26-963, ServiceNow security advisory, 25 Sep 2026; CVE records CVE-2026-13016 and CVE-2026-86860
  10. Microsoft — SharePoint Server 2019 lifecycle and SharePoint Server 2016 lifecycle
  11. Kiteworks window — Sophos, Kiteworks recommends server shutdown pending possible attack, 25 Sep 2026
  12. Indicators — BleepingComputer, Kiteworks urges 6-hour server shutdown over potential zero-day attacks, 25 Sep 2026; CISA, AA23-158A (MOVEit, historical)
  13. Indicators — Canadian Centre for Cyber Security, AL26-023, SharePoint CVE-2026-65660; Viettel Cyber Security, CVE-2026-65660 technical analysis; The Hacker News, SharePoint flaw initially listed as spoofing enables authenticated RCE; Cyber Security News, SharePoint RCE detection guidance
  14. Indicators — CERT Polska, Critical vulnerabilities in MikroTik RouterOS are being actively exploited and MikroTrick: technical analysis, 22 Sep 2026
  15. Indicators — Rapid7, Metasploit module source; Security Affairs, citing watchTowr; Greenbone, GitLab path traversal; Hive Pro, threat advisory

Provenance: compiled 25 September 2026 (afternoon) by Threat Vector Management from a sweep of vendor, government and researcher sources, with research and fact-checking support from Claude (Anthropic). Every cited figure was checked against its source on the day of publication.

Get the next one first

New Threat Briefs and Deep Dives, delivered to your feed reader the moment they publish. No sign-up, no email, no tracking.

Follow Insights