Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 212ac2f0d5 | |||
| 2e49587cc2 | |||
| 47285675b9 | |||
| 2678df2e4c | |||
| 862e9f3de9 | |||
| 260a9645be | |||
| 1d5d606085 | |||
| c249be1cc0 | |||
| 1ec3a5fe44 | |||
| 3b082ea736 | |||
| b6ebd468c6 | |||
| ceb210f281 | |||
| fcb627dccd | |||
| 098460702b | |||
| 5849898283 | |||
| 878ca9f032 | |||
| 24f86b74f5 | |||
| a8c1c8c63b | |||
| 1ff17c429b | |||
| 145724bc95 | |||
| 5aefc9831d | |||
| cf3ca440b9 | |||
| 4109a4405d | |||
| 055dc69158 | |||
| 1ea9850842 | |||
| 6e92eecdf6 | |||
| 844f1e31af | |||
| fd83be9d95 | |||
| 456f7fffc5 | |||
| 541d64168d | |||
| 26c4c80fd2 | |||
| bcc8099f91 | |||
| 6392dcd9a0 | |||
| 916e1713d8 | |||
| 08fcfcdac8 | |||
| 20daf73371 | |||
| b909643c21 | |||
| 5a0ee4aaa7 | |||
| 8289f1edda | |||
| 74672f6625 | |||
| c1ea6b167c | |||
| 71679691c4 | |||
| cbf6f647aa | |||
| fb9c5afc5f | |||
| 5d2626b639 | |||
| 2399df693d | |||
| df44c7d6f1 | |||
| 6d296b84d5 | |||
| 1dadb93452 | |||
| 6ea4f6e123 | |||
| 5a8417cf18 | |||
| 4e922a2a48 | |||
| 3ef48a97cf | |||
| 35d629d8ce | |||
| dba1391a08 | |||
| 961d34d05a | |||
| 5d0c21c5d9 | |||
| 71046216ce | |||
| 89647ae457 | |||
| 216d30fd45 | |||
| 82e36e1fd3 | |||
| 34994e615a | |||
| 6daceaeb2b | |||
| 50da5ea82d | |||
| da7fb6cd49 | |||
| cfa90b36d8 | |||
| 72fb21b132 | |||
| 948bfce5a0 | |||
| 049a43ace2 | |||
| 1c845d2b3c | |||
| cb74761675 | |||
| be63c6a231 | |||
| eab18925c7 | |||
| 4c9d23acb1 | |||
| ecdac00145 | |||
| 2555a86f5f | |||
| ebce965730 | |||
| a2af1329d7 | |||
| bf3a967fad | |||
| 3dcc80a0e8 | |||
| 624ff8bc1b | |||
| 59f51f6149 | |||
| b4090b8301 | |||
| 02b58ad1a9 | |||
| 81b7e893dc | |||
| 8c6c94cc34 | |||
| fac60679bc | |||
| 55122d523c | |||
| 5825f14ef1 | |||
| 60c2f1f18c | |||
| 44e3911e01 | |||
| 28f642d39f | |||
| 4dfe99064b | |||
| 097cbe3df3 | |||
| 3123e34076 | |||
| c334ea9f91 | |||
| bfecea00ad | |||
| 1c7c462771 | |||
| 308ba24e90 | |||
| 4a8f6550af | |||
| c56cce6319 | |||
| 832e4f9fa2 | |||
| 63008d19af | |||
| fbd78301a0 | |||
| 2961d68200 | |||
| f86683d9ca | |||
| 58bcfb9ee5 | |||
| 5be5124e70 | |||
| b8291710d9 | |||
| b51a8da125 | |||
| 2a0882869b | |||
| c3d690371b | |||
| 30c099f872 | |||
| c660c1a676 | |||
| c44a88b57a | |||
| 509017f8df | |||
| 7243059cdb | |||
| cd3cfc3837 | |||
| 509d68f023 | |||
| de757e848d | |||
| 1fb462bab4 | |||
| 4c4312f9b5 | |||
| ff684d7544 | |||
| b69b4dc98c | |||
| 34351ae14e | |||
| d9945f5e26 | |||
| 6247656a63 | |||
| c46217918f | |||
| 330e4a1e85 | |||
| bded05688e | |||
| 13e1836604 | |||
| d611d00b55 | |||
| e4a2e06def | |||
| df3c21701d | |||
| 85c816c8cb | |||
| 4633d3f520 | |||
| 60288c09e4 | |||
| 49bc3820aa | |||
| eed8aa0e9d | |||
| e13879ab77 | |||
| 8a8e4faa42 | |||
| faa2570883 | |||
| 5c0ee0c5a0 | |||
| 780a32d706 | |||
| 29f05e0b70 | |||
| 0144b569ad | |||
| dd2747d12a | |||
| 5ef63cd7e6 | |||
| ac6002028d | |||
| 5f80f1b76a | |||
| 95276760be |
@@ -1,16 +1,7 @@
|
||||
# Results
|
||||
result.json
|
||||
*.xml
|
||||
|
||||
# IDE config
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Deps
|
||||
cpp/deps/jsoncpp/
|
||||
cpp/deps/mysql-connector/
|
||||
cpp/deployment/cameradar_*_Release_Linux.tar.gz
|
||||
|
||||
# Test
|
||||
test/cameradartest.conf.json
|
||||
test/cameradar_*_Debug_Linux.tar.gz
|
||||
# Golang
|
||||
/bin/*
|
||||
/pkg/*
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# https://github.com/golangci/golangci/wiki/Configuration
|
||||
|
||||
service:
|
||||
project-path: github.com/ullaakut/cameradar
|
||||
prepare:
|
||||
- apt-get update && apt-get install -y libcurl4-gnutls-dev
|
||||
- dep ensure
|
||||
@@ -1,18 +1,54 @@
|
||||
language: generic
|
||||
sudo: required
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: go
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- echo "Testing Docker Hub credentials"
|
||||
- docker login -u=$DOCKER_USERNAME -p=$DOCKER_PASSWORD
|
||||
- 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 nmap
|
||||
- go get github.com/mattn/goveralls
|
||||
- docker version
|
||||
|
||||
install:
|
||||
- docker build -t cameradar .
|
||||
|
||||
script:
|
||||
- docker run cameradar
|
||||
# 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 fake cameras to check if cameradar is able to access them
|
||||
- docker run -d --name="fake_camera_digest" -e RTSP_ROUTE="/live.sdp" -e RTSP_USERNAME="admin" -e RTSP_PASSWORD="12345" -e RTSP_AUTHENTICATION_METHOD="digest" -p 8554:8554 ullaakut/rtspatt
|
||||
- docker run -d --name="fake_camera_basic" -e RTSP_ROUTE="/live.sdp" -e RTSP_USERNAME="root" -e RTSP_PASSWORD="root" -e RTSP_AUTHENTICATION_METHOD="digest" -p 5554:5554 ullaakut/rtspatt
|
||||
# Launch cameradar on the local machine
|
||||
- docker run --net=host -t cameradar -t 0.0.0.0 -p 8554,5554 -v > logs.txt
|
||||
# Gather the logs from the cameras
|
||||
- docker logs fake_camera_digest > camera_digest_logs.txt
|
||||
- docker logs fake_camera_basic > camera_basic_logs.txt
|
||||
# Stop the fake cameras
|
||||
- docker stop fake_camera_basic
|
||||
- docker stop fake_camera_digest
|
||||
# Print logs
|
||||
- cat camera_digest_logs.txt
|
||||
- cat camera_basic_logs.txt
|
||||
- cat logs.txt
|
||||
- grep "Successful attack" logs.txt || exit 1
|
||||
|
||||
after_success:
|
||||
- echo "Test Success - Branch($TRAVIS_BRANCH) Pull Request($TRAVIS_PULL_REQUEST) Tag($TRAVIS_TAG)"
|
||||
- if [[ "$TRAVIS_BRANCH" == "master" ]]; then echo -e "Push Container to Docker Hub" && docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD && docker tag cameradar $DOCKER_REPO:latest && docker push $DOCKER_REPO; fi
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- brendan.le-glaunec@epitech.eu
|
||||
on_success: never
|
||||
on_failure: always
|
||||
|
||||
@@ -7,8 +7,8 @@ This file lists all versions of the repository and precises all changes.
|
||||
#### Major changes:
|
||||
|
||||
* Cameradar is no longer a C++ application but a Golang library
|
||||
* Cameraccess is a Golang application replacing the former C++ one (the C++ Cameradar image can still be used with the tag `1.1.4`)
|
||||
* The docker image for Cameraccess is lighter than the one for Cameradar
|
||||
* 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
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Cameradar Contribution
|
||||
|
||||
This file will give you guidelines on how to contribute if you want to, and will list known contributors to this repo.
|
||||
|
||||
If you're not into software development or not into Golang, you can still help. Updating the dictionaries for example, would be a really cool contribution! Just make sure the credentials and routes you add are **default constructor credentials** and not custom credentials.
|
||||
|
||||
If you have other cool ideas, feel free to share them with me at [brendan.leglaunec@etixgroup.com](mailto:brendan.leglaunec@etixgroup.com) !
|
||||
|
||||
## Version 2.0.0
|
||||
|
||||
- *Cameradar* is the name of the Golang library.
|
||||
- *Cameraccess* is the name of the binary that uses Cameradar to discover and access the cameras.
|
||||
|
||||
This quite big refactoring comes from the fact that most users who want to access cameras either want to launch it with the basic cache manager, mostly using the docker image already provided in this repository, or will not use it because it does not integrate into their software solution without sharing their database with Cameradar, which would cause issues with database migrations for example.
|
||||
|
||||
Transforming it into a library allows developers to use it directly in their own code exactly as they want, allowing for a greater flexibility. The Cameraccess binary also provides a simple use example as well as maintains the old simple way of using Cameradar for non-developers.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Branches & issues
|
||||
|
||||
When an issue is opened, a branch will be automatically created. If you want to work on this issue, this is the branch you **have** to work on and create your pull request from.
|
||||
|
||||
**Always make sure you're not working on the same issue as someone else, by asking on the issue to be assigned to it.**
|
||||
|
||||
### Commit names
|
||||
|
||||
The name of the commits should always be `v[next version] : [name of the fixed issue]` (ex: `v1.1.4 : Removed unnecessary null pointer checks`), and each PR should only contain one single commit.
|
||||
|
||||
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.
|
||||
|
||||
In case you're not familiar with squashing, here is a simple way to do it :
|
||||
|
||||
+ On your branch, when everything is clean and working, launch `git log` and count the number of commits your branch is ahead from compared to the `develop` branch.
|
||||
+ Then launch `git rebase -i HEAD~X`, X being the number of commits you want to squash. For example if I had 12 commits on my branch, I will squash all of them by writing `git rebase -i HEAD~12`.
|
||||
+ This will open a file letting you decide what to do with the commits. You want to keep the first `pick` and write `s` instead of the other ones, s meaning squash.
|
||||
+ 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 by adding a `#` character in front of the commit message.
|
||||
+ Now launch `git log`, you should see only one commit by the name you chose during the 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/EtixLabs/cameradar/builds) and you should fix it. No PR will be merged before all tests are passing correctly.
|
||||
|
||||
### Coding guidelines
|
||||
|
||||
This part will tell you about what are the general coding guidelines I want to keep on this project.
|
||||
|
||||
#### Golang
|
||||
|
||||
+ All Golang code has to be formated using `gofmt`
|
||||
+ Make sure you follow the Golang [best practices](https://golang.org/doc/effective_go.html)
|
||||
|
||||
#### Shell scripting
|
||||
|
||||
+ Just make sure Codacy does not trigger warnings on your code.
|
||||
|
||||
## 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*
|
||||
@@ -1,23 +1,34 @@
|
||||
FROM golang:alpine
|
||||
WORKDIR /go/src/github.com/EtixLabs/cameradar/cameraccess
|
||||
# Build stage
|
||||
FROM golang:alpine AS build-env
|
||||
|
||||
COPY . /go/src/github.com/EtixLabs/cameradar
|
||||
COPY . /go/src/github.com/ullaakut/cameradar
|
||||
WORKDIR /go/src/github.com/ullaakut/cameradar/cmd/cameradar
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add nmap nmap-nselibs nmap-scripts \
|
||||
curl-dev \
|
||||
gcc \
|
||||
libc-dev \
|
||||
git \
|
||||
pkgconfig
|
||||
curl curl-dev \
|
||||
gcc \
|
||||
libc-dev \
|
||||
git \
|
||||
pkgconfig
|
||||
ENV GO111MODULE=on
|
||||
RUN go version
|
||||
RUN go build -o cameradar
|
||||
|
||||
RUN go get github.com/andelf/go-curl
|
||||
RUN go get github.com/pkg/errors
|
||||
RUN go get gopkg.in/go-playground/validator.v9
|
||||
RUN go get github.com/jessevdk/go-flags
|
||||
RUN go get github.com/fatih/color
|
||||
# Final stage
|
||||
FROM alpine
|
||||
|
||||
RUN go install
|
||||
RUN apk --update add --no-cache nmap \
|
||||
nmap-nselibs \
|
||||
nmap-scripts \
|
||||
curl-dev
|
||||
|
||||
ENTRYPOINT ["/go/bin/cameraccess"]
|
||||
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/cmd/cameradar/ /app/cameradar/
|
||||
|
||||
ENV CAMERADAR_CUSTOM_ROUTES="/app/dictionaries/routes"
|
||||
ENV CAMERADAR_CUSTOM_CREDENTIALS="/app/dictionaries/credentials.json"
|
||||
|
||||
ENTRYPOINT ["/app/cameradar/cameradar"]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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 `-v` to print verbose logs, and paste them here:
|
||||
|
||||
```
|
||||
<cameradar logs>
|
||||
```
|
||||
@@ -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.
|
||||
@@ -1,14 +1,39 @@
|
||||
# Cameradar
|
||||
|
||||
<p align="center">
|
||||
<img src="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
|
||||
|
||||
[](#license)
|
||||
[](https://hub.docker.com/r/ullaakut/cameradar/)
|
||||
[](https://travis-ci.org/EtixLabs/cameradar)
|
||||
[](https://goreportcard.com/report/github.com/EtixLabs/cameradar)
|
||||
[](https://github.com/EtixLabs/cameradar/releases/latest)
|
||||
|
||||
#### Cameradar allows you to:
|
||||
### Cameradar allows you to
|
||||
|
||||
* **Detect open RTSP hosts** on any accessible target host
|
||||
* Detect which device model is streaming
|
||||
@@ -16,120 +41,66 @@
|
||||
* 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/EtixLabs/cameradar/master/Cameradar.png" width="350"/></p>
|
||||
<p align="center"><img src="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](#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 Cameraccess
|
||||
## Docker Image for Cameradar
|
||||
|
||||
<p align="center"><img src="images/CameradarV4.png" width="70%"/></p>
|
||||
|
||||
Install [docker](https://docs.docker.com/engine/installation/) on your machine, and run the following command:
|
||||
|
||||
```bash
|
||||
docker run ullaakut/cameradar <command-line options>
|
||||
docker run -t ullaakut/cameradar -t <target> <other command-line options>
|
||||
```
|
||||
|
||||
[See command-line options](#command-line-options).
|
||||
|
||||
e.g.: `docker run 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 lots of logs.
|
||||
e.g.: `docker run -t ullaakut/cameradar -t 192.168.100.0/24` will scan the ports 554, 5554 and 8554 of hosts on the 192.168.100.0/24 subnetwork and attack the discovered RTSP streams and will output debug logs.
|
||||
|
||||
* `YOUR_TARGET` can be a subnet (e.g.: `172.16.100.0/24`) or even an IP (e.g.: `172.16.100.10`), a range of IPs (e.g.: `172.16.100.10-172.16.100.20`) or a mix of all those separated by commas (e.g.: `172.17.100.0/24,172.16.100.10-172.16.100.20,0.0.0.0`).
|
||||
* `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
|
||||
* 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`
|
||||
|
||||
### Library
|
||||
## Installing the binary on your machine
|
||||
|
||||
### Dependencies of the library
|
||||
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.
|
||||
|
||||
- `curl-dev` / `libcurl` (depending on your OS)
|
||||
- `nmap`
|
||||
- `github.com/pkg/errors`
|
||||
- `gopkg.in/go-playground/validator.v9`
|
||||
- `github.com/andelf/go-curl`
|
||||
### Dependencies
|
||||
|
||||
#### Installing the library
|
||||
* `go` (> `1.10`)
|
||||
|
||||
`go get github.com/EtixLabs/cameradar`
|
||||
### Steps to install
|
||||
|
||||
After this command, the *cameradar* library is ready to use. Its source will be in:
|
||||
Make sure you installed the dependencies mentionned above, and that you have Go modules enabled (`GO111MODULE=on`)
|
||||
|
||||
$GOPATH/src/pkg/github.com/EtixLabs/cameradar
|
||||
1. `go get github.com/ullaakut/cameradar`
|
||||
2. `cd $GOPATH/src/github.com/ullaakut/cameradar`
|
||||
3. `cd cameradar`
|
||||
4. `go install`
|
||||
|
||||
You can use `go get -u` to update the package.
|
||||
The `cameradar` binary is now in your `$GOPATH/bin` ready to be used. See command line options [here](#command-line-options).
|
||||
|
||||
Here is an overview of the exposed functions of this library:
|
||||
## Configuration
|
||||
|
||||
#### Discovery
|
||||
The **RTSP port used for most cameras is 554**, so you should probably specify 554 as one of the ports you scan. Not specifying any ports to the cameradar application will scan the 554, 5554 and 8554 ports.
|
||||
|
||||
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.
|
||||
`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`.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/Discover.png"/></p>
|
||||
The Discover function calls the RunNmap function as well as the ParseNmapResults function and returns the discovered streams without attempting any attack.
|
||||
It will use default values for its calls to RunNmap:
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/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 RunNmap function.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/RunNmap.png"/></p>
|
||||
The RunNmap function will execute nmap and generate an XML file containing the results of the scan.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/ParseNmapResults.png"/></p>
|
||||
The ParseNmapResult function will open the specified XML file and return all open RTSP streams found within it.
|
||||
|
||||
#### 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.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/AttackCredentials.png"/></p>
|
||||
The AttackCredentials function takes valid streams as an input (with IP addresses and ports) and will attempt to guess their credentials using the provided dictionary.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/AttackRoute.png"/></p>
|
||||
The AttackRoute function takes valid streams as an input (with IP addresses and ports) and will attempt to guess their routes using the provided dictionary.
|
||||
|
||||
#### Data models
|
||||
|
||||
Here are the different data models useful to use the exposed functions of the cameradar library.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/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.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/LoadCredentials.png"/></p>
|
||||
|
||||
LoadCredentials takes a JSON file that has the same format as [this one](dictionary/credentials.json).
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/LoadRoutes.png"/></p>
|
||||
|
||||
LoadRoutes takes a file that has the same format as [this one](dictionary/routes). Warning: This file is not JSON.
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/RTSPURL.png"/></p>
|
||||
|
||||
RTSPURL allows you to generate the full URL of a stream.
|
||||
|
||||
### 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 cameraccess application will scan the 554 and 8554 ports.
|
||||
|
||||
e.g.: `docker run 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.
|
||||
|
||||
e.g.:
|
||||
You **can use your own files for the credentials and routes dictionaries** used to attack the cameras, but the Cameradar repository already gives you a good base that works with most cameras, in the `/dictionaries` folder.
|
||||
|
||||
```bash
|
||||
docker run -v /my/folder/with/dictionaries:/tmp/dictionaries \
|
||||
docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
|
||||
ullaakut/cameradar \
|
||||
-r "/tmp/dictionaries/my_routes" \
|
||||
-c "/tmp/dictionaries/my_credentials.json" \
|
||||
@@ -138,44 +109,119 @@ docker run -v /my/folder/with/dictionaries:/tmp/dictionaries \
|
||||
|
||||
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.
|
||||
|
||||
## Output
|
||||
|
||||
For each camera, Cameraccess will output this:
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/Output.png"/></p>
|
||||
|
||||
|
||||
## Check camera access
|
||||
|
||||
If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI or the command-line to connect to the RTSP stream using this format : `rtsp://username:password@address:port/route`
|
||||
If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI or the command-line to connect to the RTSP stream using this format: `rtsp://username:password@address:port/route`
|
||||
|
||||
With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:554/live.sdp`
|
||||
## Command-line options
|
||||
|
||||
## Command line options
|
||||
|
||||
* **"-t, --target"**: Set custom target. Required.
|
||||
* **"-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: `1000`) Set custom timeout value in miliseconds after which an attack attempt without an answer should give up.
|
||||
* **"-r, --custom-routes"**: (Default: `dictionaries/routes`) Set custom dictionary path for routes
|
||||
* **"-c, --custom-credentials"**: (Default: `dictionaries/credentials.json`) Set custom dictionary path for credentials
|
||||
* **"-t, --targets"**: Set target. Required. Target can be a file (see [instructions on how to format the file](#format-input-file)), an IP, an IP range, a subnetwork, or a combination of those. Example: `--targets="192.168.1.72,192.168.1.74"`
|
||||
* **"-p, --ports"**: (Default: `554,5554,8554`) Set custom ports.
|
||||
* **"-s, --speed"**: (Default: `4`) Set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. You might also want to keep it low to keep your discovery stealthy. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||
* **"-T, --timeout"**: (Default: `2000`) Set custom timeout value in miliseconds after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on very performant and reliable networks.
|
||||
* **"-r, --custom-routes"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/routes`) Set custom dictionary path for routes
|
||||
* **"-c, --custom-credentials"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/credentials.json`) Set custom dictionary path for credentials
|
||||
* **"-o, --nmap-output"**: (Default: `/tmp/cameradar_scan.xml`) Set custom nmap output path
|
||||
* **"-l, --log"**: Enable debug logs (nmap requests, curl describe requests, etc.)
|
||||
* **"-h"** : Display the usage information
|
||||
* **"-d, --debug"**: Enable debug logs
|
||||
* **"-v, --verbose"**: Enable verbose curl logs (not recommended for most use)
|
||||
* **"-h"**: Display the usage information
|
||||
|
||||
## Environment variables
|
||||
## Format input file
|
||||
|
||||
Not yet implemented.
|
||||
The file can contain IPs, hostnames, IP ranges and subnetwork, separated by newlines. Example:
|
||||
|
||||
```go
|
||||
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,5554,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_LOGGING`
|
||||
|
||||
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
|
||||
|
||||
See [the contribution document](/CONTRIBUTION.md) to get started.
|
||||
### 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
|
||||
|
||||
Make sure you installed the [dependencies](#dependencies), and that you have Go modules enabled (`GO111MODULE=on`)
|
||||
|
||||
1. `go get github.com/ullaakut/cameradar`
|
||||
2. `cd $GOPATH/src/github.com/ullaakut/cameradar`
|
||||
3. `cd cameradar`
|
||||
4. `go build`
|
||||
|
||||
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.
|
||||
That means that either your cameras are not streaming in RTSP or that they are not on the target you are scanning. In most cases, CCTV cameras will be on a private subnetwork, isolated from the internet. Use the `-t` option to specify your target. If you are sure you did everything right but it still does not work, please open an issue with details on the device you are trying to access 🙏
|
||||
|
||||
> Cameradar detects my cameras, but does not manage to access them at all!
|
||||
|
||||
@@ -183,24 +229,60 @@ Maybe your cameras have been configured and the credentials / URL have been chan
|
||||
|
||||
> What happened to the C++ version?
|
||||
|
||||
You can still find it under the 1.1.4 tag on this repo, however it was less performant and stable than the current version written in Golang.
|
||||
You can still find it under the 1.1.4 tag on this repo, however it was less performant and stable than the current version written in Golang. It is not recommended to use it.
|
||||
|
||||
> How to use the Cameradar library for my own project?
|
||||
|
||||
See the cameraccess example. You just need to run `go get github.com/EtixLabs/cameradar/cameradar` and to use the `cmrdr` package in your code.
|
||||
See the example in `/cmd/cameradar`. You just need to run `go get github.com/ullaakut/cameradar` and to use the `cameradar` package in your code. You can find the documentation on [godoc](https://godoc.org/github.com/ullaakut/cameradar).
|
||||
|
||||
> I want to scan my own localhost for some reason and it does not work! What's going on?
|
||||
|
||||
Use the `--net=host` flag when launching the cameradar image, or use the binary by running `go run cameraccess/main.go`.
|
||||
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)).
|
||||
|
||||
> What authentication types does Cameradar support?
|
||||
|
||||
Cameradar supports both basic and digest authentication.
|
||||
|
||||
## 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`
|
||||
|
||||
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554 and 8554
|
||||
|
||||
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t 192.168.0.0/24 --custom-credentials="/tmp/dictionaries/credentials.json" --custom-routes="/tmp/dictionaries/routes" -p 554,5554,8554`
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2017 Etix Labs
|
||||
Copyright 2019 Ullaakut
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
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.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
)
|
||||
|
||||
// HTTP responses.
|
||||
const (
|
||||
httpOK = 200
|
||||
httpUnauthorized = 401
|
||||
httpForbidden = 403
|
||||
httpNotFound = 404
|
||||
)
|
||||
|
||||
// CURL RTSP request types.
|
||||
const (
|
||||
rtspDescribe = 2
|
||||
rtspSetup = 4
|
||||
)
|
||||
|
||||
// Attack attacks the given targets and returns the accessed streams.
|
||||
func (s *Scanner) Attack(targets []Stream) ([]Stream, error) {
|
||||
if len(targets) == 0 {
|
||||
return nil, fmt.Errorf("unable to attack empty list of targets")
|
||||
}
|
||||
|
||||
// Most cameras will be accessed successfully with these two attacks.
|
||||
s.term.StartStepf("Attacking routes of %d streams", len(targets))
|
||||
streams := s.AttackRoute(targets)
|
||||
|
||||
s.term.StartStepf("Attempting to detect authentication methods of %d streams", len(targets))
|
||||
streams = s.DetectAuthMethods(streams)
|
||||
|
||||
s.term.StartStepf("Attacking credentials of %d streams", len(targets))
|
||||
streams = s.AttackCredentials(streams)
|
||||
|
||||
// But some cameras run GST RTSP Server which prioritizes 401 over 404 contrary to most cameras.
|
||||
// For these cameras, running another route attack will solve the problem.
|
||||
for _, stream := range streams {
|
||||
if !stream.RouteFound || !stream.CredentialsFound {
|
||||
s.term.StartStepf("Second round of attacks")
|
||||
streams = s.AttackRoute(streams)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.term.StartStep("Validating that streams are accessible")
|
||||
streams = s.ValidateStreams(streams)
|
||||
|
||||
s.term.EndStep()
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
// ValidateStreams tries to setup the stream to validate whether or not it is available.
|
||||
func (s *Scanner) ValidateStreams(targets []Stream) []Stream {
|
||||
for i := range targets {
|
||||
targets[i].Available = s.validateStream(targets[i])
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// AttackCredentials attempts to guess the provided targets' credentials using the given
|
||||
// dictionary or the default dictionary if none was provided by the user.
|
||||
func (s *Scanner) AttackCredentials(targets []Stream) []Stream {
|
||||
resChan := make(chan Stream)
|
||||
defer close(resChan)
|
||||
|
||||
for i := range targets {
|
||||
// TODO: Perf Improvement: Skip cameras with no auth type detected, and set their
|
||||
// CredentialsFound value to true.
|
||||
go s.attackCameraCredentials(targets[i], resChan)
|
||||
}
|
||||
|
||||
attackResults := []Stream{}
|
||||
// TODO: Change this into a for+select and make a successful result close the chan.
|
||||
for range targets {
|
||||
attackResults = append(attackResults, <-resChan)
|
||||
}
|
||||
|
||||
for i := range attackResults {
|
||||
if attackResults[i].CredentialsFound {
|
||||
targets = replace(targets, attackResults[i])
|
||||
}
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// AttackRoute attempts to guess the provided targets' streaming routes using the given
|
||||
// dictionary or the default dictionary if none was provided by the user.
|
||||
func (s *Scanner) AttackRoute(targets []Stream) []Stream {
|
||||
resChan := make(chan Stream)
|
||||
defer close(resChan)
|
||||
|
||||
for i := range targets {
|
||||
go s.attackCameraRoute(targets[i], resChan)
|
||||
}
|
||||
|
||||
attackResults := []Stream{}
|
||||
// TODO: Change this into a for+select and make a successful result close the chan.
|
||||
for range targets {
|
||||
attackResults = append(attackResults, <-resChan)
|
||||
}
|
||||
|
||||
for i := range attackResults {
|
||||
if attackResults[i].RouteFound {
|
||||
targets = replace(targets, attackResults[i])
|
||||
}
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// DetectAuthMethods attempts to guess the provided targets' authentication types, between
|
||||
// digest, basic auth or none at all.
|
||||
func (s *Scanner) DetectAuthMethods(targets []Stream) []Stream {
|
||||
for i := range targets {
|
||||
targets[i].AuthenticationType = s.detectAuthMethod(targets[i])
|
||||
|
||||
var authMethod string
|
||||
switch targets[i].AuthenticationType {
|
||||
case 0:
|
||||
authMethod = "no"
|
||||
case 1:
|
||||
authMethod = "basic"
|
||||
case 2:
|
||||
authMethod = "digest"
|
||||
}
|
||||
|
||||
s.term.Debugf("Stream %s uses %s authentication method\n", GetCameraRTSPURL(targets[i]), authMethod)
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
func (s *Scanner) attackCameraCredentials(target Stream, resChan chan<- Stream) {
|
||||
for _, username := range s.credentials.Usernames {
|
||||
for _, password := range s.credentials.Passwords {
|
||||
ok := s.credAttack(target, username, password)
|
||||
if ok {
|
||||
target.CredentialsFound = true
|
||||
target.Username = username
|
||||
target.Password = password
|
||||
resChan <- target
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target.CredentialsFound = false
|
||||
resChan <- target
|
||||
}
|
||||
|
||||
func (s *Scanner) attackCameraRoute(target Stream, resChan chan<- Stream) {
|
||||
for _, route := range s.routes {
|
||||
ok := s.routeAttack(target, route)
|
||||
if ok {
|
||||
target.RouteFound = true
|
||||
target.Route = route
|
||||
resChan <- target
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
target.RouteFound = false
|
||||
resChan <- target
|
||||
}
|
||||
|
||||
func (s *Scanner) detectAuthMethod(stream Stream) int {
|
||||
c := s.curl.Duphandle()
|
||||
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%d/%s",
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
stream.Route,
|
||||
)
|
||||
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// Send a request to the URL of the stream we want to attack.
|
||||
_ = c.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the stream URL.
|
||||
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
// 2 is CURL_RTSPREQ_DESCRIBE.
|
||||
_ = c.Setopt(curl.OPT_RTSP_REQUEST, 2)
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Debugf("Perform failed: %v", err)
|
||||
return -1
|
||||
}
|
||||
|
||||
authType, err := c.Getinfo(curl.INFO_HTTPAUTH_AVAIL)
|
||||
if err != nil {
|
||||
s.term.Debugf("Getinfo failed: %v", err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if s.verbose {
|
||||
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", authType)
|
||||
}
|
||||
|
||||
return authType.(int)
|
||||
}
|
||||
|
||||
func (s *Scanner) routeAttack(stream Stream, route string) bool {
|
||||
c := s.curl.Duphandle()
|
||||
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
stream.Username,
|
||||
stream.Password,
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
route,
|
||||
)
|
||||
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// Set proper authentication type.
|
||||
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
|
||||
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
|
||||
|
||||
// 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)
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Debugf("Perform failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get return code for the request.
|
||||
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
s.term.Debugf("Getinfo failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if s.verbose {
|
||||
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
|
||||
}
|
||||
// If it's a 401 or 403, it means that the credentials are wrong but the route might be okay.
|
||||
// If it's a 200, the stream is accessed successfully.
|
||||
if rc == httpOK || rc == httpUnauthorized || rc == httpForbidden {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scanner) credAttack(stream Stream, username string, password string) bool {
|
||||
c := s.curl.Duphandle()
|
||||
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
username,
|
||||
password,
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
stream.Route,
|
||||
)
|
||||
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// Set proper authentication type.
|
||||
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
|
||||
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(username, ":", password))
|
||||
|
||||
// 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)
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Debugf("Perform failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get return code for the request.
|
||||
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
s.term.Debugf("Getinfo failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if s.verbose {
|
||||
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
|
||||
}
|
||||
|
||||
// If it's a 404, it means that the route is incorrect but the credentials might be okay.
|
||||
// If it's a 200, the stream is accessed successfully.
|
||||
if rc == httpOK || rc == httpNotFound {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scanner) validateStream(stream Stream) bool {
|
||||
c := s.curl.Duphandle()
|
||||
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
stream.Username,
|
||||
stream.Password,
|
||||
stream.Address,
|
||||
stream.Port,
|
||||
stream.Route,
|
||||
)
|
||||
|
||||
s.setCurlOptions(c)
|
||||
|
||||
// Set proper authentication type.
|
||||
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
|
||||
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
|
||||
|
||||
// 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)
|
||||
|
||||
_ = c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333")
|
||||
|
||||
// Perform the request.
|
||||
err := c.Perform()
|
||||
if err != nil {
|
||||
s.term.Debugf("Perform failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get return code for the request.
|
||||
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
s.term.Debugf("Getinfo failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if s.verbose {
|
||||
s.term.Debugln("SETUP", attackURL, "RTSP/1.0 >", rc)
|
||||
}
|
||||
// If it's a 200, the stream is accessed successfully.
|
||||
if rc == httpOK {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scanner) setCurlOptions(c Curler) {
|
||||
// Do not write sdp in stdout
|
||||
_ = c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
|
||||
// Do not use signals (would break multithreading).
|
||||
_ = c.Setopt(curl.OPT_NOSIGNAL, 1)
|
||||
// Do not send a body in the describe request.
|
||||
_ = c.Setopt(curl.OPT_NOBODY, 1)
|
||||
// Set custom timeout.
|
||||
_ = c.Setopt(curl.OPT_TIMEOUT_MS, int(s.timeout/time.Millisecond))
|
||||
}
|
||||
|
||||
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
|
||||
func doNotWrite([]uint8, interface{}) bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/ullaakut/disgo"
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
)
|
||||
|
||||
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 TestAttack(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
fakeRoutes = Routes{"live.sdp", "media.amp"}
|
||||
fakeCredentials = Credentials{
|
||||
Usernames: []string{"admin", "root"},
|
||||
Passwords: []string{"12345", "root"},
|
||||
}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
|
||||
performErr error
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "inverted RTSP RFC",
|
||||
|
||||
targets: fakeTargets,
|
||||
|
||||
performErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "attack works",
|
||||
|
||||
targets: fakeTargets,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "no targets",
|
||||
|
||||
targets: nil,
|
||||
|
||||
expectedStreams: nil,
|
||||
expectedErr: errors.New("unable to attack empty list of targets"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
if len(test.targets) != 0 {
|
||||
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(test.performErr)
|
||||
if test.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(200, nil)
|
||||
}
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: time.Millisecond,
|
||||
verbose: false,
|
||||
credentials: fakeCredentials,
|
||||
routes: fakeRoutes,
|
||||
}
|
||||
|
||||
results, err := scanner.Attack(test.targets)
|
||||
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
|
||||
assert.Len(t, results, len(test.expectedStreams))
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttackCredentials(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
fakeCredentials = Credentials{
|
||||
Usernames: []string{"admin", "root"},
|
||||
Passwords: []string{"12345", "root"},
|
||||
}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
credentials Credentials
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
performErr error
|
||||
getInfoErr error
|
||||
invalidTargets bool
|
||||
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
description: "Credentials found",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 404,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 200,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl perform fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
performErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
getInfoErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Verbose mode disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: false,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Verbose mode enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
credentials: fakeCredentials,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: true,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
if !test.invalidTargets {
|
||||
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(test.performErr)
|
||||
if test.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
|
||||
}
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
credentials: test.credentials,
|
||||
}
|
||||
|
||||
results := scanner.AttackCredentials(test.targets)
|
||||
|
||||
assert.Len(t, results, len(test.expectedStreams))
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttackRoute(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
fakeRoutes = Routes{"live.sdp", "media.amp"}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
routes Routes
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
performErr error
|
||||
getInfoErr error
|
||||
invalidTargets bool
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "Route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 401,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "Camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 200,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl perform fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
performErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
getInfoErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose mode disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: false,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose mode enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
routes: fakeRoutes,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: true,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
if !test.invalidTargets {
|
||||
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(test.performErr)
|
||||
if test.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
|
||||
}
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
routes: test.routes,
|
||||
}
|
||||
|
||||
results := scanner.AttackRoute(test.targets)
|
||||
|
||||
assert.Len(t, results, len(test.expectedStreams))
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStreams(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
performErr error
|
||||
getInfoErr error
|
||||
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
description: "route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 403,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "route found",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 401,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "camera accessed",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 200,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "unavailable stream",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 400,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl perform fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
performErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
getInfoErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: false,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: true,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(test.performErr)
|
||||
if test.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
}
|
||||
|
||||
results := scanner.ValidateStreams(test.targets)
|
||||
|
||||
assert.Equal(t, len(test.expectedStreams), len(results))
|
||||
|
||||
for _, expectedStream := range test.expectedStreams {
|
||||
assert.Contains(t, results, expectedStream)
|
||||
}
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectAuthenticationType(t *testing.T) {
|
||||
var (
|
||||
stream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
stream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
Available: true,
|
||||
}
|
||||
|
||||
fakeTargets = []Stream{stream1, stream2}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []Stream
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
|
||||
status int
|
||||
|
||||
performErr error
|
||||
getInfoErr error
|
||||
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
description: "no auth enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 0,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "basic auth enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 1,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "digest auth enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
status: 2,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl getinfo fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
getInfoErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "curl perform fails",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
|
||||
performErr: errors.New("dummy error"),
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose disabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: false,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
{
|
||||
description: "verbose enabled",
|
||||
|
||||
targets: fakeTargets,
|
||||
timeout: 1 * time.Millisecond,
|
||||
verbose: true,
|
||||
|
||||
expectedStreams: fakeTargets,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
curlerMock := &CurlerMock{}
|
||||
|
||||
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
|
||||
curlerMock.On("Perform").Return(test.performErr)
|
||||
if test.performErr == nil {
|
||||
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
curl: curlerMock,
|
||||
timeout: test.timeout,
|
||||
verbose: test.verbose,
|
||||
}
|
||||
|
||||
results := scanner.DetectAuthMethods(test.targets)
|
||||
|
||||
assert.Equal(t, len(test.expectedStreams), len(results))
|
||||
|
||||
for _, expectedStream := range test.expectedStreams {
|
||||
assert.Contains(t, results, expectedStream)
|
||||
}
|
||||
|
||||
curlerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoNotWrite(t *testing.T) {
|
||||
assert.Equal(t, true, doNotWrite(nil, nil))
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/EtixLabs/cameradar/cameradar"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
Target string `short:"t" long:"target" description:"The target on which to scan for open RTSP streams - required" required:"true"`
|
||||
Ports string `short:"p" long:"ports" description:"The ports on which to search for RTSP streams" default:"554,8554"`
|
||||
OutputFile string `short:"o" long:"nmap-output" description:"The path where nmap will create its XML result file" default:"/tmp/cameradar_scan.xml"`
|
||||
Routes string `short:"r" long:"custom-routes" description:"The path on which to load a custom routes dictionary" default:"../dictionaries/routes"`
|
||||
Credentials string `short:"c" long:"custom-credentials" description:"The path on which to load a custom credentials JSON dictionary" default:"../dictionaries/credentials.json"`
|
||||
Speed int `short:"s" long:"speed" description:"The nmap speed preset to use" default:"4"`
|
||||
Timeout int `short:"T" long:"timeout" description:"The timeout in miliseconds to use for attack attempts" default:"1000"`
|
||||
EnableLogs bool `short:"l" long:"log" description:"Enable the logs for nmap's output to stdout"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var options options
|
||||
_, err := flags.ParseArgs(&options, os.Args[1:])
|
||||
if err != nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
streams, _ := cmrdr.Discover(options.Target, options.Ports, options.OutputFile, options.Speed, options.EnableLogs)
|
||||
streams, _ = cmrdr.AttackRoute(streams, routes, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
|
||||
streams, _ = cmrdr.AttackCredentials(streams, credentials, time.Duration(options.Timeout)*time.Millisecond, 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 {
|
||||
fmt.Printf("%s\tDevice RTSP URL:\t%s\n", green("\xE2\x96\xB6"), blue(cmrdr.RTSPURL(stream)))
|
||||
success++
|
||||
} else {
|
||||
fmt.Printf("%s\tAdmin panel URL:\t%s %s\n", red("\xE2\x96\xB6"), yellow(cmrdr.AdminPanelURL(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)
|
||||
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.", red("\xE2\x9C\x96"))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("%s No streams were found. Please make sure that your target is on an accessible network.", red("\xE2\x9C\x96"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Package cameradar provides methods to be able to discover and
|
||||
// attack RTSP streams easily. RTSP streams are used by most
|
||||
// IP Cameras, often for surveillance.
|
||||
//
|
||||
// A simple example usage of the library can be found in
|
||||
// https://github.com/ullaakut/cameradar/tree/master/cameradar
|
||||
//
|
||||
// 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 cameradar
|
||||
@@ -1,232 +0,0 @@
|
||||
// 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.
|
||||
|
||||
package cmrdr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
curl "github.com/andelf/go-curl"
|
||||
"github.com/pkg/errors"
|
||||
v "gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
|
||||
func doNotWrite([]uint8, interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func routeAttack(camera Stream, route string, timeout time.Duration, enableLogs bool) bool {
|
||||
easy := curl.EasyInit()
|
||||
defer easy.Cleanup()
|
||||
|
||||
if easy != nil {
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
camera.Username,
|
||||
camera.Password,
|
||||
camera.Address,
|
||||
camera.Port,
|
||||
route,
|
||||
)
|
||||
|
||||
if enableLogs {
|
||||
// Debug logs when logs are enabled
|
||||
easy.Setopt(curl.OPT_VERBOSE, 1)
|
||||
} else {
|
||||
// Do not write sdp in stdout
|
||||
easy.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
|
||||
}
|
||||
|
||||
// Do not send a body in the describe request
|
||||
easy.Setopt(curl.OPT_NOBODY, 1)
|
||||
// Send a request to the URL of the camera we want to attack
|
||||
easy.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the camera URL
|
||||
easy.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
// 2 is CURL_RTSPREQ_DESCRIBE
|
||||
easy.Setopt(curl.OPT_RTSP_REQUEST, 2)
|
||||
// Set custom timeout
|
||||
easy.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
|
||||
|
||||
// Perform the request
|
||||
easy.Perform()
|
||||
|
||||
// Get return code for the request
|
||||
rc, err := easy.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's a 404, it means that the route was not valid
|
||||
if rc == 404 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func credAttack(camera Stream, username string, password string, timeout time.Duration, enableLogs bool) bool {
|
||||
easy := curl.EasyInit()
|
||||
defer easy.Cleanup()
|
||||
|
||||
if easy != nil {
|
||||
attackURL := fmt.Sprintf(
|
||||
"rtsp://%s:%s@%s:%d/%s",
|
||||
username,
|
||||
password,
|
||||
camera.Address,
|
||||
camera.Port,
|
||||
camera.Route,
|
||||
)
|
||||
|
||||
if enableLogs {
|
||||
// Debug logs when logs are enabled
|
||||
easy.Setopt(curl.OPT_VERBOSE, 1)
|
||||
} else {
|
||||
// Do not write sdp in stdout
|
||||
easy.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
|
||||
}
|
||||
|
||||
// Do not send a body in the describe request
|
||||
easy.Setopt(curl.OPT_NOBODY, 1)
|
||||
// Send a request to the URL of the camera we want to attack
|
||||
easy.Setopt(curl.OPT_URL, attackURL)
|
||||
// Set the RTSP STREAM URI as the camera URL
|
||||
easy.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
|
||||
// 2 is CURL_RTSPREQ_DESCRIBE
|
||||
easy.Setopt(curl.OPT_RTSP_REQUEST, 2)
|
||||
// Set custom timeout
|
||||
easy.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
|
||||
|
||||
// Perform the request
|
||||
easy.Perform()
|
||||
|
||||
// Get return code for the request
|
||||
rc, err := easy.Getinfo(curl.INFO_RESPONSE_CODE)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's a 403 or a 401, it means that the credentials are not correct
|
||||
if rc == 403 || rc == 401 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func attackCameraCredentials(target Stream, credentials Credentials, resultsChan chan<- Stream, timeout time.Duration, log bool) {
|
||||
for _, username := range credentials.Usernames {
|
||||
for _, password := range credentials.Passwords {
|
||||
ok := credAttack(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(target Stream, routes Routes, resultsChan chan<- Stream, timeout time.Duration, log bool) {
|
||||
for _, route := range routes {
|
||||
ok := routeAttack(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(targets []Stream, credentials Credentials, timeout time.Duration, log bool) (results []Stream, err 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 streams")
|
||||
}
|
||||
|
||||
go attackCameraCredentials(target, credentials, attacks, timeout, log)
|
||||
}
|
||||
|
||||
attackResults := []Stream{}
|
||||
for _ = range targets {
|
||||
attackResults = append(attackResults, <-attacks)
|
||||
}
|
||||
|
||||
found := 0
|
||||
for _, result := range attackResults {
|
||||
if result.CredentialsFound == true {
|
||||
targets = replace(targets, result)
|
||||
found++
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
return targets, errors.New("No credentials found")
|
||||
}
|
||||
|
||||
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(targets []Stream, routes Routes, timeout time.Duration, log bool) (results []Stream, err 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 streams")
|
||||
}
|
||||
|
||||
go attackCameraRoute(target, routes, attacks, timeout, log)
|
||||
}
|
||||
|
||||
attackResults := []Stream{}
|
||||
for _ = range targets {
|
||||
attackResults = append(attackResults, <-attacks)
|
||||
}
|
||||
|
||||
found := 0
|
||||
for _, result := range attackResults {
|
||||
if result.RouteFound == true {
|
||||
targets = replace(targets, result)
|
||||
found++
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
return targets, errors.New("No routes found")
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
// 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.
|
||||
|
||||
package cmrdr
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
|
||||
"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 (
|
||||
// PARANOID 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
|
||||
)
|
||||
|
||||
// RunNmap runs nmap on the specified targets's specified ports, using the given nmap speed
|
||||
func RunNmap(targets, ports string, resultFilePath string, nmapSpeed int, enableLogs bool) error {
|
||||
// Prepare nmap command
|
||||
cmd := exec.Command(
|
||||
"nmap",
|
||||
fmt.Sprintf("-T%d", nmapSpeed),
|
||||
"-A",
|
||||
targets,
|
||||
"-p",
|
||||
ports,
|
||||
"-oX",
|
||||
resultFilePath,
|
||||
)
|
||||
|
||||
// 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 {
|
||||
log.Printf(in.Text())
|
||||
}
|
||||
}
|
||||
if err := in.Err(); err != nil {
|
||||
if enableLogs {
|
||||
log.Printf("error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseNmapResult returns a slice of streams from an NMap XML result file
|
||||
// To generate one yourself, use the -X option when running NMap
|
||||
func ParseNmapResult(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
|
||||
}
|
||||
streams = append(streams, Stream{
|
||||
Device: port.Service.Product,
|
||||
Address: host.Address.Addr,
|
||||
Port: port.PortID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
// Discover scans the target networks and tries to find RTSP streams within them
|
||||
// targets - string: The addresses
|
||||
// - 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-172.16.100.20)
|
||||
// - a mix of all those separated by commas (e.g.: localhost,172.17.100.0/24,172.16.100.10-172.16.100.20,0.0.0.0).
|
||||
// ports - string :
|
||||
// - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554)
|
||||
func Discover(targets string, ports string, nmapResultPath string, speed int, log bool) ([]Stream, error) {
|
||||
var streams []Stream
|
||||
|
||||
// Run nmap command to discover open ports on the specified targets & ports
|
||||
err := RunNmap(targets, ports, nmapResultPath, speed, log)
|
||||
if err != nil {
|
||||
return streams, err
|
||||
}
|
||||
|
||||
// Get found streams from nmap results
|
||||
streams, err = ParseNmapResult(nmapResultPath)
|
||||
if err != nil {
|
||||
return streams, err
|
||||
}
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// 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.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// RTSPURL generates a stream's RTSP URL
|
||||
func RTSPURL(stream Stream) string {
|
||||
return "rtsp://" + stream.Username + ":" + stream.Password + "@" + stream.Address + ":" + fmt.Sprint(stream.Port) + "/" + stream.Route
|
||||
}
|
||||
|
||||
// AdminPanelURL returns the URL to the camera's admin panel
|
||||
func AdminPanelURL(stream Stream) string {
|
||||
return "http://" + stream.Address + "/"
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// 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.
|
||||
|
||||
package cmrdr
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// 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.
|
||||
|
||||
package cmrdr
|
||||
|
||||
// Stream represents a camera's RTSP stream
|
||||
type Stream struct {
|
||||
Device string
|
||||
Username string
|
||||
Password string
|
||||
Route string
|
||||
Address string `validate:"required"`
|
||||
Port uint `validate:"required"`
|
||||
|
||||
CredentialsFound bool
|
||||
RouteFound bool
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1,62 +0,0 @@
|
||||
// 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.
|
||||
|
||||
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"`
|
||||
Address 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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/ullaakut/cameradar"
|
||||
"github.com/ullaakut/disgo"
|
||||
"github.com/ullaakut/disgo/style"
|
||||
)
|
||||
|
||||
func parseArguments() error {
|
||||
viper.SetEnvPrefix("cameradar")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
pflag.StringSliceP("targets", "t", []string{}, "The targets on which to scan for open RTSP streams - required (ex: 172.16.100.0/24)")
|
||||
pflag.StringSliceP("ports", "p", []string{"554", "5554", "8554"}, "The ports on which to search for RTSP streams")
|
||||
pflag.StringP("custom-routes", "r", "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/routes", "The path on which to load a custom routes dictionary")
|
||||
pflag.StringP("custom-credentials", "c", "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/credentials.json", "The path on which to load a custom credentials JSON dictionary")
|
||||
pflag.IntP("speed", "s", 4, "The nmap speed preset to use for discovery")
|
||||
pflag.DurationP("timeout", "T", 2*time.Second, "The timeout in miliseconds to use for attack attempts")
|
||||
pflag.BoolP("debug", "d", true, "Enable the debug logs")
|
||||
pflag.BoolP("verbose", "v", false, "Enable the verbose logs")
|
||||
pflag.BoolP("help", "h", false, "displays this help message")
|
||||
|
||||
viper.AutomaticEnv()
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
err := viper.BindPFlags(pflag.CommandLine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if viper.GetBool("help") {
|
||||
pflag.Usage()
|
||||
fmt.Println("\nExamples of usage:")
|
||||
fmt.Println("\tScanning your home network for RTSP streams:\tcameradar -t 192.168.0.0/24")
|
||||
fmt.Println("\tScanning a remote camera on a specific port:\tcameradar -t 172.178.10.14 -p 18554 -s 2")
|
||||
fmt.Println("\tScanning an unstable remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 --timeout 10000 -l")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if viper.GetStringSlice("targets") == nil {
|
||||
return errors.New("targets (-t, --targets) argument required\n examples:\n - 172.16.100.0/24\n - localhost\n - 8.8.8.8")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := parseArguments()
|
||||
if err != nil {
|
||||
printErr(err)
|
||||
}
|
||||
|
||||
c, err := cameradar.New(
|
||||
cameradar.WithTargets(viper.GetStringSlice("targets")),
|
||||
cameradar.WithPorts(viper.GetStringSlice("ports")),
|
||||
cameradar.WithDebug(viper.GetBool("debug")),
|
||||
cameradar.WithVerbose(viper.GetBool("verbose")),
|
||||
cameradar.WithCustomCredentials(viper.GetString("custom-credentials")),
|
||||
cameradar.WithCustomRoutes(viper.GetString("custom-routes")),
|
||||
cameradar.WithSpeed(viper.GetInt("speed")),
|
||||
cameradar.WithTimeout(viper.GetDuration("timeout")),
|
||||
)
|
||||
if err != nil {
|
||||
printErr(err)
|
||||
}
|
||||
|
||||
scanResult, err := c.Scan()
|
||||
if err != nil {
|
||||
printErr(err)
|
||||
}
|
||||
|
||||
streams, err := c.Attack(scanResult)
|
||||
if err != nil {
|
||||
printErr(err)
|
||||
}
|
||||
|
||||
c.PrintStreams(streams)
|
||||
}
|
||||
|
||||
func printErr(err error) {
|
||||
disgo.Errorln(style.Failure(style.SymbolCross), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
curl "github.com/ullaakut/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()}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
)
|
||||
|
||||
func TestCurl(t *testing.T) {
|
||||
handle := Curl{
|
||||
CURL: curl.EasyInit(),
|
||||
}
|
||||
|
||||
handle2 := handle.Duphandle()
|
||||
|
||||
if reflect.DeepEqual(handle, handle2) {
|
||||
t.Errorf("unexpected identical handle from duphandle: expected %+v got %+v", handle, handle2)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,45 @@
|
||||
{
|
||||
"usernames": [
|
||||
"",
|
||||
"admin",
|
||||
"666666",
|
||||
"888888",
|
||||
"Admin",
|
||||
"admin",
|
||||
"admin1",
|
||||
"administrator",
|
||||
"Administrator",
|
||||
"aiphone",
|
||||
"Dinion",
|
||||
"root",
|
||||
"service",
|
||||
"supervisor",
|
||||
"ubnt"
|
||||
],
|
||||
"passwords" : [
|
||||
"passwords": [
|
||||
"",
|
||||
"admin",
|
||||
"9999",
|
||||
"123456",
|
||||
"pass",
|
||||
"camera",
|
||||
"111111",
|
||||
"1111111",
|
||||
"1234",
|
||||
"12345",
|
||||
"123456",
|
||||
"4321",
|
||||
"666666",
|
||||
"888888",
|
||||
"9999",
|
||||
"admin",
|
||||
"aiphone",
|
||||
"camera",
|
||||
"fliradmin",
|
||||
"system",
|
||||
"ikwd",
|
||||
"jvc",
|
||||
"meinsm",
|
||||
"root",
|
||||
"4321",
|
||||
"1111111",
|
||||
"pass",
|
||||
"password",
|
||||
"ikwd",
|
||||
"root",
|
||||
"service",
|
||||
"supervisor",
|
||||
"ubnt"
|
||||
"system",
|
||||
"ubnt",
|
||||
"wbox123"
|
||||
]
|
||||
}
|
||||
@@ -1,50 +1,98 @@
|
||||
|
||||
0/1:1/main
|
||||
0/usrnm:pwd/main
|
||||
0/video1
|
||||
1
|
||||
1.AMP
|
||||
1/h264major
|
||||
1/stream1
|
||||
CAM_ID.password.mp2
|
||||
GetData.cgi
|
||||
MediaInput/h264
|
||||
MediaInput/mpeg4
|
||||
VideoInput/1/h264/1
|
||||
11
|
||||
12
|
||||
125
|
||||
666
|
||||
access_code
|
||||
access_name_for_stream_1_to_5
|
||||
api/mjpegvideo.cgi
|
||||
av0_0
|
||||
av2
|
||||
avc
|
||||
avn=2
|
||||
AVStream1_1
|
||||
axis-media/media.amp
|
||||
axis-media/media.amp?camera=1
|
||||
axis-media/media.amp?videocodec=h264
|
||||
cam
|
||||
CAM_ID.password.mp2
|
||||
cam/realmonitor
|
||||
cam/realmonitor?channel=0&subtype=0
|
||||
cam/realmonitor?channel=1&subtype=0
|
||||
cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
|
||||
cam0_0
|
||||
cam0_1
|
||||
cam1/h264
|
||||
cam1/h264/multicast
|
||||
cam1/mjpeg
|
||||
cam1/mpeg4
|
||||
cam1/mpeg4?user='username'&pwd='password'
|
||||
cam1/onvif-h264
|
||||
camera.stm
|
||||
ch0
|
||||
ch001.sdp
|
||||
ch01.264
|
||||
ch0_0.h264
|
||||
ch0_unicast_firststream
|
||||
ch0_unicast_secondstream
|
||||
ch00/0
|
||||
ch001.sdp
|
||||
CH001.sdp
|
||||
ch01.264
|
||||
ch01.264?
|
||||
ch01.264?ptype=tcp
|
||||
ch1-s1
|
||||
channel1
|
||||
GetData.cgi
|
||||
gnz_media/main
|
||||
h264
|
||||
h264_vga.sdp
|
||||
h264.sdp
|
||||
h264/ch1/sub/av_stream
|
||||
h264/media.amp
|
||||
HighResolutionVideo
|
||||
image.mpg
|
||||
img/media.sav
|
||||
img/media.sav?channel=1
|
||||
img/video.asf
|
||||
img/video.sav
|
||||
ioImage/1
|
||||
ipcam.sdp
|
||||
ipcam_h264.sdp
|
||||
live.sdp
|
||||
live/h264
|
||||
live/mpeg4
|
||||
ipcam_mjpeg.sdp
|
||||
ipcam.sdp
|
||||
live
|
||||
live_mpeg4.sdp
|
||||
live_st1
|
||||
live.sdp
|
||||
live/av0
|
||||
live/ch0
|
||||
live/ch00_0
|
||||
live/ch01_0
|
||||
live/h264
|
||||
live/main
|
||||
live/main0
|
||||
live/mpeg4
|
||||
live1.sdp
|
||||
live3.sdp
|
||||
livestream
|
||||
livestream/
|
||||
LowResolutionVideo
|
||||
main
|
||||
media
|
||||
media.amp
|
||||
media.amp?streamprofile=Profile1
|
||||
media/media.amp
|
||||
media/video1
|
||||
MediaInput/h264
|
||||
MediaInput/mpeg4
|
||||
medias2
|
||||
mjpeg/media.smp
|
||||
mp4
|
||||
mpeg/media.amp
|
||||
mpeg4
|
||||
mpeg4/1/media.amp
|
||||
mpeg4/media.amp
|
||||
@@ -55,59 +103,63 @@ multicaststream
|
||||
now.mp4
|
||||
nph-h264.cgi
|
||||
nphMpeg4/g726-640x
|
||||
nphMpeg4/g726-640x48
|
||||
nphMpeg4/g726-640x480
|
||||
nphMpeg4/nil-320x240
|
||||
onvif-media/media.amp
|
||||
ONVIF/MediaInput
|
||||
ONVIF/MediaInput?profile=4_def_profile6
|
||||
onvif1
|
||||
pass@10.0.0.5:6667/blinkhd
|
||||
play1.sdp
|
||||
play2.sdp
|
||||
profile2/media.smp
|
||||
profile5/media.smp
|
||||
rtpvideo1.sdp
|
||||
rtsp_live0
|
||||
rtsp_live1
|
||||
rtsp_live2
|
||||
rtsp_tunnel
|
||||
rtsph264
|
||||
rtsph2641080p
|
||||
StdCh1
|
||||
stream
|
||||
stream.sdp
|
||||
stream1
|
||||
user.pin.mp2
|
||||
streaming/channels/0
|
||||
Streaming/Channels/1
|
||||
streaming/channels/1
|
||||
streaming/channels/101
|
||||
Streaming/Unicast/channels/101
|
||||
StreamingSetting?version=1.0&action=getRTSPStream&ChannelID=1&ChannelName=Channel1
|
||||
tcp/av0_0
|
||||
test
|
||||
trackID=1
|
||||
ucast/11
|
||||
udp/av0_0
|
||||
udp/unicast/aiphone_H264
|
||||
udpstream
|
||||
user_defined
|
||||
user.pin.mp2
|
||||
user=admin_password=?????_channel=1_stream=0.sdp?real_stream
|
||||
user=admin_password=R5XFY888_channel=1_stream=0.sdp?real_stream
|
||||
user=admin&password=&channel=1&stream=0.sdp?
|
||||
user=admin&password=&channel=1&stream=0.sdp?real_stream
|
||||
v2
|
||||
video
|
||||
video.3gp
|
||||
video.h264
|
||||
video.mjpg
|
||||
video.mp4
|
||||
video.pro1
|
||||
video.pro2
|
||||
video.pro3
|
||||
video0.sdp
|
||||
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/media.stm
|
||||
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
|
||||
VideoInput/1/mpeg4/1
|
||||
videoMain
|
||||
vis
|
||||
wfov
|
||||
@@ -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
|
||||
@@ -0,0 +1,19 @@
|
||||
module github.com/ullaakut/cameradar
|
||||
|
||||
require (
|
||||
github.com/Ullaakut/nmap v0.0.0-20190306183004-e38898a9bead // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gernest/wow v0.1.0
|
||||
github.com/go-playground/locales v0.12.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.16.0 // indirect
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.6 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.3.1
|
||||
github.com/ullaakut/disgo v0.3.0
|
||||
github.com/ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd
|
||||
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead
|
||||
gopkg.in/go-playground/validator.v9 v9.27.0
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
github.com/Ullaakut/nmap v0.0.0-20190306183004-e38898a9bead h1:iclmd4In7CnuZGbbnnaeF1DtSePgXxN71pq5UNI1M7c=
|
||||
github.com/Ullaakut/nmap v0.0.0-20190306183004-e38898a9bead/go.mod h1:fkC066hwfcoKwlI7DS2ARTggSVtBTZYCjVH1TzuTMaQ=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gernest/wow v0.1.0 h1:g9xdwCwP0+xgVYlA2sopI0gZHqXe7HjI/7/LykG4fks=
|
||||
github.com/gernest/wow v0.1.0/go.mod h1:dEPabJRi5BneI1Nev1VWo0ZlcTWibHWp43qxKms4elY=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
|
||||
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
|
||||
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ullaakut/disgo v0.0.0-20190310161027-e17c43d71b3d h1:tObr2ILgSQwrhpQRiVUKHtXF+0V5gYnnd/zBQGAmfuQ=
|
||||
github.com/ullaakut/disgo v0.0.0-20190310161027-e17c43d71b3d/go.mod h1:UOgLVyqihzJ7yihrHjYZikivT+AHb9NhT3r1OyPCJqg=
|
||||
github.com/ullaakut/disgo v0.3.0 h1:2zrEyNBfPRgDVDgzM/qLXZ4Yqt3Lxz7ERvZUSmqSY2M=
|
||||
github.com/ullaakut/disgo v0.3.0/go.mod h1:UOgLVyqihzJ7yihrHjYZikivT+AHb9NhT3r1OyPCJqg=
|
||||
github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70 h1:3q4hgRu9NT894aYmnoMFl5wPvdNhpHYmdi2+Njyxq5U=
|
||||
github.com/ullaakut/go-curl v0.0.0-20190310175419-50acab4cef70/go.mod h1:FTfXm4jC9Ff1yqc3/HMXCyr+SGO03vJyijJCQlNyF10=
|
||||
github.com/ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd h1:IzJ7V8S7/NXc4aLOj0QavbQZ5Z/Q2RpCifshHoJ5ytA=
|
||||
github.com/ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd/go.mod h1:FTfXm4jC9Ff1yqc3/HMXCyr+SGO03vJyijJCQlNyF10=
|
||||
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead h1:Pw5wKSAfxi8GcYJSc3GdcwtPG5tyg7zg9E3hAHbLPO0=
|
||||
github.com/ullaakut/nmap v0.0.0-20190306183004-e38898a9bead/go.mod h1:4CQy4PqZA4Snk3+MS26+1oAkJ8dCY8kGH6+kF42yajw=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/validator.v9 v9.27.0 h1:wCg/0hk9RzcB0CYw8pYV6FiBYug1on0cpco9YZF8jqA=
|
||||
gopkg.in/go-playground/validator.v9 v9.27.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -0,0 +1,27 @@
|
||||
package cameradar
|
||||
|
||||
import "fmt"
|
||||
|
||||
func replace(streams []Stream, new Stream) []Stream {
|
||||
var updatedSlice []Stream
|
||||
|
||||
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 + "/"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
validStream1 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1,
|
||||
}
|
||||
|
||||
validStream2 := Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 2,
|
||||
}
|
||||
|
||||
invalidStream := Stream{
|
||||
Device: "invalidDevice",
|
||||
Address: "anotherFakeAddress",
|
||||
Port: 3,
|
||||
}
|
||||
|
||||
invalidStreamModified := Stream{
|
||||
Device: "updatedDevice",
|
||||
Address: "anotherFakeAddress",
|
||||
Port: 3,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
streams []Stream
|
||||
newStream Stream
|
||||
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
streams: []Stream{validStream1, validStream2, invalidStream},
|
||||
newStream: invalidStreamModified,
|
||||
|
||||
expectedStreams: []Stream{validStream1, validStream2, invalidStreamModified},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
streams := replace(test.streams, test.newStream)
|
||||
|
||||
assert.Equal(t, len(test.expectedStreams), len(streams))
|
||||
|
||||
for _, expectedStream := range test.expectedStreams {
|
||||
assert.Contains(t, streams, expectedStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}{
|
||||
{
|
||||
stream: validStream,
|
||||
|
||||
expectedRTSPURL: "rtsp://ullaakut:ba69897483886f0d2b0afb6345b76c0c@1.2.3.4:1337/cameradar.sdp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
assert.Equal(t, test.expectedRTSPURL, GetCameraRTSPURL(test.stream))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCameraAdminPanelURL(t *testing.T) {
|
||||
validStream := Stream{
|
||||
Address: "1.2.3.4",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
stream Stream
|
||||
|
||||
expectedRTSPURL string
|
||||
}{
|
||||
{
|
||||
stream: validStream,
|
||||
|
||||
expectedRTSPURL: "http://1.2.3.4/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
assert.Equal(t, test.expectedRTSPURL, GetCameraAdminPanelURL(test.stream))
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,122 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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 (s *Scanner) LoadCredentials() error {
|
||||
s.term.Debugf("Loading credentials dictionary from path %q\n", s.credentialDictionaryPath)
|
||||
|
||||
// Open & Read XML file.
|
||||
content, err := ioutil.ReadFile(s.credentialDictionaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read credentials dictionary file at %q: %v", s.credentialDictionaryPath, err)
|
||||
}
|
||||
|
||||
// Unmarshal content of JSON file into data structure.
|
||||
err = json.Unmarshal(content, &s.credentials)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unmarshal dictionary contents: %v", err)
|
||||
}
|
||||
|
||||
s.term.Debugf("Loaded %d usernames and %d passwords\n", len(s.credentials.Usernames), len(s.credentials.Passwords))
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadRoutes opens a dictionary file and returns its contents as a Routes structure.
|
||||
func (s *Scanner) LoadRoutes() error {
|
||||
s.term.Debugf("Loading routes dictionary from path %q\n", s.routeDictionaryPath)
|
||||
|
||||
file, err := os.Open(s.routeDictionaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open dictionary: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
s.routes = append(s.routes, scanner.Text())
|
||||
}
|
||||
|
||||
s.term.Debugf("Loaded %d routes\n", len(s.routes))
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// ParseCredentialsFromString parses a dictionary string and returns its contents as a Credentials structure.
|
||||
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")
|
||||
}
|
||||
|
||||
// LoadTargets parses the file containing hosts to targets, if the targets are
|
||||
// just set to a file name.
|
||||
func (s *Scanner) LoadTargets() error {
|
||||
if len(s.targets) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := s.targets[0]
|
||||
|
||||
_, err := fs.Stat(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open targets file %q: %v", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read targets file %q: %v", path, err)
|
||||
}
|
||||
|
||||
s.targets = strings.Split(string(bytes), "\n")
|
||||
|
||||
s.term.Debugf("Successfylly parsed targets file with %d entries", len(s.targets))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ullaakut/disgo"
|
||||
|
||||
"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"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
input []byte
|
||||
fileExists bool
|
||||
|
||||
expectedCredentials Credentials
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "Valid baseline",
|
||||
|
||||
fileExists: true,
|
||||
input: credentialsJSONString,
|
||||
expectedCredentials: validCredentials,
|
||||
},
|
||||
{
|
||||
description: "File does not exist",
|
||||
|
||||
fileExists: false,
|
||||
input: credentialsJSONString,
|
||||
expectedErr: errors.New("could not read credentials dictionary file at \"/tmp/cameradar_test_load_credentials_1.xml\": open /tmp/cameradar_test_load_credentials_1.xml: no such file or directory"),
|
||||
},
|
||||
{
|
||||
description: "Invalid format",
|
||||
|
||||
fileExists: true,
|
||||
input: []byte("not json"),
|
||||
expectedErr: errors.New("unable to unmarshal dictionary contents: invalid character 'o' in literal null (expecting 'u')"),
|
||||
},
|
||||
{
|
||||
description: "No streams in dictionary",
|
||||
|
||||
fileExists: true,
|
||||
input: []byte("{\"invalid\":\"json\"}"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml"
|
||||
// create file.
|
||||
if test.fileExists {
|
||||
_, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filePath, test.input, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
}
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
credentialDictionaryPath: filePath,
|
||||
}
|
||||
|
||||
err := scanner.LoadCredentials()
|
||||
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
|
||||
assert.Len(t, scanner.credentials.Usernames, len(test.expectedCredentials.Usernames))
|
||||
for _, expectedUsername := range test.expectedCredentials.Usernames {
|
||||
assert.Contains(t, scanner.credentials.Usernames, expectedUsername)
|
||||
}
|
||||
|
||||
assert.Len(t, scanner.credentials.Passwords, len(test.expectedCredentials.Passwords))
|
||||
for _, expectedPassword := range test.expectedCredentials.Passwords {
|
||||
assert.Contains(t, scanner.credentials.Passwords, expectedPassword)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRoutes(t *testing.T) {
|
||||
routesJSONString := []byte("admin\nroot")
|
||||
validRoutes := Routes{"admin", "root"}
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
input []byte
|
||||
fileExists bool
|
||||
|
||||
expectedRoutes Routes
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "Valid baseline",
|
||||
|
||||
fileExists: true,
|
||||
input: routesJSONString,
|
||||
expectedRoutes: validRoutes,
|
||||
},
|
||||
{
|
||||
description: "File does not exist",
|
||||
|
||||
fileExists: false,
|
||||
input: routesJSONString,
|
||||
expectedErr: errors.New("unable to open dictionary: open /tmp/cameradar_test_load_routes_1.xml: no such file or directory"),
|
||||
},
|
||||
{
|
||||
description: "No streams in dictionary",
|
||||
|
||||
fileExists: true,
|
||||
input: []byte(""),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml"
|
||||
|
||||
// Create file.
|
||||
if test.fileExists {
|
||||
_, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filePath, test.input, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("could not write xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
routeDictionaryPath: filePath,
|
||||
}
|
||||
|
||||
err := scanner.LoadRoutes()
|
||||
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
|
||||
assert.Len(t, scanner.routes, len(test.expectedRoutes))
|
||||
for _, expectedRoute := range test.expectedRoutes {
|
||||
assert.Contains(t, scanner.routes, expectedRoute)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
str string
|
||||
expectedCredentials Credentials
|
||||
}{
|
||||
{
|
||||
str: "{\"usernames\":[\"\",\"admin\",\"Admin\",\"Administrator\",\"root\",\"supervisor\",\"ubnt\",\"service\",\"Dinion\",\"administrator\",\"admin1\"],\"passwords\":[\"\",\"admin\",\"9999\",\"123456\",\"pass\",\"camera\",\"1234\",\"12345\",\"fliradmin\",\"system\",\"jvc\",\"meinsm\",\"root\",\"4321\",\"111111\",\"1111111\",\"password\",\"ikwd\",\"supervisor\",\"ubnt\",\"wbox123\",\"service\"]}",
|
||||
expectedCredentials: defaultCredentials,
|
||||
},
|
||||
{
|
||||
str: "{}",
|
||||
expectedCredentials: Credentials{},
|
||||
},
|
||||
{
|
||||
str: "{\"invalid_field\":42}",
|
||||
expectedCredentials: Credentials{},
|
||||
},
|
||||
{
|
||||
str: "not json",
|
||||
expectedCredentials: Credentials{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
parsedCredentials, _ := ParseCredentialsFromString(test.str)
|
||||
assert.Equal(t, test.expectedCredentials, parsedCredentials)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRoutesFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
expectedRoutes Routes
|
||||
}{
|
||||
{
|
||||
str: "a\nb\nc",
|
||||
expectedRoutes: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
str: "a",
|
||||
expectedRoutes: []string{"a"},
|
||||
},
|
||||
{
|
||||
str: "",
|
||||
expectedRoutes: []string{""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.expectedRoutes, ParseRoutesFromString(test.str))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTargets(t *testing.T) {
|
||||
|
||||
oldFS := fs
|
||||
mfs := &mockedFS{}
|
||||
fs = mfs
|
||||
defer func() {
|
||||
fs = oldFS
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []string
|
||||
|
||||
fileExists bool
|
||||
openError bool
|
||||
readError bool
|
||||
|
||||
expectedTargets []string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
description: "not a file",
|
||||
|
||||
targets: []string{"0.0.0.0"},
|
||||
|
||||
fileExists: false,
|
||||
|
||||
expectedTargets: []string{"0.0.0.0"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
description: "not file targets",
|
||||
|
||||
targets: []string{"0.0.0.0", "1.2.3.4/24"},
|
||||
|
||||
expectedTargets: []string{"0.0.0.0", "1.2.3.4/24"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
description: "file contains targets",
|
||||
|
||||
targets: []string{"test_does_not_really_exist"},
|
||||
|
||||
fileExists: true,
|
||||
|
||||
expectedTargets: []string{"0.0.0.0", "localhost", "192.17.0.0/16", "192.168.1.140-255", "192.168.2-3.0-255"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
description: "open error",
|
||||
|
||||
targets: []string{"test_does_not_really_exist"},
|
||||
|
||||
fileExists: true,
|
||||
openError: true,
|
||||
|
||||
expectedTargets: []string{"test_does_not_really_exist"},
|
||||
expectedError: errors.New("unable to open targets file \"test_does_not_really_exist\": file does not exist"),
|
||||
},
|
||||
{
|
||||
description: "read error",
|
||||
|
||||
targets: []string{"test_does_not_really_exist"},
|
||||
|
||||
fileExists: true,
|
||||
readError: true,
|
||||
|
||||
expectedTargets: []string{"test_does_not_really_exist"},
|
||||
expectedError: errors.New("unable to read targets file \"test_does_not_really_exist\": file does not exist"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
mfs.fileExists = test.fileExists
|
||||
mfs.openError = test.openError
|
||||
|
||||
mfs.fileMock = &fileMock{
|
||||
readError: test.readError,
|
||||
}
|
||||
mfs.fileMock.On("Close").Return(nil)
|
||||
mfs.fileMock.WriteString("0.0.0.0\nlocalhost\n192.17.0.0/16\n192.168.1.140-255\n192.168.2-3.0-255")
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
targets: test.targets,
|
||||
}
|
||||
|
||||
err := scanner.LoadTargets()
|
||||
assert.Equal(t, test.expectedTargets, scanner.targets)
|
||||
assert.Equal(t, test.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is completely useless and just lets me
|
||||
// not look at these two red lines on the coverage
|
||||
// any longer.
|
||||
func TestFS(t *testing.T) {
|
||||
fs := osFS{}
|
||||
|
||||
fs.Open("test")
|
||||
fs.Stat("test")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cameradar
|
||||
|
||||
import "time"
|
||||
|
||||
// Stream represents a camera's RTSP stream
|
||||
type Stream struct {
|
||||
Device string `json:"device"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Route string `json:"route"`
|
||||
Address string `json:"address" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
|
||||
CredentialsFound bool `json:"credentials_found"`
|
||||
RouteFound bool `json:"route_found"`
|
||||
Available bool `json:"available"`
|
||||
|
||||
AuthenticationType int `json:"authentication_type"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Targets []string `json:"target" validate:"required"`
|
||||
Ports []string `json:"ports"`
|
||||
Routes Routes `json:"routes"`
|
||||
Credentials Credentials `json:"credentials"`
|
||||
Speed int `json:"speed"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ullaakut/nmap"
|
||||
)
|
||||
|
||||
// Scan scans the target networks and tries to find RTSP streams within them.
|
||||
//
|
||||
// targets can be:
|
||||
//
|
||||
// - a subnet (e.g.: 172.16.100.0/24)
|
||||
// - an IP (e.g.: 172.16.100.10)
|
||||
// - a hostname (e.g.: localhost)
|
||||
// - a range of IPs (e.g.: 172.16.100.10-20)
|
||||
//
|
||||
// ports can be:
|
||||
//
|
||||
// - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554)
|
||||
func (s *Scanner) Scan() ([]Stream, error) {
|
||||
s.term.StartStep("Scanning the network")
|
||||
|
||||
// Run nmap command to discover open ports on the specified targets & ports.
|
||||
nmapScanner, err := nmap.NewScanner(
|
||||
nmap.WithTargets(s.targets...),
|
||||
nmap.WithPorts(s.ports...),
|
||||
nmap.WithTimingTemplate(nmap.Timing(s.speed)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, s.term.FailStepf("unable to create network scanner: %v", err)
|
||||
}
|
||||
|
||||
return s.scan(nmapScanner)
|
||||
}
|
||||
|
||||
func (s *Scanner) scan(nmapScanner nmap.ScanRunner) ([]Stream, error) {
|
||||
results, err := nmapScanner.Run()
|
||||
if err != nil {
|
||||
return nil, s.term.FailStepf("error while scanning network: %v", err)
|
||||
}
|
||||
|
||||
// Get streams from nmap results.
|
||||
var streams []Stream
|
||||
for _, host := range results.Hosts {
|
||||
for _, port := range host.Ports {
|
||||
if port.Status() != "open" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(port.Service.Name, "rtsp") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, address := range host.Addresses {
|
||||
streams = append(streams, Stream{
|
||||
Device: port.Service.Product,
|
||||
Address: address.Addr,
|
||||
Port: port.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.term.Debugf("Found %d RTSP streams\n", len(streams))
|
||||
|
||||
s.term.EndStep()
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ullaakut/disgo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/ullaakut/nmap"
|
||||
)
|
||||
|
||||
type nmapMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *nmapMock) Run() (*nmap.Run, error) {
|
||||
args := m.Called()
|
||||
|
||||
if args.Get(0) != nil {
|
||||
return args.Get(0).(*nmap.Run), args.Error(1)
|
||||
}
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
var (
|
||||
validStream1 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
|
||||
validStream2 = Stream{
|
||||
Device: "fakeDevice",
|
||||
Address: "differentFakeAddress",
|
||||
Port: 1337,
|
||||
}
|
||||
|
||||
invalidStreamNoPort = Stream{
|
||||
Device: "invalidDevice",
|
||||
Address: "fakeAddress",
|
||||
Port: 0,
|
||||
}
|
||||
|
||||
invalidStreamNoAddress = Stream{
|
||||
Device: "invalidDevice",
|
||||
Address: "",
|
||||
Port: 1337,
|
||||
}
|
||||
)
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []string
|
||||
ports []string
|
||||
speed int
|
||||
removePath bool
|
||||
|
||||
expectedErr error
|
||||
expectedStreams []Stream
|
||||
}{
|
||||
{
|
||||
description: "create new scanner and call scan, no error",
|
||||
|
||||
targets: []string{"localhost"},
|
||||
ports: []string{"80"},
|
||||
speed: 5,
|
||||
},
|
||||
{
|
||||
description: "create new scanner with missing nmap installation",
|
||||
|
||||
removePath: true,
|
||||
ports: []string{"80"},
|
||||
|
||||
expectedErr: errors.New("unable to create network scanner: 'nmap' binary was not found"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
if test.removePath {
|
||||
os.Setenv("PATH", "")
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
targets: test.targets,
|
||||
ports: test.ports,
|
||||
speed: test.speed,
|
||||
}
|
||||
|
||||
result, err := scanner.Scan()
|
||||
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Equal(t, test.expectedStreams, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalScan(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
nmapResult *nmap.Run
|
||||
nmapError error
|
||||
|
||||
expectedStreams []Stream
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "valid streams",
|
||||
|
||||
nmapResult: &nmap.Run{
|
||||
Hosts: []nmap.Host{
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: validStream1.Address,
|
||||
},
|
||||
},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "open",
|
||||
},
|
||||
ID: validStream1.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "rtsp",
|
||||
Product: validStream1.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: validStream2.Address,
|
||||
},
|
||||
},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "open",
|
||||
},
|
||||
ID: validStream2.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "rtsp-alt",
|
||||
Product: validStream2.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectedStreams: []Stream{validStream1, validStream2},
|
||||
},
|
||||
{
|
||||
description: "two invalid targets, no error",
|
||||
|
||||
nmapResult: &nmap.Run{
|
||||
Hosts: []nmap.Host{
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: invalidStreamNoPort.Address,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Addresses: []nmap.Address{},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "open",
|
||||
},
|
||||
ID: validStream2.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "rtsp-alt",
|
||||
Product: invalidStreamNoAddress.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectedStreams: nil,
|
||||
},
|
||||
{
|
||||
description: "different port states, no error",
|
||||
|
||||
nmapResult: &nmap.Run{
|
||||
Hosts: []nmap.Host{
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: invalidStreamNoPort.Address,
|
||||
}},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "closed",
|
||||
},
|
||||
ID: validStream2.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "rtsp-alt",
|
||||
Product: invalidStreamNoAddress.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: invalidStreamNoPort.Address,
|
||||
}},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "unfiltered",
|
||||
},
|
||||
ID: validStream2.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "rtsp-alt",
|
||||
Product: invalidStreamNoAddress.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: invalidStreamNoPort.Address,
|
||||
}},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "filtered",
|
||||
},
|
||||
ID: validStream2.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "rtsp-alt",
|
||||
Product: invalidStreamNoAddress.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectedStreams: nil,
|
||||
},
|
||||
{
|
||||
description: "not rtsp, no error",
|
||||
|
||||
nmapResult: &nmap.Run{
|
||||
Hosts: []nmap.Host{
|
||||
{
|
||||
Addresses: []nmap.Address{
|
||||
{
|
||||
Addr: invalidStreamNoPort.Address,
|
||||
}},
|
||||
Ports: []nmap.Port{
|
||||
{
|
||||
State: nmap.State{
|
||||
State: "open",
|
||||
},
|
||||
ID: validStream2.Port,
|
||||
Service: nmap.Service{
|
||||
Name: "tcp",
|
||||
Product: invalidStreamNoAddress.Device,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectedStreams: nil,
|
||||
},
|
||||
{
|
||||
description: "no hosts found",
|
||||
|
||||
nmapResult: &nmap.Run{},
|
||||
expectedStreams: nil,
|
||||
},
|
||||
{
|
||||
description: "scan failed",
|
||||
|
||||
nmapError: errors.New("scan failed"),
|
||||
expectedErr: errors.New("error while scanning network: scan failed"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
nmapMock := &nmapMock{}
|
||||
|
||||
nmapMock.On("Run").Return(test.nmapResult, test.nmapError)
|
||||
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
|
||||
}
|
||||
|
||||
results, err := scanner.scan(nmapMock)
|
||||
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Equal(t, test.expectedStreams, results, "wrong streams parsed")
|
||||
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
|
||||
|
||||
nmapMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ullaakut/disgo"
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
)
|
||||
|
||||
// Scanner represents a cameradar scanner. It scans a network and
|
||||
// attacks all streams found to get their RTSP credentials.
|
||||
type Scanner struct {
|
||||
curl Curler
|
||||
term *disgo.Terminal
|
||||
|
||||
targets []string
|
||||
ports []string
|
||||
debug bool
|
||||
verbose bool
|
||||
speed int
|
||||
timeout time.Duration
|
||||
credentialDictionaryPath string
|
||||
routeDictionaryPath string
|
||||
|
||||
credentials Credentials
|
||||
routes Routes
|
||||
}
|
||||
|
||||
// New creates a new Cameradar Scanner and applies the given options.
|
||||
func New(options ...func(*Scanner)) (*Scanner, error) {
|
||||
err := curl.GlobalInit(curl.GLOBAL_ALL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize curl library: %v", err)
|
||||
}
|
||||
|
||||
handle := curl.EasyInit()
|
||||
if handle == nil {
|
||||
return nil, fmt.Errorf("unable to initialize curl handle: %v", err)
|
||||
}
|
||||
|
||||
scanner := &Scanner{
|
||||
curl: &Curl{CURL: handle},
|
||||
credentialDictionaryPath: "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/credentials.json",
|
||||
routeDictionaryPath: "<GOPATH>/src/github.com/ullaakut/cameradar/dictionaries/routes",
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(scanner)
|
||||
}
|
||||
|
||||
gopath := os.Getenv("GOPATH")
|
||||
scanner.credentialDictionaryPath = strings.Replace(scanner.credentialDictionaryPath, "<GOPATH>", gopath, 1)
|
||||
scanner.routeDictionaryPath = strings.Replace(scanner.routeDictionaryPath, "<GOPATH>", gopath, 1)
|
||||
|
||||
scanner.term = disgo.NewTerminal(
|
||||
disgo.WithDebug(scanner.debug),
|
||||
)
|
||||
|
||||
err = scanner.LoadTargets()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse target file: %v", err)
|
||||
}
|
||||
|
||||
scanner.term.StartStepf("Loading credentials")
|
||||
err = scanner.LoadCredentials()
|
||||
if err != nil {
|
||||
return nil, scanner.term.FailStepf("unable to load credentials dictionary: %v", err)
|
||||
}
|
||||
|
||||
scanner.term.StartStepf("Loading routes")
|
||||
err = scanner.LoadRoutes()
|
||||
if err != nil {
|
||||
return nil, scanner.term.FailStepf("unable to load credentials dictionary: %v", err)
|
||||
}
|
||||
|
||||
disgo.EndStep()
|
||||
|
||||
return scanner, nil
|
||||
}
|
||||
|
||||
// WithTargets specifies the targets to scan and attack.
|
||||
func WithTargets(targets []string) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.targets = targets
|
||||
}
|
||||
}
|
||||
|
||||
// WithPorts specifies the ports to scan and attack.
|
||||
func WithPorts(ports []string) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.ports = ports
|
||||
}
|
||||
}
|
||||
|
||||
// WithDebug specifies whether or not to enable debug logs.
|
||||
func WithDebug(debug bool) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
// WithVerbose specifies whether or not to enable verbose logs.
|
||||
func WithVerbose(verbose bool) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.verbose = verbose
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomCredentials specifies a custom credential dictionary
|
||||
// to use for the attacks.
|
||||
func WithCustomCredentials(dictionaryPath string) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.credentialDictionaryPath = dictionaryPath
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomRoutes specifies a custom route dictionary
|
||||
// to use for the attacks.
|
||||
func WithCustomRoutes(dictionaryPath string) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.routeDictionaryPath = dictionaryPath
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpeed specifies the speed at which the scan should be executed. Faster
|
||||
// means easier to detect, slower has bigger timeout values and is more silent.
|
||||
func WithSpeed(speed int) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.speed = speed
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout specifies the amount of time after which attack requests should
|
||||
// timeout. This should be high if the network you are attacking has a poor
|
||||
// connectivity or that you are located far away from it.
|
||||
func WithTimeout(timeout time.Duration) func(s *Scanner) {
|
||||
return func(s *Scanner) {
|
||||
s.timeout = timeout
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
targets []string
|
||||
ports []string
|
||||
debug bool
|
||||
verbose bool
|
||||
customCredentials string
|
||||
customRoutes string
|
||||
speed int
|
||||
timeout time.Duration
|
||||
|
||||
loadTargetsFail bool
|
||||
loadCredsFail bool
|
||||
loadRoutesFail bool
|
||||
|
||||
curlGlobalFail bool
|
||||
curlEasyFail bool
|
||||
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
description: "no error while loading dictionaries",
|
||||
|
||||
targets: []string{"titi", "toto"},
|
||||
ports: []string{"554"},
|
||||
debug: true,
|
||||
verbose: false,
|
||||
speed: 3,
|
||||
timeout: time.Millisecond,
|
||||
},
|
||||
{
|
||||
description: "unable to load targets",
|
||||
|
||||
loadTargetsFail: true,
|
||||
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
description: "unable to load credentials",
|
||||
|
||||
loadCredsFail: true,
|
||||
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
description: "unable to load routes",
|
||||
|
||||
loadRoutesFail: true,
|
||||
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
description: "curl fails to init",
|
||||
|
||||
curlGlobalFail: true,
|
||||
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
description: "curl fails to create handle",
|
||||
|
||||
curlEasyFail: true,
|
||||
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
if test.loadTargetsFail {
|
||||
test.targets = []string{generateTmpFileName(i, "targets")}
|
||||
ioutil.WriteFile(test.targets[0], []byte(`0.0.0.0`), 0000)
|
||||
}
|
||||
|
||||
if !test.loadCredsFail {
|
||||
test.customCredentials = generateTmpFileName(i, "creds")
|
||||
ioutil.WriteFile(test.customCredentials, []byte(`{"usernames":["admin"],"passwords":["admin"]}`), 0644)
|
||||
}
|
||||
|
||||
if !test.loadRoutesFail {
|
||||
test.customRoutes = generateTmpFileName(i, "routes")
|
||||
ioutil.WriteFile(test.customRoutes, []byte(`live.sdp`), 0644)
|
||||
}
|
||||
|
||||
curl.TestGlobalFail = test.curlGlobalFail
|
||||
curl.TestEasyFail = test.curlEasyFail
|
||||
|
||||
scanner, err := New(
|
||||
WithTargets(test.targets),
|
||||
WithPorts(test.ports),
|
||||
WithDebug(test.debug),
|
||||
WithVerbose(test.verbose),
|
||||
WithSpeed(test.speed),
|
||||
WithTimeout(test.timeout),
|
||||
WithCustomCredentials(test.customCredentials),
|
||||
WithCustomRoutes(test.customRoutes),
|
||||
)
|
||||
|
||||
if test.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if scanner != nil {
|
||||
assert.Equal(t, test.targets, scanner.targets)
|
||||
assert.Equal(t, test.ports, scanner.ports)
|
||||
assert.Equal(t, test.debug, scanner.debug)
|
||||
assert.Equal(t, test.verbose, scanner.verbose)
|
||||
assert.Equal(t, test.speed, scanner.speed)
|
||||
assert.Equal(t, test.timeout, scanner.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateTmpFileName(iteration int, purpose string) string {
|
||||
return fmt.Sprintf("/tmp/cameradar_test_scanner_%s_%d_%d", purpose, time.Now().Unix(), iteration)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"github.com/ullaakut/disgo/style"
|
||||
curl "github.com/ullaakut/go-curl"
|
||||
)
|
||||
|
||||
// PrintStreams prints information on each stream.
|
||||
func (s *Scanner) PrintStreams(streams []Stream) {
|
||||
if len(streams) == 0 {
|
||||
s.term.Infof("%s No streams were found. Please make sure that your target is on an accessible network.\n", style.Failure(style.SymbolCross))
|
||||
}
|
||||
|
||||
success := 0
|
||||
for _, stream := range streams {
|
||||
if stream.Available {
|
||||
s.term.Infof("%s\tDevice RTSP URL:\t%s\n", style.Success(style.SymbolRightTriangle), style.Link(GetCameraRTSPURL(stream)))
|
||||
s.term.Infof("\tAvailable:\t\t%s\n", style.Success(style.SymbolCheck))
|
||||
success++
|
||||
} else {
|
||||
s.term.Infof("%s\tAdmin panel URL:\t%s You can use this URL to try attacking the camera's admin panel instead.\n", style.Failure(style.SymbolCross), style.Link(GetCameraAdminPanelURL(stream)))
|
||||
s.term.Infof("\tAvailable:\t\t%s\n", style.Failure(style.SymbolCross))
|
||||
}
|
||||
|
||||
if len(stream.Device) > 0 {
|
||||
s.term.Infof("\tDevice model:\t\t%s\n\n", stream.Device)
|
||||
}
|
||||
|
||||
s.term.Infof("\tIP address:\t\t%s\n", stream.Address)
|
||||
s.term.Infof("\tRTSP port:\t\t%d\n", stream.Port)
|
||||
|
||||
switch stream.AuthenticationType {
|
||||
case curl.AUTH_NONE:
|
||||
s.term.Infoln("\tThis camera does not require authentication")
|
||||
case curl.AUTH_BASIC:
|
||||
s.term.Infoln("\tAuth type:\t\tbasic")
|
||||
case curl.AUTH_DIGEST:
|
||||
s.term.Infoln("\tAuth type:\t\tdigest")
|
||||
}
|
||||
|
||||
if stream.CredentialsFound {
|
||||
s.term.Infof("\tUsername:\t\t%s\n", style.Success(stream.Username))
|
||||
s.term.Infof("\tPassword:\t\t%s\n", style.Success(stream.Password))
|
||||
} else {
|
||||
s.term.Infof("\tUsername:\t\t%s\n", style.Failure("not found"))
|
||||
s.term.Infof("\tPassword:\t\t%s\n", style.Failure("not found"))
|
||||
}
|
||||
|
||||
if stream.RouteFound {
|
||||
s.term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Success("/"+stream.Route))
|
||||
} else {
|
||||
s.term.Infof("\tRTSP route:\t\t%s\n\n\n", style.Failure("not found"))
|
||||
}
|
||||
}
|
||||
|
||||
if success > 1 {
|
||||
s.term.Infof("%s Successful attack: %s devices were accessed", style.Success(style.SymbolCheck), style.Success(len(streams)))
|
||||
} else if success == 1 {
|
||||
s.term.Infof("%s Successful attack: %s device was accessed", style.Success(style.SymbolCheck), style.Success("one"))
|
||||
} else {
|
||||
s.term.Infof("%s Streams were found but none were accessed. They are most likely configured with secure credentials and routes. You can try adding entries to the dictionary or generating your own in order to attempt a bruteforce attack on the cameras.\n", style.Failure("\xE2\x9C\x96"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package cameradar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/ullaakut/disgo"
|
||||
)
|
||||
|
||||
var (
|
||||
unavailable = Stream{}
|
||||
|
||||
available = Stream{
|
||||
Available: true,
|
||||
}
|
||||
|
||||
deviceFound = Stream{
|
||||
Device: "devicename",
|
||||
}
|
||||
|
||||
noAuth = Stream{
|
||||
AuthenticationType: 0,
|
||||
}
|
||||
|
||||
basic = Stream{
|
||||
AuthenticationType: 1,
|
||||
}
|
||||
|
||||
digest = Stream{
|
||||
AuthenticationType: 2,
|
||||
}
|
||||
|
||||
credsFound = Stream{
|
||||
CredentialsFound: true,
|
||||
Username: "us3r",
|
||||
Password: "p4ss",
|
||||
}
|
||||
|
||||
routeFound = Stream{
|
||||
RouteFound: true,
|
||||
Route: "r0ute",
|
||||
}
|
||||
)
|
||||
|
||||
func TestPrintStreams(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
streams []Stream
|
||||
|
||||
expectedLogs []string
|
||||
}{
|
||||
{
|
||||
description: "displays the proper message when no streams found",
|
||||
|
||||
streams: nil,
|
||||
|
||||
expectedLogs: []string{"No streams were found"},
|
||||
},
|
||||
{
|
||||
description: "displays the admin panel URL when a stream is not accessible",
|
||||
|
||||
streams: []Stream{
|
||||
unavailable,
|
||||
},
|
||||
|
||||
expectedLogs: []string{"Admin panel URL"},
|
||||
},
|
||||
{
|
||||
description: "displays the device name when it is found",
|
||||
|
||||
streams: []Stream{
|
||||
deviceFound,
|
||||
},
|
||||
|
||||
expectedLogs: []string{"Device model:"},
|
||||
},
|
||||
{
|
||||
description: "displays authentication type (no auth)",
|
||||
|
||||
streams: []Stream{
|
||||
noAuth,
|
||||
},
|
||||
|
||||
expectedLogs: []string{"This camera does not require authentication"},
|
||||
},
|
||||
{
|
||||
description: "displays authentication type (basic)",
|
||||
|
||||
streams: []Stream{
|
||||
basic,
|
||||
},
|
||||
|
||||
expectedLogs: []string{"basic"},
|
||||
},
|
||||
{
|
||||
description: "displays authentication type (digest)",
|
||||
|
||||
streams: []Stream{
|
||||
digest,
|
||||
},
|
||||
|
||||
expectedLogs: []string{"digest"},
|
||||
},
|
||||
{
|
||||
description: "displays credentials properly",
|
||||
|
||||
streams: []Stream{
|
||||
credsFound,
|
||||
},
|
||||
|
||||
expectedLogs: []string{
|
||||
"Username",
|
||||
"us3r",
|
||||
"Password",
|
||||
"p4ss",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "displays route properly",
|
||||
|
||||
streams: []Stream{
|
||||
routeFound,
|
||||
},
|
||||
|
||||
expectedLogs: []string{
|
||||
"RTSP route",
|
||||
"/r0ute",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "displays successes properly (no success)",
|
||||
|
||||
streams: []Stream{
|
||||
unavailable,
|
||||
},
|
||||
|
||||
expectedLogs: []string{
|
||||
"Streams were found but none were accessed",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "displays successes properly (1 success)",
|
||||
|
||||
streams: []Stream{
|
||||
available,
|
||||
},
|
||||
|
||||
expectedLogs: []string{
|
||||
"Successful attack",
|
||||
"device was accessed",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "displays successes properly (multiple successes)",
|
||||
|
||||
streams: []Stream{
|
||||
available,
|
||||
available,
|
||||
available,
|
||||
available,
|
||||
},
|
||||
|
||||
expectedLogs: []string{
|
||||
"Successful attack",
|
||||
"devices were accessed",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
scanner := &Scanner{
|
||||
term: disgo.NewTerminal(disgo.WithDefaultOutput(writer)),
|
||||
}
|
||||
|
||||
scanner.PrintStreams(test.streams)
|
||||
|
||||
for _, expectedLog := range test.expectedLogs {
|
||||
assert.Contains(t, writer.String(), expectedLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ullaakut/disgo/style"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/ullaakut/disgo"
|
||||
"github.com/vbauerster/mpb"
|
||||
"github.com/vbauerster/mpb/decor"
|
||||
)
|
||||
|
||||
const dictionaryURL = "https://community.geniusvision.net/platform/cprndr/manulist"
|
||||
|
||||
var rtspURLsFound sync.Map
|
||||
|
||||
func main() {
|
||||
if err := updateDictionary(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func updateDictionary() error {
|
||||
disgo.SetTerminalOptions(disgo.WithColors(true), disgo.WithDebug(true))
|
||||
|
||||
disgo.StartStep("Fetching dictionary list")
|
||||
resp, err := http.Get(dictionaryURL)
|
||||
if err != nil {
|
||||
return disgo.FailStepf("unable to download dictionaries: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
disgo.StartStep("Parsing dictionary list")
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return disgo.FailStepf("unable to read from dictionary list: %v", err)
|
||||
}
|
||||
|
||||
var vendorURLs []string
|
||||
doc.Find("td.simpletable a").Each(func(i int, s *goquery.Selection) {
|
||||
url, ok := s.Attr("href")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if url != "javascript:void(0)" {
|
||||
vendorURLs = append(vendorURLs, url)
|
||||
}
|
||||
})
|
||||
|
||||
disgo.StartStep("Loading current cameradar dictionary")
|
||||
currentDictionary, err := ioutil.ReadFile("dictionaries/routes")
|
||||
if err != nil {
|
||||
return disgo.FailStepf("unable to read current dictionary: %v", err)
|
||||
}
|
||||
|
||||
dictionaryEntries := bytes.Split(currentDictionary, []byte("\n"))
|
||||
|
||||
for _, rtspURL := range dictionaryEntries {
|
||||
rtspURLsFound.Store(string(rtspURL), struct{}{})
|
||||
}
|
||||
|
||||
disgo.Debugf("Current dictionary has %d entries\n", len(dictionaryEntries))
|
||||
disgo.EndStep()
|
||||
|
||||
p := mpb.New(mpb.WithWidth(64))
|
||||
name := fmt.Sprintf("Fetching default routes from %d constructors:", len(vendorURLs))
|
||||
bar := p.AddBar(int64(len(vendorURLs)),
|
||||
// set custom bar style, default one is "[=>-]"
|
||||
mpb.BarStyle("╢▌▌░╟"),
|
||||
mpb.PrependDecorators(
|
||||
// display our name with one space on the right
|
||||
decor.Name(name, decor.WC{W: len(name), C: decor.DidentRight}),
|
||||
),
|
||||
mpb.AppendDecorators(decor.Percentage()),
|
||||
)
|
||||
|
||||
for _, url := range vendorURLs {
|
||||
go loadRoutes(url, bar)
|
||||
}
|
||||
|
||||
p.Wait()
|
||||
|
||||
disgo.StartStep("Converting found routes into proper data model")
|
||||
|
||||
var rtspURLs []string
|
||||
rtspURLsFound.Range(func(rtspURL, _ interface{}) bool {
|
||||
disgo.Infoln("Adding URL", rtspURL.(string))
|
||||
rtspURLs = append(rtspURLs, rtspURL.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
sort.Slice(rtspURLs, func(a, b int) bool {
|
||||
return rtspURLs[a] < rtspURLs[b]
|
||||
})
|
||||
|
||||
disgo.EndStep()
|
||||
|
||||
if len(dictionaryEntries) < len(rtspURLs) {
|
||||
disgo.Infof("%s Saving them in cameradar default dictionary.\n", style.Success("Found ", len(rtspURLs)-len(dictionaryEntries), " new entries!"))
|
||||
saveRoutes(rtspURLs)
|
||||
} else {
|
||||
disgo.Infoln(style.Success("No new entry found, dictionary up-to-date! :)"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRoutes(url string, bar *mpb.Bar) {
|
||||
defer bar.IncrBy(1)
|
||||
|
||||
var (
|
||||
failureCounter int
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
for failureCounter < 5 {
|
||||
resp, err = http.Get(url)
|
||||
if err != nil {
|
||||
failureCounter++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if failureCounter == 5 {
|
||||
disgo.Errorln("Request failed 5 times in a row, giving up on this vendor")
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
disgo.Errorf("unable to read from dictionary list for URL %q: %v\n", url, err)
|
||||
return
|
||||
}
|
||||
|
||||
doc.Find("tr.simpletable td.simpletable:nth-child(4) a").Each(func(i int, s *goquery.Selection) {
|
||||
rtspURL := s.Text()
|
||||
|
||||
if strings.HasPrefix(rtspURL, "(") && strings.HasSuffix(rtspURL, ")") {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rtspURL, "[") && strings.HasSuffix(rtspURL, "]") {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rtspURL, "http://") {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip the port and only get the route.
|
||||
if strings.HasPrefix(rtspURL, "rtsp://ip-addr:") {
|
||||
routeAndPort := strings.TrimSpace(strings.TrimPrefix(rtspURL, "rtsp://ip-addr:"))
|
||||
route := strings.TrimLeft(routeAndPort, "0123456789/")
|
||||
rtspURLsFound.Store(route, struct{}{})
|
||||
return
|
||||
}
|
||||
|
||||
switch rtspURL {
|
||||
case "",
|
||||
"rtsp://ip-addr/",
|
||||
"rtsp://ip-addr",
|
||||
"rtsp://ip-addr:pass@10.0.0.5:6667/blinkhd":
|
||||
return
|
||||
default:
|
||||
route := strings.TrimSpace(strings.TrimPrefix(rtspURL, "rtsp://ip-addr/"))
|
||||
rtspURLsFound.Store(route, struct{}{})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func saveRoutes(rtspURLs []string) {
|
||||
contents := strings.Join(rtspURLs, "\n")
|
||||
|
||||
disgo.StartStep("Writing new dictionary file")
|
||||
err := ioutil.WriteFile("dictionaries/routes", []byte(contents), 0644)
|
||||
if err != nil {
|
||||
disgo.FailStepf("unable to write dictionnary: %v", err)
|
||||
}
|
||||
}
|
||||