Files
cameradar/internal/attack/attacker.go
T

571 lines
18 KiB
Go

package attack
import (
"context"
"errors"
"fmt"
"slices"
"time"
"github.com/Ullaakut/cameradar/v6"
"github.com/bluenviron/gortsplib/v5/pkg/base"
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
)
// Route that should never be a constructor default.
const dummyRoute = "/0x8b6c42"
const maxIncrementalRouteAttempts = 32
// Dictionary provides dictionaries for routes, usernames and passwords.
type Dictionary interface {
Routes() []string
Usernames() []string
Passwords() []string
}
// Reporter reports progress and results of the attacks.
type Reporter interface {
Start(step cameradar.Step, message string)
Done(step cameradar.Step, message string)
Progress(step cameradar.Step, message string)
Error(step cameradar.Step, err error)
Debug(step cameradar.Step, message string)
}
// Attacker attempts to discover routes and credentials for RTSP streams.
type Attacker struct {
dictionary Dictionary
reporter Reporter
attackInterval time.Duration
timeout time.Duration
}
// New builds an Attacker with the provided dependencies.
func New(dict Dictionary, attackInterval, timeout time.Duration, reporter Reporter) (Attacker, error) {
if dict == nil {
return Attacker{}, errors.New("dictionary is required")
}
return Attacker{
dictionary: dict,
attackInterval: attackInterval,
timeout: timeout,
reporter: reporter,
}, nil
}
// Attack attacks the given targets and returns the accessed streams.
func (a Attacker) Attack(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
if len(targets) == 0 {
return nil, errors.New("no stream found")
}
streams, err := a.attackRoutesPhase(ctx, targets)
if err != nil {
return streams, err
}
streams, err = a.detectAuthPhase(ctx, streams)
if err != nil {
return streams, err
}
streams, err = a.attackCredentialsPhase(ctx, streams)
if err != nil {
return streams, err
}
streams, err = a.validateStreamsPhase(ctx, streams)
if err != nil {
return streams, err
}
// Some cameras run an inaccurate version of the RTSP protocol which prioritizes 401 over 404.
// For these cameras, running another route attack solves the problem.
if !needsReattack(streams) {
return streams, nil
}
streams, err = a.reattackRoutes(ctx, streams)
if err != nil {
return streams, err
}
return streams, nil
}
func (a Attacker) attackRoutesPhase(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
a.reporter.Start(cameradar.StepAttackRoutes, "Attacking RTSP routes")
routeAttempts := (len(a.dictionary.Routes()) + 1) * len(targets)
if routeAttempts > 0 {
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTotalMessage(routeAttempts))
}
streams, err := runParallel(ctx, targets, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
return a.attackRoutesForStream(ctx, target, true)
})
if err != nil {
a.reporter.Error(cameradar.StepAttackRoutes, err)
return streams, fmt.Errorf("attacking routes: %w", err)
}
updateSummary(a.reporter, streams)
a.reporter.Done(cameradar.StepAttackRoutes, "Finished route attacks")
return streams, nil
}
func (a Attacker) detectAuthPhase(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
a.reporter.Start(cameradar.StepDetectAuth, "Detecting authentication methods")
if len(streams) > 0 {
a.reporter.Progress(cameradar.StepDetectAuth, cameradar.ProgressTotalMessage(len(streams)))
}
streams, err := a.detectAuthMethods(ctx, streams)
if err != nil {
a.reporter.Error(cameradar.StepDetectAuth, err)
return streams, fmt.Errorf("detecting authentication methods: %w", err)
}
updateSummary(a.reporter, streams)
a.reporter.Done(cameradar.StepDetectAuth, "Authentication detection complete")
return streams, nil
}
func (a Attacker) attackCredentialsPhase(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
a.reporter.Start(cameradar.StepAttackCredentials, "Attacking credentials")
credentialsAttempts := len(streams) * len(a.dictionary.Usernames()) * len(a.dictionary.Passwords())
if credentialsAttempts > 0 {
a.reporter.Progress(cameradar.StepAttackCredentials, cameradar.ProgressTotalMessage(credentialsAttempts))
}
streams, err := runParallel(ctx, streams, a.attackCredentialsForStream)
if err != nil {
a.reporter.Error(cameradar.StepAttackCredentials, err)
return streams, fmt.Errorf("attacking credentials: %w", err)
}
updateSummary(a.reporter, streams)
a.reporter.Done(cameradar.StepAttackCredentials, "Credential attacks complete")
return streams, nil
}
func (a Attacker) validateStreamsPhase(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
a.reporter.Start(cameradar.StepValidateStreams, "Validating streams")
if len(streams) > 0 {
a.reporter.Progress(cameradar.StepValidateStreams, cameradar.ProgressTotalMessage(len(streams)))
}
streams, err := runParallel(ctx, streams, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
return a.validateStream(ctx, target, true)
})
if err != nil {
a.reporter.Error(cameradar.StepValidateStreams, err)
return streams, fmt.Errorf("validating streams: %w", err)
}
updateSummary(a.reporter, streams)
a.reporter.Done(cameradar.StepValidateStreams, "Stream validation complete")
return streams, nil
}
func (a Attacker) reattackRoutes(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
a.reporter.Progress(cameradar.StepAttackRoutes, "Re-attacking routes for partial results")
updated, err := runParallel(ctx, streams, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
return a.attackRoutesForStream(ctx, target, false)
})
if err != nil {
a.reporter.Error(cameradar.StepAttackRoutes, err)
return streams, fmt.Errorf("attacking routes: %w", err)
}
updated, err = runParallel(ctx, updated, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
return a.validateStream(ctx, target, false)
})
if err != nil {
a.reporter.Error(cameradar.StepValidateStreams, err)
return updated, fmt.Errorf("validating streams: %w", err)
}
updateSummary(a.reporter, updated)
return updated, nil
}
func needsReattack(streams []cameradar.Stream) bool {
for _, stream := range streams {
if stream.RouteFound && stream.CredentialsFound && stream.Available {
continue
}
return true
}
return false
}
type summaryUpdater interface {
UpdateSummary(streams []cameradar.Stream)
}
func updateSummary(reporter Reporter, streams []cameradar.Stream) {
updater, ok := reporter.(summaryUpdater)
if !ok {
return
}
updater.UpdateSummary(streams)
}
func (a Attacker) attackCredentialsForStream(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
for _, username := range a.dictionary.Usernames() {
for _, password := range a.dictionary.Passwords() {
if ctx.Err() != nil {
return target, ctx.Err()
}
a.reporter.Progress(cameradar.StepAttackCredentials, cameradar.ProgressTickMessage())
ok, err := a.credAttack(target, username, password)
if err != nil {
target.CredentialsFound = false
msg := fmt.Sprintf("credential attempt failed for %s:%d (%s:%s): %v", target.Address.String(), target.Port, username, password, err)
a.reporter.Debug(cameradar.StepAttackCredentials, msg)
return target, nil
}
if ok {
target.CredentialsFound = true
target.Username = username
target.Password = password
msg := fmt.Sprintf("Credentials found for %s:%d", target.Address.String(), target.Port)
a.reporter.Progress(cameradar.StepAttackCredentials, msg)
updated, err := a.tryIncrementalRoutes(ctx, target, target.Route(), true, true)
if err != nil {
return target, err
}
return updated, nil
}
time.Sleep(a.attackInterval)
}
}
target.CredentialsFound = false
return target, nil
}
func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.Stream, emitProgress bool) (cameradar.Stream, error) {
if target.RouteFound {
return target, nil
}
if emitProgress {
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
}
ok, err := a.routeAttack(target, dummyRoute)
if err != nil {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route probe failed for %s:%d: %v", target.Address.String(), target.Port, err))
return target, nil
}
if ok {
target.RouteFound = true
target.Routes = appendRouteIfMissing(target.Routes, "/")
a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Default route accepted for %s:%d", target.Address.String(), target.Port))
return target, nil
}
for _, route := range a.dictionary.Routes() {
select {
case <-ctx.Done():
return target, ctx.Err()
case <-time.After(a.attackInterval):
}
if emitProgress {
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
}
ok, err := a.routeAttack(target, route)
if err != nil {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route attempt failed for %s:%d (%s): %v", target.Address.String(), target.Port, route, err))
return target, nil
}
if ok {
target.RouteFound = true
target.Routes = appendRouteIfMissing(target.Routes, route)
a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Route found for %s:%d -> %s", target.Address.String(), target.Port, route))
updated, err := a.tryIncrementalRoutes(ctx, target, route, emitProgress, false)
if err != nil {
return target, err
}
target = updated
}
}
return target, nil
}
func (a Attacker) detectAuthMethods(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
streams, err := runParallel(ctx, targets, a.detectAuthMethod)
if err != nil {
return streams, err
}
for i := range streams {
a.reporter.Progress(cameradar.StepDetectAuth, cameradar.ProgressTickMessage())
var authMethod string
switch streams[i].AuthenticationType {
case cameradar.AuthNone:
authMethod = "no"
case cameradar.AuthBasic:
authMethod = "basic"
case cameradar.AuthDigest:
authMethod = "digest"
default:
return streams, fmt.Errorf("unknown authentication method %d for %s:%d", streams[i].AuthenticationType, streams[i].Address.String(), streams[i].Port)
}
a.reporter.Progress(cameradar.StepDetectAuth, fmt.Sprintf("Detected %s authentication for %s:%d", authMethod, streams[i].Address.String(), streams[i].Port))
}
return streams, nil
}
func (a Attacker) detectAuthMethod(ctx context.Context, stream cameradar.Stream) (cameradar.Stream, error) {
if ctx.Err() != nil {
return stream, ctx.Err()
}
u, urlStr, err := buildRTSPURL(stream, stream.Route(), "", "")
if err != nil {
return stream, fmt.Errorf("building rtsp url: %w", err)
}
client, err := a.newRTSPClient(u)
if err != nil {
return stream, fmt.Errorf("starting rtsp client: %w", err)
}
defer client.Close()
_, res, err := client.Describe(u)
if err != nil {
var badStatus liberrors.ErrClientBadStatusCode
if errors.As(err, &badStatus) && res != nil && badStatus.Code == base.StatusUnauthorized {
stream.AuthenticationType = authTypeFromHeaders(res.Header["WWW-Authenticate"])
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
return stream, nil
}
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
if res != nil {
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, res.StatusCode))
}
stream.AuthenticationType = cameradar.AuthNone
return stream, nil
}
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err)
}
code, err := a.describeStatus(u)
if err != nil {
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
return access, nil
}
func (a Attacker) routeAttackWithCredentials(stream cameradar.Stream, route string) (bool, error) {
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err)
}
code, err := a.describeStatus(u)
if err != nil {
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
return code == base.StatusOK, nil
}
func (a Attacker) tryIncrementalRoutes(ctx context.Context,
target cameradar.Stream, route string,
emitProgress, useCredentials bool,
) (cameradar.Stream, error) {
match, ok := detectIncrementalRoute(route)
if !ok {
return target, nil
}
nextNumber := match.number + 1
attempts := 0
for {
if attempts >= maxIncrementalRouteAttempts {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("incremental route attempts capped at %d for %s:%d", maxIncrementalRouteAttempts, target.Address.String(), target.Port))
return target, nil
}
select {
case <-ctx.Done():
return target, ctx.Err()
case <-time.After(a.attackInterval):
}
nextRoute := buildIncrementedRoute(match, nextNumber)
if slices.Contains(target.Routes, nextRoute) {
if !match.isChannel {
return target, nil
}
nextNumber++
continue
}
if emitProgress {
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
}
ok, err := a.incrementalRouteAttack(target, nextRoute, useCredentials)
if err != nil {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("incremental route attempt failed for %s:%d (%s): %v",
target.Address.String(),
target.Port,
nextRoute,
err,
))
return target, nil
}
attempts++
if !ok {
return target, nil
}
target.RouteFound = true
target.Routes = appendRouteIfMissing(target.Routes, nextRoute)
a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Incremental route found for %s:%d -> %s", target.Address.String(), target.Port, nextRoute))
if !match.isChannel {
return target, nil
}
nextNumber++
}
}
func (a Attacker) incrementalRouteAttack(stream cameradar.Stream, route string, useCredentials bool) (bool, error) {
if useCredentials {
return a.routeAttackWithCredentials(stream, route)
}
return a.routeAttack(stream, route)
}
func appendRouteIfMissing(routes []string, route string) []string {
if slices.Contains(routes, route) {
return routes
}
return append(routes, route)
}
func (a Attacker) credAttack(stream cameradar.Stream, username, password string) (bool, error) {
u, urlStr, err := buildRTSPURL(stream, stream.Route(), username, password)
if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err)
}
code, err := a.describeStatus(u)
if err != nil {
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
return code == base.StatusOK || code == base.StatusNotFound, nil
}
func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, emitProgress bool) (cameradar.Stream, error) {
if emitProgress {
defer a.reporter.Progress(cameradar.StepValidateStreams, cameradar.ProgressTickMessage())
}
if ctx.Err() != nil {
return stream, ctx.Err()
}
u, urlStr, err := buildRTSPURL(stream, stream.Route(), stream.Username, stream.Password)
if err != nil {
return stream, fmt.Errorf("building rtsp url: %w", err)
}
client, err := a.newRTSPClient(u)
if err != nil {
return stream, fmt.Errorf("starting rtsp client: %w", err)
}
defer client.Close()
desc, res, err := client.Describe(u)
if err != nil {
return a.handleDescribeError(stream, urlStr, err)
}
a.logDescribeResponse(urlStr, res)
if desc == nil || len(desc.Medias) == 0 {
return stream, fmt.Errorf("no media tracks found for %q", urlStr)
}
res, err = client.Setup(desc.BaseURL, desc.Medias[0], 0, 0)
if err != nil {
return a.handleSetupError(stream, urlStr, err)
}
a.logSetupResponse(urlStr, res)
stream.Available = res != nil && res.StatusCode == base.StatusOK
if stream.Available {
a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream validated for %s:%d", stream.Address.String(), stream.Port))
}
return stream, nil
}
func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
var badStatus liberrors.ErrClientBadStatusCode
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream unavailable for %s:%d (RTSP %d)",
stream.Address.String(),
stream.Port,
badStatus.Code,
))
stream.Available = false
return stream, nil
}
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
func (a Attacker) handleSetupError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
var badStatus liberrors.ErrClientBadStatusCode
if errors.As(err, &badStatus) {
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", urlStr, badStatus.Code))
stream.Available = badStatus.Code == base.StatusOK
return stream, nil
}
return stream, fmt.Errorf("performing setup request at %q: %w", urlStr, err)
}
func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) {
if res == nil {
return
}
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, res.StatusCode))
}
func (a Attacker) logSetupResponse(urlStr string, res *base.Response) {
if res == nil {
return
}
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", urlStr, res.StatusCode))
}