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:
committed by
Brendan Le Glaunec
parent
fbc0b7a66d
commit
8e7de3f59e
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -39,7 +39,7 @@ var (
|
|||||||
|
|
||||||
routeFound = Stream{
|
routeFound = Stream{
|
||||||
RouteFound: true,
|
RouteFound: true,
|
||||||
Route: "r0ute",
|
Routes: []string{"r0ute"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user