Skip to content

Commit af41fc6

Browse files
authored
fix: no longer give up on detecting auth type when getting a 401 (#391)
1 parent 777bd2a commit af41fc6

File tree

7 files changed

+406
-75
lines changed

7 files changed

+406
-75
lines changed

internal/attack/attacker.go

Lines changed: 38 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
"time"
88

99
"github.com/Ullaakut/cameradar/v6"
10+
"github.com/bluenviron/gortsplib/v5"
1011
"github.com/bluenviron/gortsplib/v5/pkg/base"
12+
"github.com/bluenviron/gortsplib/v5/pkg/description"
1113
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
1214
)
1315

1416
// Route that should never be a constructor default.
15-
const dummyRoute = "/0x8b6c42"
17+
const dummyRoute = "0x8b6c42"
1618

1719
// Dictionary provides dictionaries for routes, usernames and passwords.
1820
type Dictionary interface {
@@ -187,6 +189,7 @@ func (a Attacker) reattackRoutes(ctx context.Context, streams []cameradar.Stream
187189
func needsReattack(streams []cameradar.Stream) bool {
188190
for _, stream := range streams {
189191
if stream.RouteFound && stream.CredentialsFound && stream.Available {
192+
// This stream is fully discovered, no need to re-attack.
190193
continue
191194
}
192195
return true
@@ -257,7 +260,7 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
257260
}
258261
if ok {
259262
target.RouteFound = true
260-
target.Routes = append(target.Routes, "/")
263+
target.Routes = append(target.Routes, "") // Add empty route for default.
261264
a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Default route accepted for %s:%d", target.Address.String(), target.Port))
262265
return target, nil
263266
}
@@ -287,67 +290,6 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
287290
return target, nil
288291
}
289292

290-
func (a Attacker) detectAuthMethods(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
291-
streams, err := runParallel(ctx, targets, a.detectAuthMethod)
292-
if err != nil {
293-
return streams, err
294-
}
295-
296-
for i := range streams {
297-
a.reporter.Progress(cameradar.StepDetectAuth, cameradar.ProgressTickMessage())
298-
299-
var authMethod string
300-
switch streams[i].AuthenticationType {
301-
case cameradar.AuthNone:
302-
authMethod = "no"
303-
case cameradar.AuthBasic:
304-
authMethod = "basic"
305-
case cameradar.AuthDigest:
306-
authMethod = "digest"
307-
default:
308-
return streams, fmt.Errorf("unknown authentication method %d for %s:%d", streams[i].AuthenticationType, streams[i].Address.String(), streams[i].Port)
309-
}
310-
311-
a.reporter.Progress(cameradar.StepDetectAuth, fmt.Sprintf("Detected %s authentication for %s:%d", authMethod, streams[i].Address.String(), streams[i].Port))
312-
}
313-
314-
return streams, nil
315-
}
316-
317-
func (a Attacker) detectAuthMethod(ctx context.Context, stream cameradar.Stream) (cameradar.Stream, error) {
318-
if ctx.Err() != nil {
319-
return stream, ctx.Err()
320-
}
321-
u, urlStr, err := buildRTSPURL(stream, stream.Route(), "", "")
322-
if err != nil {
323-
return stream, fmt.Errorf("building rtsp url: %w", err)
324-
}
325-
326-
client, err := a.newRTSPClient(u)
327-
if err != nil {
328-
return stream, fmt.Errorf("starting rtsp client: %w", err)
329-
}
330-
defer client.Close()
331-
332-
_, res, err := client.Describe(u)
333-
if err != nil {
334-
var badStatus liberrors.ErrClientBadStatusCode
335-
if errors.As(err, &badStatus) && res != nil && badStatus.Code == base.StatusUnauthorized {
336-
stream.AuthenticationType = authTypeFromHeaders(res.Header["WWW-Authenticate"])
337-
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
338-
return stream, nil
339-
}
340-
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
341-
}
342-
343-
if res != nil {
344-
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, res.StatusCode))
345-
}
346-
347-
stream.AuthenticationType = cameradar.AuthNone
348-
return stream, nil
349-
}
350-
351293
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
352294
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
353295
if err != nil {
@@ -399,7 +341,7 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
399341
}
400342
defer client.Close()
401343

402-
desc, res, err := client.Describe(u)
344+
desc, res, err := a.describeWithRetry(ctx, client, u, urlStr)
403345
if err != nil {
404346
return a.handleDescribeError(stream, urlStr, err)
405347
}
@@ -413,7 +355,6 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
413355
if err != nil {
414356
return a.handleSetupError(stream, urlStr, err)
415357
}
416-
417358
a.logSetupResponse(urlStr, res)
418359

419360
stream.Available = res != nil && res.StatusCode == base.StatusOK
@@ -424,9 +365,39 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
424365
return stream, nil
425366
}
426367

368+
func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, u *base.URL, urlStr string) (*description.Session, *base.Response, error) {
369+
var (
370+
desc *description.Session
371+
res *base.Response
372+
err error
373+
)
374+
for range 5 {
375+
desc, res, err = client.Describe(u)
376+
if err == nil {
377+
return desc, res, nil
378+
}
379+
380+
var badStatus liberrors.ErrClientBadStatusCode
381+
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
382+
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", urlStr, badStatus.Code))
383+
select {
384+
case <-ctx.Done():
385+
return nil, nil, ctx.Err()
386+
case <-time.After(time.Second):
387+
}
388+
continue
389+
}
390+
391+
return nil, nil, err
392+
}
393+
394+
return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", urlStr, err)
395+
}
396+
427397
func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
428398
var badStatus liberrors.ErrClientBadStatusCode
429399
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
400+
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
430401
a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream unavailable for %s:%d (RTSP %d)",
431402
stream.Address.String(),
432403
stream.Port,
@@ -436,6 +407,8 @@ func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, er
436407
return stream, nil
437408
}
438409

410+
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))
411+
439412
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
440413
}
441414

internal/attack/attacker_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) {
214214

215215
got, err := attacker.Attack(t.Context(), streams)
216216
require.Error(t, err)
217-
assert.ErrorContains(t, err, "detecting authentication methods")
217+
assert.ErrorContains(t, err, "validating streams")
218218
require.Len(t, got, 1)
219219
assert.False(t, got[0].RouteFound)
220220
}
@@ -304,7 +304,7 @@ func TestAttacker_Attack_AllowsDummyRoute(t *testing.T) {
304304
require.NoError(t, err)
305305
require.Len(t, got, 1)
306306
assert.True(t, got[0].RouteFound)
307-
assert.Equal(t, []string{"/"}, got[0].Routes)
307+
assert.Equal(t, []string{""}, got[0].Routes)
308308
assert.True(t, got[0].Available)
309309
}
310310

internal/attack/detect_auth.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package attack
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/Ullaakut/cameradar/v6"
8+
"github.com/bluenviron/gortsplib/v5/pkg/base"
9+
)
10+
11+
func (a Attacker) detectAuthMethods(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
12+
streams, err := runParallel(ctx, targets, a.detectAuthMethod)
13+
if err != nil {
14+
return streams, err
15+
}
16+
17+
for i := range streams {
18+
a.reporter.Progress(cameradar.StepDetectAuth, cameradar.ProgressTickMessage())
19+
20+
var authMethod string
21+
switch streams[i].AuthenticationType {
22+
case cameradar.AuthNone:
23+
authMethod = "no"
24+
case cameradar.AuthBasic:
25+
authMethod = "basic"
26+
case cameradar.AuthDigest:
27+
authMethod = "digest"
28+
case cameradar.AuthUnknown:
29+
authMethod = "unknown"
30+
default:
31+
authMethod = fmt.Sprintf("unknown (%d)", streams[i].AuthenticationType)
32+
}
33+
34+
a.reporter.Progress(cameradar.StepDetectAuth, fmt.Sprintf("Detected %s authentication for %s:%d", authMethod, streams[i].Address.String(), streams[i].Port))
35+
}
36+
37+
return streams, nil
38+
}
39+
40+
func (a Attacker) detectAuthMethod(ctx context.Context, stream cameradar.Stream) (cameradar.Stream, error) {
41+
if ctx.Err() != nil {
42+
return stream, ctx.Err()
43+
}
44+
u, urlStr, err := buildRTSPURL(stream, stream.Route(), "", "")
45+
if err != nil {
46+
return stream, fmt.Errorf("building rtsp url: %w", err)
47+
}
48+
49+
statusCode, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
50+
if err != nil {
51+
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))
52+
stream.AuthenticationType = cameradar.AuthUnknown
53+
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
54+
}
55+
56+
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, statusCode))
57+
values := headerValues(headers, "WWW-Authenticate")
58+
switch statusCode {
59+
case base.StatusOK:
60+
stream.AuthenticationType = cameradar.AuthNone
61+
case base.StatusUnauthorized:
62+
stream.AuthenticationType = authTypeFromHeaders(values)
63+
default:
64+
stream.AuthenticationType = cameradar.AuthUnknown
65+
}
66+
67+
return stream, nil
68+
}

0 commit comments

Comments
 (0)