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:
@@ -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\""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user