From d589b610b2091679be949762618ac58f9f8c842d Mon Sep 17 00:00:00 2001 From: Brendan Le Glaunec Date: Wed, 28 Jan 2026 21:22:57 +0100 Subject: [PATCH] fix: increment limit, added integration tests --- internal/attack/attacker.go | 3 +- internal/attack/attacker_test.go | 141 ++++++++++++++++++++++++------- internal/attack/rtsp_test.go | 61 +++++++------ 3 files changed, 146 insertions(+), 59 deletions(-) diff --git a/internal/attack/attacker.go b/internal/attack/attacker.go index 60c7869..9f1396f 100644 --- a/internal/attack/attacker.go +++ b/internal/attack/attacker.go @@ -420,6 +420,8 @@ func (a Attacker) tryIncrementalRoutes(ctx context.Context, case <-time.After(a.attackInterval): } + attempts++ + nextRoute := buildIncrementedRoute(match, nextNumber) if slices.Contains(target.Routes, nextRoute) { if !match.isChannel { @@ -443,7 +445,6 @@ func (a Attacker) tryIncrementalRoutes(ctx context.Context, )) return target, nil } - attempts++ if !ok { return target, nil } diff --git a/internal/attack/attacker_test.go b/internal/attack/attacker_test.go index 5fbd43a..bd90916 100644 --- a/internal/attack/attacker_test.go +++ b/internal/attack/attacker_test.go @@ -1,6 +1,7 @@ package attack_test import ( + "fmt" "strings" "sync" "testing" @@ -50,11 +51,11 @@ func TestNew(t *testing.T) { func TestAttacker_Attack_BasicAuth(t *testing.T) { addr, port := startRTSPServer(t, rtspServerConfig{ - allowedRoute: "stream", - requireAuth: true, - username: "user", - password: "pass", - authMethod: headers.AuthMethodBasic, + allowRoutes: []string{"stream"}, + requireAuth: true, + username: "user", + password: "pass", + authMethod: headers.AuthMethodBasic, }) dict := testDictionary{ @@ -101,9 +102,9 @@ func TestAttacker_Attack_AuthVariants(t *testing.T) { { name: "no authentication", config: rtspServerConfig{ - allowedRoute: "stream", - requireAuth: false, - authMethod: headers.AuthMethodBasic, + allowRoutes: []string{"stream"}, + requireAuth: false, + authMethod: headers.AuthMethodBasic, }, dict: testDictionary{ routes: []string{"stream"}, @@ -117,11 +118,11 @@ func TestAttacker_Attack_AuthVariants(t *testing.T) { { name: "digest authentication", config: rtspServerConfig{ - allowedRoute: "stream", - requireAuth: true, - username: "user", - password: "pass", - authMethod: headers.AuthMethodDigest, + allowRoutes: []string{"stream"}, + requireAuth: true, + username: "user", + password: "pass", + authMethod: headers.AuthMethodDigest, }, dict: testDictionary{ routes: []string{"stream"}, @@ -193,9 +194,9 @@ func TestAttacker_Attack_ValidationErrors(t *testing.T) { func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) { addr, port := startRTSPServer(t, rtspServerConfig{ - allowedRoute: "stream", - requireAuth: false, - authMethod: headers.AuthMethodBasic, + allowRoutes: []string{"stream"}, + requireAuth: false, + authMethod: headers.AuthMethodBasic, }) dict := testDictionary{ @@ -221,11 +222,11 @@ func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) { func TestAttacker_Attack_ReturnsErrorWhenCredentialsMissing(t *testing.T) { addr, port := startRTSPServer(t, rtspServerConfig{ - allowedRoute: "stream", - requireAuth: true, - username: "user", - password: "pass", - authMethod: headers.AuthMethodBasic, + allowRoutes: []string{"stream"}, + requireAuth: true, + username: "user", + password: "pass", + authMethod: headers.AuthMethodBasic, }) dict := testDictionary{ @@ -254,12 +255,12 @@ func TestAttacker_Attack_CredentialAttemptFails(t *testing.T) { reporter := &recordingReporter{} addr, port := startRTSPServer(t, rtspServerConfig{ - allowedRoute: "stream", - requireAuth: true, - username: "user", - password: "pass", - authMethod: headers.AuthMethodBasic, - failOnAuth: true, + allowRoutes: []string{"stream"}, + requireAuth: true, + username: "user", + password: "pass", + authMethod: headers.AuthMethodBasic, + failOnAuth: true, }) dict := testDictionary{ @@ -310,10 +311,10 @@ func TestAttacker_Attack_AllowsDummyRoute(t *testing.T) { func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) { addr, port := startRTSPServer(t, rtspServerConfig{ - allowedRoute: "stream", - requireAuth: false, - authMethod: headers.AuthMethodBasic, - setupStatus: base.StatusUnsupportedTransport, + allowRoutes: []string{"stream"}, + requireAuth: false, + authMethod: headers.AuthMethodBasic, + setupStatus: base.StatusUnsupportedTransport, }) dict := testDictionary{ @@ -335,6 +336,71 @@ func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) { assert.True(t, got[0].RouteFound) } +func TestAttacker_Attack_IncrementalRoutesStopsOnFirstMissAndAvoidsDuplicates(t *testing.T) { + addr, port := startRTSPServer(t, rtspServerConfig{ + allowRoutes: []string{"channel1", "channel2"}, + requireAuth: false, + authMethod: headers.AuthMethodBasic, + }) + + dict := testDictionary{ + routes: []string{"channel1", "channel2"}, + usernames: []string{"user"}, + passwords: []string{"pass"}, + } + + attacker, err := attack.New(dict, 0, time.Second, ui.NopReporter{}) + require.NoError(t, err) + + streams := []cameradar.Stream{{ + Address: addr, + Port: port, + }} + + got, err := attacker.Attack(t.Context(), streams) + require.NoError(t, err) + require.Len(t, got, 1) + + assert.ElementsMatch(t, []string{"channel1", "channel2"}, got[0].Routes) + assert.Equal(t, 1, countRoute(got[0].Routes, "channel2")) +} + +func TestAttacker_Attack_IncrementalRoutesStopsAtCap(t *testing.T) { + allowedRoutes := make([]string, 0, 50) + for i := 1; i <= 50; i++ { + allowedRoutes = append(allowedRoutes, fmt.Sprintf("channel%d", i)) + } + + addr, port := startRTSPServer(t, rtspServerConfig{ + allowRoutes: allowedRoutes, + requireAuth: false, + authMethod: headers.AuthMethodBasic, + }) + + dict := testDictionary{ + routes: []string{"channel1"}, + usernames: []string{"user"}, + passwords: []string{"pass"}, + } + + attacker, err := attack.New(dict, 0, time.Second, ui.NopReporter{}) + require.NoError(t, err) + + streams := []cameradar.Stream{{ + Address: addr, + Port: port, + }} + + got, err := attacker.Attack(t.Context(), streams) + require.NoError(t, err) + require.Len(t, got, 1) + + const expectedRoutes = 33 // channel1 + 32 incremental attempts + assert.Len(t, got[0].Routes, expectedRoutes) + assert.Contains(t, got[0].Routes, "channel33") + assert.NotContains(t, got[0].Routes, "channel34") +} + type testDictionary struct { routes []string usernames []string @@ -376,9 +442,10 @@ func (r *recordingReporter) Summary([]cameradar.Stream, error) {} func (r *recordingReporter) Close() {} -func (r *recordingReporter) HasDebugContaining(value string) bool { +func (r *recordingReporter) ContainsDebug(value string) bool { r.mu.Lock() defer r.mu.Unlock() + for _, message := range r.debugMessages { if strings.Contains(message, value) { return true @@ -386,3 +453,13 @@ func (r *recordingReporter) HasDebugContaining(value string) bool { } return false } + +func countRoute(routes []string, route string) int { + count := 0 + for _, value := range routes { + if value == route { + count++ + } + } + return count +} diff --git a/internal/attack/rtsp_test.go b/internal/attack/rtsp_test.go index 17bd510..33afbc3 100644 --- a/internal/attack/rtsp_test.go +++ b/internal/attack/rtsp_test.go @@ -18,27 +18,27 @@ import ( ) type rtspServerConfig struct { - allowAll bool - allowedRoute string - requireAuth bool - username string - password string - authMethod headers.AuthMethod - authHeader base.HeaderValue - failOnAuth bool - setupStatus base.StatusCode + allowAll bool + allowRoutes []string + requireAuth bool + username string + password string + authMethod headers.AuthMethod + authHeader base.HeaderValue + failOnAuth bool + setupStatus base.StatusCode } type testServerHandler struct { - stream *gortsplib.ServerStream - allowAll bool - allowedRoute string - requireAuth bool - username string - password string - authHeader base.HeaderValue - failOnAuth bool - setupStatus base.StatusCode + stream *gortsplib.ServerStream + allowAll bool + allowRoutes []string + requireAuth bool + username string + password string + authHeader base.HeaderValue + failOnAuth bool + setupStatus base.StatusCode } func (h *testServerHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { @@ -86,20 +86,29 @@ func (h *testServerHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*ba func (h *testServerHandler) routeAllowed(path string) bool { path = strings.TrimLeft(path, "/") - return h.allowAll || path == h.allowedRoute + if h.allowAll { + return true + } + + for _, route := range h.allowRoutes { + if path == route { + return true + } + } + return false } func startRTSPServer(t *testing.T, cfg rtspServerConfig) (netip.Addr, uint16) { t.Helper() handler := &testServerHandler{ - allowAll: cfg.allowAll, - allowedRoute: cfg.allowedRoute, - requireAuth: cfg.requireAuth, - username: cfg.username, - password: cfg.password, - failOnAuth: cfg.failOnAuth, - setupStatus: cfg.setupStatus, + allowAll: cfg.allowAll, + allowRoutes: cfg.allowRoutes, + requireAuth: cfg.requireAuth, + username: cfg.username, + password: cfg.password, + failOnAuth: cfg.failOnAuth, + setupStatus: cfg.setupStatus, } if len(cfg.authHeader) > 0 {