Merge remote-tracking branch 'upstream/develop' into attack_pass_the_hash

This commit is contained in:
VakarisZ 2019-06-18 15:34:31 +03:00
commit 676eca6f87
19 changed files with 257 additions and 67 deletions

View File

@ -157,9 +157,8 @@ class Configuration(object):
keep_tunnel_open_time = 60 keep_tunnel_open_time = 60
# Monkey files directories # Monkey files directory name
monkey_dir_linux = '/tmp/monkey_dir' monkey_dir_name = 'monkey_dir'
monkey_dir_windows = r'C:\Windows\Temp\monkey_dir'
########################### ###########################
# scanners config # scanners config

View File

@ -29,9 +29,7 @@
"dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
"dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_linux": "/tmp/monkey",
monkey_dir_linux = '/tmp/monkey_dir', "monkey_dir_name": "monkey_dir",
monkey_dir_windows = r'C:\Windows\Temp\monkey_dir',
"kill_file_path_linux": "/var/run/monkey.not", "kill_file_path_linux": "/var/run/monkey.not",
"kill_file_path_windows": "%windir%\\monkey.not", "kill_file_path_windows": "%windir%\\monkey.not",

View File

@ -1,15 +1,16 @@
import os
import logging import logging
from time import sleep import os
import pymssql
import textwrap import textwrap
from time import sleep
import pymssql
from infection_monkey.exploit import HostExploiter, tools
from common.utils.exploit_enum import ExploitType from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter, tools
from infection_monkey.exploit.tools import HTTPTools from infection_monkey.exploit.tools import HTTPTools
from infection_monkey.config import WormConfiguration
from infection_monkey.model import DROPPER_ARG
from infection_monkey.exploit.tools import get_monkey_dest_path from infection_monkey.exploit.tools import get_monkey_dest_path
from infection_monkey.model import DROPPER_ARG
from infection_monkey.utils import get_monkey_dir_path
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -52,10 +53,10 @@ class MSSQLExploiter(HostExploiter):
LOG.info("Started http server on %s", http_path) LOG.info("Started http server on %s", http_path)
dst_path = get_monkey_dest_path(http_path) dst_path = get_monkey_dest_path(http_path)
tmp_file_path = os.path.join(WormConfiguration.monkey_dir_windows, MSSQLExploiter.TMP_FILE_NAME) tmp_file_path = os.path.join(get_monkey_dir_path(), MSSQLExploiter.TMP_FILE_NAME)
# Create monkey dir. # Create monkey dir.
commands = ["xp_cmdshell \"mkdir %s\"" % WormConfiguration.monkey_dir_windows] commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()]
MSSQLExploiter.execute_command(cursor, commands) MSSQLExploiter.execute_command(cursor, commands)
# Form download command in a file # Form download command in a file

View File

@ -153,6 +153,7 @@ class SRVSVC_Exploit(object):
class Ms08_067_Exploiter(HostExploiter): class Ms08_067_Exploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
_EXPLOITED_SERVICE = 'Microsoft Server Service'
_windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2, _windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2,
'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2} 'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2}

View File

@ -1,7 +1,9 @@
import os import os
import sys
import shutil import shutil
import struct import struct
import sys
import tempfile
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
@ -16,10 +18,9 @@ def get_dropper_log_path():
def is_64bit_windows_os(): def is_64bit_windows_os():
''' """
Checks for 64 bit Windows OS using environment variables. Checks for 64 bit Windows OS using environment variables.
:return: """
'''
return 'PROGRAMFILES(X86)' in os.environ return 'PROGRAMFILES(X86)' in os.environ
@ -53,7 +54,4 @@ def remove_monkey_dir():
def get_monkey_dir_path(): def get_monkey_dir_path():
if is_windows_os(): return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name)
return WormConfiguration.monkey_dir_windows
else:
return WormConfiguration.monkey_dir_linux

View File

@ -25,3 +25,14 @@ def is_db_server_up(mongo_url):
return True return True
except ServerSelectionTimeoutError: except ServerSelectionTimeoutError:
return False return False
def get_db_version(mongo_url):
"""
Return the mongo db version
:param mongo_url: Which mongo to check.
:return: version as a tuple (e.g. `(u'4', u'0', u'8')`)
"""
client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
server_version = tuple(client.server_info()['version'].split('.'))
return server_version

View File

@ -6,6 +6,8 @@ import sys
import time import time
import logging import logging
MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0"
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path: if BASE_PATH not in sys.path:
@ -22,7 +24,7 @@ from monkey_island.cc.app import init_app
from monkey_island.cc.exporter_init import populate_exporter_list from monkey_island.cc.exporter_init import populate_exporter_list
from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.environment.environment import env from monkey_island.cc.environment.environment import env
from monkey_island.cc.database import is_db_server_up from monkey_island.cc.database import is_db_server_up, get_db_version
def main(): def main():
@ -31,10 +33,8 @@ def main():
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
wait_for_mongo_db_server(mongo_url)
while not is_db_server_up(mongo_url): assert_mongo_db_version(mongo_url)
logger.info('Waiting for MongoDB server')
time.sleep(1)
populate_exporter_list() populate_exporter_list()
app = init_app(mongo_url) app = init_app(mongo_url)
@ -55,5 +55,27 @@ def main():
IOLoop.instance().start() IOLoop.instance().start()
def wait_for_mongo_db_server(mongo_url):
while not is_db_server_up(mongo_url):
logger.info('Waiting for MongoDB server on {0}'.format(mongo_url))
time.sleep(1)
def assert_mongo_db_version(mongo_url):
"""
Checks if the mongodb version is new enough for running the app.
If the DB is too old, quits.
:param mongo_url: URL to the mongo the Island will use
"""
required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split("."))
server_version = get_db_version(mongo_url)
if server_version < required_version:
logger.error(
'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version)))
sys.exit(-1)
else:
logger.info('Mongo DB version OK. Got {0}'.format(str(server_version)))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -55,6 +55,14 @@ class Monkey(Document):
monkey_is_dead = True monkey_is_dead = True
return monkey_is_dead return monkey_is_dead
def get_os(self):
os = "unknown"
if self.description.lower().find("linux") != -1:
os = "linux"
elif self.description.lower().find("windows") != -1:
os = "windows"
return os
class MonkeyNotFoundError(Exception): class MonkeyNotFoundError(Exception):
pass pass

View File

@ -1,13 +1,13 @@
import uuid import uuid
from time import sleep from time import sleep
from unittest import TestCase
from monkey import Monkey from monkey import Monkey
from monkey_island.cc.models.monkey import MonkeyNotFoundError from monkey_island.cc.models.monkey import MonkeyNotFoundError
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
from monkey_ttl import MonkeyTtl from monkey_ttl import MonkeyTtl
class TestMonkey(TestCase): class TestMonkey(IslandTestCase):
""" """
Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and
won't work. won't work.
@ -15,7 +15,11 @@ class TestMonkey(TestCase):
Also, the working directory needs to be the working directory from which you usually run the island so the Also, the working directory needs to be the working directory from which you usually run the island so the
server.json file is found and loaded. server.json file is found and loaded.
""" """
def test_is_dead(self): def test_is_dead(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
# Arrange # Arrange
alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30)
alive_monkey_ttl.save() alive_monkey_ttl.save()
@ -43,6 +47,9 @@ class TestMonkey(TestCase):
self.assertFalse(alive_monkey.is_dead()) self.assertFalse(alive_monkey.is_dead())
def test_get_single_monkey_by_id(self): def test_get_single_monkey_by_id(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
# Arrange # Arrange
a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey = Monkey(guid=str(uuid.uuid4()))
a_monkey.save() a_monkey.save()
@ -52,3 +59,21 @@ class TestMonkey(TestCase):
self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id)) self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id))
# Raise on non-existent monkey # Raise on non-existent monkey
self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef") self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef")
def test_get_os(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
linux_monkey = Monkey(guid=str(uuid.uuid4()),
description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu SMP Mon May 6 18:46:08 UTC 2019 x86_64 x86_64")
windows_monkey = Monkey(guid=str(uuid.uuid4()),
description="Windows bla bla bla")
unknown_monkey = Monkey(guid=str(uuid.uuid4()),
description="bla bla bla")
linux_monkey.save()
windows_monkey.save()
unknown_monkey.save()
self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects())))
self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects())))
self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects())))

View File

@ -35,11 +35,15 @@ class T1110(AttackTechnique):
result['successful_creds'].append(T1110.parse_creds(attempt)) result['successful_creds'].append(T1110.parse_creds(attempt))
if succeeded: if succeeded:
data = {'message': T1110.used_msg, 'status': ScanStatus.USED.name} data = T1110.get_message_and_status(T1110, ScanStatus.USED)
elif attempts: elif attempts:
data = {'message': T1110.scanned_msg, 'status': ScanStatus.SCANNED.name} data = T1110.get_message_and_status(T1110, ScanStatus.SCANNED)
else: else:
data = {'message': T1110.unscanned_msg, 'status': ScanStatus.UNSCANNED.name} data = T1110.get_message_and_status(T1110, ScanStatus.UNSCANNED)
# Remove data with no successful brute force attempts
attempts = [attempt for attempt in attempts if attempt['attempts']]
data.update({'services': attempts, 'title': T1110.technique_title(T1110.tech_id)}) data.update({'services': attempts, 'title': T1110.technique_title(T1110.tech_id)})
return data return data
@ -51,21 +55,39 @@ class T1110(AttackTechnique):
:return: string with username and used password/hash :return: string with username and used password/hash
""" """
username = attempt['user'] username = attempt['user']
if attempt['lm_hash']: creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])},
return '%s ; LM hash %s ...' % (username, encryptor.dec(attempt['lm_hash'])[0:5]) 'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)},
if attempt['ntlm_hash']: 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
return '%s ; NTLM hash %s ...' % (username, encryptor.dec(attempt['ntlm_hash'])[0:20]) 'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}}
if attempt['ssh_key']: for key, cred in creds.items():
return '%s ; SSH key %s ...' % (username, encryptor.dec(attempt['ssh_key'])[0:15]) if attempt[key]:
if attempt['password']: return '%s ; %s : %s' % (username,
return '%s : %s' % (username, T1110.obfuscate_password(encryptor.dec(attempt['password']))) cred['type'],
cred['output'])
@staticmethod @staticmethod
def obfuscate_password(password, plain_chars=3): def censor_password(password, plain_chars=3, secret_chars=5):
""" """
Obfuscates password by changing characters to * Decrypts and obfuscates password by changing characters to *
:param password: Password or string to obfuscate :param password: Password or string to obfuscate
:param plain_chars: How many plain-text characters should be kept at the start of the string :param plain_chars: How many plain-text characters should be kept at the start of the string
:param secret_chars: How many * symbols should be used to hide the remainder of the password
:return: Obfuscated string e.g. Pass**** :return: Obfuscated string e.g. Pass****
""" """
return password[0:plain_chars] + '*' * (len(password) - plain_chars) if not password:
return ""
password = encryptor.dec(password)
return password[0:plain_chars] + '*' * secret_chars
@staticmethod
def censor_hash(hash_, plain_chars=5):
"""
Decrypts and obfuscates hash by only showing a part of it
:param hash_: Hash to obfuscate
:param plain_chars: How many chars of hash should be shown
:return: Obfuscated string
"""
if not hash_:
return ""
hash_ = encryptor.dec(hash_)
return hash_[0: plain_chars] + ' ...'

View File

@ -60,6 +60,19 @@ class AttackTechnique(object):
else: else:
return ScanStatus.UNSCANNED return ScanStatus.UNSCANNED
@staticmethod
def get_message_and_status(technique, status):
return {'message': technique.get_message_by_status(technique, status), 'status': status.name}
@staticmethod
def get_message_by_status(technique, status):
if status == ScanStatus.UNSCANNED:
return technique.unscanned_msg
elif status == ScanStatus.SCANNED:
return technique.scanned_msg
else:
return technique.used_msg
@staticmethod @staticmethod
def technique_title(technique): def technique_title(technique):
""" """
@ -78,11 +91,7 @@ class AttackTechnique(object):
data = {} data = {}
status = AttackTechnique.technique_status(technique.tech_id) status = AttackTechnique.technique_status(technique.tech_id)
title = AttackTechnique.technique_title(technique.tech_id) title = AttackTechnique.technique_title(technique.tech_id)
data.update({'status': status.name, 'title': title}) data.update({'status': status.name,
if status == ScanStatus.UNSCANNED: 'title': title,
data.update({'message': technique.unscanned_msg}) 'message': technique.get_message_by_status(technique, status)})
elif status == ScanStatus.SCANNED:
data.update({'message': technique.scanned_msg})
else:
data.update({'message': technique.used_msg})
return data return data

View File

@ -499,17 +499,11 @@ SCHEMA = {
"default": 60, "default": 60,
"description": "Time to keep tunnel open before going down after last exploit (in seconds)" "description": "Time to keep tunnel open before going down after last exploit (in seconds)"
}, },
"monkey_dir_windows": { "monkey_dir_name": {
"title": "Monkey's windows directory", "title": "Monkey's directory name",
"type": "string", "type": "string",
"default": r"C:\Windows\temp\monkey_dir", "default": r"monkey_dir",
"description": "Directory containing all monkey files on windows" "description": "Directory name for the directory which will contain all of the monkey files"
},
"monkey_dir_linux": {
"title": "Monkey's linux directory",
"type": "string",
"default": "/tmp/monkey_dir",
"description": "Directory containing all monkey files on linux"
}, },
} }
}, },

View File

@ -1,6 +1,7 @@
from itertools import product from itertools import product
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
from bson import ObjectId from bson import ObjectId
from monkey_island.cc.services.groups_and_users_consts import USERTYPE from monkey_island.cc.services.groups_and_users_consts import USERTYPE
@ -216,15 +217,15 @@ class PTHReportService(object):
@staticmethod @staticmethod
def generate_map_nodes(): def generate_map_nodes():
monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1}) monkeys = filter(lambda m: m.get_os() == "windows", Monkey.objects())
return [ return [
{ {
'id': monkey['_id'], 'id': monkey.guid,
'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), 'label': '{0} : {1}'.format(monkey.hostname, monkey.ip_addresses[0]),
'group': 'critical' if monkey.get('critical_services', []) else 'normal', 'group': 'critical' if monkey.critical_services is not None else 'normal',
'services': monkey.get('critical_services', []), 'services': monkey.critical_services,
'hostname': monkey['hostname'] 'hostname': monkey.hostname
} for monkey in monkeys } for monkey in monkeys
] ]

View File

@ -0,0 +1,69 @@
import uuid
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.pth_report import PTHReportService
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
class TestPTHReportServiceGenerateMapNodes(IslandTestCase):
def test_generate_map_nodes(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
self.assertEqual(PTHReportService.generate_map_nodes(), [])
windows_monkey_with_services = Monkey(
guid=str(uuid.uuid4()),
hostname="A_Windows_PC_1",
critical_services=["aCriticalService", "Domain Controller"],
ip_addresses=["1.1.1.1", "2.2.2.2"],
description="windows 10"
)
windows_monkey_with_services.save()
windows_monkey_with_no_services = Monkey(
guid=str(uuid.uuid4()),
hostname="A_Windows_PC_2",
critical_services=[],
ip_addresses=["3.3.3.3"],
description="windows 10"
)
windows_monkey_with_no_services.save()
linux_monkey = Monkey(
guid=str(uuid.uuid4()),
hostname="A_Linux_PC",
ip_addresses=["4.4.4.4"],
description="linux ubuntu"
)
linux_monkey.save()
map_nodes = PTHReportService.generate_map_nodes()
self.assertEquals(2, len(map_nodes))
def test_generate_map_nodes_parsing(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
monkey_id = str(uuid.uuid4())
hostname = "A_Windows_PC_1"
windows_monkey_with_services = Monkey(
guid=monkey_id,
hostname=hostname,
critical_services=["aCriticalService", "Domain Controller"],
ip_addresses=["1.1.1.1"],
description="windows 10"
)
windows_monkey_with_services.save()
map_nodes = PTHReportService.generate_map_nodes()
self.assertEquals(map_nodes[0]["id"], monkey_id)
self.assertEquals(map_nodes[0]["label"], "A_Windows_PC_1 : 1.1.1.1")
self.assertEquals(map_nodes[0]["group"], "critical")
self.assertEquals(len(map_nodes[0]["services"]), 2)
self.assertEquals(map_nodes[0]["hostname"], hostname)

View File

@ -0,0 +1,12 @@
import unittest
from monkey_island.cc.environment.environment import env
from monkey_island.cc.models import Monkey
class IslandTestCase(unittest.TestCase):
def fail_if_not_testing_env(self):
self.failIf(not env.testing, "Change server_config.json to testing environment.")
@staticmethod
def clean_monkey_db():
Monkey.objects().delete()

View File

@ -6,6 +6,7 @@
"clean": "rimraf dist/*", "clean": "rimraf dist/*",
"copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist", "copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist",
"dist": "webpack --mode production", "dist": "webpack --mode production",
"dev": "webpack --mode development",
"lint": "eslint ./src", "lint": "eslint ./src",
"posttest": "npm run lint", "posttest": "npm run lint",
"release:major": "npm version major && npm publish && git push --follow-tags", "release:major": "npm version major && npm publish && git push --follow-tags",
@ -100,6 +101,7 @@
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"sha3": "^2.0.0", "sha3": "^2.0.0",
"react-spinners": "^0.5.4", "react-spinners": "^0.5.4",
"@emotion/core": "^10.0.10" "@emotion/core": "^10.0.10",
"react-desktop-notification": "^1.0.9"
} }
} }

View File

@ -14,6 +14,8 @@ import ReportPage from 'components/pages/ReportPage';
import LicensePage from 'components/pages/LicensePage'; import LicensePage from 'components/pages/LicensePage';
import AuthComponent from 'components/AuthComponent'; import AuthComponent from 'components/AuthComponent';
import LoginPageComponent from 'components/pages/LoginPage'; import LoginPageComponent from 'components/pages/LoginPage';
import Notifier from "react-desktop-notification"
import 'normalize.css/normalize.css'; import 'normalize.css/normalize.css';
import 'react-data-components/css/table-twbs.css'; import 'react-data-components/css/table-twbs.css';
@ -25,6 +27,7 @@ import VersionComponent from "./side-menu/VersionComponent";
let logoImage = require('../images/monkey-icon.svg'); let logoImage = require('../images/monkey-icon.svg');
let infectionMonkeyImage = require('../images/infection-monkey.svg'); let infectionMonkeyImage = require('../images/infection-monkey.svg');
let guardicoreLogoImage = require('../images/guardicore-logo.png'); let guardicoreLogoImage = require('../images/guardicore-logo.png');
let notificationIcon = require('../images/notification-logo-512x512.png');
class AppComponent extends AuthComponent { class AppComponent extends AuthComponent {
updateStatus = () => { updateStatus = () => {
@ -50,6 +53,7 @@ class AppComponent extends AuthComponent {
} }
if (isChanged) { if (isChanged) {
this.setState({completedSteps: res['completed_steps']}); this.setState({completedSteps: res['completed_steps']});
this.showInfectionDoneNotification();
} }
}); });
} }
@ -194,6 +198,20 @@ class AppComponent extends AuthComponent {
</Router> </Router>
); );
} }
showInfectionDoneNotification() {
if (this.state.completedSteps.infection_done) {
let hostname = window.location.hostname;
let url = `https://${hostname}:5000/report`;
console.log("Trying to show notification. URL: " + url + " | icon: " + notificationIcon);
Notifier.start(
"Monkey Island",
"Infection is done! Click here to go to the report page.",
url,
notificationIcon);
}
}
} }
AppComponent.defaultProps = {}; AppComponent.defaultProps = {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB