diff --git a/config.py b/config.py index d444d27..67b286d 100644 --- a/config.py +++ b/config.py @@ -2,18 +2,22 @@ config = { "parser" : { - "name":"GDocsHashParser", + "name":"Parser", "init_args":{} }, "address_generator" : { - "name":"GDocsAddressGenerator", + "name":"IpGenerator", "init_args":{} }, "scanner" : { - "name":"GDocsScanner", - "init_args":{} + "name":"FTPScanner", + "init_args":{ + "credentials": ( + ("admin", "admin") + ) + } }, "storage" : { @@ -21,17 +25,18 @@ config = { "init_args": { "path":"results.json", - "json_scheme":{ - "status": - { - "gdoc_prefix": - [ - { - "@hash": "gdoc_hash", - "@title": "gdoc_title" - } - ] - } + "json_scheme": + { + "ftp_status": + [ + { + "@ip":"ipv4_str", + "@port":"port", + "@login":"login", + "@password":"password", + "@ftp_version":"ftp_version", + } + ] } } } @@ -54,3 +59,15 @@ config = { } } }''' +'''scheme for gdocs scanner +"status": +{ + "gdoc_prefix": + [ + { + "@hash": "gdoc_hash", + "@title": "gdoc_title" + } + ] +} +''' diff --git a/core/MainPresenter.py b/core/MainPresenter.py index e3492a1..60f0853 100644 --- a/core/MainPresenter.py +++ b/core/MainPresenter.py @@ -72,6 +72,7 @@ class MainPresenter: get_return_annotations(IpGenerator.get_next_address).union(get_return_annotations(CoreModel.scan_address)), get_argument_annotations(self.storage.put_responce) ) + input() self.exit_lock = RLock() def startScan(self, ipRanges, portsStr, threadNumber, timeout): @@ -174,6 +175,7 @@ class ScanWorker(QObject): ) print(scan_result) scan_address.update(scan_result) + print(scan_address) self.previous_address = scan_address self.storage.put_responce( *convert_for_storage(scan_address) @@ -181,9 +183,9 @@ class ScanWorker(QObject): string_scan_address = " ".join(key + ":" + str(scan_address[key]) for key in scan_address.keys()) if scan_result == 0: - self.log_signal.emit('%s is open' % string_scan_address) + self.log_signal.emit(string_scan_address) else: - self.log_signal.emit('%s is closed' % string_scan_address) + self.log_signal.emit(string_scan_address) self.stop() def stop(self): diff --git a/core/communication/ConvertTable.py b/core/communication/ConvertTable.py index f27856d..4de5a4b 100644 --- a/core/communication/ConvertTable.py +++ b/core/communication/ConvertTable.py @@ -30,6 +30,7 @@ class ConvertTable(): function.__annotations__.items() if key!='return') if input_args.issubset(from_keys) and to_key.issubset(function.__annotations__['return']): + print("found converter for %s!!!" % to_key) return input_args, function raise Exception("There is no converter for %s to %s" % (from_keys, to_key)) @@ -38,23 +39,31 @@ class ConvertTable(): def get_metaconverter(self, from_keys, to_keys): '''This function constructs and returns new function used to provide fast conversion from from_keys to to_keys''' + print("from_keys",from_keys) + print("to_keys",to_keys) converters_args = [] converters = [] for key in to_keys: keys_to_convert, converter = None, None if key in from_keys: + print("%s is in from_keys" % key) keys_to_convert = [key] converter = lambda x : {key: x} else: + print("getting converter for %s." % key) keys_to_convert, converter = self.get_converter(from_keys, key) + print("needed keys: %s" % " ".join(keys_to_convert)) converters_args.append(keys_to_convert) converters.append(converter) - def metaconverter(args_dict): if args_dict == None: return [None] * len(converters) res = [] + print(converters) + print(converters_args) for i,conv in enumerate(converters): + print(converters_args[i]) + print(args_dict) args = [args_dict[arg] for arg in converters_args[i]] res.append(*[value for key, value in conv(*args).items()]) return res diff --git a/modules/address_generation/IpGenerator.py b/modules/address_generation/IpGenerator.py index 056066a..2b69ba9 100644 --- a/modules/address_generation/IpGenerator.py +++ b/modules/address_generation/IpGenerator.py @@ -1,17 +1,15 @@ from core.prototypes.AbstractAddressGenerator import AbstractAddressGenerator -from core.prototypes.AbstractModuleClass import internal from threading import RLock import ipaddress from types import GeneratorType class IpGenerator(AbstractAddressGenerator): - def set_parsed_fields(self, ips : 'ipv4_ranges', ports : 'ports') -> None: + def set_parsed_fields(self, ips : 'ipv4_objects', ports : 'ports') -> None: self.ips = ips self.ports = ports self.lock = RLock() - @internal def get_next_port_number(self, previous_port): return (self.ports.index(previous_port) + 1) % len(self.ports) diff --git a/modules/address_generation/Parser.py b/modules/address_generation/Parser.py index a421b27..019a3ce 100644 --- a/modules/address_generation/Parser.py +++ b/modules/address_generation/Parser.py @@ -1,6 +1,5 @@ import ipaddress from core.prototypes.AbstractParser import AbstractParser -from core.prototypes.AbstractModuleClass import internal class Parser(AbstractParser): @@ -9,10 +8,9 @@ class Parser(AbstractParser): 'ports'}: result = dict() result['ports'] = self.parse_port_field(ports) - result['ipv4_ranges'] = self.get_all_addresses(ips) + result['ipv4_objects'] = self.get_all_addresses(ips) return result - @internal def parse_port_field(self, ports): """ Parses ports from string, returns them as integers in the list. @@ -40,7 +38,6 @@ class Parser(AbstractParser): # Change to default ports from constant return [21, 22, 23, 25, 80, 443, 110, 111, 135, 139, 445, 8080, 8443, 53, 143, 989, 990, 3306, 1080, 5554, 6667, 2222, 4444, 666, 6666, 1337, 2020, 31337] - @internal def parse_address_field(self, ips): """ Parses ip input string, returns the generator over them. @@ -62,7 +59,6 @@ class Parser(AbstractParser): for host in ip_obj: yield host - @internal def get_all_addresses(self, ips): ip_objects = set() inputs = [ip.strip() for ip in ips.split(',')] diff --git a/modules/network_scan/FTPScanner.py b/modules/network_scan/FTPScanner.py new file mode 100644 index 0000000..2093721 --- /dev/null +++ b/modules/network_scan/FTPScanner.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +from core.prototypes.AbstractScanner import AbstractScanner +import ftplib +from ftplib import FTP + +MAX_ERRORS = 3 + + +class FTPScanner(AbstractScanner): + def __init__(self, timeout:"timeout", credentials:"credentials"): + self.__timeout__ = timeout + self.__credantials__ = credentials + + def scan_address(self, host: 'ipv4_str', port: 'port') -> {'ftp_version', 'ftp_status', 'login', 'password'}: + result = self.ftp_anonymous_login(host, port, self.__timeout__) + if result['ftp_status'] == 'ok': + #Что-то делать с ошибками + return result + if not result['ftp_status'].startswith('530'): + return result + return self.ftp_bruteforce( + host, port, self.__credentials__, self.__timeout__) + + @staticmethod + def ftp_anonymous_login(host, port, timeout): + '''Get version and check if anonympous login is enabled''' + result = { + key:None for key in ['ftp_version', 'ftp_status', 'login', + 'password'] + } + ftp_connection = FTP(timeout=timeout) + try: + version = ftp_connection.connect(host=host, port=port) + # Get something like "220 Twisted 16.6.0 FTP Server" + result['ftp_version'] = version.lstrip('220 ') + # Try to login as anonymous user + ftp_connection.login() + result['ftp_status'] = 'ok' + except ftplib.error_perm as e: + if str(e).startswith("530"): + result['ftp_status'] = 'ok' + result['anonymous_login'] = False + except ftplib.all_errors as e: + #status - error + result['ftp_status'] = str(e) + return result + finally: + ftp_connection.close() + return result + + @staticmethod + def ftp_bruteforce(host, port, creds, timeout): + '''Attempt to brute force login/password pair''' + # We want maintain connection to speed up bruteforce + # but we also want to reconnect if necessary. + # That is why I use cred iterator to pick up new login/pass only when + # we need to. + result = { + key:None for key in ['ftp_version', 'ftp_status', 'login', + 'password'] + } + result['ftp_status'] = "error" + error_count = 0 + it = iter(creds) + cred = next(it, "") + ftp_connection = FTP(timeout=timeout) + while error_count < MAX_ERRORS: + try: + # Connecting to server + ftp_connection.connect(host=host, port=port) + while cred and error_count < MAX_ERRORS: + user, password = cred + # Trying to log in + try: + ftp_connection.login(user, password) + ftp_connection.close() + result['ftp_status'] = 'ok' + result['login'] = user + result['password'] = password + return result + except ftplib.error_perm as e: + # Password was wrong, checking another + cred = next(it, "") + continue + except ftplib.all_errors as e: + error_count += 1 + # Connection was dropped or another network error happened + # We must connection, error_count would help us to + # avoid deadlock on mumbling host + break + except ftplib.all_errors as e: + # Cannot reconnect, give up + break + finally: + ftp_connection.close() + return result diff --git a/modules/network_scan/test_FTPScanner.py b/modules/network_scan/test_FTPScanner.py new file mode 100644 index 0000000..8b852ce --- /dev/null +++ b/modules/network_scan/test_FTPScanner.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +from modules.network_scan.FTPScanner import FTPScanner +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.servers import FTPServer +import unittest +from tempfile import mkdtemp +import multiprocessing +from time import sleep + +import http.server +import socketserver +import os + +TEST_CREDS = (("admin", "admin"), ("1", "1"), ('user', 'password')) +PORT = 2121 + + +def run_anonymous_ftp(temp_dir): + authorizer = DummyAuthorizer() + authorizer.add_anonymous(temp_dir) + handler = FTPHandler + handler.authorizer = authorizer + server = FTPServer(("127.0.0.1", PORT), handler) + server.serve_forever() + + +def run_bruteforce_ftp(temp_dir): + authorizer = DummyAuthorizer() + user, password = TEST_CREDS[-1] + authorizer.add_user(user, password, temp_dir, perm="elradfmw") + handler = FTPHandler + handler.authorizer = authorizer + handler.max_login_attempts = 2 # Drop connection on each 2 incorrect attempts + server = FTPServer(("127.0.0.1", PORT), handler) + server.serve_forever() + + +def run_mumble(): + handler = http.server.SimpleHTTPRequestHandler + httpd = socketserver.TCPServer(("127.0.0.1", PORT), handler) + httpd.serve_forever() + + +class TestFTPScanner(unittest.TestCase): + def test_closed_port(self): + scanner = FTPScanner(timeout=10, credentials=TEST_CREDS) + result = scanner.scan_address('127.0.0.1', 31337) + print(result) + #self.assertEqual(result['status'], 'Connection refused', "Should be error") + self.assertTrue("Connection refused" in result['ftp_status'], "Connection refused") + + def test_mumble(self): + p = multiprocessing.Process(target=run_mumble) + p.start() + sleep(5) + scanner = FTPScanner(timeout=10, credentials=TEST_CREDS) + result = scanner.scan_address('127.0.0.1', PORT) + print(result) + self.assertEqual(result['status'], 'error', "Should be error") + self.assertTrue("timed out" in result['error_type'], "Timed out") + p.terminate() + + def test_anonymous_login(self): + temp_dir = mkdtemp() + p = multiprocessing.Process(target=run_anonymous_ftp, args=(temp_dir,)) + p.start() + sleep(5) + scanner = FTPScanner(timeout=10, credentials=TEST_CREDS) + result = scanner.scan_address('127.0.0.1', PORT) + print(result) + self.assertEqual(result['login'], None, "Should be True") + self.assertEqual(result['password'], None, "Should be True") + p.terminate() + os.rmdir(temp_dir) + + def test_bruteforce(self): + temp_dir = mkdtemp() + p = multiprocessing.Process(target=run_bruteforce_ftp, args=(temp_dir,)) + p.start() + sleep(5) + scanner = FTPScanner(timeout=10, credentials=TEST_CREDS) + result = scanner.scan_address('127.0.0.1', PORT) + print(result) + self.assertEqual(result['login'], TEST_CREDS[-1][0], "Should be True") + self.assertEqual(result['password'], TEST_CREDS[-1][1], "Should be True") + p.terminate() + os.rmdir(temp_dir) + + +if __name__ == '__main__': + unittest.main() diff --git a/results.json b/results.json index 4d473bd..90e772e 100644 --- a/results.json +++ b/results.json @@ -1 +1 @@ -{"200": {"https://docs.google.com/document/d/": [{"hash": "11Sz_PyqL268V9xmcEjYqEhufFGleT5TowdKEu5cTFak", "title": null}, {"hash": "11Sz_PyqL268V9xmcEjYqEhufFGleT5TowdKEu5cTFal", "title": null}, {"hash": "11Sz_PyqL268V9xmcEjYqEhufFGleT5TowdKEu5cTFam", "title": null}]}} \ No newline at end of file +{"timed out": [{"ip": "122.3.42.1", "port": 20, "login": null, "password": null, "ftp_version": null}, {"ip": "122.3.42.1", "port": 21, "login": null, "password": null, "ftp_version": null}]} \ No newline at end of file