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
@@ -85,4 +85,22 @@ final class RouterOSCliParserTests: XCTestCase {
XCTAssertEqual(interfaces.map(\.name), ["ether1", "ether2", "bridge", "lo"])
XCTAssertEqual(interfaces.map(\.type), ["ether", "ether", "bridge", "loopback"])
}
/// Regression test for bugs.md (2026-09-17, "erneuter Gegencheck"-Durchgang): RouterOS'
/// `print terse` does not quote multi-word values at all (live-confirmed, RouterOS 7.24.4,
/// two menus) a comment "multi word test value" comes back as literal unquoted
/// `comment=multi word test value name=ether2 ...`. The old regex parser silently truncated
/// this to just "multi" and dropped "word test value" entirely; `keyValues` now token-scans
/// instead, only starting a new field on a token that itself looks like `key=...`.
func testParseGenericItemsPreservesUnquotedMultiWordValue() {
let raw = "1 RS comment=multi word test value name=ether2 default-name=ether2 mtu=1500"
let items = RouterOSCliParser.parseGenericItems(raw)
XCTAssertEqual(items.count, 1)
XCTAssertEqual(items[0].fields["comment"], "multi word test value")
XCTAssertEqual(items[0].fields["name"], "ether2")
XCTAssertEqual(items[0].fields["default-name"], "ether2")
XCTAssertEqual(items[0].fields["mtu"], "1500")
}
}
@@ -85,7 +85,7 @@ final class RouterOSCommandBuilderTests: XCTestCase {
summary: "test"
)
XCTAssertEqual(command.cliLine, "/ip route set [find .id=*1] comment=\"\"")
XCTAssertEqual(command.cliLine, "/ip route set [find .id=\"*1\"] comment=\"\"")
}
func testCliLineRendersSortedQuotedArgumentsForAdd() {
@@ -96,7 +96,7 @@ final class RouterOSCommandBuilderTests: XCTestCase {
summary: "test"
)
XCTAssertEqual(command.cliLine, "/interface pppoe-client add password=\"a secret\" user=user@isp")
XCTAssertEqual(command.cliLine, "/interface pppoe-client add password=\"a secret\" user=\"user@isp\"")
}
func testCliLineRendersFindLookupForSet() {
@@ -109,7 +109,7 @@ final class RouterOSCommandBuilderTests: XCTestCase {
summary: "test"
)
XCTAssertEqual(command.cliLine, "/interface wireless set [find name=wlan1] ssid=Home")
XCTAssertEqual(command.cliLine, "/interface wireless set [find name=\"wlan1\"] ssid=\"Home\"")
}
func testCliLineRendersFindLookupForAction() {
@@ -122,6 +122,25 @@ final class RouterOSCommandBuilderTests: XCTestCase {
summary: "test"
)
XCTAssertEqual(command.cliLine, "/ip dhcp-server lease make-static [find .id=*7]")
XCTAssertEqual(command.cliLine, "/ip dhcp-server lease make-static [find .id=\"*7\"]")
}
/// Regression test for bugs.md #1 (2026-09-17, "erneuter Gegencheck"-Durchgang): a free-text
/// value containing an embedded `"` followed by `;` used to close the CLI argument's quoting
/// early and let RouterOS' console treat the rest as a second, independent command
/// live-confirmed exploitable (`:log warning "..."` executed as its own command via a comment
/// field) and live-confirmed fixed by escaping `\`/`"` and always quoting.
func testCliLineEscapesEmbeddedQuotesPreventingCommandInjection() {
let command = RouterOSCommand.add(
menuPath: "/ip firewall address-list",
restPath: "ip/firewall/address-list",
arguments: ["comment": "test\" ; :log warning \"INJECTED"],
summary: "test"
)
XCTAssertEqual(
command.cliLine,
"/ip firewall address-list add comment=\"test\\\" ; :log warning \\\"INJECTED\""
)
}
}