diff --git a/README.md b/README.md index 4de5b80..eb1807c 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:5 ## Command line options -* **"-t, --target"**: Set custom target. Required. +* **"-t, --target"**: Set target. Required. Target can be a file (see [instructions on how to format the file](#format-input-file)), an IP, an IP range, a subnetwork, or a combination of those. * **"-p, --ports"**: (Default: `554,8554`) Set custom ports. * **"-s, --speed"**: (Default: `4`) Set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html). * **"-T, --timeout"**: (Default: `2000`) Set custom timeout value in miliseconds after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on very performant and reliable networks. @@ -175,6 +175,18 @@ With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:5 * **"-l, --log"**: Enable debug logs (nmap requests, curl describe requests, etc.) * **"-h"** : Display the usage information +## Format input file + +The file can contain IPs, hostnames, IP ranges and subnetwork, separated by newlines. Example: + +``` +0.0.0.0 +localhost +192.17.0.0/16 +192.168.1.140-255 +192.168.2-3.0-255 +``` + ## Environment Variables ### `CAMERADAR_TARGET` @@ -186,6 +198,8 @@ Examples: * `172.16.100.0/24` * `192.168.1.1` * `localhost` +* `192.168.1.140-255` +* `192.168.2-3.0-255` ### `CAMERADAR_PORTS` @@ -270,7 +284,7 @@ You can still find it under the 1.1.4 tag on this repo, however it was less perf > How to use the Cameradar library for my own project? -See the example in `/cameradar`. You just need to run `go get github.com/Ullaakut/cameradar` and to use the `cmrdr` package in your code. +See the example in `/cameradar`. You just need to run `go get github.com/Ullaakut/cameradar` and to use the `cmrdr` package in your code. You can find the documentation on [godoc](https://godoc.org/github.com/Ullaakut/cameradar). > I want to scan my own localhost for some reason and it does not work! What's going on? @@ -284,10 +298,15 @@ You forgot the `-t` flag before `ullaakut/cameradar` in your command-line. This 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)) -## Known issues +## Examples -* When running Cameradar in a docker container, specifying multiple targets does not work. Using subnetworks (such as `182.49.20.0/24`) or ranges (`182.49.20.0-44`) works. -* There is currently no way to use environment variables instead of command-line arguments in Cameradar. This will be done at some point, but isn't a priority right now. +> Running cameradar on your own machine to scan for default ports + +`docker run --net=host -t ullaakut/cameradar -t localhost` + +> Running cameradar with an input file, logs enabled on port 8554 + +`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t /tmp/test.txt -p 8554 -l` ## License diff --git a/cameradar/cameradar.go b/cameradar/cameradar.go index c668837..c9f7875 100644 --- a/cameradar/cameradar.go +++ b/cameradar/cameradar.go @@ -93,6 +93,11 @@ func main() { w := startSpinner(options.EnableLogs) + options.Target, err = cmrdr.ParseTargetsFile(options.Target) + if err != nil { + printErr(err) + } + err = curl.GlobalInit(curl.GLOBAL_ALL) handle := curl.EasyInit() if err != nil || handle == nil { diff --git a/examples/target_file_example b/examples/target_file_example new file mode 100644 index 0000000..67fbc5a --- /dev/null +++ b/examples/target_file_example @@ -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 diff --git a/loaders.go b/loaders.go index 7556df8..0e5a298 100644 --- a/loaders.go +++ b/loaders.go @@ -3,6 +3,7 @@ package cmrdr import ( "bufio" "encoding/json" + "io" "io/ioutil" "os" "strings" @@ -10,6 +11,27 @@ import ( "github.com/pkg/errors" ) +var fs fileSystem = osFS{} + +type fileSystem interface { + Open(name string) (file, error) + Stat(name string) (os.FileInfo, error) +} + +type file interface { + io.Closer + io.Reader + io.ReaderAt + io.Seeker + Stat() (os.FileInfo, error) +} + +// osFS implements fileSystem using the local disk. +type osFS struct{} + +func (osFS) Open(name string) (file, error) { return os.Open(name) } +func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } + // LoadCredentials opens a dictionary file and returns its contents as a Credentials structure func LoadCredentials(path string) (Credentials, error) { var creds Credentials @@ -63,3 +85,24 @@ func ParseCredentialsFromString(content string) (Credentials, error) { func ParseRoutesFromString(content string) Routes { return strings.Split(content, "\n") } + +// ParseTargetsFile parses an input file containing hosts to targets +func ParseTargetsFile(path string) (string, error) { + _, err := fs.Stat(path) + if err != nil { + return path, nil + } + + file, err := fs.Open(path) + if err != nil { + return path, err + } + defer file.Close() + + bytes, err := ioutil.ReadAll(file) + if err != nil { + return path, err + } + + return strings.Replace(string(bytes), "\n", " ", -1), nil +} diff --git a/loaders_test.go b/loaders_test.go index 4d5443c..9dd1c2b 100644 --- a/loaders_test.go +++ b/loaders_test.go @@ -1,14 +1,89 @@ package cmrdr import ( + "bytes" "fmt" "io/ioutil" "os" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +// Setup Mock +type mockedFS struct { + osFS + + fileExists bool + openError bool + + fileMock *fileMock + + fileSize int64 +} + +// fileMock mocks a file +type fileMock struct { + mock.Mock + + readError bool + + bytes.Buffer +} + +type mockedFileInfo struct { + os.FileInfo +} + +func (m mockedFileInfo) Size() int64 { return 1 } + +func (m mockedFS) Stat(name string) (os.FileInfo, error) { + if !m.fileExists { + return nil, os.ErrNotExist + } + return mockedFileInfo{}, nil +} + +func (m mockedFS) Open(name string) (file, error) { + if m.openError { + return nil, os.ErrNotExist + } + + return m.fileMock, nil +} + +func (m *fileMock) Read(p []byte) (n int, err error) { + if m.readError { + return 0, os.ErrNotExist + } + return m.Buffer.Read(p) +} + +func (m *fileMock) ReadAt(p []byte, off int64) (n int, err error) { + return 1, nil +} + +func (m *fileMock) Seek(offset int64, whence int) (int64, error) { + return offset, nil +} + +func (m *fileMock) Stat() (os.FileInfo, error) { + return mockedFileInfo{}, nil +} + +// Close mock +func (m *fileMock) Close() error { + args := m.Called() + return args.Error(0) +} + +// Sync mock +func (m *fileMock) Sync() error { + args := m.Called() + return args.Error(0) +} + func TestLoadCredentials(t *testing.T) { credentialsJSONString := []byte("{\"usernames\":[\"admin\",\"root\"],\"passwords\":[\"12345\",\"root\"]}") validCredentials := Credentials{ @@ -260,3 +335,74 @@ func TestParseRoutesFromString(t *testing.T) { assert.Equal(t, test.expectedResult, parsedRoutes, "unexpected result, parse error") } } + +func TestParseTargetsFile(t *testing.T) { + + oldFS := fs + mfs := &mockedFS{} + fs = mfs + defer func() { + fs = oldFS + }() + + testCases := []struct { + input string + + fileExists bool + openError bool + readError bool + + expectedResult string + expectedError error + }{ + { + input: "0.0.0.0", + + fileExists: false, + + expectedResult: "0.0.0.0", + expectedError: nil, + }, + { + input: "test_does_not_really_exist", + + fileExists: true, + + expectedResult: "0.0.0.0 localhost 192.17.0.0/16 192.168.1.140-255 192.168.2-3.0-255", + expectedError: nil, + }, + { + input: "test_does_not_really_exist", + + fileExists: true, + openError: true, + + expectedResult: "test_does_not_really_exist", + expectedError: os.ErrNotExist, + }, + { + input: "test_does_not_really_exist", + + fileExists: true, + readError: true, + + expectedResult: "test_does_not_really_exist", + expectedError: os.ErrNotExist, + }, + } + + for _, test := range testCases { + mfs.fileExists = test.fileExists + mfs.openError = test.openError + + mfs.fileMock = &fileMock{ + readError: test.readError, + } + mfs.fileMock.On("Close").Return(nil) + mfs.fileMock.WriteString("0.0.0.0 localhost 192.17.0.0/16 192.168.1.140-255 192.168.2-3.0-255") + + result, err := ParseTargetsFile(test.input) + assert.Equal(t, test.expectedResult, result, "unexpected result, parse error") + assert.Equal(t, test.expectedError, err, "unexpected error") + } +}