Cameradar v4 (#212)

* Refactor of cameradar library

* Old unit tests updated & improved. New unit tests inc

* Update documentation & issue template

* Update dependencies

* Update TravisCI build script to reflect argument change

* Remove outdated contributing guide

* Update README with more examples and remove part on library

* Add second camera to Travis build script & improve error detection

* Fix typo in travis script & add missing image to readme

* Remember that travis uses bash syntax not fish

* Use relative paths for images in the README
This commit is contained in:
Brendan Le Glaunec
2019-05-26 08:33:08 +02:00
committed by GitHub
parent 2e49587cc2
commit 212ac2f0d5
28 changed files with 1535 additions and 1163 deletions
+13 -11
View File
@@ -29,20 +29,22 @@ script:
# Run unit tests # Run unit tests
- go test -v -covermode=count -coverprofile=coverage.out - go test -v -covermode=count -coverprofile=coverage.out
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN
# Launch a fake camera to check if cameradar is able to access it # Launch fake cameras to check if cameradar is able to access them
- docker run -d --name=fake_camera -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -p 8554:8554 ullaakut/rtspatt - docker run -d --name="fake_camera_digest" -e RTSP_ROUTE="/live.sdp" -e RTSP_USERNAME="admin" -e RTSP_PASSWORD="12345" -e RTSP_AUTHENTICATION_METHOD="digest" -p 8554:8554 ullaakut/rtspatt
- docker run -d --name="fake_camera_basic" -e RTSP_ROUTE="/live.sdp" -e RTSP_USERNAME="root" -e RTSP_PASSWORD="root" -e RTSP_AUTHENTICATION_METHOD="digest" -p 5554:5554 ullaakut/rtspatt
# Launch cameradar on the local machine # Launch cameradar on the local machine
- docker run --net=host -t cameradar -t 0.0.0.0 -l > logs.txt - docker run --net=host -t cameradar -t 0.0.0.0 -p 8554,5554 -v > logs.txt
- docker logs fake_camera > camera_logs.txt # Gather the logs from the cameras
# Stop the fake camera - docker logs fake_camera_digest > camera_digest_logs.txt
- docker stop fake_camera - docker logs fake_camera_basic > camera_basic_logs.txt
# Stop the fake cameras
- docker stop fake_camera_basic
- docker stop fake_camera_digest
# Print logs # Print logs
- cat camera_logs.txt - cat camera_digest_logs.txt
- cat camera_basic_logs.txt
- cat logs.txt - cat logs.txt
# check if file contains more than one line - grep "Successful attack" logs.txt || exit 1
# 1 line: Error message because no streams were found
# More lines: Logs for all found cameras
- if [[ $(wc -l <logs.txt) -lt 2 ]]; then exit 1; fi
notifications: notifications:
email: email:
-82
View File
@@ -1,82 +0,0 @@
# Cameradar Contribution
This file will give you guidelines on how to contribute if you want to, and will list known contributors to this repo.
If you're not into software development or not into Golang, you can still help. Updating the dictionaries for example, would be a really cool contribution! Just make sure the credentials and routes you add are **default constructor credentials** and not custom credentials.
If you have other cool ideas, feel free to share them with me at [brendan.leglaunec@etixgroup.com](mailto:brendan.leglaunec@etixgroup.com) or to directly [create an issue](https://github.com/ullaakut/cameradar/issues)!
## Version 2.0.0
*Cameradar* is the name of the Golang library and the binary that serves as an example of its use, as well as the docker image that runs the binary.
The 2.0.0 version was a complete refactorring of the Cameradar C++ tool, which came from the fact that most users who want to access cameras either wanted to launch it with the basic cache manager, mostly using the docker image already provided in this repository, or did not use it because it did not integrate into their software solution easily.
Transforming it into a library allowed developers to use it directly in their own code exactly as they want, allowing for a greater flexibility. The Cameradar binary also provides a simple use example as well as maintains the old simple way of using Cameradar for non-developers.
## Workflow
### Branches & issues
If you want to work on an issue, make sure you create a specific branch for this issue using the format `issue_number-solution_explanation`. Examples are:
If issue `#64` is `Improve network scan performance`, the branch to fix it should be something like: `64-improve-network-scan-performance`. Note that it should always start with a verb conjugated in the infinitive form, and describe what the commits's effects will be on the codebase. One branch should only be for one change. If your branch fixes multiple things, you're doing it wrong.
Always make sure you're not working on the same issue as someone else, by asking on the issue thread to be assigned to it.
### Commit names
The name of the commits should always be #[issue number] [effect of the issue] (ex: `#343 Improve test coverage`).
When working on your local branch, you can do as many commits as you want, obviously. The most important is that you squash your commits before creating your pull request, or at least before it is merged.
In case you're not familiar with squashing, here is a simple way to do it :
- `git fetch origin` will make sure that you have a local version of the origin repository that is up to date (will not overwrite anything on your branch, no worries)
- `git rebase -i origin/master` will start the process of rebasing your branch
- This will open a file letting you decide what to do with the commits. You want to keep the first `pick` and write `s` or `squash` instead of `pick` for all other commits below.
- If there are conflicts, you will fix them step by step by following what git tells you, it's pretty straight-forward.
- If there are no conflicts or if they are resolved, git will let you edit the commit names. Don't forget to comment the commit names of the commits you squashed if they are not relevant by adding a # character in front of the commit message, and make sure that the commit message you left follows the aforementioned guidelines.
- Now run `git log`, you should see only one commit by the name you chose during the rebase.
- You can now `git push -f` if you already pused your branch on origin or simply push without the `-f` if it's your first push on origin. The reason for the `-f` is that when you squash your commits, you create a new one that will conflict with the state of your branch on origin. If you pull, it will overwrite your local state, so don't do that except if you messed up your rebase.
### Pull Requests
When your pull request is created, GitHub will first check for conflicts, Codacy will check the shell and C++ code's quality and then Travis CI will try to build and launch functional tests of your versions of Cameradar.
If GitHub reports conflicts with the develop branch, you should resolve them by yourself using your git command-line interface. The easiest and cleanest way is to use `git rebase -i origin/develop` and follow git's instructions.
If Codacy reports new issues, they will be added in the comments of the PR to let you know what you should fix.
If Travis CI reports errors, you should be able to view the logs [by clicking here](https://travis-ci.org/Ullaakut/cameradar/builds) and you should fix it. No PR will be merged before all tests are passing correctly.
When creating your pull request, our hooks will make sure that your code:
- Builds
- Has 100% passing unit tests
- Can actually access a camera using a functional test
- Still has equivalent or higher test coverage (using coveralls)
Make sure to write in the PR description what issue it fixes. GitHub will intepret it and automatically close the issue once your pull request is closed. Just write Fixes #IssueNumber in the description.
When your pull request is created, GitHub will first check for conflicts and then your code will be reviewed by the maintainers of this repository.
If GitHub reports conflicts with the `master` branch, you should resolve them by yourself using your git command-line interface. The easiest and cleanest way is to use `git rebase -i origin/master` and follow git's instructions. If we report issues with your code, you should resolve them and then ping the person that reported them to notify them that you did the requested changes.
Once everything is in order, we will merge your pull request.
### Coding guidelines
Your code should just
- Not decrease the results of Cameradar on https://goreportcard.com/report/github.com/ullaakut/cameradar
- Pass the code review
#### Golang
- All Golang code has to be formated using `gofmt` or `goreturns`.
- Make sure you follow the Golang [best practices](https://golang.org/doc/effective_go.html)
## Contributors
- **Brendan Le Glaunec** - [@Ullaakut](https://github.com/ullaakut) - brendan.leglaunec@etixgroup.com : *Original developer & Maintainer*
- **Jeremy Letang** - [@jeremyletang](https://github.com/jeremyletang) - letang.jeremy@gmail.com : *Idea of the project & Mentorship*
- **ishanjain28** - [@ishanjain28](https://github.com/ishanjain28) - ishanjain28@gmail.com : *Implemented the environment variables support*
+2 -2
View File
@@ -2,7 +2,7 @@
FROM golang:alpine AS build-env FROM golang:alpine AS build-env
COPY . /go/src/github.com/ullaakut/cameradar COPY . /go/src/github.com/ullaakut/cameradar
WORKDIR /go/src/github.com/ullaakut/cameradar/cameradar WORKDIR /go/src/github.com/ullaakut/cameradar/cmd/cameradar
RUN apk update && \ RUN apk update && \
apk upgrade && \ apk upgrade && \
@@ -26,7 +26,7 @@ RUN apk --update add --no-cache nmap \
WORKDIR /app/cameradar WORKDIR /app/cameradar
COPY --from=build-env /go/src/github.com/ullaakut/cameradar/dictionaries/ /app/dictionaries/ COPY --from=build-env /go/src/github.com/ullaakut/cameradar/dictionaries/ /app/dictionaries/
COPY --from=build-env /go/src/github.com/ullaakut/cameradar/cameradar/ /app/cameradar/ COPY --from=build-env /go/src/github.com/ullaakut/cameradar/cmd/cameradar/ /app/cameradar/
ENV CAMERADAR_CUSTOM_ROUTES="/app/dictionaries/routes" ENV CAMERADAR_CUSTOM_ROUTES="/app/dictionaries/routes"
ENV CAMERADAR_CUSTOM_CREDENTIALS="/app/dictionaries/credentials.json" ENV CAMERADAR_CUSTOM_CREDENTIALS="/app/dictionaries/credentials.json"
+4 -3
View File
@@ -24,6 +24,7 @@ Please select one:
## Environment ## Environment
My operating system: My operating system:
- [ ] Windows - [ ] Windows
- [ ] OSX - [ ] OSX
- [ ] Linux - [ ] Linux
@@ -34,17 +35,17 @@ OS architecture: <architecture>
## Issue ## Issue
### What was expected? ### What was expected
<expected behavior> <expected behavior>
### What happened? ### What happened
<observed behavior> <observed behavior>
### Logs ### Logs
If your issue is with Cameradar's binary or docker image, please run it with `-l` to print logs, and paste them here: If your issue is with Cameradar's binary or docker image, please run it with `-v` to print verbose logs, and paste them here:
``` ```
<cameradar logs> <cameradar logs>
+24 -62
View File
@@ -1,7 +1,7 @@
# Cameradar # Cameradar
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/Ullaakut/cameradar/master/images/Cameradar.gif" width="100%"/> <img src="images/Cameradar.gif" width="100%"/>
</p> </p>
<p align="center"> <p align="center">
@@ -41,7 +41,7 @@
* Launch automated dictionary attacks to get the **username and password** of the cameras * Launch automated dictionary attacks to get the **username and password** of the cameras
* Retrieve a complete and user-friendly report of the results * Retrieve a complete and user-friendly report of the results
<p align="center"><img src="https://raw.githubusercontent.com/Ullaakut/cameradar/master/images/Cameradar.png" width="250"/></p> <p align="center"><img src="images/Cameradar.png" width="250"/></p>
## Table of content ## Table of content
@@ -49,13 +49,15 @@
* [Configuration](#configuration) * [Configuration](#configuration)
* [Output](#output) * [Output](#output)
* [Check camera access](#check-camera-access) * [Check camera access](#check-camera-access)
* [Command line options](#command-line-options) * [Command-line options](#command-line-options)
* [Contribution](#contribution) * [Contribution](#contribution)
* [Frequently Asked Questions](#frequently-asked-questions) * [Frequently Asked Questions](#frequently-asked-questions)
* [License](#license) * [License](#license)
## Docker Image for Cameradar ## Docker Image for Cameradar
<p align="center"><img src="images/CameradarV4.png" width="70%"/></p>
Install [docker](https://docs.docker.com/engine/installation/) on your machine, and run the following command: Install [docker](https://docs.docker.com/engine/installation/) on your machine, and run the following command:
```bash ```bash
@@ -64,7 +66,7 @@ docker run -t ullaakut/cameradar -t <target> <other command-line options>
[See command-line options](#command-line-options). [See command-line options](#command-line-options).
e.g.: `docker run -t ullaakut/cameradar -t 192.168.100.0/24 -l` will scan the ports 554, 5554 and 8554 of hosts on the 192.168.100.0/24 subnetwork and attack the discovered RTSP streams and will output debug logs. e.g.: `docker run -t ullaakut/cameradar -t 192.168.100.0/24` will scan the ports 554, 5554 and 8554 of hosts on the 192.168.100.0/24 subnetwork and attack the discovered RTSP streams and will output debug logs.
* `YOUR_TARGET` can be a subnet (e.g.: `172.16.100.0/24`), an IP (e.g.: `172.16.100.10`), or a range of IPs (e.g.: `172.16.100.10-20`). * `YOUR_TARGET` can be a subnet (e.g.: `172.16.100.0/24`), an IP (e.g.: `172.16.100.10`), or a range of IPs (e.g.: `172.16.100.10-20`).
* If you want to get the precise results of the nmap scan in the form of an XML file, you can add `-v /your/path:/tmp/cameradar_scan.xml` to the docker run command, before `ullaakut/cameradar`. * If you want to get the precise results of the nmap scan in the form of an XML file, you can add `-v /your/path:/tmp/cameradar_scan.xml` to the docker run command, before `ullaakut/cameradar`.
@@ -89,56 +91,13 @@ Make sure you installed the dependencies mentionned above, and that you have Go
The `cameradar` binary is now in your `$GOPATH/bin` ready to be used. See command line options [here](#command-line-options). The `cameradar` binary is now in your `$GOPATH/bin` ready to be used. See command line options [here](#command-line-options).
## Library
### Dependencies of the library
* `curl-dev` / `libcurl` (depending on your OS)
* `github.com/ullaakut/nmap`
* `github.com/pkg/errors`
* `gopkg.in/go-playground/validator.v9`
* `github.com/ullaakut/go-curl`
#### Installing the library
`go get github.com/ullaakut/cameradar`
After this command, the _cameradar_ library is ready to use. Its source will be in:
$GOPATH/src/pkg/github.com/ullaakut/cameradar
You can use `go get -u` to update the package.
Here is an overview of the exposed functions of this library:
#### Discovery
You can use the cameradar library for simple discovery purposes if you don't need to access the cameras but just to be aware of their existence.
<p align="center"><img width="90%" src="https://raw.githubusercontent.com/Ullaakut/cameradar/master/images/NmapPresets.png"/></p>
This describes the nmap time presets. You can pass a value between 1 and 5 as described in this table, to the NmapRun function.
#### Attack
If you already know which hosts and ports you want to attack, you can also skip the discovery part and use directly the attack functions. The attack functions also take a timeout value as a parameter.
#### Data models
Here are the different data models useful to use the exposed functions of the cameradar library.
<p align="center"><img width="60%" src="https://raw.githubusercontent.com/Ullaakut/cameradar/master/images/Models.png"/></p>
#### Dictionary loaders
The cameradar library also provides two functions that take file paths as inputs and return the appropriate data models filled.
## Configuration ## Configuration
The **RTSP port used for most cameras is 554**, so you should probably specify 554 as one of the ports you scan. Not specifying any ports to the cameradar application will scan the 554, 5554 and 8554 ports. The **RTSP port used for most cameras is 554**, so you should probably specify 554 as one of the ports you scan. Not specifying any ports to the cameradar application will scan the 554, 5554 and 8554 ports.
`docker run -t --net=host ullaakut/cameradar -p "18554,19000-19010" -t localhost` will scan the ports 18554, and the range of ports between 19000 and 19010 on localhost. `docker run -t --net=host ullaakut/cameradar -p "18554,19000-19010" -t localhost` will scan the ports `18554`, and the range of ports between `19000` and `19010` on `localhost`.
You **can use your own files for the ids and routes dictionaries** used to attack the cameras, but the Cameradar repository already gives you a good base that works with most cameras, in the `/dictionaries` folder. You **can use your own files for the credentials and routes dictionaries** used to attack the cameras, but the Cameradar repository already gives you a good base that works with most cameras, in the `/dictionaries` folder.
```bash ```bash
docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \ docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
@@ -152,11 +111,9 @@ This will put the contents of your folder containing dictionaries in the docker
## Check camera access ## Check camera access
If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI or the command-line to connect to the RTSP stream using this format : `rtsp://username:password@address:port/route` If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI or the command-line to connect to the RTSP stream using this format: `rtsp://username:password@address:port/route`
With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:554/live.sdp` ## Command-line options
## Command line options
* **"-t, --targets"**: Set target. Required. Target can be a file (see [instructions on how to format the file](#format-input-file)), an IP, an IP range, a subnetwork, or a combination of those. Example: `--targets="192.168.1.72,192.168.1.74"` * **"-t, --targets"**: Set target. Required. Target can be a file (see [instructions on how to format the file](#format-input-file)), an IP, an IP range, a subnetwork, or a combination of those. Example: `--targets="192.168.1.72,192.168.1.74"`
* **"-p, --ports"**: (Default: `554,5554,8554`) Set custom ports. * **"-p, --ports"**: (Default: `554,5554,8554`) Set custom ports.
@@ -165,8 +122,9 @@ With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:5
* **"-r, --custom-routes"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/routes`) Set custom dictionary path for routes * **"-r, --custom-routes"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/routes`) Set custom dictionary path for routes
* **"-c, --custom-credentials"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/credentials.json`) Set custom dictionary path for credentials * **"-c, --custom-credentials"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/credentials.json`) Set custom dictionary path for credentials
* **"-o, --nmap-output"**: (Default: `/tmp/cameradar_scan.xml`) Set custom nmap output path * **"-o, --nmap-output"**: (Default: `/tmp/cameradar_scan.xml`) Set custom nmap output path
* **"-l, --log"**: Enable debug logs (nmap requests, curl describe requests, etc.) * **"-d, --debug"**: Enable debug logs
* **"-h"** : Display the usage information * **"-v, --verbose"**: Enable verbose curl logs (not recommended for most use)
* **"-h"**: Display the usage information
## Format input file ## Format input file
@@ -263,7 +221,7 @@ See [the contribution document](/CONTRIBUTING.md) to get started.
> Cameradar does not detect any camera! > Cameradar does not detect any camera!
That means that either your cameras are not streaming in RTSP or that they are not on the target you are scanning. In most cases, CCTV cameras will be on a private subnetwork, isolated from the internet. Use the `-t` option to specify your target. That means that either your cameras are not streaming in RTSP or that they are not on the target you are scanning. In most cases, CCTV cameras will be on a private subnetwork, isolated from the internet. Use the `-t` option to specify your target. If you are sure you did everything right but it still does not work, please open an issue with details on the device you are trying to access 🙏
> Cameradar detects my cameras, but does not manage to access them at all! > Cameradar detects my cameras, but does not manage to access them at all!
@@ -271,23 +229,23 @@ Maybe your cameras have been configured and the credentials / URL have been chan
> What happened to the C++ version? > What happened to the C++ version?
You can still find it under the 1.1.4 tag on this repo, however it was less performant and stable than the current version written in Golang. You can still find it under the 1.1.4 tag on this repo, however it was less performant and stable than the current version written in Golang. It is not recommended to use it.
> How to use the Cameradar library for my own project? > How to use the Cameradar library for my own project?
See the example in `/cameradar`. You just need to run `go get github.com/ullaakut/cameradar` and to use the `cmrdr` package in your code. You can find the documentation on [godoc](https://godoc.org/github.com/ullaakut/cameradar). See the example in `/cmd/cameradar`. You just need to run `go get github.com/ullaakut/cameradar` and to use the `cameradar` package in your code. You can find the documentation on [godoc](https://godoc.org/github.com/ullaakut/cameradar).
> I want to scan my own localhost for some reason and it does not work! What's going on? > I want to scan my own localhost for some reason and it does not work! What's going on?
Use the `--net=host` flag when launching the cameradar image, or use the binary by running `go run cameradar/cameradar.go` or [installing it](#installing-the-binary) Use the `--net=host` flag when launching the cameradar image, or use the binary by running `go run cameradar/cameradar.go` or [installing it](#installing-the-binary).
> I don't see a colored output :( > I don't see a colored output:(
You forgot the `-t` flag before `ullaakut/cameradar` in your command-line. This tells docker to allocate a pseudo-tty for cameradar, which makes it able to use colors. You forgot the `-t` flag before `ullaakut/cameradar` in your command-line. This tells docker to allocate a pseudo-tty for cameradar, which makes it able to use colors.
> I don't have a camera but I'd like to try Cameradar! > I don't have a camera but I'd like to try Cameradar!
Simply run `docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt` and then run cameradar and it should guess that the username is admin and the password is 12345. You can try this with any default constructor credentials (they can be found [here](dictionaries/credentials.json)) Simply run `docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt` and then run cameradar and it should guess that the username is admin and the password is 12345. You can try this with any default constructor credentials (they can be found [here](dictionaries/credentials.json)).
> What authentication types does Cameradar support? > What authentication types does Cameradar support?
@@ -301,7 +259,11 @@ Cameradar supports both basic and digest authentication.
> Running cameradar with an input file, logs enabled on port 8554 > Running cameradar with an input file, logs enabled on port 8554
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t /tmp/test.txt -p 8554 -l` `docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t /tmp/test.txt -p 8554`
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554 and 8554
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t 192.168.0.0/24 --custom-credentials="/tmp/dictionaries/credentials.json" --custom-routes="/tmp/dictionaries/routes" -p 554,5554,8554`
## License ## License
+189 -163
View File
@@ -1,15 +1,13 @@
package cmrdr package cameradar
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/pkg/errors"
curl "github.com/ullaakut/go-curl" curl "github.com/ullaakut/go-curl"
v "gopkg.in/go-playground/validator.v9"
) )
// HTTP responses // HTTP responses.
const ( const (
httpOK = 200 httpOK = 200
httpUnauthorized = 401 httpUnauthorized = 401
@@ -17,134 +15,166 @@ const (
httpNotFound = 404 httpNotFound = 404
) )
// CURL RTSP request types // CURL RTSP request types.
const ( const (
rtspDescribe = 2 rtspDescribe = 2
rtspSetup = 4 rtspSetup = 4
) )
// ValidateStreams tries to setup the stream to validate whether or not it is available // Attack attacks the given targets and returns the accessed streams.
func ValidateStreams(c Curler, targets []Stream, timeout time.Duration, log bool) ([]Stream, error) { func (s *Scanner) Attack(targets []Stream) ([]Stream, error) {
for i := range targets { if len(targets) == 0 {
targets[i].Available = validateStream(c, targets[i], timeout, log) return nil, fmt.Errorf("unable to attack empty list of targets")
} }
return targets, nil // Most cameras will be accessed successfully with these two attacks.
s.term.StartStepf("Attacking routes of %d streams", len(targets))
streams := s.AttackRoute(targets)
s.term.StartStepf("Attempting to detect authentication methods of %d streams", len(targets))
streams = s.DetectAuthMethods(streams)
s.term.StartStepf("Attacking credentials of %d streams", len(targets))
streams = s.AttackCredentials(streams)
// But some cameras run GST RTSP Server which prioritizes 401 over 404 contrary to most cameras.
// For these cameras, running another route attack will solve the problem.
for _, stream := range streams {
if !stream.RouteFound || !stream.CredentialsFound {
s.term.StartStepf("Second round of attacks")
streams = s.AttackRoute(streams)
break
}
}
s.term.StartStep("Validating that streams are accessible")
streams = s.ValidateStreams(streams)
s.term.EndStep()
return streams, nil
}
// ValidateStreams tries to setup the stream to validate whether or not it is available.
func (s *Scanner) ValidateStreams(targets []Stream) []Stream {
for i := range targets {
targets[i].Available = s.validateStream(targets[i])
}
return targets
} }
// AttackCredentials attempts to guess the provided targets' credentials using the given // AttackCredentials attempts to guess the provided targets' credentials using the given
// dictionary or the default dictionary if none was provided by the user. // dictionary or the default dictionary if none was provided by the user.
func AttackCredentials(c Curler, targets []Stream, credentials Credentials, timeout time.Duration, log bool) ([]Stream, error) { func (s *Scanner) AttackCredentials(targets []Stream) []Stream {
attacks := make(chan Stream) resChan := make(chan Stream)
defer close(attacks) defer close(resChan)
validate := v.New()
for _, target := range targets {
err := validate.Struct(target)
if err != nil {
return targets, errors.Wrap(err, "invalid targets")
}
for i := range targets {
// TODO: Perf Improvement: Skip cameras with no auth type detected, and set their // TODO: Perf Improvement: Skip cameras with no auth type detected, and set their
// CredentialsFound value to true. // CredentialsFound value to true.
go s.attackCameraCredentials(targets[i], resChan)
go attackCameraCredentials(c, target, credentials, attacks, timeout, log)
} }
attackResults := []Stream{} 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, <-attacks) attackResults = append(attackResults, <-resChan)
} }
for _, result := range attackResults { for i := range attackResults {
if result.CredentialsFound { if attackResults[i].CredentialsFound {
targets = replace(targets, result) targets = replace(targets, attackResults[i])
} }
} }
return targets, nil return targets
} }
// AttackRoute attempts to guess the provided targets' streaming routes using the given // AttackRoute attempts to guess the provided targets' streaming routes using the given
// dictionary or the default dictionary if none was provided by the user. // dictionary or the default dictionary if none was provided by the user.
func AttackRoute(c Curler, targets []Stream, routes Routes, timeout time.Duration, log bool) ([]Stream, error) { func (s *Scanner) AttackRoute(targets []Stream) []Stream {
attacks := make(chan Stream) resChan := make(chan Stream)
defer close(attacks) defer close(resChan)
validate := v.New() for i := range targets {
for _, target := range targets { go s.attackCameraRoute(targets[i], resChan)
err := validate.Struct(target)
if err != nil {
return targets, errors.Wrap(err, "invalid targets")
}
go attackCameraRoute(c, target, routes, attacks, timeout, log)
} }
attackResults := []Stream{} 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, <-attacks) attackResults = append(attackResults, <-resChan)
} }
for _, result := range attackResults { for i := range attackResults {
if result.RouteFound { if attackResults[i].RouteFound {
targets = replace(targets, result) targets = replace(targets, attackResults[i])
} }
} }
return targets, nil return targets
} }
// DetectAuthMethods attempts to guess the provided targets' authentication types, between // DetectAuthMethods attempts to guess the provided targets' authentication types, between
// digest, basic auth or none at all. // digest, basic auth or none at all.
func DetectAuthMethods(c Curler, targets []Stream, timeout time.Duration, log bool) ([]Stream, error) { func (s *Scanner) DetectAuthMethods(targets []Stream) []Stream {
attacks := make(chan Stream)
defer close(attacks)
for i := range targets { for i := range targets {
targets[i].AuthenticationType = detectAuthMethod(c, targets[i], timeout, log) targets[i].AuthenticationType = s.detectAuthMethod(targets[i])
var authMethod string
switch targets[i].AuthenticationType {
case 0:
authMethod = "no"
case 1:
authMethod = "basic"
case 2:
authMethod = "digest"
}
s.term.Debugf("Stream %s uses %s authentication method\n", GetCameraRTSPURL(targets[i]), authMethod)
} }
return targets, nil return targets
} }
func attackCameraCredentials(c Curler, target Stream, credentials Credentials, resultsChan chan<- Stream, timeout time.Duration, log bool) { func (s *Scanner) attackCameraCredentials(target Stream, resChan chan<- Stream) {
for _, username := range credentials.Usernames { for _, username := range s.credentials.Usernames {
for _, password := range credentials.Passwords { for _, password := range s.credentials.Passwords {
ok := credAttack(c.Duphandle(), target, username, password, timeout, log) ok := s.credAttack(target, username, password)
if ok { if ok {
target.CredentialsFound = true target.CredentialsFound = true
target.Username = username target.Username = username
target.Password = password target.Password = password
resultsChan <- target resChan <- target
return return
} }
} }
} }
target.CredentialsFound = false target.CredentialsFound = false
resultsChan <- target resChan <- target
} }
func attackCameraRoute(c Curler, target Stream, routes Routes, resultsChan chan<- Stream, timeout time.Duration, log bool) { func (s *Scanner) attackCameraRoute(target Stream, resChan chan<- Stream) {
for _, route := range routes { for _, route := range s.routes {
ok := routeAttack(c.Duphandle(), target, route, timeout, log) ok := s.routeAttack(target, route)
if ok { if ok {
target.RouteFound = true target.RouteFound = true
target.Route = route target.Route = route
resultsChan <- target resChan <- target
return return
} }
} }
target.RouteFound = false target.RouteFound = false
resultsChan <- target resChan <- target
} }
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing func (s *Scanner) detectAuthMethod(stream Stream) int {
func doNotWrite([]uint8, interface{}) bool { c := s.curl.Duphandle()
return true
}
func detectAuthMethod(c Curler, stream Stream, timeout time.Duration, enableLogs bool) int {
attackURL := fmt.Sprintf( attackURL := fmt.Sprintf(
"rtsp://%s:%d/%s", "rtsp://%s:%d/%s",
stream.Address, stream.Address,
@@ -152,42 +182,38 @@ func detectAuthMethod(c Curler, stream Stream, timeout time.Duration, enableLogs
stream.Route, stream.Route,
) )
if enableLogs { s.setCurlOptions(c)
// Debug logs when logs are enabled
c.Setopt(curl.OPT_VERBOSE, 1)
} else {
// Do not write sdp in stdout
c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
}
// Do not use signals (would break multithreading) // Send a request to the URL of the stream we want to attack.
c.Setopt(curl.OPT_NOSIGNAL, 1) _ = c.Setopt(curl.OPT_URL, attackURL)
// Do not send a body in the describe request // Set the RTSP STREAM URI as the stream URL.
c.Setopt(curl.OPT_NOBODY, 1) _ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// Send a request to the URL of the stream we want to attack // 2 is CURL_RTSPREQ_DESCRIBE.
c.Setopt(curl.OPT_URL, attackURL) _ = c.Setopt(curl.OPT_RTSP_REQUEST, 2)
// Set the RTSP STREAM URI as the stream URL
c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// 2 is CURL_RTSPREQ_DESCRIBE
c.Setopt(curl.OPT_RTSP_REQUEST, 2)
// Set custom timeout
c.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
// Perform the request // Perform the request.
err := c.Perform() err := c.Perform()
if err != nil { if err != nil {
s.term.Debugf("Perform failed: %v", err)
return -1 return -1
} }
authType, err := c.Getinfo(curl.INFO_HTTPAUTH_AVAIL) authType, err := c.Getinfo(curl.INFO_HTTPAUTH_AVAIL)
if err != nil { if err != nil {
s.term.Debugf("Getinfo failed: %v", err)
return -1 return -1
} }
if s.verbose {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", authType)
}
return authType.(int) return authType.(int)
} }
func routeAttack(c Curler, stream Stream, route string, timeout time.Duration, enableLogs bool) bool { func (s *Scanner) routeAttack(stream Stream, route string) bool {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf( attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s", "rtsp://%s:%s@%s:%d/%s",
stream.Username, stream.Username,
@@ -197,52 +223,47 @@ func routeAttack(c Curler, stream Stream, route string, timeout time.Duration, e
route, route,
) )
if enableLogs { s.setCurlOptions(c)
// Debug logs when logs are enabled
c.Setopt(curl.OPT_VERBOSE, 1)
} else {
// Do not write sdp in stdout
c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
}
// Set proper authentication type. // Set proper authentication type.
c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType) _ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password)) _ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
// Do not use signals (would break multithreading) // Send a request to the URL of the stream we want to attack.
c.Setopt(curl.OPT_NOSIGNAL, 1) _ = c.Setopt(curl.OPT_URL, attackURL)
// Do not send a body in the describe request // Set the RTSP STREAM URI as the stream URL.
c.Setopt(curl.OPT_NOBODY, 1) _ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// Send a request to the URL of the stream we want to attack // 2 is CURL_RTSPREQ_DESCRIBE.
c.Setopt(curl.OPT_URL, attackURL) _ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
// Set the RTSP STREAM URI as the stream URL
c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// 2 is CURL_RTSPREQ_DESCRIBE
c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
// Set custom timeout
c.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
// Perform the request // Perform the request.
err := c.Perform() err := c.Perform()
if err != nil { if err != nil {
s.term.Debugf("Perform failed: %v", err)
return false return false
} }
// Get return code for the request // Get return code for the request.
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE) rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil { if err != nil {
s.term.Debugf("Getinfo failed: %v", err)
return false return false
} }
// If it's a 401 or 403, it means that the credentials are wrong but the route might be okay if s.verbose {
// If it's a 200, the stream is accessed successfully 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 200, the stream is accessed successfully.
if rc == httpOK || rc == httpUnauthorized || rc == httpForbidden { if rc == httpOK || rc == httpUnauthorized || rc == httpForbidden {
return true return true
} }
return false return false
} }
func credAttack(c Curler, stream Stream, username string, password string, timeout time.Duration, enableLogs bool) bool { func (s *Scanner) credAttack(stream Stream, username string, password string) bool {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf( attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s", "rtsp://%s:%s@%s:%d/%s",
username, username,
@@ -252,52 +273,48 @@ func credAttack(c Curler, stream Stream, username string, password string, timeo
stream.Route, stream.Route,
) )
if enableLogs { s.setCurlOptions(c)
// Debug logs when logs are enabled
c.Setopt(curl.OPT_VERBOSE, 1)
} else {
// Do not write sdp in stdout
c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
}
// Set proper authentication type. // Set proper authentication type.
c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType) _ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
c.Setopt(curl.OPT_USERPWD, fmt.Sprint(username, ":", password)) _ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(username, ":", password))
// Do not use signals (would break multithreading) // Send a request to the URL of the stream we want to attack.
c.Setopt(curl.OPT_NOSIGNAL, 1) _ = c.Setopt(curl.OPT_URL, attackURL)
// Do not send a body in the describe request // Set the RTSP STREAM URI as the stream URL.
c.Setopt(curl.OPT_NOBODY, 1) _ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// Send a request to the URL of the stream we want to attack // 2 is CURL_RTSPREQ_DESCRIBE.
c.Setopt(curl.OPT_URL, attackURL) _ = c.Setopt(curl.OPT_RTSP_REQUEST, 2)
// Set the RTSP STREAM URI as the stream URL
c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// 2 is CURL_RTSPREQ_DESCRIBE
c.Setopt(curl.OPT_RTSP_REQUEST, 2)
// Set custom timeout
c.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
// Perform the request // Perform the request.
err := c.Perform() err := c.Perform()
if err != nil { if err != nil {
s.term.Debugf("Perform failed: %v", err)
return false return false
} }
// Get return code for the request // Get return code for the request.
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE) rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil { if err != nil {
s.term.Debugf("Getinfo failed: %v", err)
return false return false
} }
// If it's a 404, it means that the route is incorrect but the credentials might be okay if s.verbose {
// If it's a 200, the stream is accessed successfully s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
}
// If it's a 404, it means that the route is incorrect but the credentials might be okay.
// If it's a 200, the stream is accessed successfully.
if rc == httpOK || rc == httpNotFound { if rc == httpOK || rc == httpNotFound {
return true return true
} }
return false return false
} }
func validateStream(c Curler, stream Stream, timeout time.Duration, enableLogs bool) bool { func (s *Scanner) validateStream(stream Stream) bool {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf( attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s", "rtsp://%s:%s@%s:%d/%s",
stream.Username, stream.Username,
@@ -307,48 +324,57 @@ func validateStream(c Curler, stream Stream, timeout time.Duration, enableLogs b
stream.Route, stream.Route,
) )
if enableLogs { s.setCurlOptions(c)
// Debug logs when logs are enabled
c.Setopt(curl.OPT_VERBOSE, 1)
} else {
// Do not write sdp in stdout
c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
}
// Set proper authentication type. // Set proper authentication type.
c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType) _ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password)) _ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
// Do not use signals (would break multithreading) // Send a request to the URL of the stream we want to attack.
c.Setopt(curl.OPT_NOSIGNAL, 1) _ = c.Setopt(curl.OPT_URL, attackURL)
// Do not send a body in the describe request // Set the RTSP STREAM URI as the stream URL.
c.Setopt(curl.OPT_NOBODY, 1) _ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// Send a request to the URL of the stream we want to attack // 2 is CURL_RTSPREQ_SETUP.
c.Setopt(curl.OPT_URL, attackURL) _ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspSetup)
// Set the RTSP STREAM URI as the stream URL
c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// 2 is CURL_RTSPREQ_SETUP
c.Setopt(curl.OPT_RTSP_REQUEST, rtspSetup)
// Set custom timeout
c.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333") _ = c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333")
// Perform the request // Perform the request.
err := c.Perform() err := c.Perform()
if err != nil { if err != nil {
s.term.Debugf("Perform failed: %v", err)
return false return false
} }
// Get return code for the request // Get return code for the request.
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE) rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil { if err != nil {
s.term.Debugf("Getinfo failed: %v", err)
return false return false
} }
// If it's a 200, the stream is accessed successfully if s.verbose {
s.term.Debugln("SETUP", attackURL, "RTSP/1.0 >", rc)
}
// If it's a 200, the stream is accessed successfully.
if rc == httpOK { if rc == httpOK {
return true return true
} }
return false return false
} }
func (s *Scanner) setCurlOptions(c Curler) {
// Do not write sdp in stdout
_ = c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
// Do not use signals (would break multithreading).
_ = c.Setopt(curl.OPT_NOSIGNAL, 1)
// Do not send a body in the describe request.
_ = c.Setopt(curl.OPT_NOBODY, 1)
// Set custom timeout.
_ = c.Setopt(curl.OPT_TIMEOUT_MS, int(s.timeout/time.Millisecond))
}
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
func doNotWrite([]uint8, interface{}) bool {
return true
}
+327 -270
View File
@@ -1,12 +1,14 @@
package cmrdr package cameradar
import ( import (
"errors" "errors"
"io/ioutil"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/ullaakut/disgo"
curl "github.com/ullaakut/go-curl" curl "github.com/ullaakut/go-curl"
) )
@@ -33,35 +35,126 @@ func (m *CurlerMock) Duphandle() Curler {
return m return m
} }
func TestAttack(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
fakeTargets = []Stream{stream1, stream2}
fakeRoutes = Routes{"live.sdp", "media.amp"}
fakeCredentials = Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
)
tests := []struct {
description string
targets []Stream
performErr error
expectedStreams []Stream
expectedErr error
}{
{
description: "inverted RTSP RFC",
targets: fakeTargets,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "attack works",
targets: fakeTargets,
expectedStreams: fakeTargets,
},
{
description: "no targets",
targets: nil,
expectedStreams: nil,
expectedErr: errors.New("unable to attack empty list of targets"),
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
if len(test.targets) != 0 {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(200, nil)
}
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: time.Millisecond,
verbose: false,
credentials: fakeCredentials,
routes: fakeRoutes,
}
results, err := scanner.Attack(test.targets)
assert.Equal(t, test.expectedErr, err)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
}
}
func TestAttackCredentials(t *testing.T) { func TestAttackCredentials(t *testing.T) {
validStream1 := Stream{ var (
Device: "fakeDevice", stream1 = Stream{
Address: "fakeAddress", Device: "fakeDevice",
Port: 1337, Address: "fakeAddress",
} Port: 1337,
Available: true,
}
validStream2 := Stream{ stream2 = Stream{
Device: "fakeDevice", Device: "fakeDevice",
Address: "differentFakeAddress", Address: "differentFakeAddress",
Port: 1337, Port: 1337,
} Available: true,
}
invalidStream := Stream{ fakeTargets = []Stream{stream1, stream2}
Device: "InvalidDevice", fakeCredentials = Credentials{
} Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
)
fakeTargets := []Stream{validStream1, validStream2} tests := []struct {
invalidTargets := []Stream{invalidStream} description string
fakeCredentials := Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
testCases := []struct {
targets []Stream targets []Stream
credentials Credentials credentials Credentials
timeout time.Duration timeout time.Duration
log bool verbose bool
status int status int
@@ -70,10 +163,10 @@ func TestAttackCredentials(t *testing.T) {
invalidTargets bool invalidTargets bool
expectedStreams []Stream expectedStreams []Stream
expectedErrMsg string
}{ }{
// Credentials found
{ {
description: "Credentials found",
targets: fakeTargets, targets: fakeTargets,
credentials: fakeCredentials, credentials: fakeCredentials,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -82,8 +175,9 @@ func TestAttackCredentials(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Camera accessed
{ {
description: "Camera accessed",
targets: fakeTargets, targets: fakeTargets,
credentials: fakeCredentials, credentials: fakeCredentials,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -92,19 +186,9 @@ func TestAttackCredentials(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Invalid targets
{ {
targets: invalidTargets, description: "curl perform fails",
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
invalidTargets: true,
expectedErrMsg: "invalid targets",
expectedStreams: invalidTargets,
},
// curl perform fails
{
targets: fakeTargets, targets: fakeTargets,
credentials: fakeCredentials, credentials: fakeCredentials,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -113,8 +197,9 @@ func TestAttackCredentials(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// curl getinfo fails
{ {
description: "curl getinfo fails",
targets: fakeTargets, targets: fakeTargets,
credentials: fakeCredentials, credentials: fakeCredentials,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -123,92 +208,88 @@ func TestAttackCredentials(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logging disabled
{ {
description: "Verbose mode disabled",
targets: fakeTargets, targets: fakeTargets,
credentials: fakeCredentials, credentials: fakeCredentials,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: false, verbose: false,
status: 403, status: 403,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logging enabled
{ {
description: "Verbose mode enabled",
targets: fakeTargets, targets: fakeTargets,
credentials: fakeCredentials, credentials: fakeCredentials,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: true, verbose: true,
status: 403, status: 403,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
} }
for i, test := range testCases {
curlerMock := &CurlerMock{}
if !test.invalidTargets { for _, test := range tests {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil) t.Run(test.description, func(t *testing.T) {
curlerMock.On("Perform").Return(test.performErr) curlerMock := &CurlerMock{}
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
}
results, err := AttackCredentials(curlerMock, test.targets, test.credentials, test.timeout, test.log) if !test.invalidTargets {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
if len(test.expectedErrMsg) > 0 { curlerMock.On("Perform").Return(test.performErr)
if err == nil { if test.performErr == nil {
t.Errorf("unexpected success in AttackCredentials test, iteration %d. expected error: %s\n", i, test.expectedErrMsg) curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
assert.Contains(t, err.Error(), test.expectedErrMsg, "wrong error message")
} else {
if err != nil {
t.Errorf("unexpected error in AttackCredentials test, iteration %d: %v\n", i, err)
}
for _, stream := range test.expectedStreams {
foundStream := false
for _, result := range results {
if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port {
foundStream = true
}
} }
assert.Equal(t, true, foundStream, "wrong streams parsed")
} }
}
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed") scanner := &Scanner{
curlerMock.AssertExpectations(t) term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
credentials: test.credentials,
}
results := scanner.AttackCredentials(test.targets)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
} }
} }
func TestAttackRoute(t *testing.T) { func TestAttackRoute(t *testing.T) {
validStream1 := Stream{ var (
Device: "fakeDevice", stream1 = Stream{
Address: "fakeAddress", Device: "fakeDevice",
Port: 1337, Address: "fakeAddress",
} Port: 1337,
Available: true,
}
validStream2 := Stream{ stream2 = Stream{
Device: "fakeDevice", Device: "fakeDevice",
Address: "differentFakeAddress", Address: "differentFakeAddress",
Port: 1337, Port: 1337,
} Available: true,
}
invalidStream := Stream{ fakeTargets = []Stream{stream1, stream2}
Device: "InvalidDevice", fakeRoutes = Routes{"live.sdp", "media.amp"}
} )
fakeTargets := []Stream{validStream1, validStream2} tests := []struct {
fakeRoutes := Routes{"live.sdp", "media.amp"} description string
invalidTargets := []Stream{invalidStream}
testCases := []struct {
targets []Stream targets []Stream
routes Routes routes Routes
timeout time.Duration timeout time.Duration
log bool verbose bool
status int status int
@@ -217,10 +298,11 @@ func TestAttackRoute(t *testing.T) {
invalidTargets bool invalidTargets bool
expectedStreams []Stream expectedStreams []Stream
expectedErrMsg string expectedErr error
}{ }{
// Route found
{ {
description: "Route found",
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -229,8 +311,9 @@ func TestAttackRoute(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Route found
{ {
description: "Route found",
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -239,8 +322,9 @@ func TestAttackRoute(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Camera accessed
{ {
description: "Camera accessed",
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -249,18 +333,9 @@ func TestAttackRoute(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Invalid targets
{ {
targets: invalidTargets, description: "curl perform fails",
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
invalidTargets: true,
expectedErrMsg: "invalid targets",
expectedStreams: invalidTargets,
},
// curl perform fails
{
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -269,8 +344,9 @@ func TestAttackRoute(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// curl getinfo fails
{ {
description: "curl getinfo fails",
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -279,97 +355,82 @@ func TestAttackRoute(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logs disabled
{ {
description: "verbose mode disabled",
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: false, verbose: false,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logs enabled
{ {
description: "verbose mode enabled",
targets: fakeTargets, targets: fakeTargets,
routes: fakeRoutes, routes: fakeRoutes,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: true, verbose: true,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
} }
for i, test := range testCases { for _, test := range tests {
curlerMock := &CurlerMock{} t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
if !test.invalidTargets { if !test.invalidTargets {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil) curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr) curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil { if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr) curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
}
results, err := AttackRoute(curlerMock, test.targets, test.routes, test.timeout, test.log)
if len(test.expectedErrMsg) > 0 {
if err == nil {
t.Errorf("unexpected success in AttackRoute test, iteration %d. expected error: %s\n", i, test.expectedErrMsg)
}
assert.Contains(t, err.Error(), test.expectedErrMsg, "wrong error message")
} else {
if err != nil {
t.Errorf("unexpected error in AttackRoute test, iteration %d: %v\n", i, err)
}
for _, stream := range test.expectedStreams {
foundStream := false
for _, result := range results {
if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port {
foundStream = true
}
} }
assert.Equal(t, true, foundStream, "wrong streams parsed")
} }
}
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed") scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
routes: test.routes,
}
curlerMock.AssertExpectations(t) results := scanner.AttackRoute(test.targets)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
} }
} }
func TestValidateStreams(t *testing.T) { func TestValidateStreams(t *testing.T) {
validStream1 := Stream{ var (
Device: "fakeDevice", stream1 = Stream{
Address: "fakeAddress", Device: "fakeDevice",
Port: 1337, Address: "fakeAddress",
Available: true, Port: 1337,
} Available: true,
}
validStream2 := Stream{ stream2 = Stream{
Device: "fakeDevice", Device: "fakeDevice",
Address: "differentFakeAddress", Address: "differentFakeAddress",
Port: 1337, Port: 1337,
Available: true, Available: true,
} }
unavailableStream := Stream{ fakeTargets = []Stream{stream1, stream2}
Device: "fakeDevice", )
Available: false,
}
fakeTargets := []Stream{validStream1, validStream2} tests := []struct {
unavailableTargets := []Stream{unavailableStream} description string
testCases := []struct {
desc string
targets []Stream targets []Stream
timeout time.Duration timeout time.Duration
log bool verbose bool
status int status int
@@ -377,11 +438,9 @@ func TestValidateStreams(t *testing.T) {
getInfoErr error getInfoErr error
expectedStreams []Stream expectedStreams []Stream
expectedErrMsg string
}{ }{
// Route found
{ {
desc: "route found", description: "route found",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -390,9 +449,8 @@ func TestValidateStreams(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Route found
{ {
desc: "route found", description: "route found",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -401,9 +459,8 @@ func TestValidateStreams(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Camera accessed
{ {
desc: "camera accessed", description: "camera accessed",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -412,20 +469,18 @@ func TestValidateStreams(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Unavailable stream
{ {
desc: "unavailable stream", description: "unavailable stream",
targets: unavailableTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
status: 400, status: 400,
expectedStreams: unavailableTargets, expectedStreams: fakeTargets,
}, },
// curl perform fails
{ {
desc: "curl perform fails", description: "curl perform fails",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -434,9 +489,8 @@ func TestValidateStreams(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// curl getinfo fails
{ {
desc: "curl getinfo fails", description: "curl getinfo fails",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -445,63 +499,50 @@ func TestValidateStreams(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logs disabled
{ {
desc: "logs disabled", description: "verbose disabled",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: false, verbose: false,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logs enabled
{ {
desc: "logs enabled", description: "verbose enabled",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: true, verbose: true,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
} }
for i, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) { for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{} curlerMock := &CurlerMock{}
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil) curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(tC.performErr) curlerMock.On("Perform").Return(test.performErr)
if tC.performErr == nil { if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(tC.status, tC.getInfoErr) curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
} }
results, err := ValidateStreams(curlerMock, tC.targets, tC.timeout, tC.log) scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
if len(tC.expectedErrMsg) > 0 { curl: curlerMock,
if err == nil { timeout: test.timeout,
t.Errorf("unexpected success in ValidateStream test, iteration %d. expected error: %s\n", i, tC.expectedErrMsg) verbose: test.verbose,
}
assert.Contains(t, err.Error(), tC.expectedErrMsg, "wrong error message")
} else {
if err != nil {
t.Errorf("unexpected error in ValidateStream test, iteration %d: %v\n", i, err)
}
for _, stream := range tC.expectedStreams {
foundStream := false
for _, result := range results {
if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port {
foundStream = true
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
}
} }
assert.Equal(t, len(tC.expectedStreams), len(results), "wrong streams parsed") results := scanner.ValidateStreams(test.targets)
assert.Equal(t, len(test.expectedStreams), len(results))
for _, expectedStream := range test.expectedStreams {
assert.Contains(t, results, expectedStream)
}
curlerMock.AssertExpectations(t) curlerMock.AssertExpectations(t)
}) })
@@ -509,28 +550,30 @@ func TestValidateStreams(t *testing.T) {
} }
func TestDetectAuthenticationType(t *testing.T) { func TestDetectAuthenticationType(t *testing.T) {
validStream1 := Stream{ var (
Device: "fakeDevice", stream1 = Stream{
Address: "fakeAddress", Device: "fakeDevice",
Port: 1337, Address: "fakeAddress",
Available: true, Port: 1337,
} Available: true,
}
validStream2 := Stream{ stream2 = Stream{
Device: "fakeDevice", Device: "fakeDevice",
Address: "differentFakeAddress", Address: "differentFakeAddress",
Port: 1337, Port: 1337,
Available: true, Available: true,
} }
fakeTargets := []Stream{validStream1, validStream2} fakeTargets = []Stream{stream1, stream2}
)
testCases := []struct { tests := []struct {
desc string description string
targets []Stream targets []Stream
timeout time.Duration timeout time.Duration
log bool verbose bool
status int status int
@@ -538,11 +581,39 @@ func TestDetectAuthenticationType(t *testing.T) {
getInfoErr error getInfoErr error
expectedStreams []Stream expectedStreams []Stream
expectedErrMsg string
}{ }{
// curl getinfo fails
{ {
desc: "curl getinfo fails", description: "no auth enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 0,
expectedStreams: fakeTargets,
},
{
description: "basic auth enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 1,
expectedStreams: fakeTargets,
},
{
description: "digest auth enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 2,
expectedStreams: fakeTargets,
},
{
description: "curl getinfo fails",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -551,9 +622,8 @@ func TestDetectAuthenticationType(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// curl perform fails
{ {
desc: "curl perform fails", description: "curl perform fails",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
@@ -562,63 +632,50 @@ func TestDetectAuthenticationType(t *testing.T) {
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logs disabled
{ {
desc: "logs disabled", description: "verbose disabled",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: false, verbose: false,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
// Logs enabled
{ {
desc: "logs enabled", description: "verbose enabled",
targets: fakeTargets, targets: fakeTargets,
timeout: 1 * time.Millisecond, timeout: 1 * time.Millisecond,
log: true, verbose: true,
expectedStreams: fakeTargets, expectedStreams: fakeTargets,
}, },
} }
for i, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) { for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{} curlerMock := &CurlerMock{}
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil) curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(tC.performErr) curlerMock.On("Perform").Return(test.performErr)
if tC.performErr == nil { if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(tC.status, tC.getInfoErr) curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
} }
results, err := DetectAuthMethods(curlerMock, tC.targets, tC.timeout, tC.log) scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
if len(tC.expectedErrMsg) > 0 { curl: curlerMock,
if err == nil { timeout: test.timeout,
t.Errorf("unexpected success in DetectAuthMethods test, iteration %d. expected error: %s\n", i, tC.expectedErrMsg) verbose: test.verbose,
}
assert.Contains(t, err.Error(), tC.expectedErrMsg, "wrong error message")
} else {
if err != nil {
t.Errorf("unexpected error in DetectAuthMethods test, iteration %d: %v\n", i, err)
}
for _, stream := range tC.expectedStreams {
foundStream := false
for _, result := range results {
if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port {
foundStream = true
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
}
} }
assert.Equal(t, len(tC.expectedStreams), len(results), "wrong streams parsed") results := scanner.DetectAuthMethods(test.targets)
assert.Equal(t, len(test.expectedStreams), len(results))
for _, expectedStream := range test.expectedStreams {
assert.Contains(t, results, expectedStream)
}
curlerMock.AssertExpectations(t) curlerMock.AssertExpectations(t)
}) })
+2 -2
View File
@@ -1,4 +1,4 @@
// Package cmrdr provides methods to be able to discover and // Package cameradar provides methods to be able to discover and
// attack RTSP streams easily. RTSP streams are used by most // attack RTSP streams easily. RTSP streams are used by most
// IP Cameras, often for surveillance. // IP Cameras, often for surveillance.
// //
@@ -11,4 +11,4 @@
// access cameras, or running their own network scan, this // access cameras, or running their own network scan, this
// library allows to use simple and performant methods to // library allows to use simple and performant methods to
// attack streams. // attack streams.
package cmrdr package cameradar
-255
View File
@@ -1,255 +0,0 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/gernest/wow"
"github.com/gernest/wow/spin"
"github.com/spf13/pflag"
"github.com/spf13/viper"
cmrdr "github.com/ullaakut/cameradar"
log "github.com/ullaakut/disgo"
"github.com/ullaakut/disgo/style"
curl "github.com/ullaakut/go-curl"
)
type options struct {
Targets []string
Ports []string
Routes string
Credentials string
Speed int
Timeout int
EnableLogs bool
}
func parseArguments() error {
viper.SetEnvPrefix("cameradar")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
pflag.StringSliceP("targets", "t", []string{}, "The targets on which to scan for open RTSP streams - required (ex: 172.16.100.0/24)")
pflag.StringSliceP("ports", "p", []string{"554", "5554", "8554"}, "The ports on which to search for RTSP streams")
pflag.StringP("custom-routes", "r", "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/routes", "The path on which to load a custom routes dictionary")
pflag.StringP("custom-credentials", "c", "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/credentials.json", "The path on which to load a custom credentials JSON dictionary")
pflag.IntP("speed", "s", 4, "The nmap speed preset to use for discovery")
pflag.IntP("timeout", "T", 2000, "The timeout in miliseconds to use for attack attempts")
pflag.BoolP("log", "l", false, "Enable the logs for nmap's output to stdout")
pflag.BoolP("help", "h", false, "displays this help message")
viper.AutomaticEnv()
pflag.Parse()
err := viper.BindPFlags(pflag.CommandLine)
if err != nil {
return err
}
if viper.GetBool("help") {
pflag.Usage()
fmt.Println("\nExamples of usage:")
fmt.Println("\tScanning your home network for RTSP streams:\tcameradar -t 192.168.0.0/24")
fmt.Println("\tScanning a remote camera on a specific port:\tcameradar -t 172.178.10.14 -p 18554 -s 2")
fmt.Println("\tScanning an unstable remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 --timeout 10000 -l")
os.Exit(0)
}
if viper.GetStringSlice("targets") == nil {
return errors.New("targets (-t, --targets) argument required\n examples:\n - 172.16.100.0/24\n - localhost\n - 8.8.8.8")
}
return nil
}
func main() {
var options options
term := log.NewTerminal()
err := parseArguments()
if err != nil {
printErr(term, err)
}
options.Credentials = viper.GetString("custom-credentials")
options.EnableLogs = viper.GetBool("log") || viper.GetBool("logging")
options.Ports = viper.GetStringSlice("ports")
options.Routes = viper.GetString("custom-routes")
options.Speed = viper.GetInt("speed")
options.Timeout = viper.GetInt("timeout")
options.Targets = viper.GetStringSlice("targets")
w := startSpinner(options.EnableLogs)
if len(options.Targets) == 1 {
options.Targets, err = cmrdr.ParseTargetsFile(options.Targets[0])
if err != nil {
printErr(term, err)
}
}
err = curl.GlobalInit(curl.GLOBAL_ALL)
handle := curl.EasyInit()
if err != nil || handle == nil {
printErr(term, errors.New("libcurl initialization failed"))
}
c := &cmrdr.Curl{CURL: handle}
defer curl.GlobalCleanup()
updateSpinner(w, "Loading dictionaries...", options.EnableLogs)
gopath := os.Getenv("GOPATH")
options.Credentials = strings.Replace(options.Credentials, "<GOPATH>", gopath, 1)
options.Routes = strings.Replace(options.Routes, "<GOPATH>", gopath, 1)
credentials, err := cmrdr.LoadCredentials(options.Credentials)
if err != nil {
printErr(term, fmt.Errorf("Invalid credentials dictionary %q: %v", options.Credentials, err))
return
}
routes, err := cmrdr.LoadRoutes(options.Routes)
if err != nil {
printErr(term, fmt.Errorf("Invalid routes dictionary %q: %v", options.Routes, err))
return
}
updateSpinner(w, "Scanning the network...", options.EnableLogs)
streams, err := cmrdr.Discover(options.Targets, options.Ports, options.Speed)
if err != nil && len(streams) > 0 {
printErr(term, err)
}
// Most cameras will be accessed successfully with these two attacks
updateSpinner(w, "Found "+fmt.Sprint(len(streams))+" streams. Attacking their routes...", options.EnableLogs)
streams, err = cmrdr.AttackRoute(c, streams, routes, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
if err != nil && len(streams) > 0 {
printErr(term, err)
}
updateSpinner(w, "Found "+fmt.Sprint(len(streams))+" streams. Detecting their authentication methods...", options.EnableLogs)
streams, err = cmrdr.DetectAuthMethods(c, streams, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
updateSpinner(w, "Found "+fmt.Sprint(len(streams))+" streams. Attacking their credentials...", options.EnableLogs)
streams, err = cmrdr.AttackCredentials(c, streams, credentials, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
if err != nil && len(streams) > 0 {
printErr(term, err)
}
// But some cameras run GST RTSP Server which prioritizes 401 over 404 contrary to most cameras.
// For these cameras, running another route attack will solve the problem.
for _, stream := range streams {
if !stream.RouteFound || !stream.CredentialsFound {
updateSpinner(w, "Found "+fmt.Sprint(len(streams))+" streams. Final attack...", options.EnableLogs)
streams, err = cmrdr.AttackRoute(c, streams, routes, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
if err != nil && len(streams) > 0 {
printErr(term, err)
}
break
}
}
updateSpinner(w, "Found "+fmt.Sprint(len(streams))+" streams. Validating their availability...", options.EnableLogs)
streams, err = cmrdr.ValidateStreams(c, streams, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
if err != nil && len(streams) > 0 {
printErr(term, err)
}
clearOutput(w, options.EnableLogs)
prettyPrint(term, streams)
}
func prettyPrint(term *log.Terminal, streams []cmrdr.Stream) {
success := 0
if len(streams) == 0 {
term.Infof("%s No streams were found. Please make sure that your target is on an accessible network.\n", style.Failure(style.SymbolCross))
}
for _, stream := range streams {
if stream.CredentialsFound && stream.RouteFound && stream.Available {
term.Infof("%s\tDevice RTSP URL:\t%s\n", style.Success(style.SymbolRightTriangle), style.Link(cmrdr.GetCameraRTSPURL(stream)))
success++
} else {
term.Infof("%s\tAdmin panel URL:\t%s You can use this URL to try attacking the camera's admin panel instead.\n", style.Failure(style.SymbolCross), style.Link(cmrdr.GetCameraAdminPanelURL(stream)))
}
if len(stream.Device) > 0 {
term.Infof("\tDevice model:\t\t%s\n\n", stream.Device)
}
if stream.Available {
term.Infof("\tAvailable:\t\t%s\n", style.Success(style.SymbolCheck))
} else {
term.Infof("\tAvailable:\t\t%s\n", style.Failure(style.SymbolCross))
}
term.Infof("\tIP address:\t\t%s\n", stream.Address)
term.Infof("\tRTSP port:\t\t%d\n", stream.Port)
switch stream.AuthenticationType {
case curl.AUTH_NONE:
term.Infoln("\tThis camera does not require authentication")
case curl.AUTH_BASIC:
term.Infoln("\tAuth type:\t\tbasic")
case curl.AUTH_DIGEST:
term.Infoln("\tAuth type:\t\tdigest")
}
if stream.CredentialsFound {
term.Infof("\tUsername:\t\t%s\n", style.Success(stream.Username))
term.Infof("\tPassword:\t\t%s\n", style.Success(stream.Password))
} else {
term.Infof("\tUsername:\t\t%s\n", style.Failure("not found"))
term.Infof("\tPassword:\t\t%s\n", style.Failure("not found"))
}
if stream.RouteFound {
term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Success("/"+stream.Route))
} else {
term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Failure("not found"))
}
}
if success > 1 {
term.Infof("%s Successful attack: %s devices were accessed", style.Success(style.SymbolCheck), style.Success(len(streams)))
} else if success == 1 {
term.Infof("%s Successful attack: %s device was accessed", style.Success(style.SymbolCheck), style.Success(len(streams)))
} else {
term.Infof("%s Streams were found but none were accessed. They are most likely configured with secure credentials and routes. You can try adding entries to the dictionary or generating your own in order to attempt a bruteforce attack on the cameras.\n", style.Failure("\xE2\x9C\x96"))
}
}
func printErr(term *log.Terminal, err error) {
term.Errorln(style.Failure(style.SymbolCross), err)
os.Exit(1)
}
func updateSpinner(w *wow.Wow, text string, disabled bool) {
if !disabled {
w.Text(" " + text)
}
}
func startSpinner(disabled bool) *wow.Wow {
if !disabled {
w := wow.New(os.Stdout, spin.Get(spin.Dots), " Loading dictionaries...")
w.Start()
return w
}
return nil
}
// HACK: Waiting for a fix to issue
// https://github.com/gernest/wow/issues/5
func clearOutput(w *wow.Wow, disabled bool) {
if !disabled {
w.Text("\b")
time.Sleep(80 * time.Millisecond)
w.Stop()
}
}
+92
View File
@@ -0,0 +1,92 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/ullaakut/cameradar"
"github.com/ullaakut/disgo"
"github.com/ullaakut/disgo/style"
)
func parseArguments() error {
viper.SetEnvPrefix("cameradar")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
pflag.StringSliceP("targets", "t", []string{}, "The targets on which to scan for open RTSP streams - required (ex: 172.16.100.0/24)")
pflag.StringSliceP("ports", "p", []string{"554", "5554", "8554"}, "The ports on which to search for RTSP streams")
pflag.StringP("custom-routes", "r", "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/routes", "The path on which to load a custom routes dictionary")
pflag.StringP("custom-credentials", "c", "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/credentials.json", "The path on which to load a custom credentials JSON dictionary")
pflag.IntP("speed", "s", 4, "The nmap speed preset to use for discovery")
pflag.DurationP("timeout", "T", 2*time.Second, "The timeout in miliseconds to use for attack attempts")
pflag.BoolP("debug", "d", true, "Enable the debug logs")
pflag.BoolP("verbose", "v", false, "Enable the verbose logs")
pflag.BoolP("help", "h", false, "displays this help message")
viper.AutomaticEnv()
pflag.Parse()
err := viper.BindPFlags(pflag.CommandLine)
if err != nil {
return err
}
if viper.GetBool("help") {
pflag.Usage()
fmt.Println("\nExamples of usage:")
fmt.Println("\tScanning your home network for RTSP streams:\tcameradar -t 192.168.0.0/24")
fmt.Println("\tScanning a remote camera on a specific port:\tcameradar -t 172.178.10.14 -p 18554 -s 2")
fmt.Println("\tScanning an unstable remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 --timeout 10000 -l")
os.Exit(0)
}
if viper.GetStringSlice("targets") == nil {
return errors.New("targets (-t, --targets) argument required\n examples:\n - 172.16.100.0/24\n - localhost\n - 8.8.8.8")
}
return nil
}
func main() {
err := parseArguments()
if err != nil {
printErr(err)
}
c, err := cameradar.New(
cameradar.WithTargets(viper.GetStringSlice("targets")),
cameradar.WithPorts(viper.GetStringSlice("ports")),
cameradar.WithDebug(viper.GetBool("debug")),
cameradar.WithVerbose(viper.GetBool("verbose")),
cameradar.WithCustomCredentials(viper.GetString("custom-credentials")),
cameradar.WithCustomRoutes(viper.GetString("custom-routes")),
cameradar.WithSpeed(viper.GetInt("speed")),
cameradar.WithTimeout(viper.GetDuration("timeout")),
)
if err != nil {
printErr(err)
}
scanResult, err := c.Scan()
if err != nil {
printErr(err)
}
streams, err := c.Attack(scanResult)
if err != nil {
printErr(err)
}
c.PrintStreams(streams)
}
func printErr(err error) {
disgo.Errorln(style.Failure(style.SymbolCross), err)
os.Exit(1)
}
+1 -1
View File
@@ -1,4 +1,4 @@
package cmrdr package cameradar
import ( import (
curl "github.com/ullaakut/go-curl" curl "github.com/ullaakut/go-curl"
+1 -1
View File
@@ -1,4 +1,4 @@
package cmrdr package cameradar
import ( import (
"reflect" "reflect"
+30 -31
View File
@@ -1,5 +1,4 @@
/live/ch01_0
0/1:1/main 0/1:1/main
0/usrnm:pwd/main 0/usrnm:pwd/main
0/video1 0/video1
@@ -11,22 +10,6 @@
12 12
125 125
666 666
AVStream1_1
CAM_ID.password.mp2
CH001.sdp
GetData.cgi
HighResolutionVideo
LowResolutionVideo
MediaInput/h264
MediaInput/mpeg4
ONVIF/MediaInput
ONVIF/MediaInput?profile=4_def_profile6
StdCh1
Streaming/Channels/1
Streaming/Unicast/channels/101
StreamingSetting?version=1.0&action=getRTSPStream&ChannelID=1&ChannelName=Channel1
VideoInput/1/h264/1
VideoInput/1/mpeg4/1
access_code access_code
access_name_for_stream_1_to_5 access_name_for_stream_1_to_5
api/mjpegvideo.cgi api/mjpegvideo.cgi
@@ -34,10 +17,12 @@ av0_0
av2 av2
avc avc
avn=2 avn=2
AVStream1_1
axis-media/media.amp axis-media/media.amp
axis-media/media.amp?camera=1 axis-media/media.amp?camera=1
axis-media/media.amp?videocodec=h264 axis-media/media.amp?videocodec=h264
cam cam
CAM_ID.password.mp2
cam/realmonitor cam/realmonitor
cam/realmonitor?channel=0&subtype=0 cam/realmonitor?channel=0&subtype=0
cam/realmonitor?channel=1&subtype=0 cam/realmonitor?channel=1&subtype=0
@@ -52,52 +37,58 @@ cam1/mpeg4?user='username'&pwd='password'
cam1/onvif-h264 cam1/onvif-h264
camera.stm camera.stm
ch0 ch0
ch00/0
ch001.sdp
ch01.264
ch01.264?
ch01.264?ptype=tcp
ch0_0.h264 ch0_0.h264
ch0_unicast_firststream ch0_unicast_firststream
ch0_unicast_secondstream ch0_unicast_secondstream
ch00/0
ch001.sdp
CH001.sdp
ch01.264
ch01.264?
ch01.264?ptype=tcp
ch1-s1 ch1-s1
channel1 channel1
GetData.cgi
gnz_media/main gnz_media/main
h264 h264
h264_vga.sdp
h264.sdp h264.sdp
h264/ch1/sub/av_stream h264/ch1/sub/av_stream
h264/media.amp h264/media.amp
h264_vga.sdp HighResolutionVideo
image.mpg image.mpg
img/media.sav img/media.sav
img/media.sav?channel=1 img/media.sav?channel=1
img/video.asf img/video.asf
img/video.sav img/video.sav
ioImage/1 ioImage/1
ipcam.sdp
ipcam_h264.sdp ipcam_h264.sdp
ipcam_mjpeg.sdp ipcam_mjpeg.sdp
ipcam.sdp
live live
live_mpeg4.sdp
live_st1
live.sdp live.sdp
live/av0 live/av0
live/ch0 live/ch0
live/ch00_0 live/ch00_0
live/ch01_0
live/h264 live/h264
live/main live/main
live/main0 live/main0
live/mpeg4 live/mpeg4
live1.sdp live1.sdp
live3.sdp live3.sdp
live_mpeg4.sdp
live_st1
livestream livestream
livestream/ LowResolutionVideo
main main
media media
media.amp media.amp
media.amp?streamprofile=Profile1 media.amp?streamprofile=Profile1
media/media.amp media/media.amp
media/video1 media/video1
MediaInput/h264
MediaInput/mpeg4
medias2 medias2
mjpeg/media.smp mjpeg/media.smp
mp4 mp4
@@ -116,6 +107,8 @@ nphMpeg4/g726-640x48
nphMpeg4/g726-640x480 nphMpeg4/g726-640x480
nphMpeg4/nil-320x240 nphMpeg4/nil-320x240
onvif-media/media.amp onvif-media/media.amp
ONVIF/MediaInput
ONVIF/MediaInput?profile=4_def_profile6
onvif1 onvif1
pass@10.0.0.5:6667/blinkhd pass@10.0.0.5:6667/blinkhd
play1.sdp play1.sdp
@@ -129,12 +122,16 @@ rtsp_live2
rtsp_tunnel rtsp_tunnel
rtsph264 rtsph264
rtsph2641080p rtsph2641080p
StdCh1
stream stream
stream.sdp stream.sdp
stream1 stream1
streaming/channels/0 streaming/channels/0
Streaming/Channels/1
streaming/channels/1 streaming/channels/1
streaming/channels/101 streaming/channels/101
Streaming/Unicast/channels/101
StreamingSetting?version=1.0&action=getRTSPStream&ChannelID=1&ChannelName=Channel1
tcp/av0_0 tcp/av0_0
test test
trackID=1 trackID=1
@@ -142,12 +139,12 @@ ucast/11
udp/av0_0 udp/av0_0
udp/unicast/aiphone_H264 udp/unicast/aiphone_H264
udpstream udpstream
user_defined
user.pin.mp2 user.pin.mp2
user=admin&password=&channel=1&stream=0.sdp?
user=admin&password=&channel=1&stream=0.sdp?real_stream
user=admin_password=?????_channel=1_stream=0.sdp?real_stream user=admin_password=?????_channel=1_stream=0.sdp?real_stream
user=admin_password=R5XFY888_channel=1_stream=0.sdp?real_stream user=admin_password=R5XFY888_channel=1_stream=0.sdp?real_stream
user_defined user=admin&password=&channel=1&stream=0.sdp?
user=admin&password=&channel=1&stream=0.sdp?real_stream
v2 v2
video video
video.3gp video.3gp
@@ -160,7 +157,9 @@ video.pro3
video0.sdp video0.sdp
video1 video1
video1+audio1 video1+audio1
videoMain
videoinput_1/h264_1/media.stm videoinput_1/h264_1/media.stm
VideoInput/1/h264/1
VideoInput/1/mpeg4/1
videoMain
vis vis
wfov wfov
+1 -1
View File
@@ -13,7 +13,7 @@ require (
github.com/spf13/pflag v1.0.3 github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.1 github.com/spf13/viper v1.3.1
github.com/ullaakut/disgo v0.3.0 github.com/ullaakut/disgo v0.3.0
github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70 github.com/ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead
gopkg.in/go-playground/validator.v9 v9.27.0 gopkg.in/go-playground/validator.v9 v9.27.0
) )
+2
View File
@@ -51,6 +51,8 @@ github.com/ullaakut/disgo v0.3.0 h1:2zrEyNBfPRgDVDgzM/qLXZ4Yqt3Lxz7ERvZUSmqSY2M=
github.com/ullaakut/disgo v0.3.0/go.mod h1:UOgLVyqihzJ7yihrHjYZikivT+AHb9NhT3r1OyPCJqg= github.com/ullaakut/disgo v0.3.0/go.mod h1:UOgLVyqihzJ7yihrHjYZikivT+AHb9NhT3r1OyPCJqg=
github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70 h1:3q4hgRu9NT894aYmnoMFl5wPvdNhpHYmdi2+Njyxq5U= github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70 h1:3q4hgRu9NT894aYmnoMFl5wPvdNhpHYmdi2+Njyxq5U=
github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70/go.mod h1:FTfXm4jC9Ff1yqc3/HMXCyr+SGO03vJyijJCQlNyF10= github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70/go.mod h1:FTfXm4jC9Ff1yqc3/HMXCyr+SGO03vJyijJCQlNyF10=
github.com/ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd h1:IzJ7V8S7/NXc4aLOj0QavbQZ5Z/Q2RpCifshHoJ5ytA=
github.com/ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd/go.mod h1:FTfXm4jC9Ff1yqc3/HMXCyr+SGO03vJyijJCQlNyF10=
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead h1:Pw5wKSAfxi8GcYJSc3GdcwtPG5tyg7zg9E3hAHbLPO0= github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead h1:Pw5wKSAfxi8GcYJSc3GdcwtPG5tyg7zg9E3hAHbLPO0=
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead/go.mod h1:4CQy4PqZA4Snk3+MS26+1oAkJ8dCY8kGH6+kF42yajw= github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead/go.mod h1:4CQy4PqZA4Snk3+MS26+1oAkJ8dCY8kGH6+kF42yajw=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+4 -4
View File
@@ -1,9 +1,9 @@
package cmrdr package cameradar
import "fmt" import "fmt"
func replace(streams []Stream, new Stream) []Stream { func replace(streams []Stream, new Stream) []Stream {
updatedSlice := streams[:0] var updatedSlice []Stream
for _, old := range streams { for _, old := range streams {
if old.Address == new.Address && old.Port == new.Port { if old.Address == new.Address && old.Port == new.Port {
@@ -16,12 +16,12 @@ func replace(streams []Stream, new Stream) []Stream {
return updatedSlice return updatedSlice
} }
// 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.
func GetCameraAdminPanelURL(stream Stream) string { func GetCameraAdminPanelURL(stream Stream) string {
return "http://" + stream.Address + "/" return "http://" + stream.Address + "/"
} }
+21 -27
View File
@@ -1,4 +1,4 @@
package cmrdr package cameradar
import ( import (
"testing" "testing"
@@ -10,25 +10,25 @@ func TestReplace(t *testing.T) {
validStream1 := Stream{ validStream1 := Stream{
Device: "fakeDevice", Device: "fakeDevice",
Address: "fakeAddress", Address: "fakeAddress",
Port: 1337, Port: 1,
} }
validStream2 := Stream{ validStream2 := Stream{
Device: "fakeDevice", Device: "fakeDevice",
Address: "differentFakeAddress", Address: "differentFakeAddress",
Port: 1337, Port: 2,
} }
invalidStreamNoPort := Stream{ invalidStream := Stream{
Device: "invalidDevice", Device: "invalidDevice",
Address: "fakeAddress", Address: "anotherFakeAddress",
Port: 0, Port: 3,
} }
invalidStreamNoPortModified := Stream{ invalidStreamModified := Stream{
Device: "updatedDevice", Device: "updatedDevice",
Address: "fakeAddress", Address: "anotherFakeAddress",
Port: 1337, Port: 3,
} }
testCases := []struct { testCases := []struct {
@@ -37,25 +37,21 @@ func TestReplace(t *testing.T) {
expectedStreams []Stream expectedStreams []Stream
}{ }{
// Valid baseline
{ {
streams: []Stream{validStream1, validStream2, invalidStreamNoPort}, streams: []Stream{validStream1, validStream2, invalidStream},
newStream: invalidStreamNoPortModified, newStream: invalidStreamModified,
expectedStreams: []Stream{validStream1, validStream2, invalidStreamNoPortModified}, expectedStreams: []Stream{validStream1, validStream2, invalidStreamModified},
}, },
} }
for _, test := range testCases { for _, test := range testCases {
streams := replace(test.streams, test.newStream) streams := replace(test.streams, test.newStream)
for _, stream := range test.streams { assert.Equal(t, len(test.expectedStreams), len(streams))
foundStream := false
for _, result := range streams { for _, expectedStream := range test.expectedStreams {
if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port { assert.Contains(t, streams, expectedStream)
foundStream = true
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
} }
} }
} }
@@ -74,16 +70,15 @@ func TestGetCameraRTSPURL(t *testing.T) {
expectedRTSPURL string expectedRTSPURL string
}{ }{
// Valid baseline
{ {
stream: validStream, stream: validStream,
expectedRTSPURL: "rtsp://ullaakut:ba69897483886f0d2b0afb6345b76c0c@1.2.3.4:1337/cameradar.sdp", expectedRTSPURL: "rtsp://ullaakut:ba69897483886f0d2b0afb6345b76c0c@1.2.3.4:1337/cameradar.sdp",
}, },
} }
for _, test := range testCases { for _, test := range testCases {
output := GetCameraRTSPURL(test.stream) assert.Equal(t, test.expectedRTSPURL, GetCameraRTSPURL(test.stream))
assert.Equal(t, test.expectedRTSPURL, output, "wrong RTSP URL generated")
} }
} }
@@ -97,15 +92,14 @@ func TestGetCameraAdminPanelURL(t *testing.T) {
expectedRTSPURL string expectedRTSPURL string
}{ }{
// Valid baseline
{ {
stream: validStream, stream: validStream,
expectedRTSPURL: "http://1.2.3.4/", expectedRTSPURL: "http://1.2.3.4/",
}, },
} }
for _, test := range testCases { for _, test := range testCases {
output := GetCameraAdminPanelURL(test.stream) assert.Equal(t, test.expectedRTSPURL, GetCameraAdminPanelURL(test.stream))
assert.Equal(t, test.expectedRTSPURL, output, "wrong Admin Panel URL generated")
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

+43 -30
View File
@@ -1,14 +1,13 @@
package cmrdr package cameradar
import ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"github.com/pkg/errors"
) )
var fs fileSystem = osFS{} var fs fileSystem = osFS{}
@@ -32,48 +31,51 @@ type osFS struct{}
func (osFS) Open(name string) (file, error) { return os.Open(name) } func (osFS) Open(name string) (file, error) { return os.Open(name) }
func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) }
// LoadCredentials opens a dictionary file and returns its contents as a Credentials structure // LoadCredentials opens a dictionary file and returns its contents as a Credentials structure.
func LoadCredentials(path string) (Credentials, error) { func (s *Scanner) LoadCredentials() error {
var creds Credentials s.term.Debugf("Loading credentials dictionary from path %q\n", s.credentialDictionaryPath)
// Open & Read XML file // Open & Read XML file.
content, err := ioutil.ReadFile(path) content, err := ioutil.ReadFile(s.credentialDictionaryPath)
if err != nil { if err != nil {
return creds, errors.Wrap(err, "could not read credentials dictionary file at "+path+":") return fmt.Errorf("could not read credentials dictionary file at %q: %v", s.credentialDictionaryPath, err)
} }
// Unmarshal content of JSON file into data structure // Unmarshal content of JSON file into data structure.
err = json.Unmarshal(content, &creds) err = json.Unmarshal(content, &s.credentials)
if err != nil { if err != nil {
return creds, err return fmt.Errorf("unable to unmarshal dictionary contents: %v", err)
} }
return creds, nil s.term.Debugf("Loaded %d usernames and %d passwords\n", len(s.credentials.Usernames), len(s.credentials.Passwords))
return nil
} }
// LoadRoutes opens a dictionary file and returns its contents as a Routes structure // LoadRoutes opens a dictionary file and returns its contents as a Routes structure.
func LoadRoutes(path string) (Routes, error) { func (s *Scanner) LoadRoutes() error {
file, err := os.Open(path) s.term.Debugf("Loading routes dictionary from path %q\n", s.routeDictionaryPath)
file, err := os.Open(s.routeDictionaryPath)
if err != nil { if err != nil {
return nil, err return fmt.Errorf("unable to open dictionary: %v", err)
} }
defer file.Close() defer file.Close()
var routes Routes
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
routes = append(routes, scanner.Text()) s.routes = append(s.routes, scanner.Text())
} }
return routes, scanner.Err() s.term.Debugf("Loaded %d routes\n", len(s.routes))
return scanner.Err()
} }
// ParseCredentialsFromString parses a dictionary string and returns its contents as a Credentials structure // ParseCredentialsFromString parses a dictionary string and returns its contents as a Credentials structure.
func ParseCredentialsFromString(content string) (Credentials, error) { func ParseCredentialsFromString(content string) (Credentials, error) {
var creds Credentials var creds Credentials
// Unmarshal content of JSON file into data structure // Unmarshal content of JSON file into data structure.
err := json.Unmarshal([]byte(content), &creds) err := json.Unmarshal([]byte(content), &creds)
if err != nil { if err != nil {
return creds, err return creds, err
@@ -82,28 +84,39 @@ func ParseCredentialsFromString(content string) (Credentials, error) {
return creds, nil return creds, nil
} }
// ParseRoutesFromString parses a dictionary string and returns its contents as a Routes structure // ParseRoutesFromString parses a dictionary string and returns its contents as a Routes structure.
func ParseRoutesFromString(content string) Routes { func ParseRoutesFromString(content string) Routes {
return strings.Split(content, "\n") return strings.Split(content, "\n")
} }
// ParseTargetsFile parses an input file containing hosts to targets // LoadTargets parses the file containing hosts to targets, if the targets are
func ParseTargetsFile(path string) ([]string, error) { // just set to a file name.
func (s *Scanner) LoadTargets() error {
if len(s.targets) != 1 {
return nil
}
path := s.targets[0]
_, err := fs.Stat(path) _, err := fs.Stat(path)
if err != nil { if err != nil {
return []string{path}, nil return nil
} }
file, err := fs.Open(path) file, err := fs.Open(path)
if err != nil { if err != nil {
return []string{path}, err return fmt.Errorf("unable to open targets file %q: %v", path, err)
} }
defer file.Close() defer file.Close()
bytes, err := ioutil.ReadAll(file) bytes, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
return []string{path}, err return fmt.Errorf("unable to read targets file %q: %v", path, err)
} }
return strings.Split(string(bytes), "\n"), nil s.targets = strings.Split(string(bytes), "\n")
s.term.Debugf("Successfylly parsed targets file with %d entries", len(s.targets))
return nil
} }
+177 -165
View File
@@ -1,12 +1,15 @@
package cmrdr package cameradar
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/ullaakut/disgo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -91,91 +94,79 @@ func TestLoadCredentials(t *testing.T) {
Passwords: []string{"12345", "root"}, Passwords: []string{"12345", "root"},
} }
testCases := []struct { tests := []struct {
description string
input []byte input []byte
fileExists bool fileExists bool
expectedOutput Credentials expectedCredentials Credentials
expectedErrMsg string expectedErr error
}{ }{
// Valid baseline
{ {
fileExists: true, description: "Valid baseline",
input: credentialsJSONString,
expectedOutput: validCredentials, fileExists: true,
input: credentialsJSONString,
expectedCredentials: validCredentials,
}, },
// File does not exist
{ {
fileExists: false, description: "File does not exist",
input: credentialsJSONString,
expectedErrMsg: "could not read credentials dictionary file at", fileExists: false,
input: credentialsJSONString,
expectedErr: errors.New("could not read credentials dictionary file at \"/tmp/cameradar_test_load_credentials_1.xml\": open /tmp/cameradar_test_load_credentials_1.xml: no such file or directory"),
}, },
// Invalid format
{ {
fileExists: true, description: "Invalid format",
input: []byte("not json"),
expectedErrMsg: "invalid character", fileExists: true,
input: []byte("not json"),
expectedErr: errors.New("unable to unmarshal dictionary contents: invalid character 'o' in literal null (expecting 'u')"),
}, },
// No streams in dictionary
{ {
description: "No streams in dictionary",
fileExists: true, fileExists: true,
input: []byte("{\"invalid\":\"json\"}"), input: []byte("{\"invalid\":\"json\"}"),
}, },
} }
for i, test := range testCases { for i, test := range tests {
filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml" t.Run(test.description, func(t *testing.T) {
// create file filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml"
if test.fileExists { // create file.
_, err := os.Create(filePath) if test.fileExists {
if err != nil { _, err := os.Create(filePath)
fmt.Printf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath) if err != nil {
os.Exit(1) t.Fatalf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
}
err = ioutil.WriteFile(filePath, test.input, 0644)
if err != nil {
fmt.Printf("could not write xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
}
result, err := LoadCredentials(filePath)
if len(test.expectedErrMsg) > 0 {
if err == nil {
fmt.Printf("unexpected success in LoadCredentials test, iteration %d. expected error: %s\n", i, test.expectedErrMsg)
os.Exit(1)
}
assert.Contains(t, err.Error(), test.expectedErrMsg, "wrong error message")
} else {
if err != nil {
fmt.Printf("unexpected error in LoadCredentials test, iteration %d: %v\n", i, err)
os.Exit(1)
}
for _, expectedUsername := range test.expectedOutput.Usernames {
foundUsername := false
for _, username := range result.Usernames {
if username == expectedUsername {
foundUsername = true
}
} }
assert.Equal(t, true, foundUsername, "wrong usernames parsed") err = ioutil.WriteFile(filePath, test.input, 0644)
} if err != nil {
t.Fatalf("could not write xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
for _, expectedPassword := range test.expectedOutput.Passwords {
foundPassword := false
for _, password := range result.Passwords {
if password == expectedPassword {
foundPassword = true
}
} }
assert.Equal(t, true, foundPassword, "wrong passwords parsed")
} }
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
credentialDictionaryPath: filePath,
}
err := scanner.LoadCredentials()
assert.Equal(t, test.expectedErr, err)
assert.Len(t, scanner.credentials.Usernames, len(test.expectedCredentials.Usernames))
for _, expectedUsername := range test.expectedCredentials.Usernames {
assert.Contains(t, scanner.credentials.Usernames, expectedUsername)
}
assert.Len(t, scanner.credentials.Passwords, len(test.expectedCredentials.Passwords))
for _, expectedPassword := range test.expectedCredentials.Passwords {
assert.Contains(t, scanner.credentials.Passwords, expectedPassword)
}
})
} }
} }
@@ -183,74 +174,69 @@ func TestLoadRoutes(t *testing.T) {
routesJSONString := []byte("admin\nroot") routesJSONString := []byte("admin\nroot")
validRoutes := Routes{"admin", "root"} validRoutes := Routes{"admin", "root"}
testCases := []struct { tests := []struct {
input []byte description string
fileExists bool input []byte
fileExists bool
expectedOutput Routes expectedRoutes Routes
expectedErrMsg string expectedErr error
}{ }{
// Valid baseline
{ {
description: "Valid baseline",
fileExists: true, fileExists: true,
input: routesJSONString, input: routesJSONString,
expectedOutput: validRoutes, expectedRoutes: validRoutes,
}, },
// File does not exist
{ {
fileExists: false, description: "File does not exist",
input: routesJSONString,
expectedErrMsg: "no such file or directory", fileExists: false,
input: routesJSONString,
expectedErr: errors.New("unable to open dictionary: open /tmp/cameradar_test_load_routes_1.xml: no such file or directory"),
}, },
// No streams in dictionary
{ {
description: "No streams in dictionary",
fileExists: true, fileExists: true,
input: []byte(""), input: []byte(""),
}, },
} }
for i, test := range testCases { for i, test := range tests {
filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml" t.Run(test.description, func(t *testing.T) {
filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml"
// create file // Create file.
if test.fileExists { if test.fileExists {
_, err := os.Create(filePath) _, err := os.Create(filePath)
if err != nil { if err != nil {
fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath) fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1) os.Exit(1)
}
err = ioutil.WriteFile(filePath, test.input, 0644)
if err != nil {
fmt.Printf("could not write xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
}
result, err := LoadRoutes(filePath)
if len(test.expectedErrMsg) > 0 {
if err == nil {
fmt.Printf("unexpected success in LoadRoutes test, iteration %d. expected error: %s\n", i, test.expectedErrMsg)
os.Exit(1)
}
assert.Contains(t, err.Error(), test.expectedErrMsg, "wrong error message")
} else {
if err != nil {
fmt.Printf("unexpected error in LoadRoutes test, iteration %d: %v\n", i, err)
os.Exit(1)
}
for _, expectedRoute := range test.expectedOutput {
foundRoute := false
for _, route := range result {
if route == expectedRoute {
foundRoute = true
}
} }
assert.Equal(t, true, foundRoute, "wrong routes parsed") err = ioutil.WriteFile(filePath, test.input, 0644)
if err != nil {
fmt.Printf("could not write xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
} }
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
routeDictionaryPath: filePath,
}
err := scanner.LoadRoutes()
assert.Equal(t, test.expectedErr, err)
assert.Len(t, scanner.routes, len(test.expectedRoutes))
for _, expectedRoute := range test.expectedRoutes {
assert.Contains(t, scanner.routes, expectedRoute)
}
})
} }
} }
@@ -295,58 +281,59 @@ func TestParseCredentialsFromString(t *testing.T) {
}, },
} }
testCases := []struct { tests := []struct {
str string str string
expectedResult Credentials expectedCredentials Credentials
}{ }{
{ {
str: "{\"usernames\":[\"\",\"admin\",\"Admin\",\"Administrator\",\"root\",\"supervisor\",\"ubnt\",\"service\",\"Dinion\",\"administrator\",\"admin1\"],\"passwords\":[\"\",\"admin\",\"9999\",\"123456\",\"pass\",\"camera\",\"1234\",\"12345\",\"fliradmin\",\"system\",\"jvc\",\"meinsm\",\"root\",\"4321\",\"111111\",\"1111111\",\"password\",\"ikwd\",\"supervisor\",\"ubnt\",\"wbox123\",\"service\"]}", str: "{\"usernames\":[\"\",\"admin\",\"Admin\",\"Administrator\",\"root\",\"supervisor\",\"ubnt\",\"service\",\"Dinion\",\"administrator\",\"admin1\"],\"passwords\":[\"\",\"admin\",\"9999\",\"123456\",\"pass\",\"camera\",\"1234\",\"12345\",\"fliradmin\",\"system\",\"jvc\",\"meinsm\",\"root\",\"4321\",\"111111\",\"1111111\",\"password\",\"ikwd\",\"supervisor\",\"ubnt\",\"wbox123\",\"service\"]}",
expectedResult: defaultCredentials, expectedCredentials: defaultCredentials,
}, },
{ {
str: "{}", str: "{}",
expectedResult: Credentials{}, expectedCredentials: Credentials{},
}, },
{ {
str: "{\"invalid_field\":42}", str: "{\"invalid_field\":42}",
expectedResult: Credentials{}, expectedCredentials: Credentials{},
}, },
{ {
str: "not json", str: "not json",
expectedResult: Credentials{}, expectedCredentials: Credentials{},
}, },
} }
for _, test := range testCases {
for _, test := range tests {
parsedCredentials, _ := ParseCredentialsFromString(test.str) parsedCredentials, _ := ParseCredentialsFromString(test.str)
assert.Equal(t, test.expectedResult, parsedCredentials, "unexpected result, parse error") assert.Equal(t, test.expectedCredentials, parsedCredentials)
} }
} }
func TestParseRoutesFromString(t *testing.T) { func TestParseRoutesFromString(t *testing.T) {
testCases := []struct { tests := []struct {
str string str string
expectedResult Routes expectedRoutes Routes
}{ }{
{ {
str: "a\nb\nc", str: "a\nb\nc",
expectedResult: []string{"a", "b", "c"}, expectedRoutes: []string{"a", "b", "c"},
}, },
{ {
str: "a", str: "a",
expectedResult: []string{"a"}, expectedRoutes: []string{"a"},
}, },
{ {
str: "", str: "",
expectedResult: []string{""}, expectedRoutes: []string{""},
}, },
} }
for _, test := range testCases {
parsedRoutes := ParseRoutesFromString(test.str) for _, test := range tests {
assert.Equal(t, test.expectedResult, parsedRoutes, "unexpected result, parse error") assert.Equal(t, test.expectedRoutes, ParseRoutesFromString(test.str))
} }
} }
func TestParseTargetsFile(t *testing.T) { func TestLoadTargets(t *testing.T) {
oldFS := fs oldFS := fs
mfs := &mockedFS{} mfs := &mockedFS{}
@@ -355,65 +342,90 @@ func TestParseTargetsFile(t *testing.T) {
fs = oldFS fs = oldFS
}() }()
testCases := []struct { tests := []struct {
input string description string
targets []string
fileExists bool fileExists bool
openError bool openError bool
readError bool readError bool
expectedResult []string expectedTargets []string
expectedError error expectedError error
}{ }{
{ {
input: "0.0.0.0", description: "not a file",
targets: []string{"0.0.0.0"},
fileExists: false, fileExists: false,
expectedResult: []string{"0.0.0.0"}, expectedTargets: []string{"0.0.0.0"},
expectedError: nil, expectedError: nil,
}, },
{ {
input: "test_does_not_really_exist", description: "not file targets",
targets: []string{"0.0.0.0", "1.2.3.4/24"},
expectedTargets: []string{"0.0.0.0", "1.2.3.4/24"},
expectedError: nil,
},
{
description: "file contains targets",
targets: []string{"test_does_not_really_exist"},
fileExists: true, fileExists: true,
expectedResult: []string{"0.0.0.0", "localhost", "192.17.0.0/16", "192.168.1.140-255", "192.168.2-3.0-255"}, expectedTargets: []string{"0.0.0.0", "localhost", "192.17.0.0/16", "192.168.1.140-255", "192.168.2-3.0-255"},
expectedError: nil, expectedError: nil,
}, },
{ {
input: "test_does_not_really_exist", description: "open error",
targets: []string{"test_does_not_really_exist"},
fileExists: true, fileExists: true,
openError: true, openError: true,
expectedResult: []string{"test_does_not_really_exist"}, expectedTargets: []string{"test_does_not_really_exist"},
expectedError: os.ErrNotExist, expectedError: errors.New("unable to open targets file \"test_does_not_really_exist\": file does not exist"),
}, },
{ {
input: "test_does_not_really_exist", description: "read error",
targets: []string{"test_does_not_really_exist"},
fileExists: true, fileExists: true,
readError: true, readError: true,
expectedResult: []string{"test_does_not_really_exist"}, expectedTargets: []string{"test_does_not_really_exist"},
expectedError: os.ErrNotExist, expectedError: errors.New("unable to read targets file \"test_does_not_really_exist\": file does not exist"),
}, },
} }
for _, test := range testCases { for _, test := range tests {
mfs.fileExists = test.fileExists t.Run(test.description, func(t *testing.T) {
mfs.openError = test.openError mfs.fileExists = test.fileExists
mfs.openError = test.openError
mfs.fileMock = &fileMock{ mfs.fileMock = &fileMock{
readError: test.readError, readError: test.readError,
} }
mfs.fileMock.On("Close").Return(nil) mfs.fileMock.On("Close").Return(nil)
mfs.fileMock.WriteString("0.0.0.0\nlocalhost\n192.17.0.0/16\n192.168.1.140-255\n192.168.2-3.0-255") mfs.fileMock.WriteString("0.0.0.0\nlocalhost\n192.17.0.0/16\n192.168.1.140-255\n192.168.2-3.0-255")
result, err := ParseTargetsFile(test.input) scanner := &Scanner{
assert.Equal(t, test.expectedResult, result, "unexpected result, parse error") term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
assert.Equal(t, test.expectedError, err, "unexpected error") targets: test.targets,
}
err := scanner.LoadTargets()
assert.Equal(t, test.expectedTargets, scanner.targets)
assert.Equal(t, test.expectedError, err)
})
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
package cmrdr package cameradar
import "time" import "time"
+20 -14
View File
@@ -1,4 +1,4 @@
package cmrdr package cameradar
import ( import (
"strings" "strings"
@@ -6,7 +6,7 @@ import (
"github.com/ullaakut/nmap" "github.com/ullaakut/nmap"
) )
// Discover scans the target networks and tries to find RTSP streams within them. // Scan scans the target networks and tries to find RTSP streams within them.
// //
// targets can be: // targets can be:
// //
@@ -18,28 +18,30 @@ import (
// ports can be: // ports can be:
// //
// - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554) // - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554)
func Discover(targets, ports []string, speed int) ([]Stream, error) { func (s *Scanner) Scan() ([]Stream, error) {
// Run nmap command to discover open ports on the specified targets & ports s.term.StartStep("Scanning the network")
scanner, err := nmap.NewScanner(
nmap.WithTargets(targets...), // Run nmap command to discover open ports on the specified targets & ports.
nmap.WithPorts(ports...), nmapScanner, err := nmap.NewScanner(
nmap.WithTimingTemplate(nmap.Timing(speed)), nmap.WithTargets(s.targets...),
nmap.WithPorts(s.ports...),
nmap.WithTimingTemplate(nmap.Timing(s.speed)),
) )
if err != nil { if err != nil {
return nil, err return nil, s.term.FailStepf("unable to create network scanner: %v", err)
} }
return scan(scanner) return s.scan(nmapScanner)
} }
func scan(scanner nmap.ScanRunner) ([]Stream, error) { func (s *Scanner) scan(nmapScanner nmap.ScanRunner) ([]Stream, error) {
results, err := scanner.Run() results, err := nmapScanner.Run()
if err != nil { if err != nil {
return nil, err return nil, s.term.FailStepf("error while scanning network: %v", err)
} }
// Get streams from nmap results.
var streams []Stream var streams []Stream
// Get streams from nmap results
for _, host := range results.Hosts { for _, host := range results.Hosts {
for _, port := range host.Ports { for _, port := range host.Ports {
if port.Status() != "open" { if port.Status() != "open" {
@@ -60,5 +62,9 @@ func scan(scanner nmap.ScanRunner) ([]Stream, error) {
} }
} }
s.term.Debugf("Found %d RTSP streams\n", len(streams))
s.term.EndStep()
return streams, nil return streams, nil
} }
+56 -38
View File
@@ -1,13 +1,16 @@
package cmrdr package cameradar
import ( import (
"errors" "errors"
"io/ioutil"
"os" "os"
"testing" "testing"
"github.com/ullaakut/nmap" "github.com/ullaakut/disgo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/ullaakut/nmap"
) )
type nmapMock struct { type nmapMock struct {
@@ -23,7 +26,33 @@ func (m *nmapMock) Run() (*nmap.Run, error) {
return nil, args.Error(1) return nil, args.Error(1)
} }
func TestDiscover(t *testing.T) { var (
validStream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
validStream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
invalidStreamNoPort = Stream{
Device: "invalidDevice",
Address: "fakeAddress",
Port: 0,
}
invalidStreamNoAddress = Stream{
Device: "invalidDevice",
Address: "",
Port: 1337,
}
)
func TestScan(t *testing.T) {
tests := []struct { tests := []struct {
description string description string
@@ -32,8 +61,8 @@ func TestDiscover(t *testing.T) {
speed int speed int
removePath bool removePath bool
expectedErr error expectedErr error
expectedResult []Stream expectedStreams []Stream
}{ }{
{ {
description: "create new scanner and call scan, no error", description: "create new scanner and call scan, no error",
@@ -48,7 +77,7 @@ func TestDiscover(t *testing.T) {
removePath: true, removePath: true,
ports: []string{"80"}, ports: []string{"80"},
expectedErr: errors.New("'nmap' binary was not found"), expectedErr: errors.New("unable to create network scanner: 'nmap' binary was not found"),
}, },
} }
@@ -58,43 +87,28 @@ func TestDiscover(t *testing.T) {
os.Setenv("PATH", "") os.Setenv("PATH", "")
} }
result, err := Discover(test.targets, test.ports, test.speed) scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
targets: test.targets,
ports: test.ports,
speed: test.speed,
}
result, err := scanner.Scan()
assert.Equal(t, test.expectedErr, err) assert.Equal(t, test.expectedErr, err)
assert.Equal(t, test.expectedResult, result) assert.Equal(t, test.expectedStreams, result)
}) })
} }
} }
func TestScan(t *testing.T) { func TestInternalScan(t *testing.T) {
validStream1 := Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
validStream2 := Stream{ tests := []struct {
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
invalidStreamNoPort := Stream{
Device: "invalidDevice",
Address: "fakeAddress",
Port: 0,
}
invalidStreamNoAddress := Stream{
Device: "invalidDevice",
Address: "",
Port: 1337,
}
testCases := []struct {
description string description string
nmapResult *nmap.Run
nmapError error nmapResult *nmap.Run
nmapError error
expectedStreams []Stream expectedStreams []Stream
expectedErr error expectedErr error
@@ -281,17 +295,21 @@ func TestScan(t *testing.T) {
description: "scan failed", description: "scan failed",
nmapError: errors.New("scan failed"), nmapError: errors.New("scan failed"),
expectedErr: errors.New("scan failed"), expectedErr: errors.New("error while scanning network: scan failed"),
}, },
} }
for _, test := range testCases { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
nmapMock := &nmapMock{} nmapMock := &nmapMock{}
nmapMock.On("Run").Return(test.nmapResult, test.nmapError) nmapMock.On("Run").Return(test.nmapResult, test.nmapError)
results, err := scan(nmapMock) scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
}
results, err := scanner.scan(nmapMock)
assert.Equal(t, test.expectedErr, err) assert.Equal(t, test.expectedErr, err)
assert.Equal(t, test.expectedStreams, results, "wrong streams parsed") assert.Equal(t, test.expectedStreams, results, "wrong streams parsed")
+143
View File
@@ -0,0 +1,143 @@
package cameradar
import (
"fmt"
"os"
"strings"
"time"
"github.com/ullaakut/disgo"
curl "github.com/ullaakut/go-curl"
)
// Scanner represents a cameradar scanner. It scans a network and
// attacks all streams found to get their RTSP credentials.
type Scanner struct {
curl Curler
term *disgo.Terminal
targets []string
ports []string
debug bool
verbose bool
speed int
timeout time.Duration
credentialDictionaryPath string
routeDictionaryPath string
credentials Credentials
routes Routes
}
// New creates a new Cameradar Scanner and applies the given options.
func New(options ...func(*Scanner)) (*Scanner, error) {
err := curl.GlobalInit(curl.GLOBAL_ALL)
if err != nil {
return nil, fmt.Errorf("unable to initialize curl library: %v", err)
}
handle := curl.EasyInit()
if handle == nil {
return nil, fmt.Errorf("unable to initialize curl handle: %v", err)
}
scanner := &Scanner{
curl: &Curl{CURL: handle},
credentialDictionaryPath: "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/credentials.json",
routeDictionaryPath: "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/routes",
}
for _, option := range options {
option(scanner)
}
gopath := os.Getenv("GOPATH")
scanner.credentialDictionaryPath = strings.Replace(scanner.credentialDictionaryPath, "<GOPATH>", gopath, 1)
scanner.routeDictionaryPath = strings.Replace(scanner.routeDictionaryPath, "<GOPATH>", gopath, 1)
scanner.term = disgo.NewTerminal(
disgo.WithDebug(scanner.debug),
)
err = scanner.LoadTargets()
if err != nil {
return nil, fmt.Errorf("unable to parse target file: %v", err)
}
scanner.term.StartStepf("Loading credentials")
err = scanner.LoadCredentials()
if err != nil {
return nil, scanner.term.FailStepf("unable to load credentials dictionary: %v", err)
}
scanner.term.StartStepf("Loading routes")
err = scanner.LoadRoutes()
if err != nil {
return nil, scanner.term.FailStepf("unable to load credentials dictionary: %v", err)
}
disgo.EndStep()
return scanner, nil
}
// WithTargets specifies the targets to scan and attack.
func WithTargets(targets []string) func(s *Scanner) {
return func(s *Scanner) {
s.targets = targets
}
}
// WithPorts specifies the ports to scan and attack.
func WithPorts(ports []string) func(s *Scanner) {
return func(s *Scanner) {
s.ports = ports
}
}
// WithDebug specifies whether or not to enable debug logs.
func WithDebug(debug bool) func(s *Scanner) {
return func(s *Scanner) {
s.debug = debug
}
}
// WithVerbose specifies whether or not to enable verbose logs.
func WithVerbose(verbose bool) func(s *Scanner) {
return func(s *Scanner) {
s.verbose = verbose
}
}
// WithCustomCredentials specifies a custom credential dictionary
// to use for the attacks.
func WithCustomCredentials(dictionaryPath string) func(s *Scanner) {
return func(s *Scanner) {
s.credentialDictionaryPath = dictionaryPath
}
}
// WithCustomRoutes specifies a custom route dictionary
// to use for the attacks.
func WithCustomRoutes(dictionaryPath string) func(s *Scanner) {
return func(s *Scanner) {
s.routeDictionaryPath = dictionaryPath
}
}
// WithSpeed specifies the speed at which the scan should be executed. Faster
// means easier to detect, slower has bigger timeout values and is more silent.
func WithSpeed(speed int) func(s *Scanner) {
return func(s *Scanner) {
s.speed = speed
}
}
// WithTimeout specifies the amount of time after which attack requests should
// timeout. This should be high if the network you are attacking has a poor
// connectivity or that you are located far away from it.
func WithTimeout(timeout time.Duration) func(s *Scanner) {
return func(s *Scanner) {
s.timeout = timeout
}
}
+133
View File
@@ -0,0 +1,133 @@
package cameradar
import (
"fmt"
"io/ioutil"
"testing"
"time"
"github.com/stretchr/testify/assert"
curl "github.com/ullaakut/go-curl"
)
func TestNew(t *testing.T) {
tests := []struct {
description string
targets []string
ports []string
debug bool
verbose bool
customCredentials string
customRoutes string
speed int
timeout time.Duration
loadTargetsFail bool
loadCredsFail bool
loadRoutesFail bool
curlGlobalFail bool
curlEasyFail bool
expectedErr bool
}{
{
description: "no error while loading dictionaries",
targets: []string{"titi", "toto"},
ports: []string{"554"},
debug: true,
verbose: false,
speed: 3,
timeout: time.Millisecond,
},
{
description: "unable to load targets",
loadTargetsFail: true,
expectedErr: true,
},
{
description: "unable to load credentials",
loadCredsFail: true,
expectedErr: true,
},
{
description: "unable to load routes",
loadRoutesFail: true,
expectedErr: true,
},
{
description: "curl fails to init",
curlGlobalFail: true,
expectedErr: true,
},
{
description: "curl fails to create handle",
curlEasyFail: true,
expectedErr: true,
},
}
for i, test := range tests {
t.Run(test.description, func(t *testing.T) {
if test.loadTargetsFail {
test.targets = []string{generateTmpFileName(i, "targets")}
ioutil.WriteFile(test.targets[0], []byte(`0.0.0.0`), 0000)
}
if !test.loadCredsFail {
test.customCredentials = generateTmpFileName(i, "creds")
ioutil.WriteFile(test.customCredentials, []byte(`{"usernames":["admin"],"passwords":["admin"]}`), 0644)
}
if !test.loadRoutesFail {
test.customRoutes = generateTmpFileName(i, "routes")
ioutil.WriteFile(test.customRoutes, []byte(`live.sdp`), 0644)
}
curl.TestGlobalFail = test.curlGlobalFail
curl.TestEasyFail = test.curlEasyFail
scanner, err := New(
WithTargets(test.targets),
WithPorts(test.ports),
WithDebug(test.debug),
WithVerbose(test.verbose),
WithSpeed(test.speed),
WithTimeout(test.timeout),
WithCustomCredentials(test.customCredentials),
WithCustomRoutes(test.customRoutes),
)
if test.expectedErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if scanner != nil {
assert.Equal(t, test.targets, scanner.targets)
assert.Equal(t, test.ports, scanner.ports)
assert.Equal(t, test.debug, scanner.debug)
assert.Equal(t, test.verbose, scanner.verbose)
assert.Equal(t, test.speed, scanner.speed)
assert.Equal(t, test.timeout, scanner.timeout)
}
})
}
}
func generateTmpFileName(iteration int, purpose string) string {
return fmt.Sprintf("/tmp/cameradar_test_scanner_%s_%d_%d", purpose, time.Now().Unix(), iteration)
}
+63
View File
@@ -0,0 +1,63 @@
package cameradar
import (
"github.com/ullaakut/disgo/style"
curl "github.com/ullaakut/go-curl"
)
// PrintStreams prints information on each stream.
func (s *Scanner) PrintStreams(streams []Stream) {
if len(streams) == 0 {
s.term.Infof("%s No streams were found. Please make sure that your target is on an accessible network.\n", style.Failure(style.SymbolCross))
}
success := 0
for _, stream := range streams {
if stream.Available {
s.term.Infof("%s\tDevice RTSP URL:\t%s\n", style.Success(style.SymbolRightTriangle), style.Link(GetCameraRTSPURL(stream)))
s.term.Infof("\tAvailable:\t\t%s\n", style.Success(style.SymbolCheck))
success++
} else {
s.term.Infof("%s\tAdmin panel URL:\t%s You can use this URL to try attacking the camera's admin panel instead.\n", style.Failure(style.SymbolCross), style.Link(GetCameraAdminPanelURL(stream)))
s.term.Infof("\tAvailable:\t\t%s\n", style.Failure(style.SymbolCross))
}
if len(stream.Device) > 0 {
s.term.Infof("\tDevice model:\t\t%s\n\n", stream.Device)
}
s.term.Infof("\tIP address:\t\t%s\n", stream.Address)
s.term.Infof("\tRTSP port:\t\t%d\n", stream.Port)
switch stream.AuthenticationType {
case curl.AUTH_NONE:
s.term.Infoln("\tThis camera does not require authentication")
case curl.AUTH_BASIC:
s.term.Infoln("\tAuth type:\t\tbasic")
case curl.AUTH_DIGEST:
s.term.Infoln("\tAuth type:\t\tdigest")
}
if stream.CredentialsFound {
s.term.Infof("\tUsername:\t\t%s\n", style.Success(stream.Username))
s.term.Infof("\tPassword:\t\t%s\n", style.Success(stream.Password))
} else {
s.term.Infof("\tUsername:\t\t%s\n", style.Failure("not found"))
s.term.Infof("\tPassword:\t\t%s\n", style.Failure("not found"))
}
if stream.RouteFound {
s.term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Success("/"+stream.Route))
} else {
s.term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Failure("not found"))
}
}
if success > 1 {
s.term.Infof("%s Successful attack: %s devices were accessed", style.Success(style.SymbolCheck), style.Success(len(streams)))
} else if success == 1 {
s.term.Infof("%s Successful attack: %s device was accessed", style.Success(style.SymbolCheck), style.Success("one"))
} else {
s.term.Infof("%s Streams were found but none were accessed. They are most likely configured with secure credentials and routes. You can try adding entries to the dictionary or generating your own in order to attempt a bruteforce attack on the cameras.\n", style.Failure("\xE2\x9C\x96"))
}
}
+186
View File
@@ -0,0 +1,186 @@
package cameradar
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ullaakut/disgo"
)
var (
unavailable = Stream{}
available = Stream{
Available: true,
}
deviceFound = Stream{
Device: "devicename",
}
noAuth = Stream{
AuthenticationType: 0,
}
basic = Stream{
AuthenticationType: 1,
}
digest = Stream{
AuthenticationType: 2,
}
credsFound = Stream{
CredentialsFound: true,
Username: "us3r",
Password: "p4ss",
}
routeFound = Stream{
RouteFound: true,
Route: "r0ute",
}
)
func TestPrintStreams(t *testing.T) {
tests := []struct {
description string
streams []Stream
expectedLogs []string
}{
{
description: "displays the proper message when no streams found",
streams: nil,
expectedLogs: []string{"No streams were found"},
},
{
description: "displays the admin panel URL when a stream is not accessible",
streams: []Stream{
unavailable,
},
expectedLogs: []string{"Admin panel URL"},
},
{
description: "displays the device name when it is found",
streams: []Stream{
deviceFound,
},
expectedLogs: []string{"Device model:"},
},
{
description: "displays authentication type (no auth)",
streams: []Stream{
noAuth,
},
expectedLogs: []string{"This camera does not require authentication"},
},
{
description: "displays authentication type (basic)",
streams: []Stream{
basic,
},
expectedLogs: []string{"basic"},
},
{
description: "displays authentication type (digest)",
streams: []Stream{
digest,
},
expectedLogs: []string{"digest"},
},
{
description: "displays credentials properly",
streams: []Stream{
credsFound,
},
expectedLogs: []string{
"Username",
"us3r",
"Password",
"p4ss",
},
},
{
description: "displays route properly",
streams: []Stream{
routeFound,
},
expectedLogs: []string{
"RTSP route",
"/r0ute",
},
},
{
description: "displays successes properly (no success)",
streams: []Stream{
unavailable,
},
expectedLogs: []string{
"Streams were found but none were accessed",
},
},
{
description: "displays successes properly (1 success)",
streams: []Stream{
available,
},
expectedLogs: []string{
"Successful attack",
"device was accessed",
},
},
{
description: "displays successes properly (multiple successes)",
streams: []Stream{
available,
available,
available,
available,
},
expectedLogs: []string{
"Successful attack",
"devices were accessed",
},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
writer := &bytes.Buffer{}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(writer)),
}
scanner.PrintStreams(test.streams)
for _, expectedLog := range test.expectedLogs {
assert.Contains(t, writer.String(), expectedLog)
}
})
}
}