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.
| Item | Exploited? · Fix | Do now | Top IoCs | Top IoAs |
|---|---|---|---|---|
| 1. Kiteworks | Warned as imminent · none for the unknown flaw; 9.5.1 fixes known ones | Shut down for the window; save logs first; deny outbound by default | None published | Ours: any connection to the switched-off appliance; new outbound destinations; outbound volume spikes |
| 2. SharePoint Server CVE-2026-65660 | Yes (KEV 25 Sep) · 11 Aug 2026 update | Patch; restrict anonymous access | None published | POST to ToolPane.aspx / AddGallery.aspx with MSOTlPn_DWP and a broken Register directive; w3wp.exe child processes |
| 3. MikroTik RouterOS CVE-2026-67279 | Yes (KEV 25 Sep; attacks since 2 Sep) · 6.49.21 / 7.23.4 / 7.24.2 | Upgrade; no SSH from the internet; hunt for the IoCs | 82.192.72[.]4, 103.102.31[.]18; admin user ops; log login failure for user -2 | Users, scripts or scheduler tasks nobody created; fetch of diagnostic files to an outside address |
| 4. GitLab (self-managed) CVE-2026-85706 | Yes (KEV 11 Sep; public Metasploit module 25 Sep) · 18.11.12 / 19.0.9 / 19.1.8 / 19.2.6 / 19.3.2 | Patch; rotate secrets if it was exposed | None published | Unauthenticated 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, -86860 | No known exploitation · hot fix (KB3159623) | Self-hosted: apply the hot fix. Hosted: confirm with ServiceNow | None published | None published |
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
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
| Type | Indicator | Where to look | Source |
|---|---|---|---|
| IoC | None published. No flaw, tooling, infrastructure or threat actor disclosed | — | BleepingComputer; Kiteworks advisories |
| IoA ours | Any connection attempt to the appliance’s address while it is off | Firewall logs | Analysis |
| IoA ours | Outbound connection to a destination the appliance has never contacted, or outbound volume well above its hourly baseline | Flow logs (NetFlow/IPFIX/VPC), DNS resolver logs | Analysis |
| IoA ours | Admin login from an unfamiliar address or at an unusual hour | Kiteworks admin and web logs | Analysis |
| IoA ours | New or changed files in directories the web server serves (in 2023, MOVEit attackers planted human2.aspx) | File-integrity monitoring | Analysis; 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)
- Before the window: export application, web and system logs and take a snapshot, stored off the appliance. Upgrade to 9.5.1 before bringing it back. If a business owner insists it stays up, get that risk acceptance in writing.
- Blast radius: list and prepare to rotate what it holds: directory bind accounts, SMTP relay credentials, storage connectors, SSO configuration, integration API keys.
- Defined response: an unexplained large outbound transfer, or an admin session nobody can account for, is a confirmed compromise. Isolate the appliance, preserve everything, rotate every credential it holds and start the notification clock. Don’t wait for Kiteworks to confirm the method.
- Communication, decided today: who calls Kiteworks, your incident-response retainer and your insurer, and who owns regulatory notification. If Kiteworks hosts your instance, ask your account team whether it is included.
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.
#!/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
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
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
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)
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.
| Control | Works against an unknown flaw? | Do this |
|---|---|---|
| 1. Inbound allowlist | Yes — the best control short of power-off | HTTPS/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-default | Yes — best record against file-transfer theft | Allow 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 monitoring | Yes — needs no decryption or signature | Baseline 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 mode | Partly — generic rules catch the usual bug classes | OWASP 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/IPS | No, until a signature ships — and it can’t see inside TLS the appliance terminates itself | Confirm 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. Segmentation | Yes — limits what a compromise reaches | Own DMZ zone; no route inside beyond its named dependencies |
| 7. Logging while switched off | Yes — free targeting data | Keep 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
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
| Type | Indicator | Where to look | Source |
|---|---|---|---|
| IoC | None published — no addresses, hashes or file names | — | Cyber Centre AL26-023 |
| IoA | POST 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 attribute | IIS logs, WAF, request-body logging | Viettel Cyber Security |
| IoA | Request bodies naming ExpandedWrapper, ObjectDataProvider, XamlServices or LosFormatter | WAF, AMSI | Viettel Cyber Security |
| IoA | w3wp.exe spawning child processes or loading unfamiliar assemblies; Defender/AMSI SharePoint detections; access to IIS machine keys; unexpected web-part changes | EDR, Windows event logs, SharePoint ULS | Cyber 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)
- Apply the August update; restrict anonymous access; make sure farm and application-pool accounts aren’t domain admins.
- Hunt the IoAs above in IIS, ULS, Windows event, PowerShell and endpoint telemetry back to August.
- Defined response: an unexpected child process under
w3wp.exeis a confirmed compromise. Isolate, preserve, rotate farm credentials and machine keys, rebuild.
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.
#!/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
# 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
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
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
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
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
| Type | Indicator | Where to look | Source |
|---|---|---|---|
| IoC | 82.192.72[.]4 — successful attacks since at least 2 Sep; created the ops account; received stolen diagnostic files | Firewall, flow and router logs | CERT Polska |
| IoC | 103.102.31[.]18 — exploitation attempts | Firewall and router logs | CERT Polska |
| IoC | Admin account ops in group full | /user print | CERT Polska |
| IoC | Log: login failure for user -2 from <ip> via ssh | RouterOS log, syslog | CERT Polska |
| IoC | Log: user <name> added by ssh:-2@<ip> | RouterOS log, syslog | CERT Polska |
| IoC | “Flagged” in /system/device-mode/print | Each device, after upgrading | CERT Polska |
| IoA | fetch sending diagnostic (supout/RIF) files to an outside address | RouterOS log, flow data | CERT Polska |
| IoA | Users, scripts, scheduler tasks, proxies or tunnels nobody on your team created | Configuration export | CERT Polska |
| IoA ours | SSH rekey started before authentication completes. It’s encrypted, so it shows in router logs, not network sensors | RouterOS log | CERT 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)
- Upgrade. Independently, disable SSH on internet-facing interfaces and limit it to an admin network (IP → Services).
- Search every router for the IoCs above and block the two addresses.
- Defined response, per CERT Polska: on any hit, isolate the device, secure its logs and configuration, and reset it to factory settings. Don’t 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 it since 2 September as possibly intercepted.
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.
#!/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
# 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
```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
// 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
// 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
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
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
| Type | Indicator | Where to look | Source |
|---|---|---|---|
| IoC | None published — no attacker IPs or hashes | — | Rapid7; watchTowr via Security Affairs |
| IoA | Unauthenticated 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 logs | Metasploit module; watchTowr |
| IoA | Percent-encoded letters in the path: %63ommits, %72epository, %66iles | Same | Metasploit module |
| IoA | Form 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 logging | Metasploit module; Greenbone |
| IoA | Error responses containing Invalid parameter: invalid %-encoding (. Each one is a leaked file | Rails logs | Metasploit 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)
- Upgrade to 18.11.12, 19.0.9, 19.1.8, 19.2.6 or 19.3.2. No workaround is documented.
- Search the logs above back to 10 September.
- Defined response: one match on an unpatched instance is a completed file read. Rotate instance secrets, database credentials, runner registration tokens and CI/CD variables.
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.
#!/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
#!/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
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
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
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
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
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
| Type | Indicator | Where to look | Source |
|---|---|---|---|
| IoC | None published; no exploitation known | — | ServiceNow; Cyber Centre AV26-963 |
| IoA | None published. Ours: database errors or unusually large results on unauthenticated requests | Instance and proxy logs | Analysis |
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)
- Apply the hot fix from ServiceNow KB3159623, or get written confirmation from ServiceNow.
- Review integration and MID Server credentials for excess privilege.
- Defined response: evidence of successful injection means the instance data and integration credentials are compromised. Rotate them all.
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.
#!/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
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
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
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
- TechCrunch — Kiteworks urges customers to shut down their servers amid ‘imminent’ threat of cyberattack, 25 Sep 2026
- The CyberWire — Daily Briefing, 25 Sep 2026
- SC Media — Kiteworks warns customers to shut down servers due to possible imminent attack, citing heise online
- Historical CVE records — CVE-2021-27101 (Accellion FTA), CVE-2023-34362 (MOVEit Transfer)
- Canadian Centre for Cyber Security — AV26-804, Microsoft security advisory (August 2026 monthly rollup), update of 25 Sep 2026
- Microsoft Security Response Center — CVE-2026-65660
- Canadian Centre for Cyber Security — AV26-887, MikroTik security advisory, update 2 of 25 Sep 2026; CVE-2026-67279 record
- Rapid7 — Metasploit Wrap-Up, 25 Sep 2026; CVE-2026-85706 record
- Canadian Centre for Cyber Security — AV26-963, ServiceNow security advisory, 25 Sep 2026; CVE records CVE-2026-13016 and CVE-2026-86860
- Microsoft — SharePoint Server 2019 lifecycle and SharePoint Server 2016 lifecycle
- Kiteworks window — Sophos, Kiteworks recommends server shutdown pending possible attack, 25 Sep 2026
- Indicators — BleepingComputer, Kiteworks urges 6-hour server shutdown over potential zero-day attacks, 25 Sep 2026; CISA, AA23-158A (MOVEit, historical)
- 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
- Indicators — CERT Polska, Critical vulnerabilities in MikroTik RouterOS are being actively exploited and MikroTrick: technical analysis, 22 Sep 2026
- 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.