diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index cfda27913..b0f615552 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -3,7 +3,7 @@ import sys from network.range import FixedRange, RelativeRange, ClassCRange from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter,\ SambaCryExploiter -from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger +from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger from abc import ABCMeta from itertools import product import uuid @@ -140,7 +140,7 @@ class Configuration(object): max_iterations = 1 scanner_class = TcpScanner - finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger] + finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger] exploiter_classes = [SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter, # Windows exploits SSHExploiter, ShellShockExploiter, SambaCryExploiter # Linux ] diff --git a/chaos_monkey/example.conf b/chaos_monkey/example.conf index 4396cae34..55a716c7c 100644 --- a/chaos_monkey/example.conf +++ b/chaos_monkey/example.conf @@ -39,7 +39,8 @@ "SSHFinger", "PingScanner", "HTTPFinger", - "SMBFinger" + "SMBFinger", + "MySQLFinger" ], "max_iterations": 3, "monkey_log_path_windows": "%temp%\\~df1563.tmp", diff --git a/chaos_monkey/network/__init__.py b/chaos_monkey/network/__init__.py index 850159342..e7f22de28 100644 --- a/chaos_monkey/network/__init__.py +++ b/chaos_monkey/network/__init__.py @@ -23,5 +23,6 @@ from tcp_scanner import TcpScanner from smbfinger import SMBFinger from sshfinger import SSHFinger from httpfinger import HTTPFinger +from mysqlfinger import MySQLFinger from info import local_ips from info import get_free_tcp_port diff --git a/chaos_monkey/network/mysqlfinger.py b/chaos_monkey/network/mysqlfinger.py new file mode 100644 index 000000000..0bda6c5ac --- /dev/null +++ b/chaos_monkey/network/mysqlfinger.py @@ -0,0 +1,78 @@ +import socket +import logging +from network import HostFinger +from .tools import struct_unpack_tracker, struct_unpack_tracker_string +from model.host import VictimHost + +MYSQL_PORT = 3306 +SQL_SERVICE = 'mysqld-3306' + +LOG = logging.getLogger(__name__) + + +class MySQLFinger(HostFinger): + """ + Fingerprints mysql databases, only on port 3306 + """ + + def __init__(self): + self._config = __import__('config').WormConfiguration + + def get_host_fingerprint(self, host): + """ + Returns mySQLd data using the host header + :param host: + :return: Success/failure, data is saved in the host struct + """ + assert isinstance(host, VictimHost) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.5) + + try: + s.connect((host.ip_addr, MYSQL_PORT)) + header = s.recv(4) # max header size? + + tmp, curpos = struct_unpack_tracker(header, 0, "I") + tmp = tmp[0] + response_length = tmp & 0xff + data = s.recv(response_length) + # now we can start parsing + protocol, curpos = struct_unpack_tracker(data, 0, "B") + protocol = protocol[0] + + if protocol == 0xFF: + # error code, bug out + LOG.debug("Mysql server returned error") + return False + + version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing + version = version[0] + host.services[SQL_SERVICE]['version'] = version + version = version.split('-')[0].split('.') + host.services[SQL_SERVICE]['major_version'] = version[0] + host.services[SQL_SERVICE]['minor_version'] = version[1] + host.services[SQL_SERVICE]['build_version'] = version[2] + thread_id, curpos = struct_unpack_tracker(data, curpos, "<I") # ignore thread id + + if protocol == 10: + # new protocol + self._parse_protocol_10(curpos, data, host) + return True + if protocol == 9: + return True + s.close() + + except Exception as err: + LOG.debug("Error getting mysql fingerprint: %s", err) + + return False + + def _parse_protocol_10(self, curpos, data, host): + salt, curpos = struct_unpack_tracker(data, curpos, "s8B") + capabilities, curpos = struct_unpack_tracker(data, curpos, "<H") + host.services[SQL_SERVICE]['capabilities'] = capabilities[0] + charset, curpos = struct_unpack_tracker(data, curpos, "B") + status, curpos = struct_unpack_tracker(data, curpos, "<H") + extcapabilities, curpos = struct_unpack_tracker(data, curpos, "<H") + host.services[SQL_SERVICE]['extcapabilities'] = extcapabilities[0] + # there's more data but it doesn't matter diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index 8eb85704f..3d7fe64ef 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -1,6 +1,7 @@ import socket import select import logging +import struct DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 @@ -8,6 +9,33 @@ BANNER_READ = 1024 LOG = logging.getLogger(__name__) +def struct_unpack_tracker(data, index, fmt): + """ + Unpacks a struct from the specified index according to specified format. + Returns the data and the next index + :param data: Buffer + :param index: Position index + :param fmt: Struct format + :return: (Data, new index) + """ + unpacked = struct.unpack_from(fmt, data, index) + return unpacked, struct.calcsize(fmt) + + +def struct_unpack_tracker_string(data, index): + """ + Unpacks a null terminated string from the specified index + Returns the data and the next index + :param data: Buffer + :param index: Position index + :return: (Data, new index) + """ + ascii_len = data[index:].find('\0') + fmt = "%ds" % ascii_len + unpacked = struct.unpack_from(fmt, data, index) + return unpacked, struct.calcsize(fmt) + + def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout)