Fix live exploitierte RouterOS-CLI-Injection + Parser-Datenverlust

Deep-Dive-Gegencheck mit echtem Exploit-Nachweis gegen den Testrouter:

- RouterOSCommand.cliLine quotete Werte nur bei Leerzeichen und escapte
  eingebettete Anführungszeichen nie. Ein Kommentar wie test" ; :log
  warning "X schloss das Quoting vorzeitig und ließ RouterOS den Rest
  als zweiten Befehl ausführen. Live exploitiert (injizierter
  script,warning-Log-Eintrag) und live als behoben bestätigt. Betraf
  jede Schreiboperation über SSH - auf dem aktuellen Testrouter ist
  www-ssl deaktiviert, REST also unerreichbar, der Bug war aktiv.
- RouterOSCliParser.keyValues nahm an, print terse quote mehrwortige
  Werte - live an zwei Menüs widerlegt (RouterOS 7.24.4 quotet dort
  nichts). Trunkierte jeden mehrwortigen Wert beim ersten Leerzeichen.
  Fix: Token-Scan statt Regex.
- SSHTransport.fetchFieldValues defensiv gegen dieselbe Injection-Klasse
  gehärtet (aktuell nur hartkodiert aufgerufen, aber generische API).

2 neue Regressionstests, alle 101 Unit-Tests grün. Details in bugs.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kay
2026-09-17 18:08:34 +02:00
co-authored by Claude Sonnet 5
parent 3b84d31ec6
commit 800b48406b
7 changed files with 227 additions and 16 deletions
+91
View File
@@ -101,6 +101,97 @@ Schreibvorgangs aussetzt.
---
## 2026-09-17 (Nachtrag: "erneuter Gegencheck mit echtem Deep Dive")
Zweite, tiefere Testrunde auf expliziten Nutzerwunsch. Diesmal inkl. echter Exploit-Verifikation
gegen den Router (nicht nur Code-Lesen) — beide folgenden Funde live nachgewiesen und live
gegenverifiziert, dass der Fix greift, mit anschließendem Aufräumen der Testartefakte.
### 5. RouterOS-CLI-Injection über beliebige Textfelder (SSH-Transportpfad)
**Status:** fixed (live exploitiert UND live als behoben bestätigt, siehe unten)
**Confidence:** sehr hoch — kein Verdacht, sondern reproduzierter Exploit gegen den echten Router
`RouterOSCommand.cliLine` (`RouterOSCommand.swift`) baute CLI-Zeilen für den SSH-Transport per
String-Interpolation. Die alte `quoteIfNeeded(_:)` quotete einen Wert nur, wenn er ein Leerzeichen
enthielt, und escapte darin enthaltene `"`-Zeichen nie. RouterOS' Konsole behandelt `;` als
Befehlstrenner (dieselbe Gefahr, die `SSHTransport.runDiagnosticCommand`s Doku-Kommentar für
eigene, aufruferseitig sanitisierte Eingaben bereits benennt) — ein Wert wie
`test" ; :log warning "INJECTED` (ein völlig plausibler freier Kommentar/SSID/Hostname) schloss
das Anführungszeichen vorzeitig und ließ den Rest als zweiten, unabhängigen RouterOS-Befehl laufen.
**Exploit live bestätigt** (Testrouter, aufgeräumt danach): Befehl
`/ip firewall address-list add list=injection-test address=10.10.10.10 comment="test" ; :log warning "INJECTED-VIA-COMMENT-FIELD"`
über SSH ausgeführt → Log zeigt `script,warning INJECTED-VIA-COMMENT-FIELD` als eigenständig
ausgeführten zweiten Befehl, ausgelöst rein durch ein Kommentarfeld.
Betroffen: jeder Text, der über den SSH-Transport in ein `RouterOSCommand` läuft — praktisch jedes
Feld im Setup-Wizard (SSID, Kommentare, DNS-Server, …) und jedes Freitextfeld im Experte-Tab
("Weitere Parameter"). Der REST-Pfad ist NICHT betroffen (JSON-Encoding, kein String-Interpolieren
— siehe `RestTransport.apply`). Auf dem aktuellen hAP-lite-Testrouter ist `www-ssl` deaktiviert
(`/ip service print` bestätigt: Zeile 9, `X` = disabled) — REST ist also gar nicht erreichbar, jede
Schreiboperation läuft aktuell über SSH. Der Bug war damit live aktiv, nicht nur theoretisch.
**Fix:** `quoteIfNeeded` quotet jetzt immer und escaped `\``\\`, `"``\"` innerhalb der
Anführungszeichen — live verifiziert, dass das RouterOS' eigene Escape-Syntax ist (`\"`/`\\`
rundeten korrekt durch `print detail`). Unconditionelles Quoting live als unproblematisch bestätigt
(auch für einwortige Werte und `yes`/`no`-Booleans anstandslos akzeptiert). Gleiche Lücke defensiv
auch in `SSHTransport.fetchFieldValues` geschlossen (aktuell nur mit hartkodiertem `"no"`
aufgerufen, aber generische `RouterOSTransport`-Methode). Exploit-PoC danach erneut gegen den
Router gefahren — kein injizierter Log-Eintrag mehr, Kommentar korrekt als reiner Text gespeichert.
Neuer Regressionstest `testCliLineEscapesEmbeddedQuotesPreventingCommandInjection`. Build + alle
101 Unit-Tests grün.
### 6. Genereller RouterOS-Antwort-Parser trunkiert mehrwortige Werte
**Status:** fixed (Ursache live nachgewiesen, Fix per Unit-Test abgesichert)
**Confidence:** sehr hoch — live gegen zwei unabhängige Menüs nachgewiesen
`RouterOSCliParser.keyValues(from:)` nahm an, `print terse` quote mehrwortige Werte in
Anführungszeichen (Regex `("[^"]*"|\S+)`). Live-Test (zwei unabhängige Menüs, `/ip firewall
address-list` und `/interface ethernet`, RouterOS 7.24.4) zeigt: **RouterOS quotet dort gar
nichts** — ein Kommentar `"multi word test value"` kommt als literales, unquotiertes
`comment=multi word test value name=ether2 ...` zurück. Der alte Regex-Parser matchte dafür nur
`\S+` und schnitt den Wert beim ersten Leerzeichen ab — "word test value" ging komplett verloren,
ohne Fehler, ohne Warnung.
Betrifft praktisch jedes mehrwortige Freitextfeld, das über den generischen Parser gelesen wird —
vor allem das Experte-Tab-"Weitere Parameter (frei)"-Grid, das laut eigenem Doku-Kommentar "fast
alle" Menüs generisch parst. Der Bug war bereits indirekt im eigenen Testcode sichtbar: der
existierende Live-Dump-Test `testParseInterfacesMatchesLiveHexOutput` enthält exakt dieses Muster
(`last-link-up-time=2026-09-15 20:19:19`, ein echter Gerätedump) — nur fiel es nie auf, weil dieses
Feld von keinem kuratierten Schema gelesen wird.
**Fix:** `keyValues` von Regex-Matching auf Token-Scanning umgestellt — ein Leerzeichen-getrenntes
Wort startet nur dann ein neues Feld, wenn es selbst wie `key=...` aussieht; alles andere wird an
den Wert des zuletzt begonnenen Feldes angehängt. Bekannter Restfall (inhärent durch RouterOS'
mehrdeutiges unquotiertes Format, nicht clientseitig lösbar): ein Wert, der selbst ein
`wort=`-Muster enthält (z.B. ein Kommentar "config=broken"), wird fälschlich als neues Feld
gelesen — deutlich seltener als die vorherige 100%-Fehlerquote bei jedem mehrwortigen Wert. Neuer
Regressionstest `testParseGenericItemsPreservesUnquotedMultiWordValue`, abgeleitet vom echten
Live-Dump. Build + alle 101 Unit-Tests grün.
### 7. Beobachtung (kein Fix): Isolation wirkt nicht rückwirkend auf bereits bestehende Verbindungen
**Status:** offen (bewusst nicht automatisch gefixt — Risikoabwägung, siehe unten)
**Confidence:** hoch (Regel-Reihenfolge im Code nachvollzogen), nicht live reproduziert
`FirewallConfig.buildCommands()` (`FirewallConfig.swift`) setzt die Regel "forward
established,related → accept" auf Position 4, die Isolations-Drop-Regeln erst ab Position
`filterCommands.count` (7+). Für eine neue Verbindung zwischen zwei isolierten Netzen ist das
korrekt (erstes Paket hat `connection-state=new`, trifft also nicht Regel 4, sondern die
Isolations-Regel weiter hinten). Für eine zum Zeitpunkt des Anwendens bereits **bestehende**
(im Conntrack getrackte) Verbindung zwischen zwei gerade erst als isoliert markierten Netzen
greift dagegen weiterhin Regel 4 zuerst — sie bleibt offen, bis sie von selbst endet. Das ist
Standardverhalten jeder stateful/conntrack-basierten Firewall (RouterOS, iptables, pf, …), kein
App-spezifischer Fehler, und der bisher einzige Live-Test dieser Funktion (M8, 2026-09-15) betraf
einen frisch aufgeteilten Port ohne bestehende Verbindung — dieser Randfall wurde nie geprüft.
Nicht automatisch gefixt: ein Fix würde bedeuten, beim Aktivieren der Isolation gezielt
`/ip firewall connection remove` für die betroffenen Netzpaare auszulösen — ein zusätzlicher,
potenziell überraschender Seiteneffekt (kappt aktive Verbindungen, die der Nutzer evtl. bewusst
offen hat), der über die reine Bugfix-Aufgabe dieses Durchgangs hinausgeht und eine bewusste
Produktentscheidung ist, keine reine Korrektur. Nur als Beobachtung vermerkt.
---
## Noch nicht geprüft / außerhalb dieses Durchgangs
- UI-Interaktion selbst (Klickpfade, Darstellung) — nicht automatisierbar, siehe Hinweis oben.