Support multi-route detection

* Change stream model to support multiple routes
* Simplify attack algorithm
* Use dummy route to detect cameras which accept all routes
This commit is contained in:
Ullaakut
2020-05-04 08:02:43 +02:00
committed by Brendan Le Glaunec
parent fbc0b7a66d
commit 8e7de3f59e
7 changed files with 72 additions and 45 deletions
+46 -32
View File
@@ -21,6 +21,16 @@ const (
rtspSetup = 4 rtspSetup = 4
) )
// Authentication types.
const (
authNone = 0
authBasic = 1
authDigest = 2
)
// Route that should never be a constructor default.
const dummyRoute = "/0x8b6c42"
// Attack attacks the given targets and returns the accessed streams. // Attack attacks the given targets and returns the accessed streams.
func (s *Scanner) Attack(targets []Stream) ([]Stream, error) { func (s *Scanner) Attack(targets []Stream) ([]Stream, error) {
if len(targets) == 0 { if len(targets) == 0 {
@@ -76,20 +86,13 @@ func (s *Scanner) AttackCredentials(targets []Stream) []Stream {
defer close(resChan) defer close(resChan)
for i := range targets { for i := range targets {
// TODO: Perf Improvement: Skip cameras with no auth type detected, and set their
// CredentialsFound value to true.
go s.attackCameraCredentials(targets[i], resChan) go s.attackCameraCredentials(targets[i], resChan)
} }
attackResults := []Stream{}
// TODO: Change this into a for+select and make a successful result close the chan.
for range targets { for range targets {
attackResults = append(attackResults, <-resChan) attackResult := <-resChan
} if attackResult.CredentialsFound {
targets = replace(targets, attackResult)
for i := range attackResults {
if attackResults[i].CredentialsFound {
targets = replace(targets, attackResults[i])
} }
} }
@@ -106,15 +109,10 @@ func (s *Scanner) AttackRoute(targets []Stream) []Stream {
go s.attackCameraRoute(targets[i], resChan) go s.attackCameraRoute(targets[i], resChan)
} }
attackResults := []Stream{}
// TODO: Change this into a for+select and make a successful result close the chan.
for range targets { for range targets {
attackResults = append(attackResults, <-resChan) attackResult := <-resChan
} if attackResult.RouteFound {
targets = replace(targets, attackResult)
for i := range attackResults {
if attackResults[i].RouteFound {
targets = replace(targets, attackResults[i])
} }
} }
@@ -130,11 +128,11 @@ func (s *Scanner) DetectAuthMethods(targets []Stream) []Stream {
var authMethod string var authMethod string
switch targets[i].AuthenticationType { switch targets[i].AuthenticationType {
case 0: case authNone:
authMethod = "no" authMethod = "no"
case 1: case authBasic:
authMethod = "basic" authMethod = "basic"
case 2: case authDigest:
authMethod = "digest" authMethod = "digest"
} }
@@ -164,18 +162,27 @@ func (s *Scanner) attackCameraCredentials(target Stream, resChan chan<- Stream)
} }
func (s *Scanner) attackCameraRoute(target Stream, resChan chan<- Stream) { func (s *Scanner) attackCameraRoute(target Stream, resChan chan<- Stream) {
// If the stream responds positively to the dummy route, it means
// it doesn't require (or respect the RFC) a route and the attack
// can be skipped.
ok := s.routeAttack(target, dummyRoute)
if ok {
target.RouteFound = true
target.Routes = append(target.Routes, "/")
resChan <- target
return
}
// Otherwise, bruteforce the routes.
for _, route := range s.routes { for _, route := range s.routes {
ok := s.routeAttack(target, route) ok := s.routeAttack(target, route)
if ok { if ok {
target.RouteFound = true target.RouteFound = true
target.Route = route target.Routes = append(target.Routes, route)
resChan <- target
return
} }
time.Sleep(s.attackInterval) time.Sleep(s.attackInterval)
} }
target.RouteFound = false
resChan <- target resChan <- target
} }
@@ -186,7 +193,7 @@ func (s *Scanner) detectAuthMethod(stream Stream) int {
"rtsp://%s:%d/%s", "rtsp://%s:%d/%s",
stream.Address, stream.Address,
stream.Port, stream.Port,
stream.Route, stream.Route(),
) )
s.setCurlOptions(c) s.setCurlOptions(c)
@@ -210,7 +217,7 @@ func (s *Scanner) detectAuthMethod(stream Stream) int {
return -1 return -1
} }
if s.verbose { if s.debug {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", authType) s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", authType)
} }
@@ -255,7 +262,7 @@ func (s *Scanner) routeAttack(stream Stream, route string) bool {
return false return false
} }
if s.verbose { if s.debug {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc) s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
} }
// If it's a 401 or 403, it means that the credentials are wrong but the route might be okay. // If it's a 401 or 403, it means that the credentials are wrong but the route might be okay.
@@ -275,7 +282,7 @@ func (s *Scanner) credAttack(stream Stream, username string, password string) bo
password, password,
stream.Address, stream.Address,
stream.Port, stream.Port,
stream.Route, stream.Route(),
) )
s.setCurlOptions(c) s.setCurlOptions(c)
@@ -304,7 +311,7 @@ func (s *Scanner) credAttack(stream Stream, username string, password string) bo
return false return false
} }
if s.verbose { if s.debug {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc) s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
} }
@@ -325,7 +332,7 @@ func (s *Scanner) validateStream(stream Stream) bool {
stream.Password, stream.Password,
stream.Address, stream.Address,
stream.Port, stream.Port,
stream.Route, stream.Route(),
) )
s.setCurlOptions(c) s.setCurlOptions(c)
@@ -356,7 +363,7 @@ func (s *Scanner) validateStream(stream Stream) bool {
return false return false
} }
if s.verbose { if s.debug {
s.term.Debugln("SETUP", attackURL, "RTSP/1.0 >", rc) s.term.Debugln("SETUP", attackURL, "RTSP/1.0 >", rc)
} }
// If it's a 200, the stream is accessed successfully. // If it's a 200, the stream is accessed successfully.
@@ -375,6 +382,13 @@ func (s *Scanner) setCurlOptions(c Curler) {
_ = c.Setopt(curl.OPT_NOBODY, 1) _ = c.Setopt(curl.OPT_NOBODY, 1)
// Set custom timeout. // Set custom timeout.
_ = c.Setopt(curl.OPT_TIMEOUT_MS, int(s.timeout/time.Millisecond)) _ = c.Setopt(curl.OPT_TIMEOUT_MS, int(s.timeout/time.Millisecond))
// Enable verbose logs if verbose mode is on.
if s.verbose {
_ = c.Setopt(curl.OPT_VERBOSE, 1)
} else {
_ = c.Setopt(curl.OPT_VERBOSE, 0)
}
} }
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing // HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/Ullaakut/cameradar" "github.com/Ullaakut/cameradar/v5"
"github.com/Ullaakut/disgo" "github.com/Ullaakut/disgo"
"github.com/Ullaakut/disgo/style" "github.com/Ullaakut/disgo/style"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -25,7 +25,7 @@ func parseArguments() error {
pflag.IntP("scan-speed", "s", 4, "The nmap speed preset to use for scanning (lower is stealthier)") pflag.IntP("scan-speed", "s", 4, "The nmap speed preset to use for scanning (lower is stealthier)")
pflag.DurationP("attack-interval", "I", 0, "The interval between each attack (i.e: 2000ms, higher is stealthier)") pflag.DurationP("attack-interval", "I", 0, "The interval between each attack (i.e: 2000ms, higher is stealthier)")
pflag.DurationP("timeout", "T", 2000*time.Millisecond, "The timeout to use for attack attempts (i.e: 2000ms)") pflag.DurationP("timeout", "T", 2000*time.Millisecond, "The timeout to use for attack attempts (i.e: 2000ms)")
pflag.BoolP("debug", "d", true, "Enable the debug logs") pflag.BoolP("debug", "d", false, "Enable the debug logs")
pflag.BoolP("verbose", "v", false, "Enable the verbose logs") pflag.BoolP("verbose", "v", false, "Enable the verbose logs")
pflag.BoolP("help", "h", false, "displays this help message") pflag.BoolP("help", "h", false, "displays this help message")
+1 -1
View File
@@ -18,7 +18,7 @@ func replace(streams []Stream, new Stream) []Stream {
// GetCameraRTSPURL generates a stream's RTSP URL. // GetCameraRTSPURL generates a stream's RTSP URL.
func GetCameraRTSPURL(stream Stream) string { func GetCameraRTSPURL(stream Stream) string {
return "rtsp://" + stream.Username + ":" + stream.Password + "@" + stream.Address + ":" + fmt.Sprint(stream.Port) + "/" + stream.Route return "rtsp://" + stream.Username + ":" + stream.Password + "@" + stream.Address + ":" + fmt.Sprint(stream.Port) + "/" + stream.Route()
} }
// GetCameraAdminPanelURL returns the URL to the camera's admin panel. // GetCameraAdminPanelURL returns the URL to the camera's admin panel.
+1 -1
View File
@@ -61,7 +61,7 @@ func TestGetCameraRTSPURL(t *testing.T) {
Address: "1.2.3.4", Address: "1.2.3.4",
Username: "ullaakut", Username: "ullaakut",
Password: "ba69897483886f0d2b0afb6345b76c0c", Password: "ba69897483886f0d2b0afb6345b76c0c",
Route: "cameradar.sdp", Routes: []string{"cameradar.sdp"},
Port: 1337, Port: 1337,
} }
+9 -1
View File
@@ -7,7 +7,7 @@ type Stream struct {
Device string `json:"device"` Device string `json:"device"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Route string `json:"route"` Routes []string `json:"route"`
Address string `json:"address" validate:"required"` Address string `json:"address" validate:"required"`
Port uint16 `json:"port" validate:"required"` Port uint16 `json:"port" validate:"required"`
@@ -18,6 +18,14 @@ type Stream struct {
AuthenticationType int `json:"authentication_type"` AuthenticationType int `json:"authentication_type"`
} }
// Route returns this stream's route if there is one.
func (s Stream) Route() string {
if len(s.Routes) > 0 {
return s.Routes[0]
}
return ""
}
// Credentials is a map of credentials // Credentials is a map of credentials
// usernames are keys and passwords are values // usernames are keys and passwords are values
// creds['admin'] -> 'secure_password' // creds['admin'] -> 'secure_password'
+8 -3
View File
@@ -46,11 +46,16 @@ func (s *Scanner) PrintStreams(streams []Stream) {
s.term.Infof("\tPassword:\t\t%s\n", style.Failure("not found")) s.term.Infof("\tPassword:\t\t%s\n", style.Failure("not found"))
} }
s.term.Infoln("\tRTSP routes:")
if stream.RouteFound { if stream.RouteFound {
s.term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Success("/"+stream.Route)) for _, route := range stream.Routes {
} else { s.term.Infoln(style.Success("\t\t\t\t/" + route))
s.term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Failure("not found"))
} }
} else {
s.term.Infoln(style.Failure("not found"))
}
s.term.Info("\n\n")
} }
if success > 1 { if success > 1 {
+1 -1
View File
@@ -39,7 +39,7 @@ var (
routeFound = Stream{ routeFound = Stream{
RouteFound: true, RouteFound: true,
Route: "r0ute", Routes: []string{"r0ute"},
} }
) )