Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6486d04e61 | |||
| 96928ac43c | |||
| 8e7de3f59e | |||
| fbc0b7a66d | |||
| 78eda6672e | |||
| 9f05634531 | |||
| defc308a9d | |||
| 9a6c030a74 | |||
| afe2caddd6 | |||
| 8349bc7c3a | |||
| 04ab1cfc8d | |||
| d233fd850e | |||
| 948aca316b | |||
| 3f05737bf2 | |||
| 4aabf47a5d | |||
| cb47aef7e4 | |||
| bb05fcff6f | |||
| 8634ba84ca | |||
| 3bd6b9171e | |||
| 18a933ba45 | |||
| 0187d9a553 | |||
| 7672da5b6d | |||
| 966266f742 | |||
| 0738d08966 | |||
| 4603096b93 | |||
| a5d3455333 | |||
| c83f961ffc | |||
| a6ed312eaf | |||
| 35b0cf26d9 | |||
| 0f011a1797 | |||
| 7719110f1e | |||
| 212ac2f0d5 | |||
| 2e49587cc2 | |||
| 47285675b9 | |||
| 2678df2e4c | |||
| 862e9f3de9 | |||
| 260a9645be | |||
| 1d5d606085 | |||
| c249be1cc0 | |||
| 1ec3a5fe44 |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ullaakut] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -24,6 +24,7 @@ Please select one:
|
||||
## Environment
|
||||
|
||||
My operating system:
|
||||
|
||||
- [ ] Windows
|
||||
- [ ] OSX
|
||||
- [ ] Linux
|
||||
@@ -34,17 +35,17 @@ OS architecture: <architecture>
|
||||
|
||||
## Issue
|
||||
|
||||
### What was expected?
|
||||
### What was expected
|
||||
|
||||
<expected behavior>
|
||||
|
||||
### What happened?
|
||||
### What happened
|
||||
|
||||
<observed behavior>
|
||||
|
||||
### 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>
|
||||
@@ -5,3 +5,6 @@
|
||||
# Golang
|
||||
/bin/*
|
||||
/pkg/*
|
||||
|
||||
# Builds
|
||||
dist/
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# https://github.com/golangci/golangci/wiki/Configuration
|
||||
|
||||
service:
|
||||
project-path: github.com/ullaakut/cameradar
|
||||
project-path: github.com/Ullaakut/cameradar
|
||||
prepare:
|
||||
- apt-get update && apt-get install -y libcurl4-gnutls-dev
|
||||
- dep ensure
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
project_name: cameradar
|
||||
dist: dist/cameradar
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- binary: cameradar
|
||||
main: ./cmd/cameradar
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
|
||||
changelog:
|
||||
skip: true
|
||||
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_checksums.txt"
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm}}v{{ .Arm }}{{ end }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- CHANGELOG.md
|
||||
+27
-14
@@ -2,8 +2,18 @@ dist: trusty
|
||||
sudo: required
|
||||
language: go
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
# needed for the nfpm pipe:
|
||||
- rpm
|
||||
# needed for the snap pipe:
|
||||
- snapd
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
# needed for the snap pipe:
|
||||
- PATH=/snap/bin:$PATH
|
||||
|
||||
services:
|
||||
- docker
|
||||
@@ -18,7 +28,7 @@ before_install:
|
||||
- sudo apt-get remove docker docker-engine docker.io
|
||||
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y docker-ce nmap
|
||||
- sudo apt-get install -y docker-ce nmap libcurl4-openssl-dev
|
||||
- go get github.com/mattn/goveralls
|
||||
- docker version
|
||||
|
||||
@@ -27,22 +37,25 @@ install:
|
||||
|
||||
script:
|
||||
# Run unit tests
|
||||
- go test -v -covermode=count -coverprofile=coverage.out
|
||||
- $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
|
||||
- docker run -d --name=fake_camera -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -p 8554:8554 ullaakut/rtspatt
|
||||
- GO111MODULE=on go test -v -covermode=count -coverprofile=coverage.out
|
||||
- GO111MODULE=on $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN
|
||||
# Launch fake cameras to check if cameradar is able to access them
|
||||
- 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
|
||||
- docker run --net=host -t cameradar -t 0.0.0.0 -l > logs.txt
|
||||
- docker logs fake_camera > camera_logs.txt
|
||||
# Stop the fake camera
|
||||
- docker stop fake_camera
|
||||
- docker run --net=host -t cameradar -t 0.0.0.0 -p 8554,5554 -v > logs.txt
|
||||
# Gather the logs from the cameras
|
||||
- docker logs fake_camera_digest > camera_digest_logs.txt
|
||||
- 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
|
||||
- cat camera_logs.txt
|
||||
- cat camera_digest_logs.txt
|
||||
- cat camera_basic_logs.txt
|
||||
- cat logs.txt
|
||||
# check if file contains more than one line
|
||||
# 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
|
||||
- grep "Successful attack" logs.txt || exit 1
|
||||
- git clean -fd
|
||||
|
||||
notifications:
|
||||
email:
|
||||
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
# Cameradar Changelog
|
||||
|
||||
This file lists all versions of the repository and precises all changes.
|
||||
|
||||
## v2.0.0
|
||||
|
||||
#### Major changes:
|
||||
|
||||
* Cameradar is no longer a C++ application but a Golang library
|
||||
* It is also a Golang application replacing the former C++ one (the C++ Cameradar image can still be used with the tag `1.1.4`)
|
||||
* The new docker image is twice lighter (14MB vs 379MB before)
|
||||
* The Cameradar golang library enables users to build their own application around camera discovery and attack. Example of applications could be an automatic camera discovery daemon with scheduled scans, a security audit tool to check if CCTV cameras are protected from attacks by being isolated and having strong passwords, etc.
|
||||
|
||||
## v1.1.4
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Simplified use of Docker image
|
||||
* Renamed MySQL table name to be more explicit
|
||||
* Refactoring of the Golang functional tester done
|
||||
* The output was made more human readable
|
||||
* Added automatic code quality checks for pull requests
|
||||
* Added contribution documentation
|
||||
* Updated dictionaries to add user suggestions for Chinese cameras
|
||||
* Enhanced `result.json` file's format
|
||||
|
||||
#### Bugfixes:
|
||||
|
||||
* Fixed a bug in the functional testing in which if the `result.json` file was not formatted correctly, the test failed but was still considered a success.
|
||||
|
||||
## v1.1.3
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Added automatic pushes to DockerHub to the travis integration
|
||||
* Made travis configuration file better
|
||||
* Changed the package generation scripts to make them report errors
|
||||
* Removed old etix_rtsp_server binary from the test folder
|
||||
|
||||
#### Bugfixes:
|
||||
|
||||
* Fixed an issue that made it mandatory to launch tests at least once so that they can work the second time
|
||||
* Fixed an issue that made the golang testing tool not compile in the testing script
|
||||
* Fixed an issue that made the golang testing tool sometimes ignore some tests
|
||||
* The previous known issue has been investigated and we don't know where it came from. However after a night of testing I have been unable to reproduce it, so I will consider it closed
|
||||
|
||||
## v1.1.2
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Added travis integration
|
||||
* Added default environment value for Docker deployment
|
||||
* Updated docker image description with new easy usage
|
||||
* Updated README badges style (replaced flat with square-flat)
|
||||
* Build last package can now also generate a debug package if given the `Debug` command-line argument
|
||||
|
||||
#### Known issues
|
||||
|
||||
* There is still the issue with Camera Emulation Server, see the [previous version's patchnote](#v1.1.1) for more information.
|
||||
|
||||
## v1.1.1
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Removed unnecessary null pointer checks (thanks to https://github.com/elfring)
|
||||
* Updated package description
|
||||
* Removed debug message in CMake build
|
||||
* Added `/ch01.264` to the URL dictionary in the deployment (Comelit default RTSP URL)
|
||||
* Updated tests partially (still needs work to make the code cleaner)
|
||||
* Variable names are now compliant with Golang best practices
|
||||
* JSON variable names are back to normal
|
||||
* Functions have been moved in more appropriate source files
|
||||
* Structure definitions have been moved in more appropriate source files
|
||||
* Source files have been renamed to be more relevant
|
||||
* JUnit output now considers each camera as a test case
|
||||
* JUnit output now contains errors which makes debugging much easier
|
||||
* Added header files where it was forgotten
|
||||
|
||||
#### Bugfixes:
|
||||
|
||||
* Fixed an issue where if you loose your internet connection during thumbnail generation, FFMpeg would get stuck forever and thus Cameradar would never finish
|
||||
* Fixed an issue where multithreading could cause crashes
|
||||
* Fixed an issue where the routes dictionary was mistaken for the credentials dictionary
|
||||
* Fixed issues with the golang testing tool
|
||||
* Fixed automated camera generation
|
||||
* Fixed docker IP address resolution
|
||||
|
||||
#### Known issues:
|
||||
|
||||
* There is an issue with Camera Emulation Server that makes it impossible for Cameradar to generate thumbnails, which is why right now the verification of the thumbnails presence is commented and it is assumed correct. It is probably an issue with GST-RTSP-Server but requires investigation.
|
||||
|
||||
## v1.1.0
|
||||
|
||||
#### Major changes:
|
||||
|
||||
* There are more command line options
|
||||
* Port can now be overridden in the command line
|
||||
* Target can now be overridden in the command line
|
||||
* Bruteforce is now multithreaded and will use as many threads as there are discovered cameras
|
||||
* Thumbnail generation is now multithreaded and will use as many threads as there are discovered cameras
|
||||
* There are now default configuration values in order to make cameradar easier to use
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* The algorithms take external input into account (so that a 3rd party can change the DB to help Cameradar in real-time) and thus check the persistent data at each iteration
|
||||
* The default log level is now DEBUG instead of INFO
|
||||
* The attack logs are now INFO instead of DEBUG
|
||||
* The thumbnail generation logs are now INFO instead of DEBUG
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
* Fixed a bug in which the MySQL cache manager would consider a camera with known ids as having a valid path even if it weren't
|
||||
* Fixed a bug in which TCP RTSP streams would not generate thumbnails
|
||||
|
||||
## v1.0.5
|
||||
|
||||
* Fixed error in MySQL Cache Manager in which thumbnail generation on valid streams could not be done
|
||||
* Fixed potential crash in the case the machine running cameradar has no memory left to allocate space for the dynamic cache manager
|
||||
|
||||
## v1.0.4
|
||||
|
||||
#### Bugs fixed:
|
||||
|
||||
* Fixed nmap package detection
|
||||
|
||||
## v1.0.3
|
||||
|
||||
#### Bugs fixed:
|
||||
|
||||
* Corrected GStreamer check
|
||||
|
||||
## v1.0.2
|
||||
|
||||
#### Bugs fixed:
|
||||
|
||||
* Fixed issues in MySQL Cache Manager
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Added useful debug logs
|
||||
|
||||
## v1.0.1
|
||||
|
||||
### Ubuntu 16.04 Release
|
||||
|
||||
#### Major changes:
|
||||
|
||||
* The Docker deployment is now done using Ubuntu 16.04 instead of Ubuntu 15.10, so that it uses more recent packages.
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Removed useless dependencies
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### First production-ready release
|
||||
|
||||
#### Major changes:
|
||||
|
||||
* Added functional testing
|
||||
|
||||
## v0.2.2
|
||||
|
||||
After doing some testing on a weirdly configured camera network in a far away Datacenter, I discovered that some Cameras needed a few tweaks to the Cameradar attack method in order to be accessed.
|
||||
|
||||
#### Major changes:
|
||||
|
||||
* Cameradar can access Cameras that are configured to always send 400 Bad Requests responses
|
||||
|
||||
#### Minor changes:
|
||||
|
||||
* Changed iterator name from `it` to `stream` in dumb cache manager to improve code readability
|
||||
|
||||
#### Bugfixes:
|
||||
|
||||
* Cameradar no longer considers a timing out Camera as an accessible stream
|
||||
|
||||
## v0.2.1
|
||||
|
||||
This package adds fixes the Docker deployment package.
|
||||
|
||||
#### Minor changes
|
||||
|
||||
* Fixed the Docker deployment package
|
||||
* Updated README
|
||||
|
||||
## v0.2.0
|
||||
|
||||
### MySQL Cache Manager Release
|
||||
|
||||
This package adds a new cache manager using a MySQL database, that can store the results between mutiple uses.
|
||||
|
||||
#### Major changes
|
||||
|
||||
* Added a MySQL Cache Manager
|
||||
|
||||
#### Minor changes
|
||||
|
||||
* Removed legacy code
|
||||
* Removed boost dependency
|
||||
* Improved debugging logs
|
||||
|
||||
## v0.1.1
|
||||
|
||||
### Docker release
|
||||
|
||||
This package adds a way to deploy Cameradar using Docker.
|
||||
|
||||
#### Major changes
|
||||
|
||||
* Added a quick Docker deployment process
|
||||
* Added automatic dependencies downloading through CMake for the manual installation
|
||||
* Added CPack packaging for the Docker deployment
|
||||
|
||||
#### Minor changes
|
||||
|
||||
* Changed recommended cloning method to HTTPS
|
||||
* Added lots of informations to README.md
|
||||
|
||||
## v0.1.0
|
||||
|
||||
This package was the first OpenSource version of Cameradar. It contained only a simple cache manager and had some bugs.
|
||||
@@ -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*
|
||||
+9
-5
@@ -1,8 +1,8 @@
|
||||
# Build stage
|
||||
FROM golang:alpine AS build-env
|
||||
|
||||
COPY . /go/src/github.com/ullaakut/cameradar
|
||||
WORKDIR /go/src/github.com/ullaakut/cameradar/cameradar
|
||||
COPY . /go/src/github.com/Ullaakut/cameradar
|
||||
WORKDIR /go/src/github.com/Ullaakut/cameradar/cmd/cameradar
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
@@ -19,14 +19,18 @@ RUN go build -o cameradar
|
||||
# Final stage
|
||||
FROM alpine
|
||||
|
||||
# Necessary to install curl v7.64.0-r3.
|
||||
# Fix for https://github.com/Ullaakut/cameradar/issues/247
|
||||
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.9/main' >> /etc/apk/repositories
|
||||
|
||||
RUN apk --update add --no-cache nmap \
|
||||
nmap-nselibs \
|
||||
nmap-scripts \
|
||||
curl-dev
|
||||
curl-dev==7.64.0-r3
|
||||
|
||||
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/cameradar/ /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/cmd/cameradar/ /app/cameradar/
|
||||
|
||||
ENV CAMERADAR_CUSTOM_ROUTES="/app/dictionaries/routes"
|
||||
ENV CAMERADAR_CUSTOM_CREDENTIALS="/app/dictionaries/credentials.json"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Cameradar
|
||||
|
||||
<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 align="center">
|
||||
@@ -41,7 +41,7 @@
|
||||
* Launch automated dictionary attacks to get the **username and password** of the cameras
|
||||
* 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
|
||||
|
||||
@@ -49,13 +49,15 @@
|
||||
* [Configuration](#configuration)
|
||||
* [Output](#output)
|
||||
* [Check camera access](#check-camera-access)
|
||||
* [Command line options](#command-line-options)
|
||||
* [Command-line options](#command-line-options)
|
||||
* [Contribution](#contribution)
|
||||
* [Frequently Asked Questions](#frequently-asked-questions)
|
||||
* [License](#license)
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
@@ -64,7 +66,7 @@ docker run -t ullaakut/cameradar -t <target> <other 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`).
|
||||
* 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`.
|
||||
@@ -76,76 +78,26 @@ Only use this solution if for some reason using docker is not an option for you
|
||||
|
||||
### Dependencies
|
||||
|
||||
* `go`
|
||||
* `dep`
|
||||
|
||||
#### Installing dep
|
||||
|
||||
* OSX: `brew install dep` and `brew upgrade dep`
|
||||
* Others: Download the release package for your OS [here](https://github.com/golang/dep/releases)
|
||||
* `go` (> `1.10`)
|
||||
* `libcurl` development library (**[version has to be <7.66.0](https://github.com/Ullaakut/cameradar/issues/247)**)
|
||||
* For apt users: `apt install libcurl4-openssl-dev`
|
||||
|
||||
### Steps to install
|
||||
|
||||
Make sure you installed the dependencies mentionned above.
|
||||
|
||||
1. `go get github.com/ullaakut/cameradar`
|
||||
2. `cd $GOPATH/src/github.com/ullaakut/cameradar`
|
||||
3. `dep ensure`
|
||||
4. `cd cameradar`
|
||||
5. `go install`
|
||||
1. `go get github.com/Ullaakut/cameradar/v5`
|
||||
2. `cd $GOPATH/src/github.com/Ullaakut/cameradar`
|
||||
3. `cd cmd/cameradar`
|
||||
4. `go install`
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
|
||||
@@ -159,27 +111,27 @@ This will put the contents of your folder containing dictionaries in the docker
|
||||
|
||||
## 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"`
|
||||
* **"-p, --ports"**: (Default: `554,5554,8554`) Set custom ports.
|
||||
* **"-s, --speed"**: (Default: `4`) Set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. You might also want to keep it low to keep your discovery stealthy. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||
* **"-T, --timeout"**: (Default: `2000`) Set custom timeout value in miliseconds after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on very performant and reliable networks.
|
||||
* **"-s, --scan-speed"**: (Default: `4`) Set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. You might also want to keep it low to keep your discovery stealthy. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||
* **"-I, --attack-interval"**: (Default: `0ms`) Set custom interval after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on fast and reliable networks.
|
||||
* **"-T, --timeout"**: (Default: `2000ms`) Set custom timeout value after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on fast and reliable networks.
|
||||
* **"-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
|
||||
* **"-o, --nmap-output"**: (Default: `/tmp/cameradar_scan.xml`) Set custom nmap output path
|
||||
* **"-l, --log"**: Enable debug logs (nmap requests, curl describe requests, etc.)
|
||||
* **"-h"** : Display the usage information
|
||||
* **"-d, --debug"**: Enable debug logs
|
||||
* **"-v, --verbose"**: Enable verbose curl logs (not recommended for most use)
|
||||
* **"-h"**: Display the usage information
|
||||
|
||||
## Format input file
|
||||
|
||||
The file can contain IPs, hostnames, IP ranges and subnetwork, separated by newlines. Example:
|
||||
|
||||
```go
|
||||
```text
|
||||
0.0.0.0
|
||||
localhost
|
||||
192.17.0.0/16
|
||||
@@ -223,17 +175,23 @@ These variables are optional, allowing to replace the default dictionaries with
|
||||
|
||||
Default values: `<CAMERADAR_GOPATH>/dictionaries/routes` and `<CAMERADAR_GOPATH>/dictionaries/credentials.json`
|
||||
|
||||
### `CAMERADAR_SPEED`
|
||||
### `CAMERADAR_SCAN_SPEED`
|
||||
|
||||
This optional variable allows you to set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||
This optional variable allows you to set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a fast and reliable network. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||
|
||||
Default value: `4`
|
||||
|
||||
### `CAMERADAR_ATTACK_INTERVAL`
|
||||
|
||||
This optional variable allows you to set `custom interval` to wait between each attack in order to stay stealthy. It's recommended to increase it when attempting to scan a network that might be protected against bruteforce attacks. By default, there is no interval, in order to make attacks as fast as possible
|
||||
|
||||
Default value: `0ms`
|
||||
|
||||
### `CAMERADAR_TIMEOUT`
|
||||
|
||||
This optional variable allows you to set custom timeout value in miliseconds after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on very performant and reliable networks.
|
||||
This optional variable allows you to set custom timeout value after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on fast and reliable networks.
|
||||
|
||||
Default value: `2000`
|
||||
Default value: `2000ms`
|
||||
|
||||
### `CAMERADAR_LOGGING`
|
||||
|
||||
@@ -255,48 +213,46 @@ Your image will be called `cameradar` and NOT `ullaakut/cameradar`.
|
||||
|
||||
#### Go build
|
||||
|
||||
To build the project without docker:
|
||||
1. `go get github.com/Ullaakut/cameradar/v5`
|
||||
2. `cd $GOPATH/src/github.com/Ullaakut/cameradar`
|
||||
3. `cd cmd/cameradar`
|
||||
4. `go install`
|
||||
|
||||
1. Install dep
|
||||
* OSX: `brew install dep` and `brew upgrade dep`
|
||||
* Others: Download the release package for your OS [here](https://github.com/golang/dep/releases)
|
||||
2. `dep ensure`
|
||||
3. `go build` to build the library
|
||||
4. `cd cameradar && go build` to build the binary
|
||||
|
||||
The cameradar binary is now in the root of the directory.
|
||||
|
||||
See [the contribution document](/CONTRIBUTING.md) to get started.
|
||||
The cameradar binary is now in `$GOPATH/bin/cameradar`.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
> 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!
|
||||
|
||||
Maybe your cameras have been configured and the credentials / URL have been changed. Cameradar only guesses using default constructor values if a custom dictionary is not provided. You can use your own dictionaries in which you just have to add your credentials and RTSP routes. To do that, see how the [configuration](#configuration) works. Also, maybe your camera's credentials are not yet known, in which case if you find them it would be very nice to add them to the Cameradar dictionaries to help other people in the future.
|
||||
Maybe your cameras have been configured, and the credentials / URL have been changed. Cameradar only guesses using default constructor values if a custom dictionary is not provided. You can use your own dictionaries in which you just have to add your credentials and RTSP routes. To do that, see how the [configuration](#configuration) works. Also, maybe your camera's credentials are not yet known, in which case if you find them it would be very nice to add them to the Cameradar dictionaries to help other people in the future.
|
||||
|
||||
> 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 slower and less stable than the current version written in Golang. It is not recommended using it.
|
||||
|
||||
> 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/v5` 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](#go-build).
|
||||
|
||||
> 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.
|
||||
|
||||
> 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 that 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?
|
||||
|
||||
Cameradar supports both basic and digest authentication.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -306,7 +262,11 @@ Simply run `docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=1234
|
||||
|
||||
> 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
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
"github.com/pkg/errors"
|
||||
v "gopkg.in/go-playground/validator.v9"
|
||||
"github.com/Ullaakut/go-curl"
|
||||
)
|
||||
|
||||
// HTTP responses
|
||||
// HTTP responses.
|
||||
const (
|
||||
httpOK = 200
|
||||
httpUnauthorized = 401
|
||||
@@ -17,18 +15,218 @@ const (
|
||||
httpNotFound = 404
|
||||
)
|
||||
|
||||
// CURL RTSP request types
|
||||
// CURL RTSP request types.
|
||||
const (
|
||||
rtspDescribe = 2
|
||||
rtspSetup = 4
|
||||
)
|
||||
|
||||
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
|
||||
func doNotWrite([]uint8, interface{}) bool {
|
||||
return true
|
||||
// Authentication types.
|
||||
const (
|
||||
authNone = 0
|
||||
authBasic = 1
|
||||
authDigest = 2
|
||||
)
|
||||
|
||||
// Route that should never be a constructor default.
|
||||
const dummyRoute = "/0x8b6c42"
|
||||
|
||||
// Attack attacks the given targets and returns the accessed streams.
|
||||
func (s *Scanner) Attack(targets []Stream) ([]Stream, error) {
|
||||
if len(targets) == 0 {
|
||||
return nil, fmt.Errorf("no stream found")
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
s.term.StartStep("Validating that streams are accessible")
|
||||
streams = s.ValidateStreams(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 || !stream.Available {
|
||||
s.term.StartStepf("Second round of attacks")
|
||||
streams = s.AttackRoute(streams)
|
||||
|
||||
s.term.StartStep("Validating that streams are accessible")
|
||||
streams = s.ValidateStreams(streams)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.term.EndStep()
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
func routeAttack(c Curler, stream Stream, route string, timeout time.Duration, enableLogs bool) bool {
|
||||
// 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])
|
||||
time.Sleep(s.attackInterval)
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// AttackCredentials attempts to guess the provided targets' credentials using the given
|
||||
// dictionary or the default dictionary if none was provided by the user.
|
||||
func (s *Scanner) AttackCredentials(targets []Stream) []Stream {
|
||||
resChan := make(chan Stream)
|
||||
defer close(resChan)
|
||||
|
||||
for i := range targets {
|
||||
go s.attackCameraCredentials(targets[i], resChan)
|
||||
}
|
||||
|
||||
for range targets {
|
||||
attackResult := <-resChan
|
||||
if attackResult.CredentialsFound {
|
||||
targets = replace(targets, attackResult)
|
||||
}
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// AttackRoute attempts to guess the provided targets' streaming routes using the given
|
||||
// dictionary or the default dictionary if none was provided by the user.
|
||||
func (s *Scanner) AttackRoute(targets []Stream) []Stream {
|
||||
resChan := make(chan Stream)
|
||||
defer close(resChan)
|
||||
|
||||
for i := range targets {
|
||||
go s.attackCameraRoute(targets[i], resChan)
|
||||
}
|
||||
|
||||
for range targets {
|
||||
attackResult := <-resChan
|
||||
if attackResult.RouteFound {
|
||||
targets = replace(targets, attackResult)
|
||||
}
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// DetectAuthMethods attempts to guess the provided targets' authentication types, between
|
||||
// digest, basic auth or none at all.
|
||||
func (s *Scanner) DetectAuthMethods(targets []Stream) []Stream {
|
||||
for i := range targets {
|
||||
targets[i].AuthenticationType = s.detectAuthMethod(targets[i])
|
||||
time.Sleep(s.attackInterval)
|
||||
|
||||
var authMethod string
|
||||
switch targets[i].AuthenticationType {
|
||||
case authNone:
|
||||
authMethod = "no"
|
||||
case authBasic:
|
||||
authMethod = "basic"
|
||||
case authDigest:
|
||||
authMethod = "digest"
|
||||
}
|
||||
|
||||
s.term.Debugf("Stream %s uses %s authentication method\n", GetCameraRTSPURL(targets[i]), authMethod)
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
func (s *Scanner) attackCameraCredentials(target Stream, resChan chan<- Stream) {
|
||||
for _, username := range s.credentials.Usernames {
|
||||
for _, password := range s.credentials.Passwords {
|
||||
ok := s.credAttack(target, username, password)
|
||||
if ok {
|
||||
target.CredentialsFound = true
|
||||
target.Username = username
|
||||
target.Password = password
|
||||
resChan <- target
|
||||
return
|
||||
}
|
||||
time.Sleep(s.attackInterval)
|
||||
}
|
||||
}
|
||||
|
||||
target.CredentialsFound = false
|
||||
resChan <- target
|
||||
}
|
||||
|
||||
func (s *Scanner) attackCameraRoute(target Stream, resChan chan<- Stream) {
|
||||
// If the stream responds positively to the dummy route, it means
|
||||
// it doesn't require (or respect the RFC) a route and the attack
|
||||
// can be skipped.
|
||||
ok := s.routeAttack(target, dummyRoute)
|
||||
if ok {
|
||||
target.RouteFound = true
|
||||
target.Routes = append(target.Routes, "/")
|
||||
resChan <- target
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, bruteforce the routes.
|
||||
for _, route := range s.routes {
|
||||
ok := s.routeAttack(target, route)
|
||||
if ok {
|
||||
target.RouteFound = true
|
||||
target.Routes = append(target.Routes, route)
|
||||
}
|
||||
time.Sleep(s.attackInterval)
|
||||
}
|
||||
|
||||
resChan <- target
|
||||
}
|
||||
|
||||
func (s *Scanner) detectAuthMethod(stream Stream) int {
|
||||
c := s.curl.Duphandle()
|
||||
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%d/%s",
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
stream.Route(),
|
||||
)
|
||||
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// Send a request to the URL of the stream we want to attack.
|
||||
_ = c.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the stream URL.
|
||||
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
|
||||
return -1
|
||||
}
|
||||
|
||||
authType, err := c.Getinfo(curl.INFO_HTTPAUTH_AVAIL)
|
||||
if err != nil {
|
||||
s.term.Errorf("Getinfo failed: %v", err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if s.debug {
|
||||
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", authType)
|
||||
}
|
||||
|
||||
return authType.(int)
|
||||
}
|
||||
|
||||
func (s *Scanner) routeAttack(stream Stream, route string) bool {
|
||||
c := s.curl.Duphandle()
|
||||
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
stream.Username,
|
||||
@@ -38,246 +236,163 @@ func routeAttack(c Curler, stream Stream, route string, timeout time.Duration, e
|
||||
route,
|
||||
)
|
||||
|
||||
if enableLogs {
|
||||
// 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)
|
||||
}
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// 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)
|
||||
// Send a request to the URL of the stream we want to attack
|
||||
c.Setopt(curl.OPT_URL, attackURL)
|
||||
// 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))
|
||||
// Set proper authentication type.
|
||||
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
|
||||
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
|
||||
|
||||
// Perform the request
|
||||
// Send a request to the URL of the stream we want to attack.
|
||||
_ = c.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the stream URL.
|
||||
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get return code for the request
|
||||
// Get return code for the request.
|
||||
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
s.term.Errorf("Getinfo failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 s.debug {
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
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(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
username,
|
||||
password,
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
stream.Route,
|
||||
stream.Route(),
|
||||
)
|
||||
|
||||
if enableLogs {
|
||||
// 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)
|
||||
}
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// 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)
|
||||
// Send a request to the URL of the stream we want to attack
|
||||
c.Setopt(curl.OPT_URL, attackURL)
|
||||
// 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))
|
||||
// Set proper authentication type.
|
||||
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
|
||||
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(username, ":", password))
|
||||
|
||||
// Perform the request
|
||||
// Send a request to the URL of the stream we want to attack.
|
||||
_ = c.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the stream URL.
|
||||
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get return code for the request
|
||||
// Get return code for the request.
|
||||
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
s.term.Errorf("Getinfo failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 s.debug {
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
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(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
stream.Username,
|
||||
stream.Password,
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
stream.Route,
|
||||
stream.Route(),
|
||||
)
|
||||
|
||||
if enableLogs {
|
||||
// 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)
|
||||
}
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// 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)
|
||||
// Send a request to the URL of the stream we want to attack
|
||||
c.Setopt(curl.OPT_URL, attackURL)
|
||||
// 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))
|
||||
// Set proper authentication type.
|
||||
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
|
||||
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
|
||||
|
||||
c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333")
|
||||
// Send a request to the URL of the stream we want to attack.
|
||||
_ = c.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the stream URL.
|
||||
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspSetup)
|
||||
|
||||
// Perform the request
|
||||
_ = c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333")
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get return code for the request
|
||||
// Get return code for the request.
|
||||
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
s.term.Errorf("Getinfo failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's a 200, the stream is accessed successfully
|
||||
if s.debug {
|
||||
s.term.Debugln("SETUP", attackURL, "RTSP/1.0 >", rc)
|
||||
}
|
||||
|
||||
// If it's a 200, the stream is accessed successfully.
|
||||
if rc == httpOK {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateStreams tries to setup the stream to validate whether or not it is available
|
||||
func ValidateStreams(c Curler, targets []Stream, timeout time.Duration, log bool) ([]Stream, error) {
|
||||
for idx, target := range targets {
|
||||
targets[idx].Available = validateStream(c, target, timeout, log)
|
||||
}
|
||||
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))
|
||||
|
||||
return targets, nil
|
||||
// Enable verbose logs if verbose mode is on.
|
||||
if s.verbose {
|
||||
_ = c.Setopt(curl.OPT_VERBOSE, 1)
|
||||
} else {
|
||||
_ = c.Setopt(curl.OPT_VERBOSE, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func attackCameraCredentials(c Curler, target Stream, credentials Credentials, resultsChan chan<- Stream, timeout time.Duration, log bool) {
|
||||
for _, username := range credentials.Usernames {
|
||||
for _, password := range credentials.Passwords {
|
||||
ok := credAttack(c.Duphandle(), target, username, password, timeout, log)
|
||||
if ok {
|
||||
target.CredentialsFound = true
|
||||
target.Username = username
|
||||
target.Password = password
|
||||
resultsChan <- target
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
target.CredentialsFound = false
|
||||
resultsChan <- target
|
||||
}
|
||||
|
||||
func attackCameraRoute(c Curler, target Stream, routes Routes, resultsChan chan<- Stream, timeout time.Duration, log bool) {
|
||||
for _, route := range routes {
|
||||
ok := routeAttack(c.Duphandle(), target, route, timeout, log)
|
||||
if ok {
|
||||
target.RouteFound = true
|
||||
target.Route = route
|
||||
resultsChan <- target
|
||||
return
|
||||
}
|
||||
}
|
||||
target.RouteFound = false
|
||||
resultsChan <- target
|
||||
}
|
||||
|
||||
// AttackCredentials attempts to guess the provided targets' credentials using the given
|
||||
// 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) {
|
||||
attacks := make(chan Stream)
|
||||
defer close(attacks)
|
||||
|
||||
validate := v.New()
|
||||
for _, target := range targets {
|
||||
err := validate.Struct(target)
|
||||
if err != nil {
|
||||
return targets, errors.Wrap(err, "invalid targets")
|
||||
}
|
||||
|
||||
go attackCameraCredentials(c, target, credentials, attacks, timeout, log)
|
||||
}
|
||||
|
||||
attackResults := []Stream{}
|
||||
for range targets {
|
||||
attackResults = append(attackResults, <-attacks)
|
||||
}
|
||||
|
||||
for _, result := range attackResults {
|
||||
if result.CredentialsFound {
|
||||
targets = replace(targets, result)
|
||||
}
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
// AttackRoute attempts to guess the provided targets' streaming routes using the given
|
||||
// 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) {
|
||||
attacks := make(chan Stream)
|
||||
defer close(attacks)
|
||||
|
||||
validate := v.New()
|
||||
for _, target := range targets {
|
||||
err := validate.Struct(target)
|
||||
if err != nil {
|
||||
return targets, errors.Wrap(err, "invalid targets")
|
||||
}
|
||||
|
||||
go attackCameraRoute(c, target, routes, attacks, timeout, log)
|
||||
}
|
||||
|
||||
attackResults := []Stream{}
|
||||
for range targets {
|
||||
attackResults = append(attackResults, <-attacks)
|
||||
}
|
||||
|
||||
for _, result := range attackResults {
|
||||
if result.RouteFound {
|
||||
targets = replace(targets, result)
|
||||
}
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
|
||||
func doNotWrite([]uint8, interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
+487
-222
@@ -1,13 +1,13 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
"github.com/Ullaakut/disgo"
|
||||
"github.com/Ullaakut/go-curl"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -35,35 +35,127 @@ func (m *CurlerMock) Duphandle() Curler {
|
||||
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("no stream found"),
|
||||
},
|
||||
}
|
||||
|
||||
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: true,
|
||||
debug: true,
|
||||
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) {
|
||||
validStream1 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
validStream2 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
invalidStream := Stream{
|
||||
Device: "InvalidDevice",
|
||||
}
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
fakeCredentials = Credentials{
|
||||
Usernames: []string{"admin", "root"},
|
||||
Passwords: []string{"12345", "root"},
|
||||
}
|
||||
)
|
||||
|
||||
fakeTargets := []Stream{validStream1, validStream2}
|
||||
invalidTargets := []Stream{invalidStream}
|
||||
fakeCredentials := Credentials{
|
||||
Usernames: []string{"admin", "root"},
|
||||
Passwords: []string{"12345", "root"},
|
||||
}
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
testCases := []struct {
|
||||
targets []Stream
|
||||
credentials Credentials
|
||||
timeout time.Duration
|
||||
log bool
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
@@ -72,10 +164,10 @@ func TestAttackCredentials(t *testing.T) {
|
||||
invalidTargets bool
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErrMsg string
|
||||
}{
|
||||
// Credentials found
|
||||
{
|
||||
description: "Credentials found",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -84,8 +176,9 @@ func TestAttackCredentials(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Camera accessed
|
||||
{
|
||||
description: "Camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -94,19 +187,9 @@ func TestAttackCredentials(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Invalid targets
|
||||
{
|
||||
targets: invalidTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
description: "curl perform fails",
|
||||
|
||||
invalidTargets: true,
|
||||
|
||||
expectedErrMsg: "invalid targets",
|
||||
expectedStreams: invalidTargets,
|
||||
},
|
||||
// curl perform fails
|
||||
{
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -115,8 +198,9 @@ func TestAttackCredentials(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// curl getinfo fails
|
||||
{
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -125,94 +209,89 @@ func TestAttackCredentials(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Logging disabled
|
||||
{
|
||||
description: "Verbose mode disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
log: false,
|
||||
verbose: false,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Logging enabled
|
||||
{
|
||||
description: "Verbose mode enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
log: true,
|
||||
verbose: true,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
for i, test := range testCases {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
if !test.invalidTargets {
|
||||
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(test.status, test.getInfoErr)
|
||||
}
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
results, err := AttackCredentials(curlerMock, test.targets, test.credentials, test.timeout, test.log)
|
||||
|
||||
if len(test.expectedErrMsg) > 0 {
|
||||
if err == nil {
|
||||
fmt.Printf("unexpected success in AttackCredentials 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 AttackCredentials test, iteration %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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
|
||||
}
|
||||
if !test.invalidTargets {
|
||||
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(test.status, test.getInfoErr)
|
||||
}
|
||||
assert.Equal(t, true, foundStream, "wrong streams parsed")
|
||||
}
|
||||
}
|
||||
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
|
||||
curlerMock.AssertExpectations(t)
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
debug: 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) {
|
||||
validStream1 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
validStream2 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
invalidStream := Stream{
|
||||
Device: "InvalidDevice",
|
||||
}
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
fakeRoutes = Routes{"live.sdp", "media.amp"}
|
||||
)
|
||||
|
||||
fakeTargets := []Stream{validStream1, validStream2}
|
||||
fakeRoutes := Routes{"live.sdp", "media.amp"}
|
||||
invalidTargets := []Stream{invalidStream}
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
testCases := []struct {
|
||||
targets []Stream
|
||||
routes Routes
|
||||
timeout time.Duration
|
||||
log bool
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
@@ -221,10 +300,11 @@ func TestAttackRoute(t *testing.T) {
|
||||
invalidTargets bool
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErrMsg string
|
||||
expectedErr error
|
||||
}{
|
||||
// Route found
|
||||
{
|
||||
description: "Route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -233,8 +313,9 @@ func TestAttackRoute(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Route found
|
||||
{
|
||||
description: "Route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -243,8 +324,9 @@ func TestAttackRoute(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Camera accessed
|
||||
{
|
||||
description: "Camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -253,18 +335,9 @@ func TestAttackRoute(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Invalid targets
|
||||
{
|
||||
targets: invalidTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
invalidTargets: true,
|
||||
description: "curl perform fails",
|
||||
|
||||
expectedErrMsg: "invalid targets",
|
||||
expectedStreams: invalidTargets,
|
||||
},
|
||||
// curl perform fails
|
||||
{
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -273,8 +346,9 @@ func TestAttackRoute(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// curl getinfo fails
|
||||
{
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -283,99 +357,178 @@ func TestAttackRoute(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Logs disabled
|
||||
{
|
||||
description: "verbose mode disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
log: false,
|
||||
verbose: false,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Logs enabled
|
||||
{
|
||||
description: "verbose mode enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
log: true,
|
||||
verbose: true,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
curlerMock := &CurlerMock{}
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
if !test.invalidTargets {
|
||||
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(test.status, test.getInfoErr)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := AttackRoute(curlerMock, test.targets, test.routes, test.timeout, test.log)
|
||||
|
||||
if len(test.expectedErrMsg) > 0 {
|
||||
if err == nil {
|
||||
fmt.Printf("unexpected success in AttackRoute 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 AttackRoute test, iteration %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if !test.invalidTargets {
|
||||
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(test.status, test.getInfoErr)
|
||||
}
|
||||
|
||||
assert.Equal(t, true, foundStream, "wrong streams parsed")
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
debug: test.verbose,
|
||||
routes: test.routes,
|
||||
}
|
||||
|
||||
results := scanner.AttackRoute(test.targets)
|
||||
|
||||
assert.Len(t, results, len(test.expectedStreams))
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttackRoute_NoDummyRoute(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
fakeRoutes = Routes{"live.sdp", "media.amp"}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
routes Routes
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "Route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 401,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 200,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(nil)
|
||||
|
||||
// 404 on first call to the dummy route.
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(404, nil).Once()
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(test.status, nil)
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
routes: test.routes,
|
||||
}
|
||||
|
||||
results := scanner.AttackRoute(test.targets)
|
||||
|
||||
assert.Len(t, results, len(test.expectedStreams))
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStreams(t *testing.T) {
|
||||
validStream1 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
validStream2 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
unavailableStream := Stream{
|
||||
Device: "fakeDevice",
|
||||
Available: false,
|
||||
}
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
)
|
||||
|
||||
fakeTargets := []Stream{validStream1, validStream2}
|
||||
unavailableTargets := []Stream{unavailableStream}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
timeout time.Duration
|
||||
log bool
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
@@ -383,11 +536,9 @@ func TestValidateStreams(t *testing.T) {
|
||||
getInfoErr error
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErrMsg string
|
||||
}{
|
||||
// Route found
|
||||
{
|
||||
desc: "route found",
|
||||
description: "route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -396,9 +547,8 @@ func TestValidateStreams(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Route found
|
||||
{
|
||||
desc: "route found",
|
||||
description: "route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -407,9 +557,8 @@ func TestValidateStreams(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Camera accessed
|
||||
{
|
||||
desc: "camera accessed",
|
||||
description: "camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -418,20 +567,18 @@ func TestValidateStreams(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Unavailable stream
|
||||
{
|
||||
desc: "unavailable stream",
|
||||
description: "unavailable stream",
|
||||
|
||||
targets: unavailableTargets,
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 400,
|
||||
|
||||
expectedStreams: unavailableTargets,
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// curl perform fails
|
||||
{
|
||||
desc: "curl perform fails",
|
||||
description: "curl perform fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -440,9 +587,8 @@ func TestValidateStreams(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// curl getinfo fails
|
||||
{
|
||||
desc: "curl getinfo fails",
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
@@ -451,65 +597,184 @@ func TestValidateStreams(t *testing.T) {
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Logs disabled
|
||||
{
|
||||
desc: "logs disabled",
|
||||
description: "verbose disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
log: false,
|
||||
verbose: false,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
// Logs enabled
|
||||
{
|
||||
desc: "logs enabled",
|
||||
description: "verbose enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
log: true,
|
||||
verbose: true,
|
||||
|
||||
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.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(tC.performErr)
|
||||
if tC.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(tC.status, tC.getInfoErr)
|
||||
curlerMock.On("Perform").Return(test.performErr)
|
||||
if test.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
|
||||
}
|
||||
|
||||
results, err := ValidateStreams(curlerMock, tC.targets, tC.timeout, tC.log)
|
||||
|
||||
if len(tC.expectedErrMsg) > 0 {
|
||||
if err == nil {
|
||||
fmt.Printf("unexpected success in ValidateStream test, iteration %d. expected error: %s\n", i, tC.expectedErrMsg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assert.Contains(t, err.Error(), tC.expectedErrMsg, "wrong error message")
|
||||
} else {
|
||||
if err != nil {
|
||||
fmt.Printf("unexpected error in ValidateStream test, iteration %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
debug: test.verbose,
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectAuthenticationType(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
performErr error
|
||||
getInfoErr error
|
||||
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
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,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
getInfoErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl perform fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
performErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: false,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: true,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
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(test.status, test.getInfoErr)
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// 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
|
||||
// IP Cameras, often for surveillance.
|
||||
//
|
||||
// A simple example usage of the library can be found in
|
||||
// https://github.com/ullaakut/cameradar/tree/master/cameradar
|
||||
// https://github.com/Ullaakut/cameradar/tree/master/cameradar
|
||||
//
|
||||
// The example usage is complete enough for most users to
|
||||
// ignore the library, but for users with specific needs
|
||||
@@ -11,4 +11,4 @@
|
||||
// access cameras, or running their own network scan, this
|
||||
// library allows to use simple and performant methods to
|
||||
// attack streams.
|
||||
package cmrdr
|
||||
package cameradar
|
||||
@@ -1,238 +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. 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 {
|
||||
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)))
|
||||
}
|
||||
|
||||
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)
|
||||
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"))
|
||||
}
|
||||
} else {
|
||||
term.Infof("%s No streams were found. Please make sure that your target is on an accessible network.\n", style.Failure(style.SymbolCross))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v5"
|
||||
"github.com/Ullaakut/disgo"
|
||||
"github.com/Ullaakut/disgo/style"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
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("scan-speed", "s", 4, "The nmap speed preset to use for scanning (lower is stealthier)")
|
||||
pflag.DurationP("attack-interval", "I", 0, "The interval between each attack (i.e: 2000ms, higher is stealthier)")
|
||||
pflag.DurationP("timeout", "T", 2000*time.Millisecond, "The timeout to use for attack attempts (i.e: 2000ms)")
|
||||
pflag.BoolP("debug", "d", false, "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")
|
||||
fmt.Println("\tStealthily scanning a remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 -I 5000")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if len(viper.GetStringSlice("targets")) == 0 {
|
||||
pflag.Usage()
|
||||
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.WithScanSpeed(viper.GetInt("scan-speed")),
|
||||
cameradar.WithAttackInterval(viper.GetDuration("attack-interval")),
|
||||
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,7 +1,7 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
curl "github.com/Ullaakut/go-curl"
|
||||
)
|
||||
|
||||
// Curler is an interface that implements the CURL interface of the go-curl library
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
curl "github.com/Ullaakut/go-curl"
|
||||
)
|
||||
|
||||
func TestCurl(t *testing.T) {
|
||||
|
||||
@@ -8,36 +8,51 @@
|
||||
"admin1",
|
||||
"administrator",
|
||||
"Administrator",
|
||||
"aiphone",
|
||||
"Dinion",
|
||||
"root",
|
||||
"service",
|
||||
"supervisor",
|
||||
"ubnt"
|
||||
],
|
||||
"passwords" : [
|
||||
"passwords": [
|
||||
"",
|
||||
"111111",
|
||||
"1111111",
|
||||
"1234",
|
||||
"12345",
|
||||
"123456",
|
||||
"12345678",
|
||||
"4321",
|
||||
"666666",
|
||||
"6fJjMKYx",
|
||||
"888888",
|
||||
"9999",
|
||||
"admin",
|
||||
"administrator",
|
||||
"aiphone",
|
||||
"camera",
|
||||
"fliradmin",
|
||||
"GRwvcj8j",
|
||||
"hikvision",
|
||||
"hikadmin",
|
||||
"ikwd",
|
||||
"jvc",
|
||||
"kj3TqCWv",
|
||||
"meinsm",
|
||||
"pass",
|
||||
"password",
|
||||
"password123",
|
||||
"reolink",
|
||||
"root",
|
||||
"service",
|
||||
"supervisor",
|
||||
"system",
|
||||
"tlJwpbo6",
|
||||
"toor",
|
||||
"tp-link",
|
||||
"ubnt",
|
||||
"wbox123"
|
||||
"wbox123",
|
||||
"Y5eIMz3C"
|
||||
]
|
||||
}
|
||||
|
||||
+107
-37
@@ -1,53 +1,119 @@
|
||||
|
||||
/live/ch01_0
|
||||
0/1:1/main
|
||||
0/usrnm:pwd/main
|
||||
0/video1
|
||||
1
|
||||
1.AMP
|
||||
1/h264major
|
||||
1/stream1
|
||||
11
|
||||
12
|
||||
125
|
||||
1080p
|
||||
1440p
|
||||
480p
|
||||
4K
|
||||
666
|
||||
720p
|
||||
AVStream1_1
|
||||
CAM_ID.password.mp2
|
||||
CH001.sdp
|
||||
GetData.cgi
|
||||
HD
|
||||
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_name_for_stream_1_to_5
|
||||
api/mjpegvideo.cgi
|
||||
av0_0
|
||||
av2
|
||||
avc
|
||||
avn=2
|
||||
axis-media/media.amp
|
||||
axis-media/media.amp?camera=1
|
||||
axis-media/media.amp?videocodec=h264
|
||||
cam
|
||||
cam/realmonitor
|
||||
cam/realmonitor?channel=0&subtype=0
|
||||
cam/realmonitor?channel=1&subtype=0
|
||||
cam/realmonitor?channel=1&subtype=1
|
||||
cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
|
||||
cam0
|
||||
cam0_0
|
||||
cam0_1
|
||||
cam1
|
||||
cam1/h264
|
||||
cam1/h264/multicast
|
||||
cam1/mjpeg
|
||||
cam1/mpeg4
|
||||
cam1/mpeg4?user='username'&pwd='password'
|
||||
cam1/onvif-h264
|
||||
camera.stm
|
||||
ch0
|
||||
ch00/0
|
||||
ch001.sdp
|
||||
ch01.264
|
||||
ch01.264?
|
||||
ch01.264?ptype=tcp
|
||||
ch0_0.h264
|
||||
ch0_unicast_firststream
|
||||
ch0_unicast_secondstream
|
||||
ch1-s1
|
||||
channel1
|
||||
gnz_media/main
|
||||
h264
|
||||
h264.sdp
|
||||
h264/ch1/sub/av_stream
|
||||
h264/media.amp
|
||||
h264Preview_01_main
|
||||
h264Preview_01_sub
|
||||
h264_vga.sdp
|
||||
h264_stream
|
||||
image.mpg
|
||||
img/media.sav
|
||||
img/media.sav?channel=1
|
||||
img/video.asf
|
||||
img/video.sav
|
||||
ioImage/1
|
||||
ipcam.sdp
|
||||
ipcam_h264.sdp
|
||||
ipcam_mjpeg.sdp
|
||||
live
|
||||
live.sdp
|
||||
live/av0
|
||||
live/ch0
|
||||
live/ch00_0
|
||||
live/ch01_0
|
||||
live/h264
|
||||
live/main
|
||||
live/main0
|
||||
live/mpeg4
|
||||
live1.sdp
|
||||
live3.sdp
|
||||
live_mpeg4.sdp
|
||||
live_st1
|
||||
livestream
|
||||
livestream/
|
||||
main
|
||||
media
|
||||
media.amp
|
||||
media.amp?streamprofile=Profile1
|
||||
media/media.amp
|
||||
media/video1
|
||||
medias2
|
||||
mjpeg/media.smp
|
||||
mp4
|
||||
mpeg/media.amp
|
||||
mpeg4
|
||||
mpeg4/1/media.amp
|
||||
mpeg4/media.amp
|
||||
@@ -58,59 +124,63 @@ multicaststream
|
||||
now.mp4
|
||||
nph-h264.cgi
|
||||
nphMpeg4/g726-640x
|
||||
nphMpeg4/g726-640x48
|
||||
nphMpeg4/g726-640x480
|
||||
nphMpeg4/nil-320x240
|
||||
onvif-media/media.amp
|
||||
onvif1
|
||||
pass@10.0.0.5:6667/blinkhd
|
||||
play1.sdp
|
||||
play2.sdp
|
||||
profile2/media.smp
|
||||
profile5/media.smp
|
||||
rtpvideo1.sdp
|
||||
rtsp_live0
|
||||
rtsp_live1
|
||||
rtsp_live2
|
||||
rtsp_tunnel
|
||||
rtsph264
|
||||
rtsph2641080p
|
||||
snap.jpg
|
||||
stream
|
||||
stream/0
|
||||
stream/1
|
||||
stream/live.sdp
|
||||
stream.sdp
|
||||
stream1
|
||||
streaming/channels/0
|
||||
streaming/channels/1
|
||||
streaming/channels/101
|
||||
tcp/av0_0
|
||||
test
|
||||
tmpfs/auto.jpg
|
||||
trackID=1
|
||||
ucast/11
|
||||
udp/av0_0
|
||||
udp/unicast/aiphone_H264
|
||||
udpstream
|
||||
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=R5XFY888_channel=1_stream=0.sdp?real_stream
|
||||
user_defined
|
||||
v2
|
||||
video
|
||||
video.3gp
|
||||
video.h264
|
||||
video.mjpg
|
||||
video.mp4
|
||||
video.pro1
|
||||
video.pro2
|
||||
video.pro3
|
||||
video0
|
||||
video0.sdp
|
||||
video1
|
||||
video1.sdp
|
||||
video1+audio1
|
||||
videoMain
|
||||
videoinput_1/h264_1/media.stm
|
||||
videostream.asf
|
||||
vis
|
||||
wfov
|
||||
video.h264
|
||||
11
|
||||
12
|
||||
ch1-s1
|
||||
live3.sdp
|
||||
onvif-media/media.amp
|
||||
axis-media/media.amp
|
||||
axis-media/media.amp?videocodec=h264
|
||||
mpeg4/media.amp
|
||||
stream
|
||||
cam/realmonitor
|
||||
live
|
||||
video.pro2
|
||||
videoMain
|
||||
VideoInput/1/mpeg4/1
|
||||
VideoInput/1/h264/1
|
||||
video.pro3
|
||||
video.pro1
|
||||
video.mjpg
|
||||
h264_vga.sdp
|
||||
media.amp
|
||||
media
|
||||
ONVIF/MediaInput
|
||||
nphMpeg4/g726-640x48
|
||||
MediaInput/mpeg4
|
||||
MediaInput/h264
|
||||
Streaming/Channels/1
|
||||
ch0_0.h264
|
||||
rtsph2641080p
|
||||
live/av0
|
||||
cam1/onvif-h264
|
||||
ucast/11
|
||||
LowResolutionVideo
|
||||
1
|
||||
live/ch00_0
|
||||
medias2
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package cmrdr
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ullaakut/nmap"
|
||||
)
|
||||
|
||||
// Discover scans the target networks and tries to find RTSP streams within them.
|
||||
//
|
||||
// targets can be:
|
||||
//
|
||||
// - a subnet (e.g.: 172.16.100.0/24)
|
||||
// - an IP (e.g.: 172.16.100.10)
|
||||
// - a hostname (e.g.: localhost)
|
||||
// - a range of IPs (e.g.: 172.16.100.10-20)
|
||||
//
|
||||
// ports can be:
|
||||
//
|
||||
// - 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) {
|
||||
// Run nmap command to discover open ports on the specified targets & ports
|
||||
scanner, err := nmap.NewScanner(
|
||||
nmap.WithTargets(targets...),
|
||||
nmap.WithPorts(ports...),
|
||||
nmap.WithTimingTemplate(nmap.Timing(speed)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scan(scanner)
|
||||
}
|
||||
|
||||
func scan(scanner nmap.ScanRunner) ([]Stream, error) {
|
||||
results, err := scanner.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var streams []Stream
|
||||
// Get streams from nmap results
|
||||
for _, host := range results.Hosts {
|
||||
for _, port := range host.Ports {
|
||||
if port.Status() != "open" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(port.Service.Name, "rtsp") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, address := range host.Addresses {
|
||||
streams = append(streams, Stream{
|
||||
Device: port.Service.Product,
|
||||
Address: address.Addr,
|
||||
Port: port.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
module github.com/ullaakut/cameradar
|
||||
module github.com/Ullaakut/cameradar/v5
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Ullaakut/nmap v0.0.0-20190306183004-e38898a9bead // indirect
|
||||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/Ullaakut/disgo v0.3.1
|
||||
github.com/Ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd
|
||||
github.com/Ullaakut/nmap v2.0.0+incompatible
|
||||
github.com/VividCortex/ewma v1.1.1 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gernest/wow v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.12.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.16.0 // indirect
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.6 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/spf13/viper v1.3.1 // indirect
|
||||
github.com/ullaakut/disgo v0.3.0 // indirect
|
||||
github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70
|
||||
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead
|
||||
gopkg.in/go-playground/validator.v9 v9.27.0
|
||||
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/vbauerster/mpb v3.4.0+incompatible
|
||||
)
|
||||
|
||||
@@ -1,38 +1,100 @@
|
||||
github.com/Ullaakut/nmap v0.0.0-20190306183004-e38898a9bead h1:iclmd4In7CnuZGbbnnaeF1DtSePgXxN71pq5UNI1M7c=
|
||||
github.com/Ullaakut/nmap v0.0.0-20190306183004-e38898a9bead/go.mod h1:fkC066hwfcoKwlI7DS2ARTggSVtBTZYCjVH1TzuTMaQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/Ullaakut/disgo v0.3.1 h1:BGGVHynji41KGuGI02ztTCnILRvyzlvmiCRl5bBpjKk=
|
||||
github.com/Ullaakut/disgo v0.3.1/go.mod h1:/CSvpnYVSKOeh2dvUvx9cXshzz2t7T1/lRO/MrFj3fI=
|
||||
github.com/Ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd h1:CMe+dX1CL4pCXNytxIB2U1qp0xZObGMZosJhaQdUlUo=
|
||||
github.com/Ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd/go.mod h1:u8mVgpDT88IPIt1B+Tu8vkrcFfBKGcfGwS9I7wmvMh0=
|
||||
github.com/Ullaakut/nmap v2.0.0+incompatible h1:tNXub052dsnG8+yrgpph9nhVixIBdpRRgzvmQoc8eBA=
|
||||
github.com/Ullaakut/nmap v2.0.0+incompatible/go.mod h1:fkC066hwfcoKwlI7DS2ARTggSVtBTZYCjVH1TzuTMaQ=
|
||||
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
|
||||
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gernest/wow v0.1.0 h1:g9xdwCwP0+xgVYlA2sopI0gZHqXe7HjI/7/LykG4fks=
|
||||
github.com/gernest/wow v0.1.0/go.mod h1:dEPabJRi5BneI1Nev1VWo0ZlcTWibHWp43qxKms4elY=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
|
||||
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
@@ -41,31 +103,63 @@ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
|
||||
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ullaakut/disgo v0.0.0-20190310161027-e17c43d71b3d h1:tObr2ILgSQwrhpQRiVUKHtXF+0V5gYnnd/zBQGAmfuQ=
|
||||
github.com/ullaakut/disgo v0.0.0-20190310161027-e17c43d71b3d/go.mod h1:UOgLVyqihzJ7yihrHjYZikivT+AHb9NhT3r1OyPCJqg=
|
||||
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/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/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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/vbauerster/mpb v3.4.0+incompatible h1:mfiiYw87ARaeRW6x5gWwYRUawxaW1tLAD8IceomUCNw=
|
||||
github.com/vbauerster/mpb v3.4.0+incompatible/go.mod h1:zAHG26FUhVKETRu+MWqYXcI70POlC6N8up9p1dID7SU=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/validator.v9 v9.27.0 h1:wCg/0hk9RzcB0CYw8pYV6FiBYug1on0cpco9YZF8jqA=
|
||||
gopkg.in/go-playground/validator.v9 v9.27.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import "fmt"
|
||||
|
||||
func replace(streams []Stream, new Stream) []Stream {
|
||||
updatedSlice := streams[:0]
|
||||
var updatedSlice []Stream
|
||||
|
||||
for _, old := range streams {
|
||||
if old.Address == new.Address && old.Port == new.Port {
|
||||
@@ -16,12 +16,12 @@ func replace(streams []Stream, new Stream) []Stream {
|
||||
return updatedSlice
|
||||
}
|
||||
|
||||
// GetCameraRTSPURL generates a stream's RTSP URL
|
||||
// GetCameraRTSPURL generates a stream's RTSP URL.
|
||||
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 {
|
||||
return "http://" + stream.Address + "/"
|
||||
}
|
||||
|
||||
+22
-28
@@ -1,4 +1,4 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -10,25 +10,25 @@ func TestReplace(t *testing.T) {
|
||||
validStream1 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Port: 1,
|
||||
}
|
||||
|
||||
validStream2 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Port: 2,
|
||||
}
|
||||
|
||||
invalidStreamNoPort := Stream{
|
||||
invalidStream := Stream{
|
||||
Device: "invalidDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 0,
|
||||
Address: "anotherFakeAddress",
|
||||
Port: 3,
|
||||
}
|
||||
|
||||
invalidStreamNoPortModified := Stream{
|
||||
invalidStreamModified := Stream{
|
||||
Device: "updatedDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Address: "anotherFakeAddress",
|
||||
Port: 3,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -37,25 +37,21 @@ func TestReplace(t *testing.T) {
|
||||
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
// Valid baseline
|
||||
{
|
||||
streams: []Stream{validStream1, validStream2, invalidStreamNoPort},
|
||||
newStream: invalidStreamNoPortModified,
|
||||
streams: []Stream{validStream1, validStream2, invalidStream},
|
||||
newStream: invalidStreamModified,
|
||||
|
||||
expectedStreams: []Stream{validStream1, validStream2, invalidStreamNoPortModified},
|
||||
expectedStreams: []Stream{validStream1, validStream2, invalidStreamModified},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
streams := replace(test.streams, test.newStream)
|
||||
|
||||
for _, stream := range test.streams {
|
||||
foundStream := false
|
||||
for _, result := range streams {
|
||||
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(streams))
|
||||
|
||||
for _, expectedStream := range test.expectedStreams {
|
||||
assert.Contains(t, streams, expectedStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +61,7 @@ func TestGetCameraRTSPURL(t *testing.T) {
|
||||
Address: "1.2.3.4",
|
||||
Username: "ullaakut",
|
||||
Password: "ba69897483886f0d2b0afb6345b76c0c",
|
||||
Route: "cameradar.sdp",
|
||||
Routes: []string{"cameradar.sdp"},
|
||||
Port: 1337,
|
||||
}
|
||||
|
||||
@@ -74,16 +70,15 @@ func TestGetCameraRTSPURL(t *testing.T) {
|
||||
|
||||
expectedRTSPURL string
|
||||
}{
|
||||
// Valid baseline
|
||||
{
|
||||
stream: validStream,
|
||||
|
||||
expectedRTSPURL: "rtsp://ullaakut:ba69897483886f0d2b0afb6345b76c0c@1.2.3.4:1337/cameradar.sdp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
output := GetCameraRTSPURL(test.stream)
|
||||
assert.Equal(t, test.expectedRTSPURL, output, "wrong RTSP URL generated")
|
||||
assert.Equal(t, test.expectedRTSPURL, GetCameraRTSPURL(test.stream))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,15 +92,14 @@ func TestGetCameraAdminPanelURL(t *testing.T) {
|
||||
|
||||
expectedRTSPURL string
|
||||
}{
|
||||
// Valid baseline
|
||||
{
|
||||
stream: validStream,
|
||||
|
||||
expectedRTSPURL: "http://1.2.3.4/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
output := GetCameraAdminPanelURL(test.stream)
|
||||
assert.Equal(t, test.expectedRTSPURL, output, "wrong Admin Panel URL generated")
|
||||
assert.Equal(t, test.expectedRTSPURL, GetCameraAdminPanelURL(test.stream))
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 308 KiB |
+43
-30
@@ -1,14 +1,13 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var fs fileSystem = osFS{}
|
||||
@@ -32,48 +31,51 @@ type osFS struct{}
|
||||
func (osFS) Open(name string) (file, error) { return os.Open(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
|
||||
func LoadCredentials(path string) (Credentials, error) {
|
||||
var creds Credentials
|
||||
// LoadCredentials opens a dictionary file and returns its contents as a Credentials structure.
|
||||
func (s *Scanner) LoadCredentials() error {
|
||||
s.term.Debugf("Loading credentials dictionary from path %q\n", s.credentialDictionaryPath)
|
||||
|
||||
// Open & Read XML file
|
||||
content, err := ioutil.ReadFile(path)
|
||||
// Open & Read XML file.
|
||||
content, err := ioutil.ReadFile(s.credentialDictionaryPath)
|
||||
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
|
||||
err = json.Unmarshal(content, &creds)
|
||||
// Unmarshal content of JSON file into data structure.
|
||||
err = json.Unmarshal(content, &s.credentials)
|
||||
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
|
||||
func LoadRoutes(path string) (Routes, error) {
|
||||
file, err := os.Open(path)
|
||||
// LoadRoutes opens a dictionary file and returns its contents as a Routes structure.
|
||||
func (s *Scanner) LoadRoutes() error {
|
||||
s.term.Debugf("Loading routes dictionary from path %q\n", s.routeDictionaryPath)
|
||||
|
||||
file, err := os.Open(s.routeDictionaryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("unable to open dictionary: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var routes Routes
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return creds, err
|
||||
@@ -82,28 +84,39 @@ func ParseCredentialsFromString(content string) (Credentials, error) {
|
||||
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 {
|
||||
return strings.Split(content, "\n")
|
||||
}
|
||||
|
||||
// ParseTargetsFile parses an input file containing hosts to targets
|
||||
func ParseTargetsFile(path string) ([]string, error) {
|
||||
// LoadTargets parses the file containing hosts to targets, if the targets are
|
||||
// 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)
|
||||
if err != nil {
|
||||
return []string{path}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return []string{path}, err
|
||||
return fmt.Errorf("unable to open targets file %q: %v", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
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
@@ -1,12 +1,15 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Ullaakut/disgo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -91,91 +94,79 @@ func TestLoadCredentials(t *testing.T) {
|
||||
Passwords: []string{"12345", "root"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
input []byte
|
||||
fileExists bool
|
||||
|
||||
expectedOutput Credentials
|
||||
expectedErrMsg string
|
||||
expectedCredentials Credentials
|
||||
expectedErr error
|
||||
}{
|
||||
// Valid baseline
|
||||
{
|
||||
fileExists: true,
|
||||
input: credentialsJSONString,
|
||||
expectedOutput: validCredentials,
|
||||
description: "Valid baseline",
|
||||
|
||||
fileExists: true,
|
||||
input: credentialsJSONString,
|
||||
expectedCredentials: validCredentials,
|
||||
},
|
||||
// File does not exist
|
||||
{
|
||||
fileExists: false,
|
||||
input: credentialsJSONString,
|
||||
expectedErrMsg: "could not read credentials dictionary file at",
|
||||
description: "File does not exist",
|
||||
|
||||
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,
|
||||
input: []byte("not json"),
|
||||
expectedErrMsg: "invalid character",
|
||||
description: "Invalid format",
|
||||
|
||||
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,
|
||||
input: []byte("{\"invalid\":\"json\"}"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml"
|
||||
// create file
|
||||
if test.fileExists {
|
||||
_, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
for i, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml"
|
||||
// create file.
|
||||
if test.fileExists {
|
||||
_, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
}
|
||||
|
||||
assert.Equal(t, true, foundUsername, "wrong usernames parsed")
|
||||
}
|
||||
|
||||
for _, expectedPassword := range test.expectedOutput.Passwords {
|
||||
foundPassword := false
|
||||
for _, password := range result.Passwords {
|
||||
if password == expectedPassword {
|
||||
foundPassword = true
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
validRoutes := Routes{"admin", "root"}
|
||||
|
||||
testCases := []struct {
|
||||
input []byte
|
||||
fileExists bool
|
||||
tests := []struct {
|
||||
description string
|
||||
input []byte
|
||||
fileExists bool
|
||||
|
||||
expectedOutput Routes
|
||||
expectedErrMsg string
|
||||
expectedRoutes Routes
|
||||
expectedErr error
|
||||
}{
|
||||
// Valid baseline
|
||||
{
|
||||
description: "Valid baseline",
|
||||
|
||||
fileExists: true,
|
||||
input: routesJSONString,
|
||||
expectedOutput: validRoutes,
|
||||
expectedRoutes: validRoutes,
|
||||
},
|
||||
// File does not exist
|
||||
{
|
||||
fileExists: false,
|
||||
input: routesJSONString,
|
||||
expectedErrMsg: "no such file or directory",
|
||||
description: "File does not exist",
|
||||
|
||||
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,
|
||||
input: []byte(""),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml"
|
||||
for i, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml"
|
||||
|
||||
// create file
|
||||
if test.fileExists {
|
||||
_, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
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
|
||||
}
|
||||
// Create file.
|
||||
if test.fileExists {
|
||||
_, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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 {
|
||||
str string
|
||||
expectedResult Credentials
|
||||
tests := []struct {
|
||||
str string
|
||||
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\"]}",
|
||||
expectedResult: defaultCredentials,
|
||||
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\"]}",
|
||||
expectedCredentials: defaultCredentials,
|
||||
},
|
||||
{
|
||||
str: "{}",
|
||||
expectedResult: Credentials{},
|
||||
str: "{}",
|
||||
expectedCredentials: Credentials{},
|
||||
},
|
||||
{
|
||||
str: "{\"invalid_field\":42}",
|
||||
expectedResult: Credentials{},
|
||||
str: "{\"invalid_field\":42}",
|
||||
expectedCredentials: Credentials{},
|
||||
},
|
||||
{
|
||||
str: "not json",
|
||||
expectedResult: Credentials{},
|
||||
str: "not json",
|
||||
expectedCredentials: Credentials{},
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
|
||||
for _, test := range tests {
|
||||
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) {
|
||||
testCases := []struct {
|
||||
tests := []struct {
|
||||
str string
|
||||
expectedResult Routes
|
||||
expectedRoutes Routes
|
||||
}{
|
||||
{
|
||||
str: "a\nb\nc",
|
||||
expectedResult: []string{"a", "b", "c"},
|
||||
expectedRoutes: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
str: "a",
|
||||
expectedResult: []string{"a"},
|
||||
expectedRoutes: []string{"a"},
|
||||
},
|
||||
{
|
||||
str: "",
|
||||
expectedResult: []string{""},
|
||||
expectedRoutes: []string{""},
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
parsedRoutes := ParseRoutesFromString(test.str)
|
||||
assert.Equal(t, test.expectedResult, parsedRoutes, "unexpected result, parse error")
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.expectedRoutes, ParseRoutesFromString(test.str))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsFile(t *testing.T) {
|
||||
func TestLoadTargets(t *testing.T) {
|
||||
|
||||
oldFS := fs
|
||||
mfs := &mockedFS{}
|
||||
@@ -355,65 +342,90 @@ func TestParseTargetsFile(t *testing.T) {
|
||||
fs = oldFS
|
||||
}()
|
||||
|
||||
testCases := []struct {
|
||||
input string
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []string
|
||||
|
||||
fileExists bool
|
||||
openError bool
|
||||
readError bool
|
||||
|
||||
expectedResult []string
|
||||
expectedError error
|
||||
expectedTargets []string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
input: "0.0.0.0",
|
||||
description: "not a file",
|
||||
|
||||
targets: []string{"0.0.0.0"},
|
||||
|
||||
fileExists: false,
|
||||
|
||||
expectedResult: []string{"0.0.0.0"},
|
||||
expectedError: nil,
|
||||
expectedTargets: []string{"0.0.0.0"},
|
||||
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,
|
||||
|
||||
expectedResult: []string{"0.0.0.0", "localhost", "192.17.0.0/16", "192.168.1.140-255", "192.168.2-3.0-255"},
|
||||
expectedError: nil,
|
||||
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,
|
||||
},
|
||||
{
|
||||
input: "test_does_not_really_exist",
|
||||
description: "open error",
|
||||
|
||||
targets: []string{"test_does_not_really_exist"},
|
||||
|
||||
fileExists: true,
|
||||
openError: true,
|
||||
|
||||
expectedResult: []string{"test_does_not_really_exist"},
|
||||
expectedError: os.ErrNotExist,
|
||||
expectedTargets: []string{"test_does_not_really_exist"},
|
||||
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,
|
||||
readError: true,
|
||||
|
||||
expectedResult: []string{"test_does_not_really_exist"},
|
||||
expectedError: os.ErrNotExist,
|
||||
expectedTargets: []string{"test_does_not_really_exist"},
|
||||
expectedError: errors.New("unable to read targets file \"test_does_not_really_exist\": file does not exist"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
mfs.fileExists = test.fileExists
|
||||
mfs.openError = test.openError
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
mfs.fileExists = test.fileExists
|
||||
mfs.openError = test.openError
|
||||
|
||||
mfs.fileMock = &fileMock{
|
||||
readError: test.readError,
|
||||
}
|
||||
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 = &fileMock{
|
||||
readError: test.readError,
|
||||
}
|
||||
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")
|
||||
|
||||
result, err := ParseTargetsFile(test.input)
|
||||
assert.Equal(t, test.expectedResult, result, "unexpected result, parse error")
|
||||
assert.Equal(t, test.expectedError, err, "unexpected error")
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
targets: test.targets,
|
||||
}
|
||||
|
||||
err := scanner.LoadTargets()
|
||||
assert.Equal(t, test.expectedTargets, scanner.targets)
|
||||
assert.Equal(t, test.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import "time"
|
||||
|
||||
// Stream represents a camera's RTSP stream
|
||||
type Stream struct {
|
||||
Device string `json:"device"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Route string `json:"route"`
|
||||
Address string `json:"address" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
Device string `json:"device"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Routes []string `json:"route"`
|
||||
Address string `json:"address" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
|
||||
CredentialsFound bool `json:"credentials_found"`
|
||||
RouteFound bool `json:"route_found"`
|
||||
Available bool `json:"available"`
|
||||
|
||||
AuthenticationType int `json:"authentication_type"`
|
||||
}
|
||||
|
||||
// Route returns this stream's route if there is one.
|
||||
func (s Stream) Route() string {
|
||||
if len(s.Routes) > 0 {
|
||||
return s.Routes[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Credentials is a map of credentials
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/Ullaakut/nmap"
|
||||
)
|
||||
|
||||
// Scan scans the target networks and tries to find RTSP streams within them.
|
||||
//
|
||||
// targets can be:
|
||||
//
|
||||
// - a subnet (e.g.: 172.16.100.0/24)
|
||||
// - an IP (e.g.: 172.16.100.10)
|
||||
// - a hostname (e.g.: localhost)
|
||||
// - a range of IPs (e.g.: 172.16.100.10-20)
|
||||
//
|
||||
// ports can be:
|
||||
//
|
||||
// - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554)
|
||||
func (s *Scanner) Scan() ([]Stream, error) {
|
||||
s.term.StartStep("Scanning the network")
|
||||
|
||||
// Run nmap command to discover open ports on the specified targets & ports.
|
||||
nmapScanner, err := nmap.NewScanner(
|
||||
nmap.WithTargets(s.targets...),
|
||||
nmap.WithPorts(s.ports...),
|
||||
nmap.WithServiceInfo(),
|
||||
nmap.WithTimingTemplate(nmap.Timing(s.scanSpeed)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, s.term.FailStepf("unable to create network scanner: %v", err)
|
||||
}
|
||||
|
||||
return s.scan(nmapScanner)
|
||||
}
|
||||
|
||||
func (s *Scanner) scan(nmapScanner nmap.ScanRunner) ([]Stream, error) {
|
||||
results, warnings, err := nmapScanner.Run()
|
||||
if err != nil {
|
||||
return nil, s.term.FailStepf("error while scanning network: %v", err)
|
||||
}
|
||||
|
||||
for _, warning := range warnings {
|
||||
s.term.Infoln("[Nmap Warning]", warning)
|
||||
}
|
||||
|
||||
// Get streams from nmap results.
|
||||
var streams []Stream
|
||||
for _, host := range results.Hosts {
|
||||
for _, port := range host.Ports {
|
||||
if port.Status() != "open" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(port.Service.Name, "rtsp") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, address := range host.Addresses {
|
||||
streams = append(streams, Stream{
|
||||
Device: port.Service.Product,
|
||||
Address: address.Addr,
|
||||
Port: port.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.term.Debugf("Found %d RTSP streams\n", len(streams))
|
||||
|
||||
s.term.EndStep()
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package cmrdr
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ullaakut/nmap"
|
||||
"github.com/Ullaakut/disgo"
|
||||
|
||||
"github.com/Ullaakut/nmap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -14,16 +17,42 @@ type nmapMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *nmapMock) Run() (*nmap.Run, error) {
|
||||
func (m *nmapMock) Run() (*nmap.Run, []string, error) {
|
||||
args := m.Called()
|
||||
|
||||
if args.Get(0) != nil {
|
||||
return args.Get(0).(*nmap.Run), args.Error(1)
|
||||
if args.Get(0) != nil && args.Get(1) != nil {
|
||||
return args.Get(0).(*nmap.Run), args.Get(1).([]string), args.Error(2)
|
||||
}
|
||||
return nil, args.Error(1)
|
||||
return nil, nil, args.Error(2)
|
||||
}
|
||||
|
||||
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 {
|
||||
description string
|
||||
|
||||
@@ -32,8 +61,8 @@ func TestDiscover(t *testing.T) {
|
||||
speed int
|
||||
removePath bool
|
||||
|
||||
expectedErr error
|
||||
expectedResult []Stream
|
||||
expectedErr error
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
description: "create new scanner and call scan, no error",
|
||||
@@ -48,7 +77,7 @@ func TestDiscover(t *testing.T) {
|
||||
removePath: true,
|
||||
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", "")
|
||||
}
|
||||
|
||||
result, err := Discover(test.targets, test.ports, test.speed)
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
targets: test.targets,
|
||||
ports: test.ports,
|
||||
scanSpeed: test.speed,
|
||||
}
|
||||
|
||||
result, err := scanner.Scan()
|
||||
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Equal(t, test.expectedResult, result)
|
||||
assert.Equal(t, test.expectedStreams, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
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,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
func TestInternalScan(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
nmapResult *nmap.Run
|
||||
nmapError error
|
||||
|
||||
nmapResult *nmap.Run
|
||||
nmapWarnings []string
|
||||
nmapError error
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErr error
|
||||
@@ -280,18 +294,23 @@ func TestScan(t *testing.T) {
|
||||
{
|
||||
description: "scan failed",
|
||||
|
||||
nmapError: errors.New("scan failed"),
|
||||
expectedErr: errors.New("scan failed"),
|
||||
nmapError: errors.New("scan failed"),
|
||||
nmapWarnings: []string{"invalid host"},
|
||||
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) {
|
||||
nmapMock := &nmapMock{}
|
||||
|
||||
nmapMock.On("Run").Return(test.nmapResult, test.nmapError)
|
||||
nmapMock.On("Run").Return(test.nmapResult, test.nmapWarnings, 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.expectedStreams, results, "wrong streams parsed")
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/Ullaakut/disgo"
|
||||
"github.com/Ullaakut/disgo/style"
|
||||
curl "github.com/Ullaakut/go-curl"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCredentialDictionaryPath = "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/credentials.json"
|
||||
defaultRouteDictionaryPath = "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/routes"
|
||||
)
|
||||
|
||||
// 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
|
||||
scanSpeed int
|
||||
attackInterval time.Duration
|
||||
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: defaultCredentialDictionaryPath,
|
||||
routeDictionaryPath: defaultRouteDictionaryPath,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(scanner)
|
||||
}
|
||||
|
||||
gopath := os.Getenv("GOPATH")
|
||||
if gopath == "" && scanner.credentialDictionaryPath == defaultCredentialDictionaryPath && scanner.routeDictionaryPath == defaultRouteDictionaryPath {
|
||||
disgo.Errorln(style.Failure("No $GOPATH was found.\nDictionaries may not be loaded properly, please set your $GOPATH to use the default dictionaries."))
|
||||
}
|
||||
|
||||
scanner.credentialDictionaryPath = os.ExpandEnv(scanner.credentialDictionaryPath)
|
||||
scanner.routeDictionaryPath = os.ExpandEnv(scanner.routeDictionaryPath)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WithScanSpeed 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 WithScanSpeed(speed int) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.scanSpeed = speed
|
||||
}
|
||||
}
|
||||
|
||||
// WithAttackInterval specifies the interval of time during which Cameradar
|
||||
// should wait between each attack attempt during bruteforcing.
|
||||
// Setting a high value for this obviously makes attacks much slower.
|
||||
func WithAttackInterval(interval time.Duration) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.attackInterval = interval
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
curl "github.com/Ullaakut/go-curl"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []string
|
||||
ports []string
|
||||
debug bool
|
||||
verbose bool
|
||||
customCredentials string
|
||||
customRoutes string
|
||||
speed int
|
||||
attackInterval time.Duration
|
||||
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,
|
||||
},
|
||||
{
|
||||
description: "gopath not set and default dicts",
|
||||
|
||||
customCredentials: defaultCredentialDictionaryPath,
|
||||
customRoutes: defaultRouteDictionaryPath,
|
||||
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Temporarily empty the gopath for testing purposes.
|
||||
defer os.Setenv("GOPATH", os.Getenv("GOPATH"))
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
os.Setenv("GOPATH", "")
|
||||
|
||||
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 == "" {
|
||||
test.customCredentials = generateTmpFileName(i, "creds")
|
||||
ioutil.WriteFile(test.customCredentials, []byte(`{"usernames":["admin"],"passwords":["admin"]}`), 0644)
|
||||
}
|
||||
|
||||
if !test.loadRoutesFail && test.customRoutes == "" {
|
||||
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),
|
||||
WithScanSpeed(test.speed),
|
||||
WithAttackInterval(test.attackInterval),
|
||||
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.scanSpeed)
|
||||
assert.Equal(t, test.attackInterval, scanner.attackInterval)
|
||||
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)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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"))
|
||||
}
|
||||
|
||||
s.term.Infoln("\tRTSP routes:")
|
||||
if stream.RouteFound {
|
||||
for _, route := range stream.Routes {
|
||||
s.term.Infoln(style.Success("\t\t\t\t/" + route))
|
||||
}
|
||||
} else {
|
||||
s.term.Infoln(style.Failure("not found"))
|
||||
}
|
||||
|
||||
s.term.Info("\n\n")
|
||||
}
|
||||
|
||||
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
@@ -0,0 +1,186 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/Ullaakut/disgo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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,
|
||||
Routes: []string{"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Ullaakut/disgo/style"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/Ullaakut/disgo"
|
||||
"github.com/vbauerster/mpb"
|
||||
"github.com/vbauerster/mpb/decor"
|
||||
)
|
||||
|
||||
const dictionaryURL = "https://community.geniusvision.net/platform/cprndr/manulist"
|
||||
|
||||
var rtspURLsFound sync.Map
|
||||
|
||||
func main() {
|
||||
if err := updateDictionary(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func updateDictionary() error {
|
||||
disgo.SetTerminalOptions(disgo.WithColors(true), disgo.WithDebug(true))
|
||||
|
||||
disgo.StartStep("Fetching dictionary list")
|
||||
resp, err := http.Get(dictionaryURL)
|
||||
if err != nil {
|
||||
return disgo.FailStepf("unable to download dictionaries: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
disgo.StartStep("Parsing dictionary list")
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return disgo.FailStepf("unable to read from dictionary list: %v", err)
|
||||
}
|
||||
|
||||
var vendorURLs []string
|
||||
doc.Find("td.simpletable a").Each(func(i int, s *goquery.Selection) {
|
||||
url, ok := s.Attr("href")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if url != "javascript:void(0)" {
|
||||
vendorURLs = append(vendorURLs, url)
|
||||
}
|
||||
})
|
||||
|
||||
disgo.StartStep("Loading current cameradar dictionary")
|
||||
currentDictionary, err := ioutil.ReadFile("dictionaries/routes")
|
||||
if err != nil {
|
||||
return disgo.FailStepf("unable to read current dictionary: %v", err)
|
||||
}
|
||||
|
||||
dictionaryEntries := bytes.Split(currentDictionary, []byte("\n"))
|
||||
|
||||
for _, rtspURL := range dictionaryEntries {
|
||||
rtspURLsFound.Store(string(rtspURL), struct{}{})
|
||||
}
|
||||
|
||||
disgo.Debugf("Current dictionary has %d entries\n", len(dictionaryEntries))
|
||||
disgo.EndStep()
|
||||
|
||||
p := mpb.New(mpb.WithWidth(64))
|
||||
name := fmt.Sprintf("Fetching default routes from %d constructors:", len(vendorURLs))
|
||||
bar := p.AddBar(int64(len(vendorURLs)),
|
||||
// set custom bar style, default one is "[=>-]"
|
||||
mpb.BarStyle("╢▌▌░╟"),
|
||||
mpb.PrependDecorators(
|
||||
// display our name with one space on the right
|
||||
decor.Name(name, decor.WC{W: len(name), C: decor.DidentRight}),
|
||||
),
|
||||
mpb.AppendDecorators(decor.Percentage()),
|
||||
)
|
||||
|
||||
for _, url := range vendorURLs {
|
||||
go loadRoutes(url, bar)
|
||||
}
|
||||
|
||||
p.Wait()
|
||||
|
||||
disgo.StartStep("Converting found routes into proper data model")
|
||||
|
||||
var rtspURLs []string
|
||||
rtspURLsFound.Range(func(rtspURL, _ interface{}) bool {
|
||||
disgo.Infoln("Adding URL", rtspURL.(string))
|
||||
rtspURLs = append(rtspURLs, rtspURL.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
sort.Slice(rtspURLs, func(a, b int) bool {
|
||||
return rtspURLs[a] < rtspURLs[b]
|
||||
})
|
||||
|
||||
disgo.EndStep()
|
||||
|
||||
if len(dictionaryEntries) < len(rtspURLs) {
|
||||
disgo.Infof("%s Saving them in cameradar default dictionary.\n", style.Success("Found ", len(rtspURLs)-len(dictionaryEntries), " new entries!"))
|
||||
saveRoutes(rtspURLs)
|
||||
} else {
|
||||
disgo.Infoln(style.Success("No new entry found, dictionary up-to-date! :)"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRoutes(url string, bar *mpb.Bar) {
|
||||
defer bar.IncrBy(1)
|
||||
|
||||
var (
|
||||
failureCounter int
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
for failureCounter < 5 {
|
||||
resp, err = http.Get(url)
|
||||
if err != nil {
|
||||
failureCounter++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if failureCounter == 5 {
|
||||
disgo.Errorln("Request failed 5 times in a row, giving up on this vendor")
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
disgo.Errorf("unable to read from dictionary list for URL %q: %v\n", url, err)
|
||||
return
|
||||
}
|
||||
|
||||
doc.Find("tr.simpletable td.simpletable:nth-child(4) a").Each(func(i int, s *goquery.Selection) {
|
||||
rtspURL := s.Text()
|
||||
|
||||
if strings.HasPrefix(rtspURL, "(") && strings.HasSuffix(rtspURL, ")") {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rtspURL, "[") && strings.HasSuffix(rtspURL, "]") {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rtspURL, "http://") {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip the port and only get the route.
|
||||
if strings.HasPrefix(rtspURL, "rtsp://ip-addr:") {
|
||||
routeAndPort := strings.TrimSpace(strings.TrimPrefix(rtspURL, "rtsp://ip-addr:"))
|
||||
route := strings.TrimLeft(routeAndPort, "0123456789/")
|
||||
rtspURLsFound.Store(route, struct{}{})
|
||||
return
|
||||
}
|
||||
|
||||
switch rtspURL {
|
||||
case "",
|
||||
"rtsp://ip-addr/",
|
||||
"rtsp://ip-addr",
|
||||
"rtsp://ip-addr:pass@10.0.0.5:6667/blinkhd":
|
||||
return
|
||||
default:
|
||||
route := strings.TrimSpace(strings.TrimPrefix(rtspURL, "rtsp://ip-addr/"))
|
||||
rtspURLsFound.Store(route, struct{}{})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func saveRoutes(rtspURLs []string) {
|
||||
contents := strings.Join(rtspURLs, "\n")
|
||||
|
||||
disgo.StartStep("Writing new dictionary file")
|
||||
err := ioutil.WriteFile("dictionaries/routes", []byte(contents), 0644)
|
||||
if err != nil {
|
||||
disgo.FailStepf("unable to write dictionnary: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user