diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 4733d5089..b6d680a87 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -17,6 +17,7 @@ from cc.resources.monkey import Monkey from cc.resources.monkey_configuration import MonkeyConfiguration from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap +from cc.resources.pthmap import PthMap from cc.resources.node import Node from cc.resources.report import Report from cc.resources.root import Root @@ -101,5 +102,7 @@ def init_app(mongo_url): api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') api.add_resource(Report, '/api/report', '/api/report/') api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') + + api.add_resource(PthMap, '/api/pthmap', '/api/pthmap/') return app diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py new file mode 100644 index 000000000..b027991ee --- /dev/null +++ b/monkey_island/cc/resources/pthmap.py @@ -0,0 +1,327 @@ +import flask_restful + +from cc.auth import jwt_required +from cc.services.edge import EdgeService +from cc.services.node import NodeService +from cc.database import mongo + +import hashlib +import binascii +from pymongo import MongoClient + +class PthMap(flask_restful.Resource): + @jwt_required() + def get(self, **kw): + graph = PassTheHashMap() + + return \ + { + "nodes": [{"id": x} for x in graph.vertices], + "edges": [{"id": str(s) + str(t), "from": s, "to": t} for s, t in graph.edges] + } + +DsRole_RoleStandaloneWorkstation = 0 +DsRole_RoleMemberWorkstation = 1 +DsRole_RoleStandaloneServer = 2 +DsRole_RoleMemberServer = 3 +DsRole_RoleBackupDomainController = 4 +DsRole_RolePrimaryDomainController = 5 + +def myntlm(x): + hash = hashlib.new('md4', x.encode('utf-16le')).digest() + return str(binascii.hexlify(hash)) + +class Machine(object): + def __init__(self, monkey_guid): + self.monkey_guid = str(monkey_guid) + + def GetMimikatzOutput(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + output = set() + + for doc in cur: + output.add(doc["data"]["mimikatz"]) + + if len(output) == 1: + return output.pop() + + return None + + def GetHostName(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + names.add(eval(comp["Name"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetIp(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for addr in doc["data"]["network_info"]["networks"]: + return str(addr["addr"]) + + return None + + def GetDomainName(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + names.add(eval(comp["Domain"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetDomainRole(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + roles = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + roles.add(comp["DomainRole"]) + + if len(roles) == 1: + return roles.pop() + + return None + + def GetSidByUsername(self, username): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.Name":"u'%s'" % (username,)}) + + SIDs = set() + + for doc in cur: + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["Name"]) != username: + continue + + SIDs.add(eval(user["SID"])) + + if len(SIDs) == 1: + return SIDs.pop() + + return None + + def GetUsernameBySid(self, sid): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.SID":"u'%s'" % (sid,)}) + + names = set() + + for doc in cur: + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["SID"]) != sid: + continue + + names.add(eval(user["Name"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetGroupSidByGroupName(self, group_name): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_Group.Name":"u'%s'" % (group_name,)}) + SIDs = set() + + for doc in cur: + for group in doc["data"]["Win32_Group"]: + if eval(group["Name"]) != group_name: + continue + + SIDs.add(eval(group["SID"])) + + if len(SIDs) == 1: + return SIDs.pop() + + return None + + def GetUsersByGroupSid(self, sid): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_GroupUser.GroupComponent.SID":"u'%s'" % (sid,)}) + + users = dict() + + for doc in cur: + for group_user in doc["data"]["Win32_GroupUser"]: + if eval(group_user["GroupComponent"]["SID"]) != sid: + continue + + if "PartComponent" not in group_user.keys(): + continue + + users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) + + return users + + def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "data.Win32_ComputerSystem.Domain":"u'%s'" % (domain_name,)}) + + GUIDs = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + if ((comp["DomainRole"] != DsRole_RolePrimaryDomainController) and + (comp["DomainRole"] != DsRole_RoleBackupDomainController)): + continue + + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + + def GetLocalAdmins(self): + return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys()) + + def GetLocalAdminNames(self): + return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) + + def GetLocalAdminSecrets(self): + admin_names = self.GetLocalAdminNames() + sam_users = str(self.GetMimikatzOutput()).split("\nSAMKey :")[1].split("\n\n")[1:] + + admin_secrets = set() + + for sam_user_txt in sam_users: + sam_user = dict([map(str.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + + if sam_user["User"] not in admin_names: + continue + + admin_secrets.add(sam_user["NTLM"].replace("[hashed secret]", "").strip()) + + return admin_secrets + + def GetCachedSecrets(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + secrets = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + user = doc["data"]["credentials"][username] + + if "password" in user.keys(): + ntlm = myntlm(str(user["password"])) + elif "ntlm_hash" in user.keys(): + ntlm = str(user["ntlm_hash"]) + else: + continue + + secret = hashlib.md5(ntlm.decode("hex")).hexdigest() + secrets.add(secret) + + return secrets + + def GetDomainAdminsOfMachine(self): + domain_name = self.GetDomainName() + DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) + + domain_admins = set() + + for dc_monkey_guid in DCs: + domain_admins |= Machine(dc_monkey_guid).GetLocalAdmins() + + return domain_admins + + def GetAdmins(self): + return self.GetLocalAdmins() | self.GetDomainAdminsOfMachine() + + def GetAdminNames(self): + return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) + + def GetCachedSids(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + SIDs = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + SIDs.add(self.GetSidByUsername(username)) + + return SIDs + + def GetCachedUsernames(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + SIDs = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + SIDs.add(username) + + return SIDs + +class PassTheHashMap(object): + def __init__(self): + self.vertices = self.GetAllMachines() + self.edges = set() + + #self.GenerateEdgesBySid() # Useful for non-cached domain users + self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username + + def GetAllMachines(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection"}) + + GUIDs = set() + + for doc in cur: + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + + def GenerateEdgesBySid(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedSids() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdmins() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def GenerateEdgesBySamHash(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedSecrets() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetLocalAdminSecrets() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def GenerateEdgesByUsername(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedUsernames() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdminNames() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def Print(self): + print map(lambda x: Machine(x).GetIp(), self.vertices) + print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index 771e2257a..48410bc94 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -7,6 +7,7 @@ import RunServerPage from 'components/pages/RunServerPage'; import ConfigurePage from 'components/pages/ConfigurePage'; import RunMonkeyPage from 'components/pages/RunMonkeyPage'; import MapPage from 'components/pages/MapPage'; +import PassTheHashMapPage from 'components/pages/PassTheHashMapPage'; import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; import ReportPage from 'components/pages/ReportPage'; @@ -162,6 +163,7 @@ class AppComponent extends AuthComponent { {this.renderRoute('/run-monkey', )} {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} + {this.renderRoute('/pth', )} {this.renderRoute('/start-over', )} {this.renderRoute('/report', )} {this.renderRoute('/license', )} diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js new file mode 100644 index 000000000..bea2f7b63 --- /dev/null +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -0,0 +1,58 @@ +import React from 'react'; +import {Col} from 'react-bootstrap'; +import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; +import AuthComponent from '../AuthComponent'; + +class PassTheHashMapPageComponent extends AuthComponent { + constructor(props) { + super(props); + this.state = { + graph: {nodes: [], edges: []}, + selected: null, + selectedType: null, + killPressed: false, + showKillDialog: false, + telemetry: [], + telemetryLastTimestamp: null + }; + } + + componentDidMount() { + this.updateMapFromServer(); + this.interval = setInterval(this.timedEvents, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + timedEvents = () => { + this.updateMapFromServer(); + }; + + updateMapFromServer = () => { + this.authFetch('/api/pthmap') + .then(res => res.json()) + .then(res => { + this.setState({graph: res}); + this.props.onStatusChange(); + }); + }; + + render() { + return ( +
+ +

3. Pass The Hash Map

+ + +
+ +
+ +
+ ); + } +} + +export default PassTheHashMapPageComponent;