fix: increment limit, added integration tests
This commit is contained in:
@@ -420,6 +420,8 @@ func (a Attacker) tryIncrementalRoutes(ctx context.Context,
|
|||||||
case <-time.After(a.attackInterval):
|
case <-time.After(a.attackInterval):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attempts++
|
||||||
|
|
||||||
nextRoute := buildIncrementedRoute(match, nextNumber)
|
nextRoute := buildIncrementedRoute(match, nextNumber)
|
||||||
if slices.Contains(target.Routes, nextRoute) {
|
if slices.Contains(target.Routes, nextRoute) {
|
||||||
if !match.isChannel {
|
if !match.isChannel {
|
||||||
@@ -443,7 +445,6 @@ func (a Attacker) tryIncrementalRoutes(ctx context.Context,
|
|||||||
))
|
))
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
attempts++
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package attack_test
|
package attack_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -50,11 +51,11 @@ func TestNew(t *testing.T) {
|
|||||||
|
|
||||||
func TestAttacker_Attack_BasicAuth(t *testing.T) {
|
func TestAttacker_Attack_BasicAuth(t *testing.T) {
|
||||||
addr, port := startRTSPServer(t, rtspServerConfig{
|
addr, port := startRTSPServer(t, rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
username: "user",
|
username: "user",
|
||||||
password: "pass",
|
password: "pass",
|
||||||
authMethod: headers.AuthMethodBasic,
|
authMethod: headers.AuthMethodBasic,
|
||||||
})
|
})
|
||||||
|
|
||||||
dict := testDictionary{
|
dict := testDictionary{
|
||||||
@@ -101,9 +102,9 @@ func TestAttacker_Attack_AuthVariants(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "no authentication",
|
name: "no authentication",
|
||||||
config: rtspServerConfig{
|
config: rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: false,
|
requireAuth: false,
|
||||||
authMethod: headers.AuthMethodBasic,
|
authMethod: headers.AuthMethodBasic,
|
||||||
},
|
},
|
||||||
dict: testDictionary{
|
dict: testDictionary{
|
||||||
routes: []string{"stream"},
|
routes: []string{"stream"},
|
||||||
@@ -117,11 +118,11 @@ func TestAttacker_Attack_AuthVariants(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "digest authentication",
|
name: "digest authentication",
|
||||||
config: rtspServerConfig{
|
config: rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
username: "user",
|
username: "user",
|
||||||
password: "pass",
|
password: "pass",
|
||||||
authMethod: headers.AuthMethodDigest,
|
authMethod: headers.AuthMethodDigest,
|
||||||
},
|
},
|
||||||
dict: testDictionary{
|
dict: testDictionary{
|
||||||
routes: []string{"stream"},
|
routes: []string{"stream"},
|
||||||
@@ -193,9 +194,9 @@ func TestAttacker_Attack_ValidationErrors(t *testing.T) {
|
|||||||
|
|
||||||
func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) {
|
func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) {
|
||||||
addr, port := startRTSPServer(t, rtspServerConfig{
|
addr, port := startRTSPServer(t, rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: false,
|
requireAuth: false,
|
||||||
authMethod: headers.AuthMethodBasic,
|
authMethod: headers.AuthMethodBasic,
|
||||||
})
|
})
|
||||||
|
|
||||||
dict := testDictionary{
|
dict := testDictionary{
|
||||||
@@ -221,11 +222,11 @@ func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) {
|
|||||||
|
|
||||||
func TestAttacker_Attack_ReturnsErrorWhenCredentialsMissing(t *testing.T) {
|
func TestAttacker_Attack_ReturnsErrorWhenCredentialsMissing(t *testing.T) {
|
||||||
addr, port := startRTSPServer(t, rtspServerConfig{
|
addr, port := startRTSPServer(t, rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
username: "user",
|
username: "user",
|
||||||
password: "pass",
|
password: "pass",
|
||||||
authMethod: headers.AuthMethodBasic,
|
authMethod: headers.AuthMethodBasic,
|
||||||
})
|
})
|
||||||
|
|
||||||
dict := testDictionary{
|
dict := testDictionary{
|
||||||
@@ -254,12 +255,12 @@ func TestAttacker_Attack_CredentialAttemptFails(t *testing.T) {
|
|||||||
reporter := &recordingReporter{}
|
reporter := &recordingReporter{}
|
||||||
|
|
||||||
addr, port := startRTSPServer(t, rtspServerConfig{
|
addr, port := startRTSPServer(t, rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
username: "user",
|
username: "user",
|
||||||
password: "pass",
|
password: "pass",
|
||||||
authMethod: headers.AuthMethodBasic,
|
authMethod: headers.AuthMethodBasic,
|
||||||
failOnAuth: true,
|
failOnAuth: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
dict := testDictionary{
|
dict := testDictionary{
|
||||||
@@ -310,10 +311,10 @@ func TestAttacker_Attack_AllowsDummyRoute(t *testing.T) {
|
|||||||
|
|
||||||
func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) {
|
func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) {
|
||||||
addr, port := startRTSPServer(t, rtspServerConfig{
|
addr, port := startRTSPServer(t, rtspServerConfig{
|
||||||
allowedRoute: "stream",
|
allowRoutes: []string{"stream"},
|
||||||
requireAuth: false,
|
requireAuth: false,
|
||||||
authMethod: headers.AuthMethodBasic,
|
authMethod: headers.AuthMethodBasic,
|
||||||
setupStatus: base.StatusUnsupportedTransport,
|
setupStatus: base.StatusUnsupportedTransport,
|
||||||
})
|
})
|
||||||
|
|
||||||
dict := testDictionary{
|
dict := testDictionary{
|
||||||
@@ -335,6 +336,71 @@ func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) {
|
|||||||
assert.True(t, got[0].RouteFound)
|
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 {
|
type testDictionary struct {
|
||||||
routes []string
|
routes []string
|
||||||
usernames []string
|
usernames []string
|
||||||
@@ -376,9 +442,10 @@ func (r *recordingReporter) Summary([]cameradar.Stream, error) {}
|
|||||||
|
|
||||||
func (r *recordingReporter) Close() {}
|
func (r *recordingReporter) Close() {}
|
||||||
|
|
||||||
func (r *recordingReporter) HasDebugContaining(value string) bool {
|
func (r *recordingReporter) ContainsDebug(value string) bool {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
for _, message := range r.debugMessages {
|
for _, message := range r.debugMessages {
|
||||||
if strings.Contains(message, value) {
|
if strings.Contains(message, value) {
|
||||||
return true
|
return true
|
||||||
@@ -386,3 +453,13 @@ func (r *recordingReporter) HasDebugContaining(value string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func countRoute(routes []string, route string) int {
|
||||||
|
count := 0
|
||||||
|
for _, value := range routes {
|
||||||
|
if value == route {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,27 +18,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type rtspServerConfig struct {
|
type rtspServerConfig struct {
|
||||||
allowAll bool
|
allowAll bool
|
||||||
allowedRoute string
|
allowRoutes []string
|
||||||
requireAuth bool
|
requireAuth bool
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
authMethod headers.AuthMethod
|
authMethod headers.AuthMethod
|
||||||
authHeader base.HeaderValue
|
authHeader base.HeaderValue
|
||||||
failOnAuth bool
|
failOnAuth bool
|
||||||
setupStatus base.StatusCode
|
setupStatus base.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
type testServerHandler struct {
|
type testServerHandler struct {
|
||||||
stream *gortsplib.ServerStream
|
stream *gortsplib.ServerStream
|
||||||
allowAll bool
|
allowAll bool
|
||||||
allowedRoute string
|
allowRoutes []string
|
||||||
requireAuth bool
|
requireAuth bool
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
authHeader base.HeaderValue
|
authHeader base.HeaderValue
|
||||||
failOnAuth bool
|
failOnAuth bool
|
||||||
setupStatus base.StatusCode
|
setupStatus base.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *testServerHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
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 {
|
func (h *testServerHandler) routeAllowed(path string) bool {
|
||||||
path = strings.TrimLeft(path, "/")
|
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) {
|
func startRTSPServer(t *testing.T, cfg rtspServerConfig) (netip.Addr, uint16) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
handler := &testServerHandler{
|
handler := &testServerHandler{
|
||||||
allowAll: cfg.allowAll,
|
allowAll: cfg.allowAll,
|
||||||
allowedRoute: cfg.allowedRoute,
|
allowRoutes: cfg.allowRoutes,
|
||||||
requireAuth: cfg.requireAuth,
|
requireAuth: cfg.requireAuth,
|
||||||
username: cfg.username,
|
username: cfg.username,
|
||||||
password: cfg.password,
|
password: cfg.password,
|
||||||
failOnAuth: cfg.failOnAuth,
|
failOnAuth: cfg.failOnAuth,
|
||||||
setupStatus: cfg.setupStatus,
|
setupStatus: cfg.setupStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.authHeader) > 0 {
|
if len(cfg.authHeader) > 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user