Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 629bc7df33 |
@@ -28,7 +28,7 @@ jobs:
|
|||||||
if: steps.install-go.outputs.cache-hit != 'true'
|
if: steps.install-go.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ Cameradar scans RTSP endpoints on authorized targets, and uses dictionary attack
|
|||||||
- [Security and responsible use](#security-and-responsible-use)
|
- [Security and responsible use](#security-and-responsible-use)
|
||||||
- [Output](#output)
|
- [Output](#output)
|
||||||
- [Check camera access](#check-camera-access)
|
- [Check camera access](#check-camera-access)
|
||||||
- [Command-line options and environment variables](#command-line-options-and-environment-variables)
|
- [Command-line options](#command-line-options)
|
||||||
- [Input file format](#input-file-format)
|
- [Input file format](#input-file-format)
|
||||||
|
- [Environment variables](#environment-variables)
|
||||||
- [Build and contribute](#build-and-contribute)
|
- [Build and contribute](#build-and-contribute)
|
||||||
- [Frequently asked questions](#frequently-asked-questions)
|
- [Frequently asked questions](#frequently-asked-questions)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
@@ -74,7 +75,7 @@ docker run --rm -t --net=host ullaakut/cameradar --targets 192.168.100.0/24
|
|||||||
|
|
||||||
This scans ports 554, 5554, and 8554 on the target subnet.
|
This scans ports 554, 5554, and 8554 on the target subnet.
|
||||||
It attempts to enumerate RTSP streams.
|
It attempts to enumerate RTSP streams.
|
||||||
For all options, see [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
|
For all options, see [command-line options](#command-line-options).
|
||||||
|
|
||||||
- Targets can be CIDRs, IPs, IP ranges or a hostname.
|
- Targets can be CIDRs, IPs, IP ranges or a hostname.
|
||||||
- Subnet: `172.16.100.0/24`
|
- Subnet: `172.16.100.0/24`
|
||||||
@@ -106,7 +107,7 @@ Use this option if Docker is not available or if you want a local build.
|
|||||||
1. `go install github.com/Ullaakut/cameradar/v6/cmd/cameradar@latest`
|
1. `go install github.com/Ullaakut/cameradar/v6/cmd/cameradar@latest`
|
||||||
|
|
||||||
The `cameradar` binary is now in your `$GOPATH/bin`.
|
The `cameradar` binary is now in your `$GOPATH/bin`.
|
||||||
For available flags, see [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
|
For available flags, see [command-line options](#command-line-options).
|
||||||
|
|
||||||
## Install on Android (Termux)
|
## Install on Android (Termux)
|
||||||
|
|
||||||
@@ -272,11 +273,117 @@ localhost
|
|||||||
When you use `--skip-scan`, Cameradar expands each entry into explicit IP
|
When you use `--skip-scan`, Cameradar expands each entry into explicit IP
|
||||||
addresses before building the target list.
|
addresses before building the target list.
|
||||||
|
|
||||||
## Command-line options and environment variables
|
## Options
|
||||||
|
|
||||||
The complete CLI and environment variable reference is maintained in [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
|
### `TARGETS` / `--targets` / `-t`
|
||||||
|
|
||||||
This includes all supported flags, defaults, accepted values, and env var mapping.
|
This variable is required.
|
||||||
|
It specifies the target that Cameradar scans and attempts to access.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* `172.16.100.0/24`
|
||||||
|
* `192.168.1.1`
|
||||||
|
* `localhost`
|
||||||
|
* `192.168.1.140-255`
|
||||||
|
* `192.168.2-3.0-255`
|
||||||
|
|
||||||
|
### `PORTS` / `--ports` / `-p`
|
||||||
|
|
||||||
|
This variable is optional and allows you to specify the ports to scan.
|
||||||
|
|
||||||
|
Default value: `554,5554,8554`
|
||||||
|
|
||||||
|
Change these only if you are sure cameras stream over different ports.
|
||||||
|
Most cameras use these defaults.
|
||||||
|
|
||||||
|
### `CUSTOM_ROUTES` / `--custom-routes` / `-r`
|
||||||
|
|
||||||
|
This option is optional.
|
||||||
|
It replaces the default routes dictionary used for the dictionary attack.
|
||||||
|
|
||||||
|
If unset, Cameradar uses the built-in routes dictionary.
|
||||||
|
|
||||||
|
### `CUSTOM_CREDENTIALS` / `--custom-credentials` / `-c`
|
||||||
|
|
||||||
|
This option is optional.
|
||||||
|
It replaces the default credentials dictionary used for the dictionary attack.
|
||||||
|
|
||||||
|
If unset, Cameradar uses the built-in credentials dictionary.
|
||||||
|
|
||||||
|
### `SCANNER` / `--scanner`
|
||||||
|
|
||||||
|
This optional variable sets the discovery backend.
|
||||||
|
|
||||||
|
* `nmap` includes service discovery and is generally more reliable when you want
|
||||||
|
to specifically identify RTSP services.
|
||||||
|
* `masscan` is generally more efficient for large-scale discovery, but it does
|
||||||
|
not identify services and therefore can be less specific for RTSP.
|
||||||
|
|
||||||
|
Supported values: `nmap`, `masscan`
|
||||||
|
|
||||||
|
Default value: `nmap`
|
||||||
|
|
||||||
|
### `SCAN_SPEED` / `--scan-speed` / `-s`
|
||||||
|
|
||||||
|
This optional variable sets nmap discovery presets for speed or accuracy.
|
||||||
|
Lower it on slow networks and raise it on fast networks.
|
||||||
|
See [nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||||
|
|
||||||
|
This option is ignored when `--scanner masscan` is used.
|
||||||
|
|
||||||
|
Default value: `4`
|
||||||
|
|
||||||
|
### `SKIP_SCAN` / `--skip-scan`
|
||||||
|
|
||||||
|
This optional flag skips network discovery and assumes every target and port
|
||||||
|
pair is an RTSP stream.
|
||||||
|
|
||||||
|
Use it when you already know the RTSP endpoints or when discovery is blocked.
|
||||||
|
For best results, specify only RTSP ports.
|
||||||
|
|
||||||
|
Default value: `false`
|
||||||
|
|
||||||
|
### `ATTACK_INTERVAL` / `--attack-interval` / `-I`
|
||||||
|
|
||||||
|
This optional variable sets a delay between attacks.
|
||||||
|
Increase it for networks that may block brute-force attempts.
|
||||||
|
Default: no delay.
|
||||||
|
|
||||||
|
Default value: `0ms`
|
||||||
|
|
||||||
|
### `TIMEOUT` / `--timeout` / `-T`
|
||||||
|
|
||||||
|
This optional variable sets the timeout for requests sent to the cameras.
|
||||||
|
Increase it for slow networks and decrease it for fast networks.
|
||||||
|
|
||||||
|
Default value: `2000ms`
|
||||||
|
|
||||||
|
### `DEBUG` / `--debug` / `-d`
|
||||||
|
|
||||||
|
This optional variable enables more verbose output.
|
||||||
|
|
||||||
|
It outputs discovery results (`nmap` or `masscan`), cURL requests, and more.
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
### `UI` / `--ui`
|
||||||
|
|
||||||
|
This option selects the UI mode.
|
||||||
|
|
||||||
|
* `auto` selects `tui` if your terminal is interactive, `plain` otherwise
|
||||||
|
* `tui` shows a fullscreen interface with a progress bar and shows the results in a table
|
||||||
|
* `plain` logs the steps taken by cameradar as plain text and is meant to be used by non-interactive terminals
|
||||||
|
|
||||||
|
Supported values: `auto`, `tui`, `plain`
|
||||||
|
|
||||||
|
Default: `auto`
|
||||||
|
|
||||||
|
### `OUTPUT` / `--output`
|
||||||
|
|
||||||
|
This optional variable writes an M3U playlist of the discovered streams to the given file path.
|
||||||
|
|
||||||
|
Example: `/tmp/cameradar.m3u`
|
||||||
|
|
||||||
## Build and contribute
|
## Build and contribute
|
||||||
|
|
||||||
@@ -296,7 +403,41 @@ The `cameradar` binary is now in `$GOPATH/bin/cameradar`.
|
|||||||
|
|
||||||
## Frequently asked questions
|
## Frequently asked questions
|
||||||
|
|
||||||
See [Troubleshooting & FAQ](https://github.com/Ullaakut/cameradar/wiki/Troubleshooting-%26-FAQ)
|
> Cameradar does not detect any camera!
|
||||||
|
|
||||||
|
This usually means the cameras are not streaming over RTSP.
|
||||||
|
It can also mean the targets are not in your scan range.
|
||||||
|
CCTV cameras are often on private subnets.
|
||||||
|
Use `-t` to set the correct targets.
|
||||||
|
If you still see no results, open an issue with device details.
|
||||||
|
|
||||||
|
> Cameradar detects my cameras, but does not manage to access them!
|
||||||
|
|
||||||
|
The camera configuration may have changed, so defaults do not match.
|
||||||
|
Cameradar uses defaults unless you provide custom dictionaries.
|
||||||
|
Add your credentials and routes, then follow the [configuration](#configuration) section.
|
||||||
|
|
||||||
|
> What happened to the C++ version?
|
||||||
|
|
||||||
|
The 1.1.4 tag contains the legacy C++ implementation.
|
||||||
|
It is slower and less stable than the Go version, so it is not recommended to use.
|
||||||
|
|
||||||
|
> I want to scan my local network or my own machine, and it does not work! What's going on?
|
||||||
|
|
||||||
|
Use `--net=host` when running the Docker image, or use the installed binary.
|
||||||
|
|
||||||
|
> I don't have a camera, but I'd like to try Cameradar!
|
||||||
|
|
||||||
|
Run the following container, then run Cameradar against it:
|
||||||
|
|
||||||
|
`docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt`
|
||||||
|
|
||||||
|
Cameradar should discover the `admin` / `12345` credentials.
|
||||||
|
You can try other default credentials listed in the dictionaries.
|
||||||
|
|
||||||
|
> What authentication types does Cameradar support?
|
||||||
|
|
||||||
|
Cameradar supports both basic and digest authentication.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -308,7 +449,7 @@ See [Troubleshooting & FAQ](https://github.com/Ullaakut/cameradar/wiki/Troublesh
|
|||||||
|
|
||||||
`docker run --rm -t --net=host -v /tmp:/tmp ullaakut/cameradar --targets /tmp/test.txt --ports 8554`
|
`docker run --rm -t --net=host -v /tmp:/tmp ullaakut/cameradar --targets /tmp/test.txt --ports 8554`
|
||||||
|
|
||||||
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554, 8554, 322, and 8322
|
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554 and 8554
|
||||||
|
|
||||||
`docker run --rm -t --net=host -v /tmp:/tmp ullaakut/cameradar --targets 192.168.0.0/24 --custom-credentials "/tmp/dictionaries/credentials.json" --custom-routes "/tmp/dictionaries/routes" --ports 554,5554,8554`
|
`docker run --rm -t --net=host -v /tmp:/tmp ullaakut/cameradar --targets 192.168.0.0/24 --custom-credentials "/tmp/dictionaries/credentials.json" --custom-routes "/tmp/dictionaries/routes" --ports 554,5554,8554`
|
||||||
|
|
||||||
|
|||||||
+17
-10
@@ -38,17 +38,18 @@ var (
|
|||||||
|
|
||||||
var flags = cmd.Flags{
|
var flags = cmd.Flags{
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: flagTargets,
|
Name: flagTargets,
|
||||||
Usage: "The targets on which to scan for open RTSP streams in a network range format",
|
Usage: "The targets on which to scan for open RTSP streams in a network range format",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
|
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
|
||||||
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: flagPorts,
|
Name: flagPorts,
|
||||||
Usage: "The ports on which to search for RTSP streams",
|
Usage: "The ports on which to search for RTSP streams",
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"p"},
|
||||||
Sources: cli.EnvVars(strcase.ToSNAKE(flagPorts)),
|
Sources: cli.EnvVars(strcase.ToSNAKE(flagPorts)),
|
||||||
Value: []string{"554", "5554", "8554", "http", "322", "8322"},
|
Value: []string{"554", "5554", "8554", "http"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: flagCustomRoutes,
|
Name: flagCustomRoutes,
|
||||||
@@ -127,13 +128,19 @@ func realMain() (code int) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
scanCommand := &cli.Command{
|
||||||
|
Name: "scan",
|
||||||
|
Usage: "Scan targets for RTSP streams",
|
||||||
|
Flags: flags,
|
||||||
|
Action: runCameradar,
|
||||||
|
}
|
||||||
|
|
||||||
app := &cli.Command{
|
app := &cli.Command{
|
||||||
Name: "Cameradar",
|
Name: "Cameradar",
|
||||||
Version: version,
|
Version: version,
|
||||||
Usage: "Scan targets for RTSP streams",
|
DefaultCommand: scanCommand.Name,
|
||||||
Flags: flags,
|
|
||||||
Action: runCameradar,
|
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
|
scanCommand,
|
||||||
{
|
{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "Print version information",
|
Usage: "Print version information",
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func (a Attacker) attackCredentialsForStream(ctx context.Context, target camerad
|
|||||||
msg := fmt.Sprintf("credential attempt failed for %s:%d (%s:%s): %v", target.Address.String(), target.Port, username, password, err)
|
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)
|
a.reporter.Debug(cameradar.StepAttackCredentials, msg)
|
||||||
|
|
||||||
continue
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
@@ -253,7 +253,7 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
|
|||||||
if emitProgress {
|
if emitProgress {
|
||||||
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
|
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
|
||||||
}
|
}
|
||||||
ok, err := a.routeAttack(ctx, &target, dummyRoute)
|
ok, err := a.routeAttack(target, dummyRoute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route probe failed for %s:%d: %v", target.Address.String(), target.Port, err))
|
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route probe failed for %s:%d: %v", target.Address.String(), target.Port, err))
|
||||||
return target, nil
|
return target, nil
|
||||||
@@ -275,7 +275,7 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
|
|||||||
if emitProgress {
|
if emitProgress {
|
||||||
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
|
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
|
||||||
}
|
}
|
||||||
ok, err := a.routeAttack(ctx, &target, route)
|
ok, err := a.routeAttack(target, route)
|
||||||
if err != nil {
|
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))
|
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
|
return target, nil
|
||||||
@@ -290,30 +290,18 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
|
|||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) routeAttack(ctx context.Context, stream *cameradar.Stream, route string) (bool, error) {
|
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
|
||||||
u, urlStr, err := buildRTSPURL(*stream, route, stream.Username, stream.Password)
|
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("building rtsp url: %w", err)
|
return false, fmt.Errorf("building rtsp url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
|
code, err := a.describeStatus(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
|
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))
|
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
|
||||||
|
|
||||||
if code == base.StatusMovedPermanently {
|
|
||||||
a.handleRedirect(stream, headers)
|
|
||||||
u, urlStr, err = buildRTSPURL(*stream, route, stream.Username, stream.Password)
|
|
||||||
if err == nil {
|
|
||||||
code, _, err = a.probeDescribeHeaders(ctx, u, urlStr)
|
|
||||||
if err == nil {
|
|
||||||
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 (redirect followed) > %d", urlStr, code))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
|
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
|
||||||
return access, nil
|
return access, nil
|
||||||
}
|
}
|
||||||
@@ -326,12 +314,10 @@ func (a Attacker) credAttack(stream cameradar.Stream, username, password string)
|
|||||||
|
|
||||||
code, err := a.describeStatus(u)
|
code, err := a.describeStatus(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("Error testing %s:%s -> %v", username, password, err))
|
|
||||||
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
|
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))
|
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
|
||||||
|
|
||||||
return code == base.StatusOK || code == base.StatusNotFound, nil
|
return code == base.StatusOK || code == base.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-58
@@ -3,11 +3,9 @@ package attack
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -21,16 +19,10 @@ import (
|
|||||||
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
rtsp = "rtsp"
|
|
||||||
rtsps = "rtsps"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) {
|
func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) {
|
||||||
client := &gortsplib.Client{
|
client := &gortsplib.Client{
|
||||||
ReadTimeout: a.timeout,
|
ReadTimeout: a.timeout,
|
||||||
WriteTimeout: a.timeout,
|
WriteTimeout: a.timeout,
|
||||||
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
}
|
||||||
client.Scheme = u.Scheme
|
client.Scheme = u.Scheme
|
||||||
client.Host = u.Host
|
client.Host = u.Host
|
||||||
@@ -71,16 +63,7 @@ func (a Attacker) describeStatus(u *base.URL) (base.StatusCode, error) {
|
|||||||
// which is exactly what we need in order to detect authentication methods.
|
// which is exactly what we need in order to detect authentication methods.
|
||||||
func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr string) (base.StatusCode, base.Header, error) {
|
func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr string) (base.StatusCode, base.Header, error) {
|
||||||
dialer := &net.Dialer{Timeout: a.timeout}
|
dialer := &net.Dialer{Timeout: a.timeout}
|
||||||
var conn net.Conn
|
conn, err := dialer.DialContext(ctx, "tcp", u.Host)
|
||||||
var err error
|
|
||||||
|
|
||||||
if u.Scheme == rtsps {
|
|
||||||
tlsDialer := &tls.Dialer{NetDialer: dialer, Config: &tls.Config{InsecureSkipVerify: true}}
|
|
||||||
conn, err = tlsDialer.DialContext(ctx, "tcp", u.Host)
|
|
||||||
} else {
|
|
||||||
conn, err = dialer.DialContext(ctx, "tcp", u.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -134,40 +117,6 @@ func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr
|
|||||||
return base.StatusCode(code), headers, nil
|
return base.StatusCode(code), headers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) handleRedirect(stream *cameradar.Stream, resHeaders base.Header) {
|
|
||||||
locations := headerValues(resHeaders, "Location")
|
|
||||||
if len(locations) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
location, err := url.Parse(locations[0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch location.Scheme {
|
|
||||||
case rtsps:
|
|
||||||
stream.Secure = true
|
|
||||||
case rtsp:
|
|
||||||
stream.Secure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if location.Hostname() != "" {
|
|
||||||
addr, err := netip.ParseAddr(location.Hostname())
|
|
||||||
if err == nil {
|
|
||||||
stream.Address = addr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if location.Port() != "" {
|
|
||||||
port, err := strconv.Atoi(location.Port())
|
|
||||||
if err == nil {
|
|
||||||
if port >= 0 && port <= 65535 {
|
|
||||||
stream.Port = uint16(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authTypeFromHeaders(values base.HeaderValue) cameradar.AuthType {
|
func authTypeFromHeaders(values base.HeaderValue) cameradar.AuthType {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return cameradar.AuthUnknown
|
return cameradar.AuthUnknown
|
||||||
@@ -219,13 +168,8 @@ func buildRTSPURL(stream cameradar.Stream, route, username, password string) (*b
|
|||||||
host := net.JoinHostPort(stream.Address.String(), strconv.Itoa(int(stream.Port)))
|
host := net.JoinHostPort(stream.Address.String(), strconv.Itoa(int(stream.Port)))
|
||||||
path := "/" + strings.TrimLeft(strings.TrimSpace(route), "/") // Ensure path starts with a single "/"
|
path := "/" + strings.TrimLeft(strings.TrimSpace(route), "/") // Ensure path starts with a single "/"
|
||||||
|
|
||||||
scheme := rtsp
|
|
||||||
if stream.Secure {
|
|
||||||
scheme = rtsps
|
|
||||||
}
|
|
||||||
|
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: scheme,
|
Scheme: "rtsp",
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: path,
|
Path: path,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ func TestBuildRTSPURL(t *testing.T) {
|
|||||||
route string
|
route string
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
secure bool
|
|
||||||
wantURL string
|
wantURL string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -75,27 +74,11 @@ func TestBuildRTSPURL(t *testing.T) {
|
|||||||
username: "user",
|
username: "user",
|
||||||
wantURL: "rtsp://user:@192.168.0.10:554/stream",
|
wantURL: "rtsp://user:@192.168.0.10:554/stream",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "secure rtsps scheme without credentials",
|
|
||||||
route: "stream",
|
|
||||||
secure: true,
|
|
||||||
wantURL: "rtsps://192.168.0.10:554/stream",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "secure rtsps scheme with credentials",
|
|
||||||
route: "stream",
|
|
||||||
username: "admin",
|
|
||||||
password: "password",
|
|
||||||
secure: true,
|
|
||||||
wantURL: "rtsps://admin:password@192.168.0.10:554/stream",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
s := stream
|
_, gotURL, err := buildRTSPURL(stream, test.route, test.username, test.password)
|
||||||
s.Secure = test.secure
|
|
||||||
_, gotURL, err := buildRTSPURL(s, test.route, test.username, test.password)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.wantURL, gotURL)
|
require.Equal(t, test.wantURL, gotURL)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -119,10 +119,5 @@ func formatRTSPURL(stream cameradar.Stream) string {
|
|||||||
credentials = stream.Username + ":" + stream.Password + "@"
|
credentials = stream.Username + ":" + stream.Password + "@"
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := "rtsp"
|
return "rtsp://" + credentials + stream.Address.String() + ":" + strconv.FormatUint(uint64(stream.Port), 10) + path
|
||||||
if stream.Secure {
|
|
||||||
scheme = "rtsps"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s%s:%s%s", scheme, credentials, stream.Address.String(), strconv.FormatUint(uint64(stream.Port), 10), path)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,12 +83,9 @@ func runScan(ctx context.Context, runner Runner, reporter Reporter) ([]cameradar
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
isSecure := port.Number == 322 || port.Number == 8322
|
|
||||||
|
|
||||||
streams = append(streams, cameradar.Stream{
|
streams = append(streams, cameradar.Stream{
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: uint16(port.Number),
|
Port: uint16(port.Number),
|
||||||
Secure: isSecure,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,16 +53,15 @@ func TestRunScan(t *testing.T) {
|
|||||||
name: "collects streams from multiple hosts",
|
name: "collects streams from multiple hosts",
|
||||||
result: &masscanlib.Run{
|
result: &masscanlib.Run{
|
||||||
Hosts: []masscanlib.Host{
|
Hosts: []masscanlib.Host{
|
||||||
{Address: "192.0.2.10", Ports: []masscanlib.Port{{Number: 8554, Status: "open"}, {Number: 8322, Status: "open"}}},
|
{Address: "192.0.2.10", Ports: []masscanlib.Port{{Number: 8554, Status: "open"}}},
|
||||||
{Address: "198.51.100.9", Ports: []masscanlib.Port{{Number: 554, Status: "open"}}},
|
{Address: "198.51.100.9", Ports: []masscanlib.Port{{Number: 554, Status: "open"}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantStreams: []cameradar.Stream{
|
wantStreams: []cameradar.Stream{
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8554, Secure: false},
|
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8554},
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8322, Secure: true},
|
{Address: netip.MustParseAddr("198.51.100.9"), Port: 554},
|
||||||
{Address: netip.MustParseAddr("198.51.100.9"), Port: 554, Secure: false},
|
|
||||||
},
|
},
|
||||||
wantProgress: []string{"Found 3 RTSP streams"},
|
wantProgress: []string{"Found 2 RTSP streams"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "returns error when scan fails",
|
name: "returns error when scan fails",
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
isSecure := strings.Contains(port.Service.Name, "rtsps") || strings.Contains(port.Service.Name, "ssl") || port.ID == 322 || port.ID == 8322
|
|
||||||
|
|
||||||
for _, address := range host.Addresses {
|
for _, address := range host.Addresses {
|
||||||
addr, err := netip.ParseAddr(address.Addr)
|
addr, err := netip.ParseAddr(address.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,7 +82,6 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
|
|||||||
Device: port.Service.Product,
|
Device: port.Service.Product,
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: port.ID,
|
Port: port.ID,
|
||||||
Secure: isSecure,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
Addresses: []nmaplib.Address{{Addr: "192.0.2.10"}, {Addr: "192.0.2.11"}},
|
Addresses: []nmaplib.Address{{Addr: "192.0.2.10"}, {Addr: "192.0.2.11"}},
|
||||||
Ports: []nmaplib.Port{
|
Ports: []nmaplib.Port{
|
||||||
openPort(8554, "rtsp-alt", "Model A"),
|
openPort(8554, "rtsp-alt", "Model A"),
|
||||||
openPort(322, "rtsps", "Model C"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nmaplib.Host{
|
nmaplib.Host{
|
||||||
@@ -69,34 +68,19 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
Device: "Model A",
|
Device: "Model A",
|
||||||
Address: netip.MustParseAddr("192.0.2.10"),
|
Address: netip.MustParseAddr("192.0.2.10"),
|
||||||
Port: 8554,
|
Port: 8554,
|
||||||
Secure: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Device: "Model A",
|
Device: "Model A",
|
||||||
Address: netip.MustParseAddr("192.0.2.11"),
|
Address: netip.MustParseAddr("192.0.2.11"),
|
||||||
Port: 8554,
|
Port: 8554,
|
||||||
Secure: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Device: "Model C",
|
|
||||||
Address: netip.MustParseAddr("192.0.2.10"),
|
|
||||||
Port: 322,
|
|
||||||
Secure: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Device: "Model C",
|
|
||||||
Address: netip.MustParseAddr("192.0.2.11"),
|
|
||||||
Port: 322,
|
|
||||||
Secure: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Device: "Model B",
|
Device: "Model B",
|
||||||
Address: netip.MustParseAddr("198.51.100.9"),
|
Address: netip.MustParseAddr("198.51.100.9"),
|
||||||
Port: 554,
|
Port: 554,
|
||||||
Secure: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantProgress: "Found 5 RTSP streams",
|
wantProgress: "Found 3 RTSP streams",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "returns error when scan fails",
|
name: "returns error when scan fails",
|
||||||
|
|||||||
@@ -51,11 +51,9 @@ func buildStreamsFromTargets(ctx context.Context, targets, ports []string) ([]ca
|
|||||||
streams := make([]cameradar.Stream, 0, len(resolvedTargets)*len(resolvedPorts))
|
streams := make([]cameradar.Stream, 0, len(resolvedTargets)*len(resolvedPorts))
|
||||||
for _, addr := range resolvedTargets {
|
for _, addr := range resolvedTargets {
|
||||||
for _, port := range resolvedPorts {
|
for _, port := range resolvedPorts {
|
||||||
isSecure := port == 322 || port == 8322
|
|
||||||
streams = append(streams, cameradar.Stream{
|
streams = append(streams, cameradar.Stream{
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: port,
|
Port: port,
|
||||||
Secure: isSecure,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
|
|||||||
"192.0.2.15",
|
"192.0.2.15",
|
||||||
"192.0.2.10-11",
|
"192.0.2.10-11",
|
||||||
}
|
}
|
||||||
ports := []string{"554", "322", "8554-8555"}
|
ports := []string{"554", "8554-8555"}
|
||||||
|
|
||||||
scanner := skip.New(targets, ports)
|
scanner := skip.New(targets, ports)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
|
|||||||
netip.MustParseAddr("192.0.2.11"),
|
netip.MustParseAddr("192.0.2.11"),
|
||||||
netip.MustParseAddr("192.0.2.15"),
|
netip.MustParseAddr("192.0.2.15"),
|
||||||
}
|
}
|
||||||
portsExpected := []uint16{554, 322, 8554, 8555}
|
portsExpected := []uint16{554, 8554, 8555}
|
||||||
|
|
||||||
var want []string
|
var want []string
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
@@ -44,13 +44,6 @@ func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
|
|||||||
var got []string
|
var got []string
|
||||||
for _, stream := range streams {
|
for _, stream := range streams {
|
||||||
got = append(got, stream.Address.String()+":"+strconv.Itoa(int(stream.Port)))
|
got = append(got, stream.Address.String()+":"+strconv.Itoa(int(stream.Port)))
|
||||||
|
|
||||||
if stream.Port == 322 || stream.Port == 8322 {
|
|
||||||
assert.True(t, stream.Secure)
|
|
||||||
} else {
|
|
||||||
assert.False(t, stream.Secure)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.ElementsMatch(t, want, got)
|
assert.ElementsMatch(t, want, got)
|
||||||
|
|||||||
@@ -126,12 +126,7 @@ func formatRTSPURL(stream cameradar.Stream) string {
|
|||||||
credentials = stream.Username + ":" + stream.Password + "@"
|
credentials = stream.Username + ":" + stream.Password + "@"
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := "rtsp"
|
return fmt.Sprintf("rtsp://%s%s:%d%s", credentials, stream.Address.String(), stream.Port, path)
|
||||||
if stream.Secure {
|
|
||||||
scheme = "rtsps"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s%s:%d%s", scheme, credentials, stream.Address.String(), stream.Port, path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAdminPanelURL(stream cameradar.Stream) string {
|
func formatAdminPanelURL(stream cameradar.Stream) string {
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ func TestFormatSummary(t *testing.T) {
|
|||||||
Device: "Model A",
|
Device: "Model A",
|
||||||
Address: netip.MustParseAddr("10.0.0.1"),
|
Address: netip.MustParseAddr("10.0.0.1"),
|
||||||
Port: 8554,
|
Port: 8554,
|
||||||
Secure: true,
|
|
||||||
Available: true,
|
Available: true,
|
||||||
Routes: []string{"stream1", "stream2"},
|
Routes: []string{"stream1", "stream2"},
|
||||||
RouteFound: true,
|
RouteFound: true,
|
||||||
@@ -74,7 +73,7 @@ func TestFormatSummary(t *testing.T) {
|
|||||||
"Authentication: digest",
|
"Authentication: digest",
|
||||||
"Routes: stream1, stream2",
|
"Routes: stream1, stream2",
|
||||||
"Credentials: user:pass",
|
"Credentials: user:pass",
|
||||||
"RTSP URL: rtsps://user:pass@10.0.0.1:8554/stream1",
|
"RTSP URL: rtsp://user:pass@10.0.0.1:8554/stream1",
|
||||||
"Admin panel: http://10.0.0.1/",
|
"Admin panel: http://10.0.0.1/",
|
||||||
"Admin panel: http://10.0.0.2/",
|
"Admin panel: http://10.0.0.2/",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ type Stream struct {
|
|||||||
Address netip.Addr `json:"address" validate:"required"`
|
Address netip.Addr `json:"address" validate:"required"`
|
||||||
Port uint16 `json:"port" validate:"required"`
|
Port uint16 `json:"port" validate:"required"`
|
||||||
|
|
||||||
Secure bool `json:"secure"`
|
|
||||||
|
|
||||||
CredentialsFound bool `json:"credentials_found"`
|
CredentialsFound bool `json:"credentials_found"`
|
||||||
RouteFound bool `json:"route_found"`
|
RouteFound bool `json:"route_found"`
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
|
|||||||
Reference in New Issue
Block a user